geth共识替换方法本文档基于geth v1.9.25 stable。目前内容基于代码阅读,还没有实际应用来检验。未来可能会进行修补。
在创世块中,config中的一个字段指示了链所用的共识算法,以及该算法所需的参数:
{
"config": {
"chainId": 114514,
"clique": {
"period": 5,
"epoch": 30000
},
...
创世块的config字段会被解析为在params/config.go中定义的ChainConfig类
为了支持在创世块中配置新的共识算法,需要在该类中增加属性,并通过标签指示和JSON字段的对应关系,如:
MyBft *MyBFTConfig `json:"mybft,omitempty"`
在上面的例子中,MyBFTConfig为一个我们自己定义的类,其中包含我们要实现的共识所需的属性
在读取创世块配置后,一个指向params.ChainConfig的指针会被传到eth/backend.go中的CreateConsensusEngine方法
// CreateConsensusEngine creates the required type of consensus engine instance for an Ethereum service
func CreateConsensusEngine(stack *node.Node, chainConfig *params.ChainConfig, config *ethash.Config, notify []string, noverify bool, db ethdb.Database) consensus.Engine {
// If proof-of-authority is requested, set it up
if chainConfig.Clique != nil {
return clique.New(chainConfig.Clique, db)
}
// Otherwise assume proof-of-work
...
该方法需要根据传入的链配置,返回一个实现了consensus.Engine接口的对象。我们在其中增加一个if,构造自己实现的对象即可。关于该接口的细节详见下文
consensus.Engine共识的核心接口,包含了有关提出区块、验证区块等的各种方法。新的共识算法需要实现下面的全部方法。
对于实现接口的包如何组织文件,并没有要求
Prepare在向新区块中添加交易前,会被调用,来向Header中填充交易无关的已知字段
FinalizeAndAssemble区块塞满交易后会被调用。若区块中的交易除了本身的转账和合约运行外,还通过其他方式(比如blockDAG中的区块奖励)对状态有影响,应该在这里更新状态数据库。在此之后,将不完整的区块头和区块交易列表等组装为待Seal的区块并返回
如果我们并不需要额外更新状态,用types.NewBlock(header, txs, uncles, receipts, new(trie.Trie))就可以完成组装
Seal组装好的区块发布前要做的工作。该方法异步返回,在完全准备好区块后才通过channel将区块交给上层
若我们要实现BFT共识中,需要进行额外的节点通信来获得多签。注意到event/event.go中,Post方法可以用于广播数据
VerifySeal验证一个区块头是否满足共识中的密码学要求(如PoW中哈希是否符合难度限制,以及PoA中验证签名)
之所以将密码学验证从区块中验证中提取出来,是因为geth支持Ultra Light Client (ULC)模式,该模式需要尽可能避免复杂计算
VerifyHeader大多数情况下,该方法检查从外界收到的区块头是否合法(考虑共识、时间戳、分叉等因素)
该方法有一个bool类型的参数seal,当seal为false时,最好避免运行VerifySeal中的密码学检查
VerifyHeaders一次性验证一批区块头,允许并发(当然也可以不并发)
在Quorum的IBFT中,以及原生的Clique中,都没有进行并发验证,原因未知。为了保险我们最好也不要并发了
verifyUncles与ethash中的BlockDAG有关
我们不需要考虑,直接通过就好
Finalize若外来区块中的交易除了本身的转账和合约运行外,还通过其他方式(比如blockDAG中的区块奖励)对状态有影响,应该在这里更新状态数据库
我们应该不需要在这里做任何事
Author输入块头,返回一个地址:在PoW中为块的Coinbase受益者,在PoA中可以自定义为其他地址,如共识中的某个签名者。
作为块的上下文,智能合约可能会使用该属性,因此需要保证一个块打包后,在不同节点上能得到相同的Author
APIs返回共识特有的RPC-API,用于注册到RPC服务器
这里参考consensus/ethash/api.go和consensus/ethash/ethash.go写就行
CalcDifficulty因为types.block.go的Header类中,将Difficulty写死在了区块头数据结构里,所以我们必须给每个区块设定一个难度值
在非PoW的共识中,可以像Quorum的IBFT一样直接返回0,也可以活用这一块空间放点东西,比如Clique就用它来表示是否"in turn"
SealHash输入一个未签名区块头,返回它的哈希
注意传入的区块头可能是带签名的,此时要主动去掉签名部分