以太坊交易签名过程源码解析
向以太坊网络发起一笔交易时,需要使用私钥对交易进行签名,那么从原始的请求数据到最终的签名后的数据,这中间的数据流转是怎样的,经过了什么过程,今天从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 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() {
// 一、ABI编码请求参数
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))
// 二、构造交易对象
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.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}
}
在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 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)
}
SignTx
方法有三个参数:
-
tx *Transaction
,构造Transaction
对象 -
s Signer
,signer签名方式,包括EIP155Signer
,HomesteadSigner
和FrontierSigner
,其中HomesteadSigner
继承FrontierSigner
。之所以需要该字段,是因为在EIP155中修复了简单重复攻击漏洞后,需要保持旧区块链的签名方式不变,但又需要提供新版本的签名方式。因此根据区块高度创建不同的签名器。 -
prv *ecdsa.PrivateKey
,secp256k1标准的私钥
SignTx
方法的签名过程分为三步:
- 对交易信息计算rlpHash
- 对rlpHash使用私钥进行签名
- 填充交易对象中的
V
,R
,S
字段
4.1 计算rlpHash
EIP155Signer
实现的hash算法相比FrontierSigner
多了一个链ID和两个uint空值,这样的话,一笔已签名的交易只可能属于一条链。
Hash计算代码如下所示。
// 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),
})
}
rlpHash
的计算结果为:
0x9ef7f101dae55081553998d52d0ce57c4cf37271f800b70c0863c4a749977ef1
4.2 私钥签名
crypto.Sign(h[:], prv)
源代码如下所示。
// 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)
}
Sign
方法调用secp256k1
的椭圆曲线算法进行签名,签名后返回结果为:
41c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8ed5f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d00
4.3 填充交易对象中的V
,R
,S
字段
tx.WithSignature(s, sig)
源代码如下所示。
// 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
}
在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.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))
}
最终计算得到的签名后的交易数据为:
0xf889188504a817c800832dc6c09405e56888360ae54acf2a389bab39bd41e3934d2b80a4ee919d50000000000000000000000000000000000000000000000000000000000000007b25a041c4a2eb073e6df89c3f467b3516e9c313590d8d57f7c217fe7e72a7b4a6b8eda05f20a758396a5e681ce1ab4cec749f8560e28c9eb91072ec7a8acc002a11bb1d
六、总结
至此,交易的签名已完成,得到了签名数据。从原始数据到签名数据,核心的技术点包括:
- ABI编码
- 交易信息rpl编码
- 椭圆曲线
secp256k1
签名 - 根据签名结果计算
V
,R
,S
参考:
https://learnblockchain.cn/books/geth/part3/sign-and-valid.html