基础概念

在计算机里,一个字节(Byte)是8个比特(Bit),即8位二进制数,即1Byte=8bit。8位二进制数也就是2位十六进制数。
举个例子:
全是0的1个字节就是 00000000,转换为十六进制字符串就是"0x00"
全是1的1个字节就是 11111111,转换为十六进制字符串就是"0xFF"
最低位是1的1个字节就是 00000001,转换为十六进制字符串就是"0x01"
前缀0x表示十六进制。
我们每进行一笔以太坊的交易,就会得到该笔交易的交易id,它是一个64位的十六进制字符串,例如:
"0x22a1264249302682475703933c819e32b4c1524938f93ff35b87c19f4e8beeb2"
在使用计算机编程语言go语言实现的以太坊里,它实际上是用32个字节的字节数组来表示的。所以这32个字节转换为十六进制字符串,也就是64位的十六进制字符串。
那么,这32个字节又是如何来的呢?是对整个交易结构的所有数据进行哈希的结果。该哈希值(即交易id)会被写入区块链里。我们平时在区块链浏览器里通过交易哈希查询一笔交易时使用的就是这个哈希值来进行查询的。
这里解释一下哈希是什么?哈希,英文Hash,从百度的解释结果就是:
Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来确定唯一的输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数
简单一点,你可以把哈希看作一种单向的算法或函数(散列函数),对于任意长度的输入,经过哈希函数的处理后,都能得到一个固定长度的输出结果(哈希值)。一般来说,不同的输入,经过哈希函数的处理后得到相同的哈希值(也就是冲突)概率是很低很低的(差不多和中彩票特等奖的概率差不多,哈哈)。对于同一个输入内容,在使用同一种哈希函数处理后得到的哈希值是唯一的,即对于同一个输入内容,在使用同一种哈希函数处理,无论运行多少遍,其得到的哈希值是唯一的。因此,对于2笔不同的以太坊的交易,哪怕交易内容只有1个字节,甚至是1比特(bit)的不同,它们的交易id都不会一样。另一方面,一般很难从哈希值反推出原来的输入值,也就是不可逆。常用的哈希算法有MD5和SHA。

基本流程

下面我们在以太坊的命令行终端通过交易哈希来查询一笔交易,可以看到交易的内容大概有哪些字段
>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
}
以太坊一笔交易的交易id就是对上面主要的字段,通过哈希函数计算出来的,进行哈希的字段包括:
from:交易发送者
gas:该笔交易可以使用的手续费gas的最大值,该笔交易实际使用多少gas由系统来计算
gasPrice:gas的价格,单位是 Wei。即1gas等于多少 Wei。以太坊的最小单位是 Wei。1 个以太币 = 10 的18 次方 Wei。gas的价格越高,该笔交易越容易被矿工打包进区块里
input:扩展字段,发起智能合约交易时使用
nonce:交易发送者的交易计数器
r:签名值的r字段
s:签名值的s字段
v:签名值的v字段
to:交易接收者
value:交易转账数量,单位是 Wei
但不包括这些字段:blockNumber(区块号)、blockHash(区块id)、hash(交易id)这些字段。
上面的交易是一笔普通的转账交易,转账金额是value字段里的值,即20个eth。当使用以太坊的客户端钱包发送一笔转账时,客户端钱包先使用钱包私钥对转账内容进行签名,计算出r、s、v这3个字段,生成签名值。然后再对转账交易的相关字段进行哈希运算,此时该笔转账的交易id就已经确定了,无需等待该笔转账写入区块时才能确定交易id。
看到这里,你应该大概了解以太坊的交易id的基本处理流程。如果你想进一步了解交易id的计算过程,可以从以太坊的源码进行更进一步的学习。如果你对以太坊的源码不感兴趣,下面的内容就可以忽略了。

源码分析

以太坊计算交易id的源码在文件
go-ethereum/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
}
结构txdata的定义如下:
type txdata struct {
AccountNonce uint64 `json:"nonce" gencodec:"required"`
Price *http://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 `json:"value" gencodec:"required"`
Payload []byte `json:"input" gencodec:"required"`
// Signature values
V *http://big.Int `json:"v" gencodec:"required"`
R *http://big.Int `json:"r" gencodec:"required"`
S *http://big.Int `json:"s" gencodec:"required"`
// This is only used when marshaling to JSON.
Hash *common.Hash `json:"hash" rlp:"-"`
}
以太坊使用的哈希函数是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编码后得到的是一个字节数组。
我的程序员主页:https://blog.csdn.net/powervip
我的知乎:https://www.zhihu.com/people/powervip
如果你觉得这篇文章写得还可以,请帮忙点个赞,谢谢!
你的鼓励,我的动力!