首页 > 世链号 > 以太坊交易签名过程源码解析
币小葱  

以太坊交易签名过程源码解析

摘要:向以太坊网络发起一笔交易时,需要使用私钥对交易进行签名。那么从原始的请求数据到最终的签名后的数据,这中间的数据流转是怎样的,经过了什么过程

以太坊网络发起一笔交易时,需要使用私钥对交易进行签名。那么从原始的请求数据到最终的签名后的数据,这中间的数据流转是怎样的,经过了什么过程,今天从 go-ethereum 源码入手,解析下数据的转换。

一、准备工作

我以一个简单合约为例,调用合约的 setA 方法,参数为 123。合约代码如下。

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

调用代码如下所示。

 package mainimport ( "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() { // 一、ABI 编码请求参数 methodId := crypto.Keccak256([]byte("setA(uint256)"))[:4] fmt.Println("methodId: ", common.Bytes2Hex(methodId)) paramValue := math.U256 Bytes(new(big.Int).Set(big.NewInt(123))) fmt.Println("paramValue: ", common.Bytes2Hex(paramValue)) input := append(methodId, paramValue...) fmt.Println("input: ", common.Bytes2Hex(input)) // 二、构造交易对象 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)) // 三、交易签名 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)) // 四、发送交易 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())} 

从请求代码中也可以看出,数据流转的过程包括:

•合约方法及参数进行 ABI 编码•构造 Transaction 交易对象•交易对象 RLP 编码•对编码后交易数据使用私钥进行椭圆曲线签名得到签名串•根据签名串生成签名后交易对象•对签名后的交易对象进行 RLP 编码得到签名后的交易数据

二、ABI 编码请求参数

setA(123) 经过 ABI 编码后得到的数据是: 0xee919d50000000000000000000000000000000000000000000000000000000000000007b

这个数据包含两部分:

methodId,函数标识码(4 个字节),对 setA(uint256) 求 Keccak256,然后取前 4 位,值为:ee919d50。•paramValue,函数参数(32 字节),对值为 123 的 BigInt 类型转 byte,值为:,000000000000000000000000000000000000000000000000000000000000007b

三、构造 Transaction 对象

构造交易对象需要的参数包括:

nonce,请求账号 nonce 值•address,合约地址•value,转账的以太币个数,单位 wei•gasLimit,最大消耗 gas•gasPrice,gas 价格•input,请求的合约输入参数

如果是部署合约时,address 为空。如果是以太币转账交易, input 为空,address 为接收者地址。

交易的核心数据结构是 txdata

 // go-ethereum/core/types/transaction.gotype 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}} 

在 txdata 中的 V,R,S 三个字段是与签名相关。构造后的交易对象输出结果为(此时 v、r、s 为默认空值):

*

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

四、交易签名

交易签名核心调用 types.SignTx 方法,源码如下所示。

 // go-ethereum/core/types/transaction_signing.go// SignTx signs the transaction using the given signer and private keyfunc 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)} 

SignTx 方法有三个参数:

tx *Transaction,构造 Transaction 对象•s Signer,signer 签名方式,包括 EIP155SignerHomesteadSigner 和 FrontierSigner,其中 HomesteadSigner 继承 FrontierSigner。之所以需要该字段,是因为在 EIP155 中修复了简单重复攻击漏洞后,需要保持旧区块链的签名方式不变,但又需要提供新版本的签名方式。因此根据区块高度创建不同的签名器。•prv *ecdsa.PrivateKey,secp256k1 标准的私钥

SignTx 方法的签名过程分为三步:

  1. 对交易信息计算 rlpHash2. 对 rlpHash 使用私钥进行签名 3. 填充交易对象中的 V,R,S 字段

4.1 计算 rlpHash

EIP155Signer 实现的 hash 算法相比 FrontierSigner 多了一个链 ID 和两个 uint 空值,这样的话,一笔已签名的交易只可能属于一条链。

Hash 计算代码如下所示。

 // go-ethereum/core/types/transaction_signing.gofunc (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), })} 

rlpHash 的计算结果为: 0x9ef7f101dae55081553998d52d0ce57c4cf37271f800b70c0863c4a749977ef1

4.2 私钥签名

crypto.Sign(h[:], prv) 源代码如下所示。

 * 
 // go-ethereum/crypto/signature_cgo.gofunc 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)} 

Sign 方法调用 secp256k1 的椭圆曲线算法进行签名,签名后返回结果为: 41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d00

4.3 填充交易对象中的 V,R,S 字段

tx.WithSignature(s, sig) 源代码如下所示。

 // go-ethereum/core/types/transaction_signing.gofunc (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} 

在 WithSignature 方法中,核心调用了 SignatureValues 方法。 EIP155Signer 的 SignatureValues 方法相比 FrontierSigner 的方法,区别是在计算 V 值上。

FrontierSigner 的 SignatureValues 方法中,将签名结果 41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d00 分为三份,分别是:

•前 32 字节的 R,41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed•中间 32 字节的 S,5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d•最后一个字节 00 加上 27,得到 V,十进制为 27

在 EIP155Signer 的 SignatureValues 方法中,根据链 ID 重新计算 V 值,我这里的链 ID 是 1,重新计算得到的 V 值十进制结果是 37。

签名后的交易对象结果为: {"nonce":"0x18","gasPrice":"0x4a817c800","gas":"0x2dc6c0","to":"0x05e56888360ae54acf2a389bab39bd41e3934d2b","value":"0x0","input":"0xee919d50000000000000000000000000000000000000000000000000000000000000007b","v":"0x25","r":"0x41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed","s":"0x5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d","hash":"0xf8a3bf13828d50b107da40188c8e772b83a613f0044593a4e49438a214a79c83"}

五、发送交易

发送交易 SendTransaction 方法首先会对具有签名信息的交易对象进行 rlp 编码,编码后调用的 jsonrpc 的 eth_sendRawTransaction 方法发送交易。源代码如下所示:

 // go-ethereum/ethclient/ethclient.gofunc (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))} 

最终计算得到的签名后的交易数据为: 0xf889188504a817c800832dc6c09405e56888360ae54acf2a389bab39bd41e3934d2b80a4ee919d50000000000000000000000000000000000000000000000000000000000000007b25a041c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8eda05f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d

六、总结

至此,交易的签名已完成,得到了签名数据。从原始数据到签名数据,核心的技术点包括:

•ABI 编码•交易信息 rpl 编码•椭圆曲线 secp256k1 签名•根据签名结果计算 V,R,S

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


本文作者:六天

来源链接:mp.weixin.qq.com

免责声明
世链财经作为开放的信息发布平台,所有资讯仅代表作者个人观点,与世链财经无关。如文章、图片、音频或视频出现侵权、违规及其他不当言论,请提供相关材料,发送到:2785592653@qq.com。
风险提示:本站所提供的资讯不代表任何投资暗示。投资有风险,入市须谨慎。
世链粉丝群:提供最新热点新闻,空投糖果、红包等福利,微信:juu3644。