这篇文字作者从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
}