星云链并行交易执行逻辑
总体来说分三步:
第一步准备世界状态
txWorldState, err := block.WorldState().Prepare(tx.Hash().String())
func (ws *worldState) Prepare(txid interface{}) (TxWorldState, error) {
s, err := ws.states.Prepare(txid)
if err != nil {
return nil, err
}
txState := &txWorldState{
states: s,
txid: txid,
parent: ws,
}
return txState, nil
}
这个产生的txWorldState相当于包含三部分内容,一个是使用txid生成的一个新的世界状态,一个之前的总的世界状态ws,一个是txid值。
第二步,执行交易
giveback, err := block.ExecuteTransaction(tx, txWorldState)
其中执行交易又分三个小步骤。
其中VerifyExection方法会执行以下逻辑:
Tx的世界状态将会存入from和to(如果在之前的世界状态不存在该from和to地址)的地址,
fromAcc, err := ws.GetOrCreateUserAccount(tx.from.address)
toAcc, err := ws.GetOrCreateUserAccount(tx.to.address)
记录gas的花费,最终会存入一个gasConsumed的map里。
return ws.RecordGas(tx.from.String(), gasCost)
func (tx *Transaction) recordGas(gasCnt *util.Uint128, ws WorldState) error {
gasCost, err := tx.GasPrice().Mul(gasCnt)
if err != nil {
return err
}
return ws.RecordGas(tx.from.String(), gasCost)
}
func (s *states) RecordGas(from string, gas *util.Uint128) error {
consumed, ok := s.gasConsumed[from]
if !ok {
consumed = util.NewUint128()
}
consumed, err := consumed.Add(gas)
if err != nil {
return err
}
s.gasConsumed[from] = consumed
return nil
}
记录tx的事件
event := &state.Event{
Topic: TopicTransactionExecutionResult,
Data: string(txData),
}
ws.RecordEvent(tx.hash, event)
而RecordEvent利用v8引擎执行把源码编译成llvm的代码,得到的结果作为event存入世界状态
其中AcceptTransaction方法会把tx存入世界状态,并且获取tx的from地址,把地址的随机数加1.
第三步:合并世界状态
dependency, err := txWorldState.CheckAndUpdate()
func (tws *txWorldState) CheckAndUpdate() ([]interface{}, error) {
dependencies, err := tws.states.CheckAndUpdateTo(tws.parent.states)
if err != nil {
return nil, err
}
tws.parent = nil
return dependencies, nil
}
其中CheckAndUpdateTo方法实际上是计算出之前的世界状态和加入了这条tx的世界状态的依赖关系。
func (s *states) CheckAndUpdateTo(parent *states) ([]interface{}, error) {
dependency, err := s.changelog.CheckAndUpdate()
if err != nil {
return nil, err
}
_, err = s.stateDB.CheckAndUpdate()
if err != nil {
return nil, err
}
if err := parent.Replay(s); err != nil {
return nil, err
}
return dependency, nil
}
最后会调用changelLog和stateDB的CheckAndUpdate方法,而CheckAndUpdate方法最终会调用staging_table.go 里的MergeToParent方法
// CheckAndUpdate merge current changes to `FinalVersionizedValues`.
func (db *MVCCDB) CheckAndUpdate() ([]interface{}, error) {
db.mutex.Lock()
defer db.mutex.Unlock()
if !db.isInTransaction {
return nil, ErrTransactionNotStarted
}
if !db.isPreparedDB {
return nil, ErrDisallowedCallingInNoPreparedDB
}
if db.isPreparedDBClosed {
return nil, ErrPreparedDBIsClosed
}
ret, err := db.stagingTable.MergeToParent()
if err == nil {
// cleanup.
db.stagingTable.Purge()
}
return ret, err
}
// MergeToParent merge key/value pair of tid to `finalVersionizedValues` which the version of value are the same.
func (tbl *StagingTable) MergeToParent() ([]interface{}, error) {
if tbl.parentStagingTable == nil {
return nil, ErrParentStagingTableIsNil
}
tbl.parentStagingTable.mutex.Lock()
defer tbl.parentStagingTable.mutex.Unlock()
tbl.mutex.Lock()
defer tbl.mutex.Unlock()
dependentTids := make(map[interface{}]bool)
conflictKeys := make(map[string]interface{})
// 1. check version.
targetValues := tbl.parentStagingTable.versionizedValues
for keyStr, fromValueItem := range tbl.versionizedValues {
targetValueItem := targetValues[keyStr]
if targetValueItem == nil {
continue
}
// 1. record conflict.
if fromValueItem.isConflict(targetValueItem, tbl.isTrieSameKeyCompatibility) {
conflictKeys[keyStr] = targetValueItem.tid
continue
}
// 2. record dependentTids.
// skip default value loaded from storage.
if targetValueItem.isDefault() {
continue
}
// ignore same parent tid for dependentTids.
if targetValueItem.tid == tbl.parentStagingTable.tid {
continue
}
// ignore version check when TrieSameKeyCompatibility is enabled.
if tbl.isTrieSameKeyCompatibility {
continue
}
dependentTids[targetValueItem.tid] = true
}
if len(conflictKeys) > 0 {
logging.VLog().WithFields(logrus.Fields{
"tid": tbl.tid,
"parentTid": tbl.parentStagingTable.tid,
"conflictKeys": conflictKeys,
}).Debug("Failed to be merged into parent.")
return nil, ErrStagingTableKeyConfliction
}
// 2. merge to final.
// incr parentStagingTable.globalVersion.
tbl.parentStagingTable.globalVersion++
for keyStr, fromValueItem := range tbl.versionizedValues {
// ignore default value item.
if fromValueItem.isDefault() {
continue
}
// ignore non-dirty.
if !fromValueItem.dirty {
continue
}
// merge.
value := fromValueItem.CloneForMerge(tbl.parentStagingTable.globalVersion)
targetValues[keyStr] = value
}
tids := make([]interface{}, 0, len(dependentTids))
for key := range dependentTids {
tids = append(tids, key)
}
return tids, nil
}
这个MergeToParent方法如果发现上一个世界状态和执行完tx的世界状态有冲突,则会直接报错,返回合并失败。记录下该tx,然后把冲突的tx重新放回txpoll池。
这里的tbl.versionizedValues都是在调用staging_table.go的Get方法时生成的,
// Get return value by key. If key does not exist, copy and incr version from `parentStagingTable` to record previous version.
func (tbl *StagingTable) Get(key []byte) (*VersionizedValueItem, error) {
return tbl.GetByKey(key, true)
}
// GetByKey return value by key. If key does not exist, copy and incr version from `parentStagingTable` to record previous version.
func (tbl *StagingTable) GetByKey(key []byte, loadFromStorage bool) (*VersionizedValueItem, error) {
// double check lock to prevent dead lock while call MergeToParent().
tbl.mutex.Lock()
keyStr := byteutils.Hex(key)
value := tbl.versionizedValues[keyStr]
tbl.mutex.Unlock()
if value == nil {
var err error
if tbl.parentStagingTable != nil {
value, err = tbl.parentStagingTable.GetByKey(key, loadFromStorage)
if err != nil {
return nil, err
}
// global version of keys are not the same, error.
if !tbl.disableStrictGlobalVersionCheck && value.globalVersion > tbl.prepareingGlobalVersion {
return nil, ErrStagingTableKeyConfliction
}
value = CloneVersionizedValueItem(tbl.tid, value)
} else {
if loadFromStorage {
// load from storage.
value, err = tbl.loadFromStorage(key)
if err != nil && err != storage.ErrKeyNotFound {
return nil, err
}
} else {
value = NewDefaultVersionizedValueItem(key, nil, tbl.tid, 0)
}
}
// lock and check again.
tbl.mutex.Lock()
regetValue := tbl.versionizedValues[keyStr]
if regetValue == nil {
tbl.versionizedValues[keyStr] = value
}
tbl.mutex.Unlock()
}
return value, nil
}
这里的Get逻辑是:如果key存在,则返回key的值,如果key不存在,并且key在父的世界状态存在,则返回父世界状态key的值,如果父世界状态不存在,则在本世界状态创建一个新的key,key的value为 nil,版本号为1.
世界状态里的key /value集合可能是:
From Address:From Address
To Address:To Address
Base58(From Address):gas费用
TxHash:event结果事件
(1)无依赖的情况:
之前总的世界状态的key/value为 a:version1 b: version1 1,c: version1 1,如果有一个新的tx1的世界状态为d: version1,则表示这个tx的dag依赖集合为空。
(2)有依赖的情况:
之前总的世界状态的key/value为 a: version1 b: version1,c: version1, 如果有一个新的tx1的世界状态为c:version1,d:version1,则表示这个tx依赖于之前的世界状态c。(version相同的情况一般发生在只读)Dag图为:
c ->Tx1
之前总的世界状态的key/value为 a: version1 b: version1,c: version1, 如果有一个新的tx2的世界状态为c:version1,b:version1,则表示这个tx依赖于之前的世界状态c和b。Dag图为:
c->Tx2
b->Tx2
(3)冲突的情况:
之前总的世界状态的key/value为 a: version1 b: version1,c: version1, 如果有一个新的tx1的世界状态为c:version2,d:version1,则表示这个tx和之前的世界状态有冲突,则会直接报错,返回合并失败。记录下该tx,然后把冲突的tx重新放回txpoll池。不会再加入dag。
worldState里的PutTx方法
func (s *states) PutTx(txHash byteutils.Hash, txBytes []byte) error {
_, err := s.txsState.Put(txHash, txBytes)
if err != nil {
return err
}
return nil
}
第四步:根据第三步得到的依赖的世界状态的key集合,在总dag对象里加入该tx的dag依赖集合。
transactions = append(transactions, tx)
txid := tx.Hash().String()
dag.AddNode(txid)
for _, node := range dependency {
dag.AddEdge(node, txid)
}
// AddNode add node
func (dag *Dag) AddNode(key interface{}) error {
if _, ok := dag.nodes[key]; ok {
return ErrKeyIsExisted
}
dag.nodes[key] = NewNode(key, dag.index)
dag.indexs[dag.index] = key
dag.index++
return nil
}
// AddEdge add edge fromKey toKey
func (dag *Dag) AddEdge(fromKey, toKey interface{}) error {
var from, to *Node
var ok bool
if from, ok = dag.nodes[fromKey]; !ok {
return ErrKeyNotFound
}
if to, ok = dag.nodes[toKey]; !ok {
return ErrKeyNotFound
}
for _, childNode := range from.children {
if childNode == to {
return ErrKeyIsExisted
}
}
dag.nodes[toKey].parentCounter++
dag.nodes[fromKey].children = append(from.children, to)
return nil
}
全部执行完后,得到的block的dag依赖集合形如:
c->Tx1
c->Tx2
b ->Tx2
…
这种方法和之前的做法的区别是:通过mvccdb去做,实际上是有锁的,而之前那种做法,实际上通过串行队列来代替了锁。
整体总结一下全部逻辑:
对一个区块里的所有tx直接开n个协程执行,n为区块里的tx数量。
每个协程的执行过程都会进行以下四步:
- 准备世界状态环境,得到使用该txid生成的一个新的世界状态txworldState和一个当前的总的世界状态ws。
- 检验tx并执行。会调用v8引擎执行,执行完把相应的数据存入新的世界状态。
- 合并新的世界状态到总的世界状态里,并得到该tx相对于当前的总的世界状态的依赖集合。如果遇到状态冲突或者其他异常情况,直接返回报错,把该tx重新放回txpoll。
- 循环把该tx和tx的依赖集合加入总的dag对象。