以太坊 数据结构
一、区块(Block)
区块 (Block)是 以太坊 (Ethereum)的核心数据结构之一。所有账户的相关活动,以 交易 (Transaction) 的格式存储,每个区块有一个交易对象的列表;每个交易的执行结果,由一个 收据 (Receipt)对象与其包含的一组 日志 (Log)对象记录;所有交易执行完后生成的收据列表,存储在区块中(经过压缩加密)。不同区块之间,通过前向指针 父哈希 (ParentHash)一个一个串联起来成为一个链表, 区块链 (BlockChain)结构体管理着这个链表。
数据定义:
type Block struct {
header *Header //区块头
uncles []*Header //叔块头集合
transactions Transactions //交易集合
hash atomic.Value //区块哈希
size atomic.Value //区块大小
td *big.Int //累计的区块难度
ReceivedAt time.Time //区块接收时间
ReceivedFrom interface{} //区块来源
}
数据项注解:td(total difficulty)为当前链的总难度,即到当前区块截止,累积的所有区块难度之和
核心方法 :
- func (b *Block) Size() common.StorageSize // 区块 rlp 编码后的大小
- func (b *Block) Hash() common.Hash // 区块 keccak256 哈希,实际是 header 的哈希
1、区块头
区块头(Header)是 Block 的核心,注意到它的成员变量全都是公共的,这使得它可以很方便的向调用者提供关于 Block 属性的操作。
type Header struct {
ParentHash common.Hash //父区块哈希。除了创世块外,每个区块有且只有一个父区块
UncleHash common.Hash //所属区块uncles成员的哈希值。uncles是一个Header数组
Coinbase common.Address //打包该区块的矿工的地址,用于接收矿工费
Root common.Hash //状态树的根哈希值
TxHash common.Hash //交易树的根哈希值
ReceiptHash common.Hash //收据树的根哈希值
Bloom Bloom //交易收据日志组成的Bloom过滤器
Difficulty *big.Int //本区块的难度
Number *big.Int //区块的序号。等于其父区块Number+1
GasLimit uint64 //区块内所有Gas消耗的上限,该数值在区块创建时设置,用于限定区块大小。
GasUsed uint64 //区块内所有Transaction执行时所实际消耗的Gas总和
Time uint64 //区块产生的时间戳,一般是打包区块的时间,这个字段不是出块的时间戳
Extra []byte //区块的附加数据
MixDigest common.Hash //哈希值,与Nonce的组合用于工作量计算
Nonce BlockNonce //一个64bit的哈希数,它被应用在区块的"挖掘"阶段,并且在使用中会被修改
}
GasLimit 是用于限制区块中的交易个数用的。具体由每个矿工自己设置,有些矿工觉得本区块的 GasLimit 太大,可以在挖下一个区块时间 Gas Limit 下调 1 /1024, 觉得 GasLimit 太小的时候,就会上调 1 /1024。整个区块链中区块的大小,就是所有矿工微调的平均值。以太坊中正是通过 GasLimit 来限制区块的大小。
区块头中比较重要的三个字段是 Root、TxHash 和 ReceiptHash 三个哈希值。收据树必须在区块的所有交易执行完成才能生成;交易树理论上只需交易数组即可,不过依然被限制在所有交易执行完后才生成;最有趣的是状态树,由于它存储了所有账户的信息,比如余额,发起交易次数,虚拟机指令数组等等,所以随着每次交易的执行,状态其实一直在变化,这就使得 Root 值也在变化中。于是 StateDB 定义了一个函数 IntermediateRoot(),用来生成那一时刻的 Root 值:
2、交易
区块的成员 transactions 其实是 Transaction 数组,交易 (Transaction) 是指由一个外部账户转移一定资产给某个账户, 或者发出一个消息指令到某个智能合约。在以太坊网络中,交易执行属于一个事务。具有原子性、一致性、隔离性、持久性特点。为了安全性,交易需要发送者签名才有效,别人不能伪造签名发起交易而盗取以太币。
需要注意的是,TxByNonceh 是按 nonce 排序的 Transaction 数组,TxByPrice 是按 gas 价格排序的 Transaction 数组
数据定义:
type Transaction struct {
data txdata //交易详情
// caches
hash atomic.Value //交易哈希
size atomic.Value //交易大小,data成员rlp编码大小
from atomic.Value //交易来源
}
type txdata struct {
AccountNonce uint64 //账户nonce
Price *big.Int //gas价格
GasLimit uint64 //gas上限
Recipient *common.Address //交易接受者
Amount *big.Int //以太币数量
Payload []byte //交易附加数据,智能合约所需
// Signature values
V *big.Int //交易签名r、s、v,发送者签名
R *big.Int //
S *big.Int //
// This is only used when marshaling to JSON.
Hash *common.Hash //交易哈希
}
注解 :
- 账户 nonce 用于撤销交易、防止双花等
- 交易的哈希、大小、来源等需要经常读写,所以可以把它们缓存起来
- 特别注意的是交易的来源,它是从交易的签名 R、S、V 中提取出来
- 另外从签名 V 中可以提取出 chainId
核心方法 :
- func (tx Transaction) ChainId() big.Int
- func (tx *Transaction) MarshalJSON() ([]byte, error)
- func (tx *Transaction) UnmarshalJSON(input []byte) error
- func (tx *Transaction) WithSignature(signer Signer, sig []byte)
- func (tx *Transaction) AsMessage(s Signer) (Message, error)
二、交易池
交易池是一个节点存储交易的容器,无论是本节点创建的交易还是其他节点广播过来的交易,都会缓存在交易池中,当需要生成区块时,就从交易池中选择合适的交易打包成块,经由共识最终确认生成新的区块。另外交易池配置限定了交易的 gas 价格下限、每个账户可保留的交易数、交易最大排队时间等等,一定程度控制了交易池的大小。
数据定义:
type TxPool struct {
config TxPoolConfig //交易池配置
chainconfig *params.ChainConfig //区块链配置
chain blockChain //区块链接口
gasPrice *big.Int //交易池gas价格下限
txFeed event.Feed //交易订阅器
scope event.SubscriptionScope //订阅范围,可以批量关闭订阅
signer types.Signer //交易签名者接口,用于从交易解析发送者
mu sync.RWMutex //读写锁
currentState *state.StateDB //当前区块链的世界状态(包括普通账户和合约账户),
pendingNonces *txNoncer //Pending状态交易的nonce,如果没有就从currentState取
currentMaxGas uint64 //当前最大交易gasLimit
locals *accountSet //本地账户集合,可以避免被踢出交易池
journal *txJournal //备份到磁盘的本地交易记录
pending map[common.Address]*txList //所有当前可处理的交易
queue map[common.Address]*txList //所有当前等待处理的交易
beats map[common.Address]time.Time //每个已知帐户的最后一次心跳
all *txLookup //所有交易集合,按哈希索引并加锁
priced *txPricedList //按价格排序的所有交易
chainHeadCh chan ChainHeadEvent //新区块事件通道
chainHeadSub event.Subscription //新区块事件订阅器
reqResetCh chan *txpoolResetRequest //重置交易池请求通道
reqPromoteCh chan *accountSet //账户提升请求通道,升级为本地账户
queueTxEventCh chan *types.Transaction //新交易事件,放入成员queue中,等待处理
reorgDoneCh chan chan struct{} //重新排序完成
reorgShutdownCh chan struct{} //重新排序终止
wg sync.WaitGroup //等待一组操作完成
}
type txList struct {
strict bool //nonce值是否严格连续
txs *txSortedMap //已排序的交易集合
costcap *big.Int //最高的交易成本价格(gasprice * gaslimit)
gascap uint64 //最高的交易GasLimit
}
type txLookup struct {
all map[common.Hash]*types.Transaction
lock sync.RWMutex
}
type txPricedList struct {
all *txLookup
items *priceHeap //按价格排序的交易堆
stales int //重构堆阀值
}
注解 :
- 交易池用于缓存交易,过滤无效交易
- 新的交易会尝试放入 pending,否则放入 queued,如果 queued 也不行就舍弃
- 低于设定的 gasPrice(节点自行设置)交易会被舍弃
- 交易来源有本地的和远程的,本地的有特权, 本地交易会缓存到磁盘中
- 交易池存在内存中,不可能无限大,等超过一定阈值就需要对交易池里面的交易进行清理
- 收到新区块会重构交易池
核心方法 :
- func (pool TxPool) validateTx(tx types.Transaction, local bool) error
- func (pool TxPool) journalTx(from common.Address, tx types.Transaction)
- func (pool TxPool) AddLocal(tx types.Transaction) error
- func (pool TxPool) AddRemote(tx types.Transaction) error
- func (pool *TxPool) Status(hashes []common.Hash) []TxStatus
- func (pool TxPool) Get(hash common.Hash) types.Transaction
- func (pool *TxPool) removeTx(hash common.Hash, outofbound bool)
三、收据
交易通过 evm 虚拟机执行完成之后会产生一些 收据 (Receipt),一个交易一个收据并包括多个 日志 (Log),收据记载的是 交易执行的结果。在以太坊虚拟机里,日志代表对事件的存储,主要记录触发的事件及其数据(比如一个代币转账事件,Topics 是转账事件的一些签名,Data 记录的是谁给谁转多少代币)。
type Receipt struct {
// 这些字段由黄皮书定义
PostState []byte //执行交易完成后状态树的根
Status uint64 //交易执行结果,成功为1,失败为0
CumulativeGasUsed uint64 //一个区块里执行到此交易(包括)的gas花费
Bloom Bloom //布隆过滤器
Logs []*Log //虚拟机智能合约执行日志
// 这些字段在处理交易时由geth节点添加。它们存储在区块链数据库中
TxHash common.Hash //交易哈希
ContractAddress common.Address //合约地址
GasUsed uint64 //gas消耗
//这些字段提供与此收据对应的交易记录的信息
BlockHash common.Hash //所属区块的哈希
BlockNumber *big.Int //所属区块的高度
TransactionIndex uint //所在交易列表的序号
}
type Log struct {
Address common.Address //生成日志的合约地址
Topics []common.Hash //日志提供的主题列表
Data []byte //日志提供的数据,通常为abi编码
BlockNumber uint64 //区块高度
TxHash common.Hash //交易哈希
TxIndex uint //交易在区块里的索引
BlockHash common.Hash //区块哈希
Index uint //在区块中的索引
Removed bool //是否已删除
}
注解 :
- 收据的日志只有执行智能合约的事件才会产生,实质是由虚拟机 log 指令(LOG0,LOG1,LOG2,LOG3,LOG4)生成的。
- 如果由于链重组日志不需要了,则 Removed 字段设为 true。
- 日志里面 topics 数组里的第一个元素就是事件的 sha3 签名,其余的为 indexed 的参数签名,参考合约 ABI 文件
核心方法:
- func NewReceipt(root []byte, failed bool, cumulativeGasUsed uint64) *Receipt
- func (r *Receipt) setStatus(postStateOrStatus []byte) error
- func (r *Receipt) Size() common.StorageSize