写点什么

以太坊剖析 - 区块(Block)

作者:起航
  • 2022 年 2 月 04 日
  • 本文字数:4784 字

    阅读完需:约 16 分钟

区块

以太坊在整体上可以看作一个基于交易的状态机:起始于一个创世区块(Genesis)状态,逐笔执行交易直到其转变为某个版本的最终状态。这里的版本是以区块为单位,所以我们会说某个区块的世界状态。


在以太坊网络中,参与方可以在任意时间,通过任意账户经由任意节点发起一笔交易。此时,交易尚未生效(写入账本),只是由网络中某些节点传播和暂存。以太坊是以区块为单位进行记账,需要担任矿工角色的节点将一定数量的交易打包为区块,然后通过挖矿(PoW 共识算法)来争夺下一区块的记账(出块)权,最后拥有记账权的矿工将新区块写入本地账本,并同步给其他节点。节点将区块写入本地账本,包含一系列的操作过程:区块的验证和解析,交易的验证和执行,然后将交易执行后的账户状态数据写入状态数据库,将区块写入账本数据库;也包含一些其他逻辑(比如处理区块分叉)。我们可以发现,区块在以太坊的设计理念中占据重要的地位,与账本结构、记账粒度、PoW 共识算法都密切相关。


在讨论概念细节之前,笔者想强调一个观点:以太坊是区块链技术的一个具体实现方案,而区块链是分布式账本的一种技术范式。有些概念可能是分布式账本或区块链技术自身的,而有些概念可能是以太坊引入的。


比如,分叉并非区块链本身的概念,或者说并非所有的区块链技术都会出现分叉,使用 BFT(拜占庭容错)共识算法的区块链技术就不会产生分叉。再者,区块的概念在不同的分布式账本中的地位是不一样的,有的会弱化区块的作用,甚至可能没有区块(以交易为粒度进行记账)。


当然,每种技术都有其适用场景、整体设计理念、技术特性,不能单一的从某个点去评判好与坏。


笔者尽量对各类概念加以区分和标注,更重要的是读者可以认识到这些概念可能属于不同的技术范畴,并加以思考。


区块的属性列表:


区块头部

区块头部包含一些区块相关的基本信息,以及区块生命周期中关键数据的验证信息,每个区块包含一个区块头部(详见 BlockHeader.java 的解读)。


//Block.java:L54private BlockHeader header; 
复制代码


打包新区块是一系列的操作过程,仅区块头部的创建过程就可分为 3 个阶段:


  1. 挖矿前:此阶段主要生成区块头部的基本信息,根据父区块、交易列表、叔父块头部列表、当前时间戳等信息,给区块头部的部分属性赋值。


//BlockchainImpl.java:L482,createNewBlock函数部分代码段final long blockNumber = parent.getNumber() + 1;//当前区块高度等于父区块高度加1,对应黄皮书的40公式
final byte[] extraData = config.getBlockchainConfig().getConfigForBlock(blockNumber).getExtraData(minerExtraData, blockNumber);
Block block = new Block(parent.getHash(),//获取父区块头部哈希值,对应黄皮书的39公式 EMPTY_LIST_HASH, // uncleHash minerCoinbase, new byte[0], // log bloom - from tx receipts new byte[0], // difficulty computed right after block creation blockNumber, parent.getGasLimit(), // (add to config ?) 0, // gas used - computed after running all transactions time, // block time extraData, // extra data new byte[0], // mixHash (to mine) new byte[0], // nonce (to mine) new byte[0], // receiptsRoot - computed after running all transactions calcTxTrie(txs), // TransactionsRoot - computed after running all transactions//根据交易列表,计算区块头部的交易树根节点哈希值(txTrieRoot)属性,对应黄皮书的31公式的Ht部分 new byte[] {0}, // stateRoot - computed after running all tranxsactions txs, null); // uncle list
for (BlockHeader uncle : uncles) {//根据叔父块列表,计算区块头部unclesHash属性,对应黄皮书的31公式的部分逻辑 block.addUncle(uncle);}
block.getHeader().setDifficulty(ByteUtil.bigIntegerToBytes(block.getHeader().//计算区块头部的难度值,对应黄皮书41、42、43、44、45、46公式 calcDifficulty(config.getBlockchainConfig(), parent.getHeader())));
复制代码


  1. 交易执行后:生成区块头部的验证信息:世界状态树的根节点哈希、交易收据树的根节点哈希、交易日志 Bloom 过滤器、交易消耗 Gas 数量。


//BlockchainImpl.java:L507,createNewBlock函数部分代码段Repository track = repository.getSnapshotTo(parent.getStateRoot());//根据父区块的世界状态树的根节点哈希值(Hr),获取对应版本的世界状态,对应黄皮书的33公式BlockSummary summary = applyBlock(track, block);List<TransactionReceipt> receipts = summary.getReceipts();block.setStateRoot(track.getRoot());//行507-510:计算世界状态树的根节点哈希值,并赋值给区块头部的stateRoot属性,对应黄皮书的31公式的Hr部分和169公式
Bloom logBloom = new Bloom();for (TransactionReceipt receipt : receipts) { logBloom.or(receipt.getBloomFilter());}block.getHeader().setLogsBloom(logBloom.getData());//行512-516:根据交易收据列表中的Bloom过滤器信息,计算区块头部的日志Bloom属性,对应黄皮书的31公式的Hb部分block.getHeader().setGasUsed(receipts.size() > 0 ? receipts.get(receipts.size() - 1).getCumulativeGasLong() : 0);//根据交易收据列表,取最后一个交易收据的Gas累计使用量,对应黄皮书的158公式block.getHeader().setReceiptsRoot(calcReceiptsTrie(receipts));//根据交易收据列表,计算区块头部的交易收取哈希值,对应黄皮书的31公式的He部分逻辑
复制代码


  1. 挖矿后:根据交易执行后的区块头部(以及一些其他信息),通过 Ethash PoW 共识算法进行挖矿计算,直到挖矿成功(计算结果满足区块头部的难度值)。然后,将 PoW 共识算法的相关参数和返回值,分别赋值给区块头部的 nonce 和 mixHash,作为参与过挖矿的(工作量)证明。


//Ethash.java:L343protected void postProcess(MiningResult result) {    Pair<byte[], byte[]> pair = hashimotoLight(block.getHeader(), result.nonce);    block.setNonce(longToBytes(result.nonce));//获取Ethash PoW参数中随机数,赋值给区块头部的nonce属性,对应黄皮书的167公式    block.setMixHash(pair.getLeft());//获取Ethash PoW函数返回值中的工作量证明哈希,赋值给区块头部的mixHash属性,对应黄皮书的168公式}
复制代码

交易列表

组成当前区块的一些交易,整个区块的核心内容,一个区块中包含多笔交易(详见 Transaction.java 的解读)。


//Block.java:L57private List<Transaction> transactionsList = new CopyOnWriteArrayList<>();
复制代码


单个区块可以包含的交易数量,与区块头部的 gasLimit 属性有关,因为每笔交易的执行都需要消耗一定数量的 gas,以太坊通过设定单个区块可以使用的 gas 数量,来控制区块大小。


思考:区块的合理大小,应该综合考虑技术架构、网络环境、交易吞吐量目标等因素。通过以太坊黄皮书的第 47 公式,能看到以太坊的设计支持区块 Gas 上限值的浮动,可以随着区块高度递增。

叔父区块头列表

祖先区块的兄弟(分叉)块,在给定高度范围内,选择一定数量的叔父区块头部进行打包。


//Block.java:L60private List<BlockHeader> uncleList = new CopyOnWriteArrayList<>();
复制代码


区块打包时,矿工可以在叔父块中按照规则选取一定数量的叔父块,将其区块头打包到区块中。具体选取方法是:


  1. 祖先区块范围:找到当前区块的最近 6 代的祖先区块,包含父区块、祖父区块、曾祖区块,以及再往前的 3 代。

  2. 叔父区块范围:寻找上述祖先区块的兄弟块,即与祖先区块的高度相同,且有共同的父区块。

  3. 在区块链网络中,叔父块代表过去一段时间出现过分叉,反应了有多个矿工节点在竞争同一区块的记账权,且在相差无几的时间内挖矿成功。而这些同一高度的区块,会按照规则选择其中一个写入账本(包含的交易生效),而其他的作为区块链的新分叉区块(包含的交易不生效)。

  4. 选择要打包的叔父块:在上诉叔父区块范围内选择 2 个,将其区块头部打包至新区块。

  5. 关于被选择的叔父块,虽然其包含的交易依然无法生效,无法收取交易费用(Gas),但是可以获得一定比例(与区块高度成正比)的挖矿奖励。这是以太坊对矿工利益的维护,降低矿工对分叉区块的顾虑,提高其参与以太坊网络建设的积极性。


//BlockMiner.java:L207protected List<BlockHeader> getUncles(Block mineBest) {    List<BlockHeader> ret = new ArrayList<>();    long miningNum = mineBest.getNumber() + 1;    Block mineChain = mineBest;    long limitNum = max(0, miningNum - UNCLE_GENERATION_LIMIT);    Set<ByteArrayWrapper> ancestors = BlockchainImpl.getAncestors(blockStore, mineBest, UNCLE_GENERATION_LIMIT + 1, true);    Set<ByteArrayWrapper> knownUncles = ((BlockchainImpl)blockchain).getUsedUncles(blockStore, mineBest, true);    knownUncles.addAll(ancestors);    knownUncles.add(new ByteArrayWrapper(mineBest.getHash()));    if (blockStore instanceof IndexedBlockStore) {        outer:        while (mineChain.getNumber() > limitNum) {            List<Block> genBlocks = ((IndexedBlockStore) blockStore).getBlocksByNumber(mineChain.getNumber());            if (genBlocks.size() > 1) {                for (Block uncleCandidate : genBlocks) {                    if (!knownUncles.contains(new ByteArrayWrapper(uncleCandidate.getHash())) &&                            ancestors.contains(new ByteArrayWrapper(blockStore.getBlockByHash(uncleCandidate.getParentHash()).getHash()))) {                        ret.add(uncleCandidate.getHeader());                        if (ret.size() >= UNCLE_LIST_LIMIT) {                            break outer;                        }                    }                }            }            mineChain = blockStore.getBlockByHash(mineChain.getParentHash());        }    } else {        logger.warn("BlockStore is not instance of IndexedBlockStore: miner can't include uncles");    }    return ret;}
复制代码


思考:通过分析上述代码,发现在选择要打包的 2 个叔父块时,其逻辑是优先选择较高的叔父块,即从父区块开始往前查找。这样做的特点是,可以最大化当前区块的叔父块奖励总额,对整个矿工群体是好的。如果想让自己的收益最大化,在现有逻辑上加个条件,来判断叔父块矿工账户是否属于自己的,这段代码怎么改呢?

区块序列化

区块的 RLP 序列化的属性顺序:B<sub>H</sub>, B<sub>T</sub>, B<sub>U</sub>


//Block.java:L425public byte[] getEncoded() {//行425-436:getEncoded,函数,获取区块内容的RLP编码字节数组,参与RLP编码的区块属性依次为:header(含nonce)、transactionsList、uncleList。    if (rlpEncoded == null) {//此函数被当前类和BlockchainImpl.java、IndexedBlockStore.java、BlockMiner.java、SyncManager.java等源文件引用。        byte[] header = this.header.getEncoded();
List<byte[]> block = getBodyElements();//获取区块体:交易列表、叔父块列表 block.add(0, header);//区块序列化顺序:区块头、区块列表、叔父块列表 byte[][] elements = block.toArray(new byte[block.size()][]);
this.rlpEncoded = RLP.encodeList(elements);//行426-433:区块的RLP序列化的属性顺序:区块头部、交易列表、叔父块列表,对应黄皮书的35、36公式 } return rlpEncoded;}
复制代码


发布于: 刚刚阅读数: 2
用户头像

起航

关注

还未添加个人签名 2020.10.10 加入

还未添加个人简介

评论

发布
暂无评论
以太坊剖析 - 区块(Block)