Sign
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
}