长安链源码学习--提案(Proposer)(五)

作者:明神特烦恼
公众号:明神特烦恼

提案一般是共识流程中第一步,后面步骤为多阶段的投票,最终达到一致。这里分析提案将独立于共识,分析如何生成待共识的消息。提案的高度信息、提案人确认等由其他共识模块传入,这里不做分析。

带着问题读代码:
1)提案的触发点有哪些?
2)从交易池获取的交易集合,作为待提案的消息,还要经过哪些检查?
3)提案的最终数据结构是什么?
4)生成的提案消息的生命周期是如何管理的?
5)交易的读写集是如何生成的?

第一个问题:提案的触发点有哪些?

触发点入口比较收敛,在下面的函数中,有两个条件可以触发提案:
1)提案定时器proposeTimer到达定时时间

2)交易池打包成批

  • 生成批超时时间500ms
  • 某些交易重新放入交易池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是否冲突,能否减少这部分读取数据库的操作?

第三个问题: 提案的最终数据结构是什么?

提案的最终信息为:

  • Header 区块头信息,包括链ID、块高度、前块Hash、世界状态默克尔根等。
  • Dag 交易关联拓扑图,后续专门会讲解
  • Txs 交易信息集合
  • AdditionalData 扩展数据,例如:投票信息等。
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 是如何管理提案区块的。

1. SetProposedBlock

SetProposedBlock负责记录提案块到缓存,录入时机为:

  • 本节点生成提案块后,在函数generateNewBlock
  • 在共识期间验证区块有效性后,在函数VerifyBlock
2. SetProposedAt

SetProposedAt负责设置某提案块为正在执行共识的提案块(当前轮次round)。记录时机为:

  • 本节点生成提案块后,在函数generateNewBlock
  • 本节点生成提案过程中,如果已经存在某提案块,则直接调用SetProposedAt修改状态,在函数proposing
3. ResetProposedAt

ResetProposedAt负责清空当前正在执行的共识为本提案块的标记。记录时机为:

  • 本节点接收到共识发送的提案状态变更信号,在函数OnReceiveProposeStatusChange
4. ClearProposedBlockAt

ClearProposedBlockAt负责清空某个height的所有提案块,操作时机为:

  • 共识完成落块期间,在函数AddBlock
5. ClearTheBlock

ClearTheBlock负责清除某个提案块,操作时机为:

  • 当本节点提案时,发现之前在该height上的提案块已无法使用(前块Hash不匹配),需要删除。在函数proposing中。
6. KeepProposedBlock

KeepProposedBlock负责获取并清除某个高度下非指定hash的所有提案块,操作时机为:

  • 在区块验证期间,确定使用某个提案块,则其他相同高度的提案块需要删除,其中包含的、未使用的交易需要回归交易池。

第五个问题:交易的读写集是如何生成的?

1. 读写集:

   世界状态是一组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元,这种情况是不被允许的。

2. fabric与长安链读写集区别
  • fabric每笔交易先发送至节点背书,背书过程中获取读写集,也就是每笔交易并不知晓其他交易的存在,背书过程都是基于同一世界状态生成,在后续排序、验证时发现读写集冲突,将冲突交易设置为无效。
  • 长安链在发送交易时只有交易内容,并没有构造读写集。在提案过程中的所有交易生成读写集,理论上长安链并不会存在读写集冲突的情况,因为所有交易顺次执行即使有依赖,也会依赖前一交易的新读写集。长安链中的读写集冲突只发生在程序内部,外部并不感知。长安链是为了提高读写集生成效率,并发生成读写集,即使出现冲突也是内部处理逻辑,不影响程序执行。。
3. 长安链读写集生成

官方参考文档: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有向无环图,将交易的关联关系以数据结构形式记录,记录的目的是更快的验证区块有效性。

读写集生成流程:

  • 设置并发线程,所有交易并发执行VM,生成读写集。
  • 每笔交易生成读写集后,调用ApplyTxSimContext判断是否与已生成读写集的交易存在冲突。如果有重复,则重新执行VM生成新的读写集。
  • 第二步 由于读写集冲突会重新执行VM生成读写集,假设一批交易10W笔交易,每笔都是冲突的读写集,那么流程执行时间过长,也会影响共识的流程。因此增加超时定时器,超过指定时间(10s)发生截取,未执行的交易则不会进入本次提案。未进入提案的交易会重新归还给交易池,逻辑在generateNewBlock中。
       读写集的生成到此为止,看上去不需要生成DAG也可以发起提案流程,为什么需要DAG呢?
       答:因为上述的交易执行流程是并发的,每个节点的交易执行顺序是随机的,也就是其他节点无法构造出相同的最终读写集状态,需要有辅助数据提供节点的执行顺序。也可以表示出哪些交易可以并发验证,提高区块验证速度。
4. 长安链由读写集生成DAG

   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,该函数返回readBitmapwriteBitmap
    2)调用buildCumulativeBitmap函数,对读集和写集分别做叠加(或操作),例如:第一笔交易读集的bitmap为 1 0 1 1 0,其中每个bit位为1的位置表示一个读集的key。第二笔交易读集的bitmap为 0 1 0 1 0,那么经过叠加后的第二笔交易的读集为 1 1 1 1 0,后面依次对前面交易进行叠加,最终形成叠加后的读集与写集,cumulativeReadBitmapcumulativeWriteBitmap
    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的使用在验证区块模块中,本章节主要介绍提案流程,这部分会在介绍区块验证流程时详细阐述。
关注作者,共同学习区块链技术。

你可能感兴趣的:(长安链,区块链)