以太坊源码阅读01

时间:2024-04-17 13:31:40

正所谓区块链,怎能不熟悉区块的数据结构呢?区块的结构体被保存在core/types/block.go文件中,下面是我截取出来的:

type Block struct {
    header       *Header
    uncles       []*Header
    transactions Transactions
    withdrawals  Withdrawals
​
    // caches
    hash atomic.Value
    size atomic.Value
​
    // These fields are used by package eth to track
    // inter-peer block relay.
    ReceivedAt   time.Time
    ReceivedFrom interface{}
}

我之前主要使用C++语言,可以看出来上面的代码和C++语言还是有一定的差异的,C++结构体中先声明变量的类型,然后给出变量的名称,而上面的代码则是反过来了,先给出了成员变量的名称,然后给出了类型。下面是对各个字段的解释:

字段 描述
header 指向Header类型的指针,表示该区块的头部信息
uncles 指向Header类型的数组,表示该区块的叔父区块
transactions 区块所包含的交易列表
withdrawals 区块中的提款列表
hash 区块的哈希值
size 区块的大小
ReceivedAt 接收到该区块的时间
ReceivedFrom 区块是被谁挖出来的

现在对于区块的内容已经有所了解了,再来看看区块头的数据结构:

type Header struct {
    ParentHash  common.Hash    `json:"parentHash"       gencodec:"required"`
    UncleHash   common.Hash    `json:"sha3Uncles"       gencodec:"required"`
    Coinbase    common.Address `json:"miner"`
    Root        common.Hash    `json:"stateRoot"        gencodec:"required"`
    TxHash      common.Hash    `json:"transactionsRoot" gencodec:"required"`
    ReceiptHash common.Hash    `json:"receiptsRoot"     gencodec:"required"`
    Bloom       Bloom          `json:"logsBloom"        gencodec:"required"`
    Difficulty  *big.Int       `json:"difficulty"       gencodec:"required"`
    Number      *big.Int       `json:"number"           gencodec:"required"`
    GasLimit    uint64         `json:"gasLimit"         gencodec:"required"`
    GasUsed     uint64         `json:"gasUsed"          gencodec:"required"`
    Time        uint64         `json:"timestamp"        gencodec:"required"`
    Extra       []byte         `json:"extraData"        gencodec:"required"`
    MixDigest   common.Hash    `json:"mixHash"`
    Nonce       BlockNonce     `json:"nonce"`
​
    // BaseFee was added by EIP-1559 and is ignored in legacy headers.
    BaseFee *big.Int `json:"baseFeePerGas" rlp:"optional"`
​
    // WithdrawalsHash was added by EIP-4895 and is ignored in legacy headers.
    WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"`
​
    // BlobGasUsed was added by EIP-4844 and is ignored in legacy headers.
    BlobGasUsed *uint64 `json:"blobGasUsed" rlp:"optional"`
​
    // ExcessBlobGas was added by EIP-4844 and is ignored in legacy headers.
    ExcessBlobGas *uint64 `json:"excessBlobGas" rlp:"optional"`
​
    // ParentBeaconRoot was added by EIP-4788 and is ignored in legacy headers.
    ParentBeaconRoot *common.Hash `json:"parentBeaconBlockRoot" rlp:"optional"`
}

下面是对于字段的解释:

字段 描述
ParentHash 父区块的哈希值
UncleHash 叔父区块的哈希值
Coinbase 挖矿成功的奖励地址
Root 状态树的根哈希值
TxHash 交易树的根哈希值
ReceiptHash 收据树的根哈希值
Bloom 布隆过滤器
Difficulty 挖矿难度
Number 区块的编号
GasLimit 汽油的最大量限制
GasUsed 汽油的使用量
Time 区块的生成时间
Extra 附加数据
MixDigest 区块的混合哈希值
Nonce 挖矿所需要不断尝试的随机数(4字节)
BaseFee 基础手续费
WithdrawalsHash 提款根的哈希值
BlobGasUsed 新的燃气类型的使用量
ExcessBlobGas 辅助识别处理异常情况,当恶意节点试图使用超额的新的燃气类型
ParentBeaconRoo 跟踪对应的父分片,以太坊2.0引入了分片链的概念,每个分片都是相对独立的链,有一个对应的父分片,目的是提高网络的吞吐量和扩展性。

上面的字段中,最后五个字段是新提出来的,在传统的区块中会忽略对应的字段。接下来是区块体:

type Body struct {
    Transactions []*Transaction
    Uncles       []*Header
    Withdrawals  []*Withdrawal `rlp:"optional"`
}
字段 描述
Transactions 保存当前区块的所有交易列表
Uncles 保存当前区块的所有叔父区块
Withdrawals 保存当前区块的所有提款列表
type headerMarshaling struct {
    Difficulty    *hexutil.Big
    Number        *hexutil.Big
    GasLimit      hexutil.Uint64
    GasUsed       hexutil.Uint64
    Time          hexutil.Uint64
    Extra         hexutil.Bytes
    BaseFee       *hexutil.Big
    Hash          common.Hash `json:"hash"` // adds call to Hash() in MarshalJSON
    BlobGasUsed   *hexutil.Uint64
    ExcessBlobGas *hexutil.Uint64
}

如上面结构体所示,以太坊提供了一个将区块头编码的结构体,将区块头中的部分变量换成了十六进制。在以太坊中,还提供了类型覆盖结构体,用于进行编码和解码时指定特定字段的编码方式,以满足不同的需求或协议规范。这种方式允许开发者根据实际情况定制编码和解码的行为,使得数据的序列哈和反序列化更加灵活和可定制。

以太坊中的区块头包含了诸多重要的信息,例如区块号、难度、气体限制、时间戳等等。在不同的协议中,可能需要不同的方式对这些数据进行编码和解码,以满足不同的需求,因此,使用覆盖结构体可以灵活的指定特定字段的编码方式,使得以太坊在不同的环境中具有更强的适应性和可扩展性。

在文件中,还有行代码:

type BlockNonce [8]byte

在工作量证明中,用户打包交易后生成Merck树,然后将其根哈希值保存到区块头中,对整个区块头取哈希的到一个值,这个值需要满足足够小的条件,由于哈希正向容易反向难的特点,矿工很难反向计算得到Nonce值,因此会一个一个数字去尝试,由于不同的矿工打包的交易可能不一样,因此矿工需要找到的Nonce值也可能不一样。上面这行代码说明了Nonce是一个8字节64位的值。通过查询资料得知,由于全网算力的不断增大,64位的难度难以满足要求,因此将Coinbase字段的前4字节也作为取哈希的内容,这部分的内容可以用户自己设置,通常包含挖矿的奖励地址。

言归正传,先声明变量名,然后声明类型已经很奇怪了,后来发现还有更加怪异的,看下面的函数:

func (n BlockNonce) Uint64() uint64 {
    return binary.BigEndian.Uint64(n[:])
}

先不用管函数的意义,看看函数的声明是否很奇怪???通过查找资料发现:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

通过上网查询资料发现,接收者类型类似于C++中的this指针,而使用this指针时会修改实际的值,如果是传递一般的参数,则会拷贝一份副本,副本的改变不会导致实参的改变。接收者的声明规则如下:

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。

  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。

  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

在Go语言中,nil 是一个预定义的标识符,用于表示指针、切片、映射、通道、函数和接口类型的零值或空值。具体含义取决于它所用于的类型。

  • 对于指针类型,nil 表示一个空指针,即指针未指向任何有效的内存地址。

  • 对于切片、映射、通道、函数和接口类型,nil 表示这些数据结构是空的,即切片、映射、通道为空,函数和接口未指向任何具体的实现或值。

在条件语句中,可以使用 nil 来检查指针、切片、映射、通道、函数和接口是否为零值或空值。

下面是以太坊外部区块的结构体:

type extblock struct {
    Header      *Header
    Txs         []*Transaction
    Uncles      []*Header
    Withdrawals []*Withdrawal `rlp:"optional"`
}
  • Header: 代表了区块的头部信息,包括区块的元数据,如区块号、时间戳、难度等。

  • Txs: 是一个包含了所有交易的列表,每个交易都是一个 Transaction 结构体的实例。

  • Uncles: 是一个包含了叔块(Uncle Block)的列表,每个叔块也是一个 Header 结构体的实例。

  • Withdrawals: 是一个包含了所有提现的列表,每个提现也是一个 Withdrawal 结构体的实例。

现在对于区块的相关结构体,我们已经有了一定的了解,那么来看看以太坊是如何产生新区块的。

func NewBlock(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt, hasher TrieHasher) *Block {        
    // 创建一个block对象,拷贝一个header对象来初始化block的header字段
    b := &Block{header: CopyHeader(header)}
​
    // 设置交易列表,当交易列表不为空时,根据交易列表计算出哈希值并赋值给TxHash字段并设置block的交易列表
    if len(txs) == 0 {
        b.header.TxHash = EmptyTxsHash
    } else {
        b.header.TxHash = DeriveSha(Transactions(txs), hasher)
        b.transactions = make(Transactions, len(txs))
        copy(b.transactions, txs)
    }
    // 设置收据树以及布隆过滤器
    if len(receipts) == 0 {
        b.header.ReceiptHash = EmptyReceiptsHash
    } else {
        b.header.ReceiptHash = DeriveSha(Receipts(receipts), hasher)
        b.header.Bloom = CreateBloom(receipts)
    }
    // 设置叔节点
    if len(uncles) == 0 {
        b.header.UncleHash = EmptyUncleHash
    } else {
        b.header.UncleHash = CalcUncleHash(uncles)
        b.uncles = make([]*Header, len(uncles))
        for i := range uncles {
            b.uncles[i] = CopyHeader(uncles[i])
        }
    }
​
    return b
}
​
// 下面的函数允许区块指定额外的体现信息
func NewBlockWithWithdrawals(header *Header, txs []*Transaction, uncles []*Header, receipts []*Receipt, withdrawals []*Withdrawal, hasher TrieHasher) *Block {
    b := NewBlock(header, txs, uncles, receipts, hasher)
​
    if withdrawals == nil {
        b.header.WithdrawalsHash = nil
    } else if len(withdrawals) == 0 {
        b.header.WithdrawalsHash = &EmptyWithdrawalsHash
    } else {
        h := DeriveSha(Withdrawals(withdrawals), hasher)
        b.header.WithdrawalsHash = &h
    }
​
    return b.WithWithdrawals(withdrawals)
}
​
// 实现对区块头的深拷贝
func CopyHeader(h *Header) *Header {
    cpy := *h
    if cpy.Difficulty = new(big.Int); h.Difficulty != nil {
        cpy.Difficulty.Set(h.Difficulty)
    }
    if cpy.Number = new(big.Int); h.Number != nil {
        cpy.Number.Set(h.Number)
    }
    if h.BaseFee != nil {
        cpy.BaseFee = new(big.Int).Set(h.BaseFee)
    }
    if len(h.Extra) > 0 {
        cpy.Extra = make([]byte, len(h.Extra))
        copy(cpy.Extra, h.Extra)
    }
    if h.WithdrawalsHash != nil {
        cpy.WithdrawalsHash = new(common.Hash)
        *cpy.WithdrawalsHash = *h.WithdrawalsHash
    }
    if h.ExcessBlobGas != nil {
        cpy.ExcessBlobGas = new(uint64)
        *cpy.ExcessBlobGas = *h.ExcessBlobGas
    }
    if h.BlobGasUsed != nil {
        cpy.BlobGasUsed = new(uint64)
        *cpy.BlobGasUsed = *h.BlobGasUsed
    }
    if h.ParentBeaconRoot != nil {
        cpy.ParentBeaconRoot = new(common.Hash)
        *cpy.ParentBeaconRoot = *h.ParentBeaconRoot
    }
    return &cpy
}

下面是一些函数的作用:

函数 功能
func EncodeNonce(i uint64) BlockNonce 将输入的64位无符号整数转换成字节数组,转换成BlockNorce类型(大端序)
func (n BlockNonce) Uint64() uint64 将BlockNonce按照大端序转换成无符号64位整数
func (n BlockNonce) MarshalText() ([]byte, error) 将BlockNonce类型的值编码为带有0x前缀的十六进制字符串
func (n *BlockNonce) UnmarshalText(input []byte) error 从带有0x前缀的十六进制字符串解码并填充BlockNonce类型的值
func (h *Header) Hash() common.Hash 计算区块头的哈希值
func (h *Header) Size() common.StorageSize 区块头所有内容所占内存大小
func (h *Header) SanityCheck() error 判断区块头部字段是否在合理范围内
func (h *Header) EmptyBody() bool 检查头部信息的主体部分是否是空
func (h *Header) EmptyReceipts() bool 检查头部的收据是否为空
func (b *Block) DecodeRLP(s *rlp.Stream) error 从流中解码区块数据
func (b *Block) EncodeRLP(w io.Writer) error 将区块数据序列格式化RLP格式,并写入流中

此外还提供了一个更换区块区块头、更换区块体、更换交易列表等函数。