当前位置: 首页 > 工具软件 > Burrow > 使用案例 >

burrow, Sign

澹台成龙
2023-12-01

Will try to explain how TX is signed.

Protobuf

BroadcastTxSync/BroadcastTxAsync/SignTx take TxEnvelopeParam as the input.

// Transaction Service Definition
service Transact {
    // Broadcast a transaction to the mempool - if the transaction is not signed signing will be attempted server-side
    // and wait for it to be included in block
    rpc BroadcastTxSync (TxEnvelopeParam) returns (exec.TxExecution);
    // Broadcast a transaction to the mempool - if the transaction is not signed signing will be attempted server-side
    rpc BroadcastTxAsync (TxEnvelopeParam) returns (txs.Receipt);

    // Sign transaction server-side
    rpc SignTx (TxEnvelopeParam) returns (TxEnvelope);
    // Formulate a transaction from a Payload and retrun the envelop with the Tx bytes ready to sign
    rpc FormulateTx (payload.Any) returns (TxEnvelope);

    // Formulate and sign a CallTx transaction signed server-side and wait for it to be included in a block, retrieving response
    rpc CallTxSync (payload.CallTx) returns (exec.TxExecution);
    // Formulate and sign a CallTx transaction signed server-side
    rpc CallTxAsync (payload.CallTx) returns (txs.Receipt);
    // Perform a 'simulated' call of a contract against the current committed EVM state without any changes been saved
    // and wait for the transaction to be included in a block
    rpc CallTxSim (payload.CallTx) returns (exec.TxExecution);
    // Perform a 'simulated' execution of provided code against the current committed EVM state without any changes been saved
    rpc CallCodeSim (CallCodeParam) returns (exec.TxExecution);

    // Formulate a SendTx transaction signed server-side and wait for it to be included in a block, retrieving response
    rpc SendTxSync (payload.SendTx) returns (exec.TxExecution);
    // Formulate and  SendTx transaction signed server-side
    rpc SendTxAsync (payload.SendTx) returns (txs.Receipt);

    // Formulate a NameTx signed server-side and wait for it to be included in a block returning the registered name
    rpc NameTxSync (payload.NameTx) returns (exec.TxExecution);
    // Formulate a NameTx signed server-side
    rpc NameTxAsync (payload.NameTx) returns (txs.Receipt);
}

On server side, TxEnvelopeParam will be converted to txs.Envelope, then passed for further handling.

func (ts *transactServer) BroadcastTxSync(ctx context.Context, param *TxEnvelopeParam) (*exec.TxExecution, error) {
	const errHeader = "BroadcastTxSync():"
	if param.Timeout == 0 {
		param.Timeout = maxBroadcastSyncTimeout
	}
	ctx, cancel := context.WithTimeout(ctx, param.Timeout)
	defer cancel()
	txEnv := param.GetEnvelope(ts.transactor.BlockchainInfo.ChainID())
	if txEnv == nil {
		return nil, fmt.Errorf("%s no transaction envelope or payload provided", errHeader)
	}
	return ts.transactor.BroadcastTxSync(ctx, txEnv)
}

txs.Envelope

Signatories stands for the signatures.

type Envelope struct {
	Signatories []Signatory `protobuf:"bytes,1,rep,name=Signatories,proto3" json:"Signatories"`
	// Canonical bytes of the Tx ready to be signed
	Tx                   *Tx               `protobuf:"bytes,2,opt,name=Tx,proto3,customtype=Tx" json:"Tx,omitempty"`
	Enc                  Envelope_Encoding `protobuf:"varint,3,opt,name=Enc,proto3,enum=txs.Envelope_Encoding" json:"Enc,omitempty"`
	XXX_NoUnkeyedLiteral struct{}          `json:"-"`
	XXX_unrecognized     []byte            `json:"-"`
	XXX_sizecache        int32             `json:"-"`
}

BroadcastTxSync

txs.Envelope will be checked by MaybeSignTxMempool, which will try to sign the tx if Signatories do not exist. Actually it will use local account to sign the inputs one by one accordingly.


func (trans *Transactor) BroadcastTxSync(ctx context.Context, txEnv *txs.Envelope) (*exec.TxExecution, error) {
	// Sign unless already signed - note we must attempt signing before subscribing so we get accurate final TxHash
	unlock, err := trans.MaybeSignTxMempool(txEnv)
	if err != nil {
		return nil, err
	}
	// We will try and call this before the function exits unless we error but it is idempotent
	defer unlock()
	// Subscribe before submitting to mempool
	txHash := txEnv.Tx.Hash()
	subID := event.GenSubID()
	out, err := trans.Emitter.Subscribe(ctx, subID, exec.QueryForTxExecution(txHash), SubscribeBufferSize)
	if err != nil {
		// We do not want to hold the lock with a defer so we must
		return nil, err
	}
	defer trans.Emitter.UnsubscribeAll(context.Background(), subID)
	// Push Tx to mempool
	checkTxReceipt, err := trans.CheckTxSync(ctx, txEnv)
	unlock()
	if err != nil {
		return nil, err
	}
	// Get all the execution events for this Tx
	select {
	case <-ctx.Done():
		syncInfo := bcm.GetSyncInfo(trans.BlockchainInfo)
		bs, err := json.Marshal(syncInfo)
		syncInfoString := string(bs)
		if err != nil {
			syncInfoString = fmt.Sprintf("{error could not marshal SyncInfo: %v}", err)
		}
		return nil, fmt.Errorf("waiting for tx %v, SyncInfo: %s", checkTxReceipt.TxHash, syncInfoString)
	case msg := <-out:
		txe := msg.(*exec.TxExecution)
		callError := txe.CallError()
		if callError != nil && callError.ErrorCode() != errors.Codes.ExecutionReverted {
			return nil, errors.Wrap(callError, "exception during transaction execution")
		}
		return txe, nil
	}
}

func (trans *Transactor) MaybeSignTxMempool(txEnv *txs.Envelope) (UnlockFunc, error) {
	// Sign unless already signed
	if len(txEnv.Signatories) == 0 {
		var err error
		var unlock UnlockFunc
		// We are writing signatures back to txEnv so don't shadow txEnv here
		txEnv, unlock, err = trans.SignTxMempool(txEnv)
		if err != nil {
			return nil, fmt.Errorf("error signing transaction: %v", err)
		}
		// Hash will have change since we signed
		txEnv.Tx.Rehash()
		// Make this idempotent for defer
		var once sync.Once
		return func() { once.Do(unlock) }, nil
	}
	return func() {}, nil
}

Sign by a Key server or local key store

It is also possible to sign by a local store or a key server.

KeyClient interface

type KeyClient interface {
	// Sign returns the signature bytes for given message signed with the key associated with signAddress
	Sign(signAddress crypto.Address, message []byte) (*crypto.Signature, error)

	// PublicKey returns the public key associated with a given address
	PublicKey(address crypto.Address) (publicKey crypto.PublicKey, err error)

	// Generate requests that a key be generate within the keys instance and returns the address
	Generate(keyName string, keyType crypto.CurveType) (keyAddress crypto.Address, err error)

	// Get the address for a keyname or the adress itself
	GetAddressForKeyName(keyName string) (keyAddress crypto.Address, err error)

	// Returns nil if the keys instance is healthy, error otherwise
	HealthCheck() error
}

Singer

A signer can be create from the KeyClient, and the used to sign the TXs.

	signer, err := keys.AddressableSigner(srv.keyClient, to)
	sig, err := signer.Sign(crypto.Keccak256(msg))

local key client

It can be created from local key store:

	keyStore := keys.NewKeyStore(dir, conf.Keys.AllowBadFilePermissions)
	keyClient := keys.NewLocalKeyClient(keyStore, logging.NewNoopLogger())
	...
	
// NewLocalKeyClient returns a new keys client, backed by the local filesystem
func NewLocalKeyClient(ks *KeyStore, logger *logging.Logger) KeyClient {
	logger = logger.WithScope("LocalKeyClient")
	return &localKeyClient{ks: ks, logger: logger}
}

remote key client

The remote key client means connecting to a GRPC service/server:

// NewRemoteKeyClient returns a new keys client for provided rpc location
func NewRemoteKeyClient(rpcAddress string, logger *logging.Logger) (KeyClient, error) {
	logger = logger.WithScope("RemoteKeyClient")
	var opts []grpc.DialOption
	opts = append(opts, grpc.WithInsecure())
	conn, err := grpc.Dial(rpcAddress, opts...)
	if err != nil {
		return nil, err
	}
	kc := NewKeysClient(conn)

	return &remoteKeyClient{kc: kc, rpcAddress: rpcAddress, logger: logger}, nil
}

built-in key client

Burrow daemon can start a key service on its gRPC server, along with the other 4 services. In this case, any Tx can use this built-in key service to sign the Txs.

// NewRemoteKeyClient returns a new keys client for provided rpc location
func NewBuiltinKeyClient(cc *grpc.ClientConn, logger *logging.Logger) (KeyClient, error) {
	kc := NewKeysClient(cc)
	return &remoteKeyClient{kc: kc, rpcAddress: "", logger: logger}, nil
}

// sample code to create a built-in key client from gRPC
func (c *Client) dial(logger *logging.Logger) error {
	if c.transactClient == nil {
		conn, err := grpc.Dial(c.ChainAddress, grpc.WithInsecure())
		if err != nil {
			return err
		}
		c.transactClient = rpctransact.NewTransactClient(conn)
		c.queryClient = rpcquery.NewQueryClient(conn)
		c.executionEventsClient = rpcevents.NewExecutionEventsClient(conn)
		if c.KeysClientAddress == "" {
			logger.InfoMsg("Using builtin keys server")
			c.keyClient, err = keys.NewBuiltinKeyClient(conn, logger)
		} else {
			logger.InfoMsg("Using keys server", "server", c.KeysClientAddress)
			c.keyClient, err = keys.NewRemoteKeyClient(c.KeysClientAddress, logger)
		}

		if err != nil {
			return err
		}
		ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
		defer cancel()

		stat, err := c.queryClient.Status(ctx, &rpcquery.StatusParam{})
		if err != nil {
			return err
		}
		c.chainID = stat.ChainID
	}
	return nil
}

How Burrow decide to create key client

It depends on the configuration.

// LoadKeysFromConfig sets the keyClient & keyStore based on the given config
func (kern *Kernel) LoadKeysFromConfig(conf *keys.KeysConfig) (err error) {
	kern.keyStore = keys.NewKeyStore(conf.KeysDirectory, conf.AllowBadFilePermissions)
	if conf.RemoteAddress != "" {
		kern.keyClient, err = keys.NewRemoteKeyClient(conf.RemoteAddress, kern.Logger)
		if err != nil {
			return err
		}
	} else {
		kern.keyClient = keys.NewLocalKeyClient(kern.keyStore, kern.Logger)
	}
	return nil
}

Key Server

Key Server can also be run as a standalone server, which is actually a GRPC service:

				server := keys.StandAloneServer(conf.Keys.KeysDirectory, conf.Keys.AllowBadFilePermissions)
				address := fmt.Sprintf("%s:%s", *keysHost, *keysPort)
				listener, err := net.Listen("tcp", address)
				if err != nil {
					output.Fatalf("Could not listen on %s: %v", address, err)
				}
				err = server.Serve(listener)
				if err != nil {
					output.Fatalf("Keys server terminated with error: %v", err)
				}
				...

func StandAloneServer(keysDir string, AllowBadFilePermissions bool) *grpc.Server {
	grpcServer := grpc.NewServer()
	RegisterKeysServer(grpcServer, NewKeyStore(keysDir, AllowBadFilePermissions))
	return grpcServer
}

Sign locally

Simply encode the Tx into txs.Envelope, then make signer according to the Tx.Inputs, and sign them in the end.

Sample code:

func (c *Client) SignTx(tx payload.Payload) (*txs.Envelope, error) {

	txEnv := txs.Enclose(c.chainID, tx)
	/*	if c.MempoolSigning {
			logger.InfoMsg("Using mempool signing")
			return txEnv, nil
		}
	*/
	inputs := tx.GetInputs()
	signers := make([]acm.AddressableSigner, len(inputs))
	var err error
	for i, input := range inputs {
		signers[i], err = keys.AddressableSigner(c.keyClient, input.Address)
		if err != nil {
			return nil, err
		}
	}
	err = txEnv.Sign(signers...)
	if err != nil {
		return nil, err
	}
	return txEnv, nil
}

Verify Signature in Transactor server

  • Sanity check for signatoties
  • Sanity check for chain ID
  • Number of signatoties must match with inputs
  • Verify sinagure with Public key

Call stack

github.com/hyperledger/burrow/txs.(*Envelope).Verify at envelope.go:112
github.com/hyperledger/burrow/execution.(*executor).Execute at execution.go:231
github.com/hyperledger/burrow/consensus/abci.ExecuteTx at execute_tx.go:30
github.com/hyperledger/burrow/consensus/abci.(*App).CheckTx at app.go:189
github.com/tendermint/tendermint/abci/client.(*localClient).CheckTxAsync at local_client.go:99
github.com/tendermint/tendermint/proxy.(*appConnMempool).CheckTxAsync at app_conn.go:114
github.com/tendermint/tendermint/mempool.(*CListMempool).CheckTx at clist_mempool.go:281
github.com/tendermint/tendermint/mempool.Mempool.CheckTx-fm at mempool.go:18
github.com/hyperledger/burrow/execution.(*Transactor).CheckTxAsyncRaw at transactor.go:237
github.com/hyperledger/burrow/execution.(*Transactor).CheckTxSyncRaw at transactor.go:206
github.com/hyperledger/burrow/execution.(*Transactor).CheckTxSync at transactor.go:134
github.com/hyperledger/burrow/execution.(*Transactor).BroadcastTxAsync at transactor.go:111
github.com/hyperledger/burrow/rpc/rpctransact.(*transactServer).BroadcastTxAsync at transact_server.go:67
github.com/hyperledger/burrow/rpc/rpctransact._Transact_BroadcastTxAsync_Handler.func1 at rpctransact.pb.go:523
github.com/hyperledger/burrow/rpc.unaryInterceptor.func1 at grpc.go:29
github.com/hyperledger/burrow/rpc/rpctransact._Transact_BroadcastTxAsync_Handler at rpctransact.pb.go:525
google.golang.org/grpc.(*Server).processUnaryRPC at server.go:1024
google.golang.org/grpc.(*Server).handleStream at server.go:1313
google.golang.org/grpc.(*Server).serveStreams.func1.1 at server.go:722
runtime.goexit at asm_amd64.s:1357
 - Async stack trace
google.golang.org/grpc.(*Server).serveStreams.func1 at server.go:720

Code:

// Verifies the validity of the Signatories' Signatures in the Envelope. The Signatories must
// appear in the same order as the inputs as returned by Tx.GetInputs().
func (txEnv *Envelope) Verify(chainID string) error {
	err := txEnv.Validate()
	if err != nil {
		return err
	}
	errPrefix := fmt.Sprintf("could not verify transaction %X", txEnv.Tx.Hash())
	if txEnv.Tx.ChainID != chainID {
		return fmt.Errorf("%s: ChainID in envelope is %s but receiving chain has ID %s",
			errPrefix, txEnv.Tx.ChainID, chainID)
	}
	inputs := txEnv.Tx.GetInputs()
	if len(inputs) != len(txEnv.Signatories) {
		return fmt.Errorf("%s: number of inputs (= %v) should equal number of signatories (= %v)",
			errPrefix, len(inputs), len(txEnv.Signatories))
	}
	signBytes, err := txEnv.Tx.SignBytes(txEnv.GetEnc())
	if err != nil {
		return fmt.Errorf("%s: could not generate SignBytes: %v", errPrefix, err)
	}
	// Expect order to match (we could build lookup but we want Verify to be quicker than Sign which does order sigs)
	for i, s := range txEnv.Signatories {
		if inputs[i].Address != *s.Address {
			return fmt.Errorf("signatory %v has address %v but input %v has address %v",
				i, *s.Address, i, inputs[i].Address)
		}
		err = s.PublicKey.Verify(signBytes, s.Signature)
		if err != nil {
			return fmt.Errorf("invalid signature in signatory %v: %v", *s.Address, err)
		}
	}
	return nil
}

 类似资料: