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

向以太坊网络发起一笔交易时,需要使用私钥对交易进行签名,那么从原始的请求数据到最终的签名后的数据,这中间的数据流转是怎样的,经过了什么过程,今天从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签名方式,包括EIP155SignerHomesteadSignerFrontierSigner,其中HomesteadSigner继承FrontierSigner。之所以需要该字段,是因为在EIP155中修复了简单重复攻击漏洞后,需要保持旧区块链的签名方式不变,但又需要提供新版本的签名方式。因此根据区块高度创建不同的签名器。
  • prv *ecdsa.PrivateKey,secp256k1标准的私钥

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

  1. 对交易信息计算rlpHash
  2. 对rlpHash使用私钥进行签名
  3. 填充交易对象中的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方法。
EIP155SignerSignatureValues方法相比FrontierSigner的方法,区别是在计算V值上。

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

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

EIP155SignerSignatureValues方法中,根据链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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342