以太坊设计与实现

 主页   资讯   文章   代码   电子书 

title: "区块存储" menuTitle: "区块存储" date: 2019-09-05T22:58:46+08:00 draft: false weight: 20306

这篇文章所说的挖矿环节中的存储环节,当矿工通过穷举计算找到了符合难道要去的区块 Nonce 后,标志着新区块已经成功被挖掘。

此时,矿工将在本地将这个合法的区块直接在本地存储,下面具体讲讲,在 geth 中矿工是如何存储自己挖掘的新区块的。

image-20200716061257891

在上一环节“PoW 寻找 Nonce” 后,已经拥有了完整的区块信息。

image-20200716062021598

而在“处理本地交易”和“处理远程交易”后,便拥有了完整的区块交易回执清单:

img

区块中的每一笔交易在处理后,都会存在一份交易回执。在交易回执中记录着这边交易的执行结果信息,对于交易回执,我们已经在前面的课程有讲解,这里不再复述。

同时在“发放区块奖励”后,区块的状态不会再发生变化,此时,我们就已经拿到了一个可以代表该区块的状态数据。状态state,在内存中将记录着本次区块中交易执行后状态所发送的变化信息,包括新增、变更和删除的数据。

前面所说的区块(Block)、交易回执(Receipt)、状态(State)就是本次挖矿的产物,在本地需要存储的也只有这三部分数据。

image-20200716064017293

这些数据,在挖矿中处理存储的代码如下:

//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)))

  • ❶ 遍历交易回执,给每一个交易回执添加本次区块信息(blockHash,BlockNumber、TransactionIndex),这样就可以在本地记录交易回执和区块间的查找关系。
  • ❷ 同时将交易回执中生成的日志信息提取到一个大集合中,以便作为一个区块日志整体存储。
  • ❸ 开始提交区块(Block)、交易回执(Receipt)、状态(State)和日志(log)到本地数据库中。

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中查找出数据呢?

  1. 通过交易哈希获取交易信息:eth_getTransactionByHash("0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238")

  2. 查询最后一个区块信息:

    eth_getBlockByNumber("latest")

  3. 通过交易哈希获取交易回执eth_getTransactionReceipt("0x444172bef57ad978655171a8af2cfd89baa02a97fcb773067aef7794d6913374")

image-20200721215427554