这篇文章所说的挖矿环节中的存储环节,当矿工通过穷举计算找到了符合难道要去的区块 Nonce 后,标志着新区块已经成功被挖掘。
此时,矿工将在本地将这个合法的区块直接在本地存储,下面具体讲讲,在 geth 中矿工是如何存储自己挖掘的新区块的。
在上一环节“PoW 寻找 Nonce” 后,已经拥有了完整的区块信息。
而在“处理本地交易”和“处理远程交易”后,便拥有了完整的区块交易回执清单:
区块中的每一笔交易在处理后,都会存在一份交易回执。在交易回执中记录着这边交易的执行结果信息,对于交易回执,我们已经在前面的课程有讲解,这里不再复述。
同时在“发放区块奖励”后,区块的状态不会再发生变化,此时,我们就已经拿到了一个可以代表该区块的状态数据。状态state
,在内存中将记录着本次区块中交易执行后状态所发送的变化信息,包括新增、变更和删除的数据。
前面所说的区块(Block)、交易回执(Receipt)、状态(State)就是本次挖矿的产物,在本地需要存储的也只有这三部分数据。
这些数据,在挖矿中处理存储的代码如下:
//miner/worker.go:595
var (
receipts = make([]*types.Receipt, len(task.receipts))
logs []*types.Log
)
for i, receipt := range task.receipts {//❶
// add block location fields
receipt.BlockHash = hash
receipt.BlockNumber = block.Number()
receipt.TransactionIndex = uint(i)
receipts[i] = new(types.Receipt)
*receipts[i] = *receipt
for _, log := range receipt.Logs {
log.BlockHash = hash
}
logs = append(logs, receipt.Logs...)//❷
}
// Commit block and state to database. //❸
_, err := w.chain.WriteBlockWithState(block, receipts, logs, task.state, true)
if err != nil {
log.Error("Failed writing block to chain", "err", err)
continue
}
log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash,
"elapsed", common.PrettyDuration(time.Since(task.createdAt)))
在writeBlockWithState
中,是将所有数据以一个批处理事务写入到数据库中:
blockBatch := bc.db.NewBatch()
rawdb.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd)
rawdb.WriteBlock(blockBatch, block)
rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts)
rawdb.WritePreimages(blockBatch, state.Preimages())
if err := blockBatch.Write(); err != nil {
log.Crit("Failed to write block into disk", "err", err)
}
// Commit all cached state changes into underlying memory database.
root, err := state.Commit(bc.chainConfig.IsEIP158(block.Number()))
//...
// Set new head.
if status == CanonStatTy {
bc.writeHeadBlock(block)
}
在一个事务中,分别想数据库中写入了区块难度、区块、交易回执、Preimages(key映射),最后将 state 提交。
那么,geth 是如何在本地将这些数据存放到键值数据库 levelDB 中的呢?这里,给大家整理一份键值信息表。
Key | Value | 说明 |
---|---|---|
“b”.blockNumber.blockHash | blockBody: uncles + transactions | 通过区块哈希和高度存储对应的区块叔块和交易信息 |
"H".blockHash | blockNumber | 通过区块哈希记录对于的区块高度 |
“h”.blockNumber.blockHash | blockHeader | 通过区块哈希和高度存储对于的区块头 |
”r“.blockNumber | receipts | 通过区块高度记录区块的交易回执记录 |
"h".blockNumber | blockHash | 区块高度对应的区块哈希 |
”l“.txHash | blockNumber | 记录交易哈希所在的区块高度 |
”LastBlock“ | blockHash | 更新最后一个区块哈希值 |
”LastHeader“ | blockHash | 更新最后一个区块头所在位置 |
注意,上面的 value 信息,是需要序列化为 bytes 才能存储到 leveldb 中,序列化是以太坊自定义的 RLP 编码技术。你有没有想过它为何要添加一个前缀呢?比如”b“、”H“等等,第一个好处是将不同数据分类,另一个重要的原因是在leveldb中数据是以 key 值排序存储的,这样在按顺序遍历区块头、查询同类型数据时,读的性能会更好。
正是因为在我们在本地了区块数据的一些映射关系,我们才能快速的从本地数据库中只需要提供少量的信息就就能组合一个或者多个键值关系查询到目标数据。下面我列举了一些常见的以太坊API,你觉得该如何从DB中查找出数据呢?
通过交易哈希获取交易信息:eth_getTransactionByHash("0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238")
查询最后一个区块信息:
eth_getBlockByNumber("latest")
通过交易哈希获取交易回执eth_getTransactionReceipt("0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374")