Source code analysis of Ethereum transaction signature process

When initiating a transaction to the Ethereum network, you need to use the private key to sign the transaction. What is the data flow from the original request data to the final signed data? What is the process? Today, start with the go Ethereum source code to analyze the data conversion.

1, Preparation

I take a simple contract as an example, and call the setA method of the contract. The parameter is 123. The contract code is as follows.

pragma solidity >=0.4.22 <0.6.0;
contract Test {
    uint256 internal a;
    event SetA(address indexed _from, uint256 _value);
    
    function setA(uint256 _a) public {
        a = _a;
        emit SetA(msg.sender, _a);
    }
    
    function getA() public view returns (uint256) {
        return a;
    }
}

The calling code is as follows.

package main
import (
	"context"
	"fmt"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/common/math"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
	"math/big"
)

func main() {
	// 1, ABI encoding request parameters
	methodId := crypto.Keccak256([]byte("setA(uint256)"))[:4]
	fmt.Println("methodId: ", common.Bytes2Hex(methodId))
	paramValue := math.U256Bytes(new(big.Int).Set(big.NewInt(123)))
	fmt.Println("paramValue: ", common.Bytes2Hex(paramValue))
	input := append(methodId, paramValue...)
	fmt.Println("input: ", common.Bytes2Hex(input))

	// 2, Construct trading partner
	nonce := uint64(24)
	value := big.NewInt(0)
	gasLimit := uint64(3000000)
	gasPrice := big.NewInt(20000000000)
	rawTx := types.NewTransaction(nonce, common.HexToAddress("0x05e56888360ae54acf2a389bab39bd41e3934d2b"), value, gasLimit, gasPrice, input)
	jsonRawTx, _ := rawTx.MarshalJSON()
	fmt.Println("rawTx: ", string(jsonRawTx))

	// 3, Transaction signature
	signer := types.NewEIP155Signer(big.NewInt(1))
	key, err := crypto.HexToECDSA("e8e14120bb5c085622253540e886527d24746cd42d764a5974be47090d3cbc42")
	if err != nil {
		fmt.Println("crypto.HexToECDSA failed: ", err.Error())
		return
	}
	sigTransaction, err := types.SignTx(rawTx, signer, key)
	if err != nil {
		fmt.Println("types.SignTx failed: ", err.Error())
		return
	}
	jsonSigTx, _ := sigTransaction.MarshalJSON()
	fmt.Println("sigTransaction: ", string(jsonSigTx))

	// 4, Send transaction
	ethClient, err := ethclient.Dial("http://127.0.0.1:7545")
	if err != nil {
		fmt.Println("ethclient.Dial failed: ", err.Error())
		return
	}
	err = ethClient.SendTransaction(context.Background(), sigTransaction)
	if err != nil {
		fmt.Println("ethClient.SendTransaction failed: ", err.Error())
		return
	}
	fmt.Println("send transaction success,tx: ", sigTransaction.Hash().Hex())
}

2, ABI encoding request parameters

The data of setA(123) after ABI coding is:
0xee919d50000000000000000000000000000000000000000000000000000000000000007b

This data consists of two parts:

  • methodId, function identification code (4 bytes), find Keccak256 for setA(uint256), and then take the first 4 bits. The value is: ee919d50.
  • paramValue, a function parameter (32 bytes), converts the BigInt type of 123 to byte, and the value is: 000000000000000000000000000000000000000000000000000000000000007b

3, Construct Transaction object

The parameters required to construct a trading partner include:

  • Nonce, request the nonce value of the account
  • Address, contract address
  • value, the number of etheric coins transferred, unit wei
  • gasLimit, maximum gas consumption
  • gasPrice
  • Input, requested contract input parameter

If it is a deployment contract, the address is empty.
If it is an Ethereum transfer transaction, input is empty and address is the recipient address.

The core data structure of the transaction is txdata.

// go-ethereum/core/types/transaction.go
type Transaction struct {
	data txdata
	// caches
	hash atomic.Value
	size atomic.Value
	from atomic.Value
}

type txdata struct {
	AccountNonce uint64          `json:"nonce"    gencodec:"required"`
	Price        *big.Int        `json:"gasPrice" gencodec:"required"`
	GasLimit     uint64          `json:"gas"      gencodec:"required"`
	Recipient    *common.Address `json:"to"       rlp:"nil"` // nil means contract creation
	Amount       *big.Int        `json:"value"    gencodec:"required"`
	Payload      []byte          `json:"input"    gencodec:"required"`

	// Signature values
	V *big.Int `json:"v" gencodec:"required"`
	R *big.Int `json:"r" gencodec:"required"`
	S *big.Int `json:"s" gencodec:"required"`

	// This is only used when marshaling to JSON.
	Hash *common.Hash `json:"hash" rlp:"-"`
}

func newTransaction(nonce uint64, to *common.Address, amount *big.Int, gasLimit uint64, gasPrice *big.Int, data []byte) *Transaction {
	if len(data) > 0 {
		data = common.CopyBytes(data)
	}
	d := txdata{
		AccountNonce: nonce,
		Recipient:    to,
		Payload:      data,
		Amount:       new(big.Int),
		GasLimit:     gasLimit,
		Price:        new(big.Int),
		V:            new(big.Int),
		R:            new(big.Int),
		S:            new(big.Int),
	}
	if amount != nil {
		d.Amount.Set(amount)
	}
	if gasPrice != nil {
		d.Price.Set(gasPrice)
	}

	return &Transaction{data: d}
}

The V, R and s fields in txdata are related to signatures.
The output result of the constructed trading partner is (at this time, v, r and s are null by default):

rawTx:  {"nonce":"0x18","gasPrice":"0x4a817c800","gas":"0x2dc6c0","to":"0x05e56888360ae54acf2a389bab39bd41e3934d2b","value":"0x0","input":"0xee919d50000000000000000000000000000000000000000000000000000000000000007b","v":"0x0","r":"0x0","s":"0x0","hash":"0x629d42fd16be0b5dc22d53d63dcce8144d5fc843e056465bc2bea25f4ebe8249"}

4, Transaction signature

Transaction signature core calls types The source code of signtx method is as follows.

// go-ethereum/core/types/transaction_signing.go
// SignTx signs the transaction using the given signer and private key
func SignTx(tx *Transaction, s Signer, prv *ecdsa.PrivateKey) (*Transaction, error) {
	h := s.Hash(tx)
	sig, err := crypto.Sign(h[:], prv)
	if err != nil {
		return nil, err
	}
	return tx.WithSignature(s, sig)
}

The SignTx method has three parameters:

  • tx *Transaction, construct Transaction object
  • s Signer, signer signature method, including EIP155Signer, HomesteadSigner and FrontierSigner, wherein HomesteadSigner inherits FrontierSigner. This field is required because after the simple repeated attack vulnerability is fixed in EIP155, the signature method of the old blockchain needs to be kept unchanged, but a new version of the signature method needs to be provided. Therefore, different signers are created according to the block height.
  • PRV *ecdsa Privatekey, secp256k1 standard private key

The signing process of SignTx method is divided into three steps:

  1. Calculate rlpHash for transaction information
  2. Sign rlpHash with private key
  3. Populate the V,R,S fields in the trading partner

4.1 calculate rlpHash

The hash algorithm implemented by EIP155Signer has one more Chain ID and two empty uint values than FrontierSigner. In this case, a signed transaction can only belong to one chain.

The Hash calculation code is shown below.

// go-ethereum/core/types/transaction_signing.go
func (s EIP155Signer) Hash(tx *Transaction) common.Hash {
	return rlpHash([]interface{}{
		tx.data.AccountNonce,
		tx.data.Price,
		tx.data.GasLimit,
		tx.data.Recipient,
		tx.data.Amount,
		tx.data.Payload,
		s.chainId, uint(0), uint(0),
	})
}

The calculation result of rlpHash is:
0x9ef7f101dae55081553998d52d0ce57c4cf37271f800b70c0863c4a749977ef1

4.2 private key signature

Crypto The source code of sign (h[:], PRV) is as follows.

// go-ethereum/crypto/signature_cgo.go
func Sign(hash []byte, prv *ecdsa.PrivateKey) (sig []byte, err error) {
	if len(hash) != 32 {
		return nil, fmt.Errorf("hash is required to be exactly 32 bytes (%d)", len(hash))
	}
	seckey := math.PaddedBigBytes(prv.D, prv.Params().BitSize/8)
	defer zeroBytes(seckey)
	return secp256k1.Sign(hash, seckey)
}

The Sign method calls secp256k1's elliptic curve algorithm to Sign. After signing, the returned result is:
41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d00

4.3 fill in the V,R,S fields in the trading partner

The source code of tx.WithSignature(s, sig) is as follows.

// go-ethereum/core/types/transaction_signing.go
func (tx *Transaction) WithSignature(signer Signer, sig []byte) (*Transaction, error) {
	r, s, v, err := signer.SignatureValues(tx, sig)
	if err != nil {
		return nil, err
	}
	cpy := &Transaction{data: tx.data}
	cpy.data.R, cpy.data.S, cpy.data.V = r, s, v
	return cpy, nil
}

func (s EIP155Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) {
	R, S, V, err = HomesteadSigner{}.SignatureValues(tx, sig)
	if err != nil {
		return nil, nil, nil, err
	}
	if s.chainId.Sign() != 0 {
		V = big.NewInt(int64(sig[64] + 35))
		V.Add(V, s.chainIdMul)
	}
	return R, S, V, nil
}
func (hs HomesteadSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
	return hs.FrontierSigner.SignatureValues(tx, sig)
}
func (fs FrontierSigner) SignatureValues(tx *Transaction, sig []byte) (r, s, v *big.Int, err error) {
	if len(sig) != 65 {
		panic(fmt.Sprintf("wrong size for signature: got %d, want 65", len(sig)))
	}
	r = new(big.Int).SetBytes(sig[:32])
	s = new(big.Int).SetBytes(sig[32:64])
	if tx.IsPrivate() {
		v = new(big.Int).SetBytes([]byte{sig[64] + 37})
	} else {
		v = new(big.Int).SetBytes([]byte{sig[64] + 27})
	}
	return r, s, v, nil
}

In the WithSignature method, the core calls the SignatureValues method.
EIP155Signer's SignatureValues method is different from FrontierSigner's method in calculating V value.

In the SignatureValues method of FrontierSigner, the signature result 41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d00 is divided into three parts, namely:

  • R of the first 32 bytes, 41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed
  • S in the middle 32 bytes, 5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d
  • Add 27 to the last byte 00 to get V, and the decimal system is 27

In the SignatureValues method of EIP155Signer, the V value is recalculated according to the chain ID. here, the chain ID is 1, and the decimal result of the recalculated V value is 37.

The signed trading partner result is:
{"nonce":"0x18","gasPrice":"0x4a817c800","gas":"0x2dc6c0","to":"0x05e56888360ae54acf2a389bab39bd41e3934d2b","value":"0x0","input":"0xee919d50000000000000000000000000000000000000000000000000000000000000007b","v":"0x25","r":"0x41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed","s":"0x5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d","hash":"0xf8a3bf13828d50b107da40188c8e772b83a613f0044593a4e49438a214a79c83"}

5, Send transaction

The SendTransaction method will first rlp code the transaction object with signature information, and then call the ETH of jsonrpc_ The sendrawtransaction method sends a transaction.
The source code is as follows:

// go-ethereum/ethclient/ethclient.go
func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) error {
	data, err := rlp.EncodeToBytes(tx)
	if err != nil {
		return err
	}
	return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data))
}

The final calculated signed transaction data is:
0xf889188504a817c800832dc6c09405e56888360ae54acf2a389bab39bd41e3934d2b80a4ee919d50000000000000000000000000000000000000000000000000000000000000007b25a041c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8eda05f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d

6, Summary

So far, the signature of the transaction has been completed and the signature data has been obtained. From original data to signature data, the core technical points include:

  • ABI code
  • rpl code of transaction information
  • Elliptic curve secp256k1 signature
  • Calculate V,R,S based on signature results

Reference:
https://learnblockchain.cn/books/geth/part3/sign-and-valid.html

Tags: Blockchain Ethereum

Posted by jasons61 on Wed, 01 Jun 2022 07:51:20 +0530