作者:明神特烦恼
公众号:明神特烦恼
提案一般是共识流程中第一步,后面步骤为多阶段的投票,最终达到一致。这里分析提案将独立于共识,分析如何生成待共识的消息。提案的高度信息、提案人确认等由其他共识模块传入,这里不做分析。
带着问题读代码:
1)提案的触发点有哪些?
2)从交易池获取的交易集合,作为待提案的消息,还要经过哪些检查?
3)提案的最终数据结构是什么?
4)生成的提案消息的生命周期是如何管理的?
5)交易的读写集是如何生成的?
触发点入口比较收敛,在下面的函数中,有两个条件可以触发提案:
1)提案定时器proposeTimer
到达定时时间
2)交易池打包成批
RetryAndRemoveTxs
case <-bp.proposeTimer.C:
if !bp.isSelfProposer() {
break
}
go bp.proposeBlock()
case signal := <-bp.txPoolSignalC:
if !bp.isSelfProposer() {
break
}
if signal.SignalType != txpoolpb.SignalType_BLOCK_PROPOSE {
break
}
go bp.proposeBlock()
case <-bp.exitC:
bp.proposeTimer.Stop()
bp.log.Info("block proposer loop stoped")
return
}
触发:
检测函数:txDuplicateCheck
,主要逻辑为并发查询数据库是否有相同的Txid,如果有则剔除。
在长安链共识结束后,需要同步将成功共识的交易从交易池中剔除,以防止提案过程中打包重复交易。
根据在之前的章节分析,交易进入交易池前已经判断过交易的Txid是否存在,但在提案前又进行检测,为什么这里会检测是否有重复交易?
答:交易池会同步接收各节点广播的batch,节点会检测这些batch是否在已有区块中有重复的Txid,但为了提高性能并没有加锁检测,这里并不会保证过来的batch中没有冲突Txid。也就是节点的各个Batch会有重复的Txid。
**此处留下思考点一: txDuplicateCheck
是会从数据库中比较Txid是否冲突,能否减少这部分读取数据库的操作?
提案的最终信息为:
type Block struct {
// header of the block
Header *BlockHeader `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
// execution sequence of intra block transactions is generated by proposer
Dag *DAG `protobuf:"bytes,2,opt,name=dag,proto3" json:"dag,omitempty"`
Txs []*Transaction `protobuf:"bytes,3,rep,name=txs,proto3" json:"txs,omitempty"`
// stores the voting information of the current block
// not included in block hash value calculation
AdditionalData *AdditionalData `protobuf:"bytes,4,opt,name=additional_data,json=additionalData,proto3" json:"additional_data,omitempty"`
}
上面流程分析一个提案Block是如何产生的,那提案过程中的提案Block如何保存、提案成功落块后如何清理?
长安链保存提案消息的数据结构为,主要结构为lastProposedBlock
, 存储的数据结构注释写的很清晰map[height]map[hash]*blockProposal
,即每个块高度会有多个提案,每个提案以Hash作为索引进行检索。
PS: ledgerCache 可否不暴露给其他模块,对于其他系统组件,他们认为账本信息应该从账本模块获取,而不是另外一个缓存模块。
type ProposalCache struct {
// block height -> block hash -> block with rw set
// since one block height may have multiple block proposals
lastProposedBlock map[int64]map[string]*blockProposal
rwMu sync.RWMutex
chainConf protocol.ChainConf
ledgerCache protocol.LedgerCache
}
下面我们来分析lastProposedBlock 是如何管理提案区块的。
SetProposedBlock
负责记录提案块到缓存,录入时机为:
generateNewBlock
中VerifyBlock
中SetProposedAt
负责设置某提案块为正在执行共识的提案块(当前轮次round)。记录时机为:
generateNewBlock
中SetProposedAt
修改状态,在函数proposing
中ResetProposedAt
负责清空当前正在执行的共识为本提案块的标记。记录时机为:
OnReceiveProposeStatusChange
中ClearProposedBlockAt
负责清空某个height的所有提案块,操作时机为:
AddBlock
中ClearTheBlock
负责清除某个提案块,操作时机为:
proposing
中。KeepProposedBlock
负责获取并清除某个高度下非指定hash的所有提案块,操作时机为:
世界状态是一组key-vaule集合,为每个key-value 添加 version属性,来实现多版本并发控制(MVCC)。
假设已存在世界状态(K1,V1,1)、(K2,V2,1)、(K3,V3,1)、(K4,V4,1)、(K5,V5,1)
,现有2笔交易并发T1 -> Read(K1) Write(K1,V1`)
、T2 -> Read(K1) Write(K1,V1``)
。
T1:读取K1、Version为1;写入K1,Version为2。
T2:读取K1、Version为1;写入K1,Version为2。
当这两笔交易发送至验证节点,会被识别为读写集冲突,为了便于理解,这里举个例子。张三账户原有金额10元,将T1、T2理解为给张三账户增加10元,那么执行完成张三账户应有30元。但由于读写集冲突,最终张三的账户只有20元,这种情况是不被允许的。
官方参考文档:https://docs.chainmaker.org.cn/tech/%E5%B9%B6%E8%A1%8C%E8%B0%83%E5%BA%A6.html
在提案期间对提案的交易集合生成读写集,在函数Schedule
中。
bp.txScheduler.Schedule(block, validatedTxs, snapshot)
长安链除了构造读写集外,还构造DAG有向无环图,将交易的关联关系以数据结构形式记录,记录的目的是更快的验证区块有效性。
读写集生成流程:
ApplyTxSimContext
判断是否与已生成读写集的交易存在冲突。如果有重复,则重新执行VM生成新的读写集。generateNewBlock
中。 1. 关联关系判定
当两笔交易顺序互换,将产生不同结果的都属于有关联的交易。
1) t1: Read(K1) t2:Write(K1, V1)
2) t1:Write(K1, V1) t2: Read(K1)
3) t1:Write(K1,V1) t2:Write(K1,V2)
生成DAG的第一步是找到上述三种关系,具体实现如下:
1)调用buildRWBitmaps
函数,将原有交易读写集使用bitmap表示。将原读写集中的每个key转化为一个int类型,通过[]*bitmap.Bitmap
数据结构标记每笔交易涉及到的key,该函数返回readBitmap
、writeBitmap
。
2)调用buildCumulativeBitmap
函数,对读集和写集分别做叠加(或操作),例如:第一笔交易读集的bitmap为 1 0 1 1 0
,其中每个bit位为1的位置表示一个读集的key。第二笔交易读集的bitmap为 0 1 0 1 0
,那么经过叠加后的第二笔交易的读集为 1 1 1 1 0
,后面依次对前面交易进行叠加,最终形成叠加后的读集与写集,cumulativeReadBitmap
、cumulativeWriteBitmap
。
3)通过第一步与第二步形成的结果,来判断哪些交易存在关联性。
3.1)第 readBitmap[i]
与cumulativeWriteBitmap[i-1]
中指向的key一致。
3.2)第 writeBitmap[i]
与cumulativeReadBitmap[i-1]
中指向的key一致。
3.3)第 writeBitmap[i]
与cumulativeWriteBitmap[i-1]
中指向的key一致。
这三个判定条件刚好符合上面提到的关联关系判定。
4)记录关联关系。经过第三步判断某个交易与前面叠加后的交易有关联,这里需要进一步展开确定到底是与哪笔交易有关联。以3.1情况举例,将 readBitmap[i]
与 writeBitmap[0 ~ (i-1)]
逐一比较记录关联关系。
2. DAG表现方式
type DAG struct {
// sequence number of transaction topological sort
//the sequence number of the transaction topological sort associated with the transaction
Vertexes []*DAG_Neighbor `protobuf:"bytes,2,rep,name=vertexes,proto3" json:"vertexes,omitempty"`
}
type DAG_Neighbor struct {
Neighbors []int32 `protobuf:"varint,1,rep,packed,name=neighbors,proto3" json:"neighbors,omitempty"`
}
其中Vertexes[i]
表示第i笔交易,Neighbors
数组中每个值表示与之关联的交易编号。
3. DAG的使用
DAG的使用在验证区块模块中,本章节主要介绍提案流程,这部分会在介绍区块验证流程时详细阐述。
关注作者,共同学习区块链技术。