【转】深入理解以太坊交易Hash和交易签名

这篇文字作者从go-ethereum的源码出发,讲解什么是交易Hash,签名Hash,理解的比较深入。以下对代码做了重新排版,便于阅读。感谢原作者powervip!

0.概述

进行一笔以太坊交易时涉及的哈希计算有2个:

a) 用于签名的哈希值的计算:在使用私钥计算一笔交易的签名内容时,先要对交易结构的相关数据以及链id(chainId)进行哈希,然后再使用私钥对哈希结果进行签名得到签名结果(签名结果包含R, S, V 3个字段)。该哈希值不会被写入区块链里。

b) 交易哈希的计算:对整个交易结构的所有数据进行哈希。该哈希值(即交易id)会被写入区块链里。我们平时在区块链浏览器里通过交易哈希查询一笔交易时使用的就是这个哈希值。

1.用于签名的哈希值的计算

交易发起者使用私钥对交易进行签名,并不是对整个交易直接签名。而是先对交易结构的数据以及链id(chainId)进行哈希计算,然后再使用私钥对哈希结果(也就是一串32字节的字符串,转化为16进制的字符串就是64位的16进制数字符串)进行ECDSA算法签名得到签名结果(R, S, V)。

以太坊上定义了Signer这个签名接口,用于对交易进行签名。Signer这个签名接口的具体实现是3种签名器:

  • FrontierSigner
  • HomesteadSigner
  • EIP155Signer

3种签名器中,FrontierSigner是签名器最早实现的版本,HomesteadSigner是后来实现的版本,EIP155Signer则是最新的版本,并在签名的哈希值计算中引入了chainId来防止跨链实施攻击。

在EIP155Signer版本签名器中,节点需要对客户端发送过来的签名结果中的V值进行校验,校验V值中包含的chainId是否和本节点的chainId相比较,如果不一致,则认为该笔交易不是本条链上的交易,从而判断该交易非法。具体可以参考core/types/transaction_signing.go里如下函数实现:

func (s EIP155Signer) Sender(tx *Transaction) (common.Address, error)

对于使用EIP155Signer版本签名器,签名结果中的V值的计算方法是:V = chainId * 2 + 35

具体可以参考下面这个函数的实现:

func (s EIP155Signer) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error)

对一笔以太坊交易进行签名的过程如下:

core/types/transaction_signing.go

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

s.Hash(tx)计算出用于签名的哈希值h,然后再使用私钥prv对哈希值h进行签名。s的具体实例是FrontierSigner,HomesteadSigner,EIP155Signer这3种签名器中的一个。

需要注意的是,即使是对于同一个哈希值h,每次使用同一个私钥prv对哈希值h进行签名得到的R, S这2个字段不一定会相同。

下面以使用EIP155Signer版本签名器为例,看看用于签名的哈希值的计算过程:

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),
    })
}

这里可以看到,用于签名的哈希值的计算,只是把交易的基础输入数据加上chainId进行RLP编码再获取哈希值。基础输入数据包括:AccountNonce(发起者的Nonce),Price(gasPrice),GasLimit,Recipient(接收者),Amount(转账金额),Payload(智能合约相关的内容,如果是普通转账交易,该字段为空)

哈希函数rlpHash()的具体定义见core/types/block.go

func rlpHash(x interface{}) (h common.Hash) {
    hw := sha3.NewLegacyKeccak256()
    rlp.Encode(hw, x)
    hw.Sum(h[:0])
    return h
}

该函数先对接口数据进行RLP编码,再运用Keccak256哈希算法获得交易哈希。

RLP编码的规则网上有不少的文章都有介绍,这里就不详细介绍了。RLP编码的重点是给原始数据前面添加若干字节的前缀,而且这个前缀是和数据的长度相关的。RLP编码后得到的是一个字节数组。

2.交易哈希的计算

对于交易哈希的计算,它是在core/types/transaction.go里的Hash()函数:

func (tx *Transaction) Hash() common.Hash {
    if hash := tx.hash.Load(); hash != nil {
        return hash.(common.Hash)
    }
    v := rlpHash(tx)
    tx.hash.Store(v)
    return v
}

我们再看一下Transaction这个结构体的定义:

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 *[http://big.Int](https://link.zhihu.com/?target=http%3A//big.Int) `json:"gasPrice" gencodec:"required"`

    GasLimit uint64 `json:"gas" gencodec:"required"`

    Recipient *common.Address `json:"to" rlp:"nil"` // nil means contract creation

    Amount *[http://big.Int](https://link.zhihu.com/?target=http%3A//big.Int) `json:"value" gencodec:"required"`

    Payload []byte `json:"input" gencodec:"required"`



    // Signature values
    V *[http://big.Int](https://link.zhihu.com/?target=http%3A//big.Int) `json:"v" gencodec:"required"`
        
    R *[http://big.Int](https://link.zhihu.com/?target=http%3A//big.Int) `json:"r" gencodec:"required"`

    S *[http://big.Int](https://link.zhihu.com/?target=http%3A//big.Int) `json:"s" gencodec:"required"`

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

}

可以看出,Hash()函数是对整个交易数据(包括签名后的签名结果,但不包括hash字段。因为hash字段使用了rlp:"-",表示rlp编码解码时忽略该字段。hash,size 和from这3个atomic.Value类型的字段也是不包括的 )进行处理,处理过程是先对整个交易数据进行RLP编码,再运用Keccak256哈希算法获得交易哈希。对于每一笔交易,其交易哈希都是确定(不需要等待交易打包进区块链,在钱包端签名交易后交易哈希就已经是可以确定的)和唯一的(不同的2笔交易计算出来的交易哈希相同的可能性基本为0)

在控制台中,我们通过调用eth.signTransaction()这个接口就能获取一笔交易的交易哈希,下面输出结果中红色的部分就是交易哈希:

\> eth.signTransaction({
...... nonce: 4,
...... gasPrice: '0x09184e72a000',
...... gas: '1000000',
...... from:"0x06fe0338213124b2d8f198eafb1914956824e218",
...... to:"0x815261DC4186502eC0D8CCFEf163785e1617b1A8",
...... value: '0x1'
...... })

{

raw: "0xf8a8048609184e72a000830f42409406fe0338213124b2d8f198eafb1914956824ef1801801ba093e4f549a65405ea931e525a7880ef0e802cd7fa275523d31faf1dae49f54268a0b20f0b87d34e866582b7cfb687a78df7ed88cf404f9947647b7a07670b62897fa0a45a909bbbfe4ed27ec9ad98c41bac220e8cd6aac7e860ed23e2185128d6cf13a088642f0cc9f37afd8d0923871773f52ae314bdd060be7625a9c3f4793aac3557",

tx: {
    gas: "0xf4240",
    gasPrice: "0x9184e72a000",
    hash: "0x6387fd1ad72e604151b2c5900ffb23069c3cdeebf2768d1e8277d43b721448fa",
    input: "0x",
    nonce: "0x4",
    r: "0x93e4f549a65405ea931e525a7880ef0e802cd7fa275523d31faf1dae49f54268",
    s: "0xb20f0b87d34e866582b7cfb687a78df7ed88cf404f9947647b7a07670b62897f",
    to: "0x815261DC4186502eC0D8CCFEf163785e1617b1A8",
    v: "0x1b",
    value: "0x1"
  }

}

eth.SendTransaction()这个接口的返回结果也是该笔交易的交易哈希:

\>eth.sendTransaction({from:"0x06FE0338213124B2D8f198eafB1914956824e218",to:"0x815261DC4186502eC0D8CCFEf163785e1617b1A8",value:web3.toWei(20,"ether")})

"0x7cf3167b50e10428d368197e0e00b8631076a930d9f7f58500ecafd6bce16a4b"

我们也可以通过交易哈希来查询一笔交易:

\>eth.getTransaction("0x22a1264249302682475703933c819e32b4c1524938f94ff35b87c19f4e8beeb2")

{
    blockHash: "0x3be26d1481eeb783742f2792d0c6fa1c6398ce15bf1ec3c1c5a863fd46014035",
    blockNumber: 22027,
    from: "0x06fe0338213124b2d8f198eafb1914956824e218",
    gas: 21000,
    gasPrice: 1000000000,
    hash: "0x22a1264249302682475703933c819e32b4c1524938f94ff35b87c19f4e8beeb2",
    input: "0x",
    nonce: 3,
    r: "0x325ead2013e351133a98876e83e2cb39954c3f370bdba397b091a06c3026f30d",
    s: "0x2ba924507b42faa0f709fd7c7a5b785d5f26eff391c804598920cfaf850f8239",
    to: "0x815261DC4186502eC0D8CCFEf163785e1617b1A8",
    transactionIndex: 0,
    v: "0x1df",
    value: 20000000000000000000
}

也可以通过交易哈希来查询一笔交易的收据:

\>eth.getTransactionReceipt("0x22a1264249302682475703933c819e32b4c1524938f94ff35b87c19f4e8beeb2")

{
    blockHash: "0x3be26d1481eeb783742f2792d0c6fa1c6398ce15bf1ec3c1c5a863fd46014035",
    blockNumber: 22027,
    contractAddress: null,
    cumulativeGasUsed: 21000,
    from: "0x06fe0338213124b2d8f198eafb1914956824e218",
    gasUsed: 21000,
    logs: [],
    logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    status: "0x1",
    to: "0x815261DC4186502eC0D8CCFEf163785e1617b1A8",
    transactionHash: "0x22a1264249302682475703933c819e32b4c1524938f94ff35b87c19f4e8beeb2",
    transactionIndex: 0
}

原文链接:进行一笔以太坊交易时涉及的哈希计算 - 知乎 (zhihu.com)

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

推荐阅读更多精彩内容