diff options
Diffstat (limited to 'vendor/github.com/dexon-foundation/dexon-consensus-core/core/total-ordering.go')
-rw-r--r-- | vendor/github.com/dexon-foundation/dexon-consensus-core/core/total-ordering.go | 1355 |
1 files changed, 0 insertions, 1355 deletions
diff --git a/vendor/github.com/dexon-foundation/dexon-consensus-core/core/total-ordering.go b/vendor/github.com/dexon-foundation/dexon-consensus-core/core/total-ordering.go deleted file mode 100644 index a4778f593..000000000 --- a/vendor/github.com/dexon-foundation/dexon-consensus-core/core/total-ordering.go +++ /dev/null @@ -1,1355 +0,0 @@ -// Copyright 2018 The dexon-consensus Authors -// This file is part of the dexon-consensus library. -// -// The dexon-consensus library is free software: you can redistribute it -// and/or modify it under the terms of the GNU Lesser General Public License as -// published by the Free Software Foundation, either version 3 of the License, -// or (at your option) any later version. -// -// The dexon-consensus library is distributed in the hope that it will be -// useful, but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser -// General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the dexon-consensus library. If not, see -// <http://www.gnu.org/licenses/>. - -package core - -import ( - "errors" - "math" - "sort" - "sync" - "time" - - "github.com/dexon-foundation/dexon-consensus/common" - "github.com/dexon-foundation/dexon-consensus/core/types" -) - -const ( - infinity uint64 = math.MaxUint64 -) - -const ( - // TotalOrderingModeError returns mode error. - TotalOrderingModeError uint32 = iota - // TotalOrderingModeNormal returns mode normal. - TotalOrderingModeNormal - // TotalOrderingModeEarly returns mode early. - TotalOrderingModeEarly - // TotalOrderingModeFlush returns mode flush. - TotalOrderingModeFlush -) - -var ( - // ErrNotValidDAG would be reported when block subbmitted to totalOrdering - // didn't form a DAG. - ErrNotValidDAG = errors.New("not a valid dag") - // ErrFutureRoundDelivered means some blocks from later rounds are - // delivered, this means program error. - ErrFutureRoundDelivered = errors.New("future round delivered") - // ErrBlockFromPastRound means we receive some block from past round. - ErrBlockFromPastRound = errors.New("block from past round") - // ErrTotalOrderingHangs means total ordering hangs somewhere. - ErrTotalOrderingHangs = errors.New("total ordering hangs") - // ErrForwardAck means a block acking some blocks from newer round. - ErrForwardAck = errors.New("forward ack") - // ErrUnexpected means general (I'm lazy) errors. - ErrUnexpected = errors.New("unexpected") -) - -// totalOrderingConfig is the configuration for total ordering. -type totalOrderingConfig struct { - roundBasedConfig - // k represents the k in 'k-level total ordering'. - // In short, only block height equals to (global minimum height + k) - // would be taken into consideration. - k uint64 - // phi is a const to control how strong the leading preceding block - // should be. - phi uint64 - // chainNum is the count of chains. - numChains uint32 - // Is round cutting required? - isFlushRequired bool -} - -func (config *totalOrderingConfig) fromConfig( - roundID uint64, cfg *types.Config) { - config.k = uint64(cfg.K) - config.numChains = cfg.NumChains - config.phi = uint64(float32(cfg.NumChains-1)*cfg.PhiRatio + 1) - config.setupRoundBasedFields(roundID, cfg) -} - -func newGenesisTotalOrderingConfig( - dMoment time.Time, config *types.Config) *totalOrderingConfig { - c := &totalOrderingConfig{} - c.fromConfig(0, config) - c.setRoundBeginTime(dMoment) - return c -} - -func newTotalOrderingConfig( - prev *totalOrderingConfig, cur *types.Config) *totalOrderingConfig { - c := &totalOrderingConfig{} - c.fromConfig(prev.roundID+1, cur) - c.setRoundBeginTime(prev.roundEndTime) - prev.isFlushRequired = c.k != prev.k || - c.phi != prev.phi || - c.numChains != prev.numChains - return c -} - -// totalOrderingWinRecord caches which chains this candidate -// wins another one based on their height vector. -type totalOrderingWinRecord struct { - wins []int8 - count uint -} - -func (rec *totalOrderingWinRecord) reset() { - rec.count = 0 - for idx := range rec.wins { - rec.wins[idx] = 0 - } -} - -func newTotalOrderingWinRecord(numChains uint32) ( - rec *totalOrderingWinRecord) { - rec = &totalOrderingWinRecord{} - rec.reset() - rec.wins = make([]int8, numChains) - return -} - -// grade implements the 'grade' potential function described in white paper. -func (rec *totalOrderingWinRecord) grade( - numChains uint32, phi uint64, globalAnsLength uint64) int { - if uint64(rec.count) >= phi { - return 1 - } else if uint64(rec.count) < phi-uint64(numChains)+globalAnsLength { - return 0 - } else { - return -1 - } -} - -// totalOrderingHeightRecord records two things: -// - the minimum heiht of block from that chain acking this block. -// - the count of blocks from that chain acking this block. -type totalOrderingHeightRecord struct{ minHeight, count uint64 } - -// totalOrderingObjectCache caches objects for reuse. -// The target object is map because: -// - reuse map would prevent it grows during usage, when map grows, -// hashes of key would be recaculated, bucket reallocated, and values -// are copied. -// However, to reuse a map, we have no easy way to erase its content but -// iterating its keys and delete corresponding values. -type totalOrderingObjectCache struct { - ackedStatus [][]*totalOrderingHeightRecord - heightVectors [][]uint64 - winRecordContainers [][]*totalOrderingWinRecord - ackedVectors []map[common.Hash]struct{} - winRecordPool sync.Pool - numChains uint32 -} - -// newTotalOrderingObjectCache constructs an totalOrderingObjectCache -// instance. -func newTotalOrderingObjectCache(numChains uint32) *totalOrderingObjectCache { - return &totalOrderingObjectCache{ - winRecordPool: sync.Pool{ - New: func() interface{} { - return newTotalOrderingWinRecord(numChains) - }, - }, - numChains: numChains, - } -} - -// resize makes sure internal storage of totalOrdering instance can handle -// maximum possible numChains in future configs. -func (cache *totalOrderingObjectCache) resize(numChains uint32) { - // Basically, everything in cache needs to be cleaned. - if cache.numChains >= numChains { - return - } - cache.ackedStatus = nil - cache.heightVectors = nil - cache.winRecordContainers = nil - cache.ackedVectors = nil - cache.numChains = numChains - cache.winRecordPool = sync.Pool{ - New: func() interface{} { - return newTotalOrderingWinRecord(numChains) - }, - } -} - -// requestAckedStatus requests a structure to record acking status of one -// candidate (or a global view of acking status of pending set). -func (cache *totalOrderingObjectCache) requestAckedStatus() ( - acked []*totalOrderingHeightRecord) { - if len(cache.ackedStatus) == 0 { - acked = make([]*totalOrderingHeightRecord, cache.numChains) - for idx := range acked { - acked[idx] = &totalOrderingHeightRecord{count: 0} - } - } else { - acked, cache.ackedStatus = - cache.ackedStatus[len(cache.ackedStatus)-1], - cache.ackedStatus[:len(cache.ackedStatus)-1] - // Reset acked status. - for idx := range acked { - acked[idx].count = 0 - } - } - return -} - -// recycleAckedStatys recycles the structure to record acking status. -func (cache *totalOrderingObjectCache) recycleAckedStatus( - acked []*totalOrderingHeightRecord) { - // If the recycled objects supports lower numChains than we required, - // don't recycle it. - if uint32(len(acked)) != cache.numChains { - return - } - cache.ackedStatus = append(cache.ackedStatus, acked) -} - -// requestWinRecord requests an totalOrderingWinRecord instance. -func (cache *totalOrderingObjectCache) requestWinRecord() ( - win *totalOrderingWinRecord) { - win = cache.winRecordPool.Get().(*totalOrderingWinRecord) - win.reset() - return -} - -// recycleWinRecord recycles an totalOrderingWinRecord instance. -func (cache *totalOrderingObjectCache) recycleWinRecord( - win *totalOrderingWinRecord) { - if win == nil { - return - } - // If the recycled objects supports lower numChains than we required, - // don't recycle it. - if uint32(len(win.wins)) != cache.numChains { - return - } - cache.winRecordPool.Put(win) -} - -// requestHeightVector requests a structure to record acking heights -// of one candidate. -func (cache *totalOrderingObjectCache) requestHeightVector() (hv []uint64) { - if len(cache.heightVectors) == 0 { - hv = make([]uint64, cache.numChains) - } else { - hv, cache.heightVectors = - cache.heightVectors[len(cache.heightVectors)-1], - cache.heightVectors[:len(cache.heightVectors)-1] - } - for idx := range hv { - hv[idx] = infinity - } - return -} - -// recycleHeightVector recycles an instance to record acking heights -// of one candidate. -func (cache *totalOrderingObjectCache) recycleHeightVector(hv []uint64) { - // If the recycled objects supports lower numChains than we required, - // don't recycle it. - if uint32(len(hv)) != cache.numChains { - return - } - cache.heightVectors = append(cache.heightVectors, hv) -} - -// requestWinRecordContainer requests a map of totalOrderingWinRecord. -func (cache *totalOrderingObjectCache) requestWinRecordContainer() ( - con []*totalOrderingWinRecord) { - if len(cache.winRecordContainers) == 0 { - con = make([]*totalOrderingWinRecord, cache.numChains) - } else { - con, cache.winRecordContainers = - cache.winRecordContainers[len(cache.winRecordContainers)-1], - cache.winRecordContainers[:len(cache.winRecordContainers)-1] - for idx := range con { - con[idx] = nil - } - } - return -} - -// recycleWinRecordContainer recycles a map of totalOrderingWinRecord. -func (cache *totalOrderingObjectCache) recycleWinRecordContainer( - con []*totalOrderingWinRecord) { - // If the recycled objects supports lower numChains than we required, - // don't recycle it. - if uint32(len(con)) != cache.numChains { - return - } - cache.winRecordContainers = append(cache.winRecordContainers, con) -} - -// requestAckedVector requests an acked vector instance. -func (cache *totalOrderingObjectCache) requestAckedVector() ( - acked map[common.Hash]struct{}) { - if len(cache.ackedVectors) == 0 { - acked = make(map[common.Hash]struct{}) - } else { - acked, cache.ackedVectors = - cache.ackedVectors[len(cache.ackedVectors)-1], - cache.ackedVectors[:len(cache.ackedVectors)-1] - for k := range acked { - delete(acked, k) - } - } - return -} - -// recycleAckedVector recycles an acked vector instance. -func (cache *totalOrderingObjectCache) recycleAckedVector( - acked map[common.Hash]struct{}) { - if acked == nil { - return - } - cache.ackedVectors = append(cache.ackedVectors, acked) -} - -// totalOrderingCandidateInfo describes proceeding status for one candidate, -// including: -// - acked status as height records, which could keep 'how many blocks from -// one chain acking this candidate. -// - cached height vector, which valid height based on K-level used for -// comparison in 'grade' function. -// - cached result of grade function to other candidates. -// -// Height Record: -// When block A acks block B, all blocks proposed from the same proposer -// as block A with higher height would also acks block B. Therefore, -// we just need to record: -// - the minimum height of acking block from that proposer -// - count of acking blocks from that proposer -// to repsent the acking status for block A. -type totalOrderingCandidateInfo struct { - ackedStatus []*totalOrderingHeightRecord - cachedHeightVector []uint64 - winRecords []*totalOrderingWinRecord - hash common.Hash -} - -// newTotalOrderingCandidateInfo constructs an totalOrderingCandidateInfo -// instance. -func newTotalOrderingCandidateInfo( - candidateHash common.Hash, - objCache *totalOrderingObjectCache) *totalOrderingCandidateInfo { - return &totalOrderingCandidateInfo{ - ackedStatus: objCache.requestAckedStatus(), - winRecords: objCache.requestWinRecordContainer(), - hash: candidateHash, - } -} - -// clean clear information related to another candidate, which should be called -// when that candidate is selected as deliver set. -func (v *totalOrderingCandidateInfo) clean(otherCandidateChainID uint32) { - v.winRecords[otherCandidateChainID] = nil -} - -// recycle objects for later usage, this eases the loading of -// golangs' GC. -func (v *totalOrderingCandidateInfo) recycle( - objCache *totalOrderingObjectCache) { - if v.winRecords != nil { - for _, win := range v.winRecords { - objCache.recycleWinRecord(win) - } - objCache.recycleWinRecordContainer(v.winRecords) - } - if v.cachedHeightVector != nil { - objCache.recycleHeightVector(v.cachedHeightVector) - } - objCache.recycleAckedStatus(v.ackedStatus) -} - -// addBlock would update totalOrderingCandidateInfo, it's caller's duty -// to make sure the input block acutally acking the target block. -func (v *totalOrderingCandidateInfo) addBlock(b *types.Block) (err error) { - rec := v.ackedStatus[b.Position.ChainID] - if rec.count == 0 { - rec.minHeight = b.Position.Height - rec.count = 1 - } else { - if b.Position.Height < rec.minHeight { - err = ErrNotValidDAG - return - } - rec.count++ - } - return -} - -// getAckingNodeSetLength would generate the Acking Node Set and return its -// length. Only block height larger than -// -// global minimum height + k -// -// would be taken into consideration, ex. -// -// For some chain X: -// - the global minimum acking height = 1, -// - k = 1 -// then only block height >= 2 would be added to acking node set. -func (v *totalOrderingCandidateInfo) getAckingNodeSetLength( - global *totalOrderingCandidateInfo, - k uint64, - numChains uint32) (count uint64) { - var rec *totalOrderingHeightRecord - for idx, gRec := range global.ackedStatus[:numChains] { - if gRec.count == 0 { - continue - } - rec = v.ackedStatus[idx] - if rec.count == 0 { - continue - } - // This line would check if these two ranges would overlap: - // - (global minimum height + k, infinity) - // - (local minimum height, local minimum height + count - 1) - if rec.minHeight+rec.count-1 >= gRec.minHeight+k { - count++ - } - } - return -} - -// updateAckingHeightVector would cached acking height vector. -// -// Only block height equals to (global minimum block height + k) would be -// taken into consideration. -func (v *totalOrderingCandidateInfo) updateAckingHeightVector( - global *totalOrderingCandidateInfo, - k uint64, - dirtyChainIDs []int, - objCache *totalOrderingObjectCache) { - var ( - idx int - gRec, rec *totalOrderingHeightRecord - ) - // The reason not to merge the two loops is the iteration over map - // is expensive when chain count is large, iterating over dirty - // chains is cheaper. - // TODO(mission): merge the code in this if/else if the performance won't be - // downgraded when adding a function for the shared part. - if v.cachedHeightVector == nil { - // Generate height vector from scratch. - v.cachedHeightVector = objCache.requestHeightVector() - for idx, gRec = range global.ackedStatus { - if gRec.count <= k { - continue - } - rec = v.ackedStatus[idx] - if rec.count == 0 { - v.cachedHeightVector[idx] = infinity - } else if rec.minHeight <= gRec.minHeight+k { - // This check is sufficient to make sure the block height: - // - // gRec.minHeight + k - // - // would be included in this totalOrderingCandidateInfo. - v.cachedHeightVector[idx] = gRec.minHeight + k - } else { - v.cachedHeightVector[idx] = infinity - } - } - } else { - // Return the cached one, only update dirty fields. - for _, idx = range dirtyChainIDs { - gRec = global.ackedStatus[idx] - if gRec.count == 0 || gRec.count <= k { - v.cachedHeightVector[idx] = infinity - continue - } - rec = v.ackedStatus[idx] - if rec.count == 0 { - v.cachedHeightVector[idx] = infinity - } else if rec.minHeight <= gRec.minHeight+k { - v.cachedHeightVector[idx] = gRec.minHeight + k - } else { - v.cachedHeightVector[idx] = infinity - } - } - } - return -} - -// updateWinRecord setup win records between two candidates. -func (v *totalOrderingCandidateInfo) updateWinRecord( - otherChainID uint32, - other *totalOrderingCandidateInfo, - dirtyChainIDs []int, - objCache *totalOrderingObjectCache, - numChains uint32) { - var ( - idx int - height uint64 - ) - // The reason not to merge the two loops is the iteration over map - // is expensive when chain count is large, iterating over dirty - // chains is cheaper. - // TODO(mission): merge the code in this if/else if add a function won't - // affect the performance. - win := v.winRecords[otherChainID] - if win == nil { - win = objCache.requestWinRecord() - v.winRecords[otherChainID] = win - for idx, height = range v.cachedHeightVector[:numChains] { - if height == infinity { - continue - } - if other.cachedHeightVector[idx] == infinity { - win.wins[idx] = 1 - win.count++ - } - } - } else { - for _, idx = range dirtyChainIDs { - if v.cachedHeightVector[idx] == infinity { - if win.wins[idx] == 1 { - win.wins[idx] = 0 - win.count-- - } - continue - } - if other.cachedHeightVector[idx] == infinity { - if win.wins[idx] == 0 { - win.wins[idx] = 1 - win.count++ - } - } else { - if win.wins[idx] == 1 { - win.wins[idx] = 0 - win.count-- - } - } - } - } -} - -// totalOrderingBreakpoint is a record to store the height discontinuity -// on a chain. -type totalOrderingBreakpoint struct { - roundID uint64 - // height of last block in previous round. - lastHeight uint64 -} - -// totalOrderingGroupVector keeps global status of current pending set. -type totalOrderingGlobalVector struct { - // blocks stores all blocks grouped by their proposers and - // sorted by their block height. - // - // TODO(mission): the way we use this slice would make it reallocate - // frequently. - blocks [][]*types.Block - - // breakpoints caches rounds for chains that blocks' height on them are - // not continuous. Ex. - // ChainID Round Height - // 1 0 0 - // 1 0 1 - // 1 1 2 - // 1 1 3 - // 1 1 4 - // 1 3 0 <- a breakpoint for round 3 would be cached - // for chain 1 as (roundID=1, lastHeight=4). - breakpoints [][]*totalOrderingBreakpoint - - // curRound caches the last round ID used to purge breakpoints. - curRound uint64 - - // tips records the last seen block for each chain. - tips []*types.Block - - // cachedCandidateInfo is an totalOrderingCandidateInfo instance, - // which is just used for actual candidates to calculate height vector. - cachedCandidateInfo *totalOrderingCandidateInfo -} - -func newTotalOrderingGlobalVector(numChains uint32) *totalOrderingGlobalVector { - return &totalOrderingGlobalVector{ - blocks: make([][]*types.Block, numChains), - tips: make([]*types.Block, numChains), - breakpoints: make([][]*totalOrderingBreakpoint, numChains), - } -} - -func (global *totalOrderingGlobalVector) resize(numChains uint32) { - if len(global.blocks) >= int(numChains) { - return - } - // Resize blocks. - newBlocks := make([][]*types.Block, numChains) - copy(newBlocks, global.blocks) - global.blocks = newBlocks - // Resize breakpoints. - newBreakPoints := make([][]*totalOrderingBreakpoint, numChains) - copy(newBreakPoints, global.breakpoints) - global.breakpoints = newBreakPoints - // Resize tips. - newTips := make([]*types.Block, numChains) - copy(newTips, global.tips) - global.tips = newTips -} - -func (global *totalOrderingGlobalVector) switchRound(roundID uint64) { - if global.curRound+1 != roundID { - panic(ErrUnexpected) - } - global.curRound = roundID - for chainID, bs := range global.breakpoints { - if len(bs) == 0 { - continue - } - if bs[0].roundID == roundID { - global.breakpoints[chainID] = bs[1:] - } - } -} - -func (global *totalOrderingGlobalVector) prepareHeightRecord( - candidate *types.Block, - info *totalOrderingCandidateInfo, - acked map[common.Hash]struct{}) { - var ( - chainID = candidate.Position.ChainID - breakpoints = global.breakpoints[chainID] - breakpoint *totalOrderingBreakpoint - rec *totalOrderingHeightRecord - ) - // Setup height record for own chain. - rec = &totalOrderingHeightRecord{ - minHeight: candidate.Position.Height, - } - if len(breakpoints) == 0 { - rec.count = uint64(len(global.blocks[chainID])) - } else { - rec.count = breakpoints[0].lastHeight - candidate.Position.Height + 1 - } - info.ackedStatus[chainID] = rec - if acked == nil { - return - } - for idx, blocks := range global.blocks { - if idx == int(candidate.Position.ChainID) { - continue - } - breakpoint = nil - if len(global.breakpoints[idx]) > 0 { - breakpoint = global.breakpoints[idx][0] - } - for i, b := range blocks { - if breakpoint != nil && b.Position.Round >= breakpoint.roundID { - break - } - if _, acked := acked[b.Hash]; !acked { - continue - } - // If this block acks this candidate, all newer blocks - // from the same chain also 'indirect' acks it. - rec = info.ackedStatus[idx] - rec.minHeight = b.Position.Height - if breakpoint == nil { - rec.count = uint64(len(blocks) - i) - } else { - rec.count = breakpoint.lastHeight - b.Position.Height + 1 - } - break - } - } - -} - -func (global *totalOrderingGlobalVector) addBlock( - b *types.Block) (pos int, pending bool, err error) { - curPosition := b.Position - tip := global.tips[curPosition.ChainID] - pos = len(global.blocks[curPosition.ChainID]) - if tip != nil { - // Perform light weight sanity check based on tip. - lastPosition := tip.Position - if lastPosition.Round > curPosition.Round { - err = ErrNotValidDAG - return - } - if DiffUint64(lastPosition.Round, curPosition.Round) > 1 { - if curPosition.Height != 0 { - err = ErrNotValidDAG - return - } - // Add breakpoint. - global.breakpoints[curPosition.ChainID] = append( - global.breakpoints[curPosition.ChainID], - &totalOrderingBreakpoint{ - roundID: curPosition.Round, - lastHeight: lastPosition.Height, - }) - } else { - if curPosition.Height != lastPosition.Height+1 { - err = ErrNotValidDAG - return - } - } - } else { - if curPosition.Round < global.curRound { - err = ErrBlockFromPastRound - return - } - if curPosition.Round > global.curRound { - // Add breakpoint. - global.breakpoints[curPosition.ChainID] = append( - global.breakpoints[curPosition.ChainID], - &totalOrderingBreakpoint{ - roundID: curPosition.Round, - lastHeight: 0, - }) - } - } - breakpoints := global.breakpoints[b.Position.ChainID] - pending = len(breakpoints) > 0 && breakpoints[0].roundID <= b.Position.Round - global.blocks[b.Position.ChainID] = append( - global.blocks[b.Position.ChainID], b) - global.tips[b.Position.ChainID] = b - return -} - -// updateCandidateInfo udpate cached candidate info. -func (global *totalOrderingGlobalVector) updateCandidateInfo( - dirtyChainIDs []int, objCache *totalOrderingObjectCache) { - var ( - idx int - blocks []*types.Block - block *types.Block - info *totalOrderingCandidateInfo - rec *totalOrderingHeightRecord - breakpoint *totalOrderingBreakpoint - ) - if global.cachedCandidateInfo == nil { - info = newTotalOrderingCandidateInfo(common.Hash{}, objCache) - for idx, blocks = range global.blocks { - if len(blocks) == 0 { - continue - } - rec = info.ackedStatus[idx] - if len(global.breakpoints[idx]) > 0 { - breakpoint = global.breakpoints[idx][0] - block = blocks[0] - if block.Position.Round >= breakpoint.roundID { - continue - } - rec.minHeight = block.Position.Height - rec.count = breakpoint.lastHeight - block.Position.Height + 1 - } else { - rec.minHeight = blocks[0].Position.Height - rec.count = uint64(len(blocks)) - } - } - global.cachedCandidateInfo = info - } else { - info = global.cachedCandidateInfo - for _, idx = range dirtyChainIDs { - blocks = global.blocks[idx] - if len(blocks) == 0 { - info.ackedStatus[idx].count = 0 - continue - } - rec = info.ackedStatus[idx] - if len(global.breakpoints[idx]) > 0 { - breakpoint = global.breakpoints[idx][0] - block = blocks[0] - if block.Position.Round >= breakpoint.roundID { - continue - } - rec.minHeight = block.Position.Height - rec.count = breakpoint.lastHeight - block.Position.Height + 1 - } else { - rec.minHeight = blocks[0].Position.Height - rec.count = uint64(len(blocks)) - } - } - } - return -} - -// totalOrdering represent a process unit to handle total ordering -// for blocks. -type totalOrdering struct { - // pendings stores blocks awaiting to be ordered. - pendings map[common.Hash]*types.Block - - // The round of config used when performing total ordering. - curRound uint64 - - // duringFlush is a flag to switch the flush mode and normal mode. - duringFlush bool - - // flushReadyChains checks if the last block of that chain arrived. Once - // last blocks from all chains in current config are arrived, we can - // perform flush. - flushReadyChains map[uint32]struct{} - - // flush is a map to record which blocks are already flushed. - flushed map[uint32]struct{} - - // globalVector group all pending blocks by proposers and - // sort them by block height. This structure is helpful when: - // - // - build global height vector - // - picking candidates next round - globalVector *totalOrderingGlobalVector - - // candidates caches result of potential function during generating - // preceding sets. - candidates []*totalOrderingCandidateInfo - - // acked cache the 'block A acked by block B' relation by - // keeping a record in acked[A.Hash][B.Hash] - acked map[common.Hash]map[common.Hash]struct{} - - // dirtyChainIDs records which chainID that should be updated - // for all cached status (win record, acking status). - dirtyChainIDs []int - - // objCache caches allocated objects, like map. - objCache *totalOrderingObjectCache - - // candidateChainMapping keeps a mapping from candidate's hash to - // their chain IDs. - candidateChainMapping map[uint32]common.Hash - - // candidateChainIDs records chain ID of all candidates. - candidateChainIDs []uint32 - - // configs keeps configuration for each round in continuous way. - configs []*totalOrderingConfig -} - -// newTotalOrdering constructs an totalOrdering instance. -func newTotalOrdering(config *totalOrderingConfig) *totalOrdering { - globalVector := newTotalOrderingGlobalVector(config.numChains) - objCache := newTotalOrderingObjectCache(config.numChains) - candidates := make([]*totalOrderingCandidateInfo, config.numChains) - to := &totalOrdering{ - pendings: make(map[common.Hash]*types.Block), - globalVector: globalVector, - dirtyChainIDs: make([]int, 0, config.numChains), - acked: make(map[common.Hash]map[common.Hash]struct{}), - objCache: objCache, - candidateChainMapping: make(map[uint32]common.Hash), - candidates: candidates, - candidateChainIDs: make([]uint32, 0, config.numChains), - curRound: config.roundID, - } - to.configs = []*totalOrderingConfig{config} - return to -} - -// appendConfig add new configs for upcoming rounds. If you add a config for -// round R, next time you can only add the config for round R+1. -func (to *totalOrdering) appendConfig( - round uint64, config *types.Config) error { - if round != uint64(len(to.configs))+to.configs[0].roundID { - return ErrRoundNotIncreasing - } - to.configs = append( - to.configs, - newTotalOrderingConfig(to.configs[len(to.configs)-1], config)) - // Resize internal structures. - to.globalVector.resize(config.NumChains) - to.objCache.resize(config.NumChains) - if int(config.NumChains) > len(to.candidates) { - newCandidates := make([]*totalOrderingCandidateInfo, config.NumChains) - copy(newCandidates, to.candidates) - to.candidates = newCandidates - } - return nil -} - -func (to *totalOrdering) switchRound() { - to.curRound++ - to.globalVector.switchRound(to.curRound) -} - -// buildBlockRelation populates the acked according their acking relationships. -// This function would update all blocks implcitly acked by input block -// recursively. -func (to *totalOrdering) buildBlockRelation(b *types.Block) { - var ( - curBlock, nextBlock *types.Block - ack common.Hash - acked map[common.Hash]struct{} - exists, alreadyPopulated bool - toCheck = []*types.Block{b} - ) - for { - if len(toCheck) == 0 { - break - } - curBlock, toCheck = toCheck[len(toCheck)-1], toCheck[:len(toCheck)-1] - if curBlock.Position.Round > b.Position.Round { - // It's illegal for a block to acking some block from future - // round, this rule should be promised before delivering to - // total ordering. - panic(ErrForwardAck) - } - for _, ack = range curBlock.Acks { - if acked, exists = to.acked[ack]; !exists { - acked = to.objCache.requestAckedVector() - to.acked[ack] = acked - } - // This means we've walked this block already. - if _, alreadyPopulated = acked[b.Hash]; alreadyPopulated { - continue - } - acked[b.Hash] = struct{}{} - // See if we need to go forward. - if nextBlock, exists = to.pendings[ack]; !exists { - continue - } else { - toCheck = append(toCheck, nextBlock) - } - } - } -} - -// clean a block from working set. This behaviour would prevent -// our memory usage growing infinity. -func (to *totalOrdering) clean(b *types.Block) { - var ( - h = b.Hash - chainID = b.Position.ChainID - ) - to.objCache.recycleAckedVector(to.acked[h]) - delete(to.acked, h) - delete(to.pendings, h) - to.candidates[chainID].recycle(to.objCache) - to.candidates[chainID] = nil - delete(to.candidateChainMapping, chainID) - // Remove this candidate from candidate IDs. - to.candidateChainIDs = - removeFromSortedUint32Slice(to.candidateChainIDs, chainID) - // Clear records of this candidate from other candidates. - for _, idx := range to.candidateChainIDs { - to.candidates[idx].clean(chainID) - } -} - -// updateVectors is a helper function to update all cached vectors. -func (to *totalOrdering) updateVectors(b *types.Block) (pos int, err error) { - var ( - candidateHash common.Hash - chainID uint32 - acked bool - pending bool - ) - // Update global height vector - if pos, pending, err = to.globalVector.addBlock(b); err != nil { - return - } - if to.duringFlush { - // It makes no sense to calculate potential functions of total ordering - // when flushing would be happened. - return - } - if pending { - // The chain of this block contains breakpoints, which means their - // height are not continuous. This implementation of DEXON total - // ordering algorithm assumes the height of blocks in working set should - // be continuous. - // - // To workaround this issue, when block arrived after breakpoints, - // their information would not be contributed to current working set. - // This mechanism works because we switch rounds by flushing and - // reset the whole working set. - return - } - // Update acking status of candidates. - for chainID, candidateHash = range to.candidateChainMapping { - if _, acked = to.acked[candidateHash][b.Hash]; !acked { - continue - } - if err = to.candidates[chainID].addBlock(b); err != nil { - return - } - } - return -} - -// prepareCandidate is a helper function to -// build totalOrderingCandidateInfo for new candidate. -func (to *totalOrdering) prepareCandidate(candidate *types.Block) { - var ( - info = newTotalOrderingCandidateInfo(candidate.Hash, to.objCache) - chainID = candidate.Position.ChainID - ) - to.candidates[chainID] = info - to.candidateChainMapping[chainID] = candidate.Hash - // Add index to slot to allocated list, make sure the modified list sorted. - to.candidateChainIDs = append(to.candidateChainIDs, chainID) - sort.Slice(to.candidateChainIDs, func(i, j int) bool { - return to.candidateChainIDs[i] < to.candidateChainIDs[j] - }) - to.globalVector.prepareHeightRecord( - candidate, info, to.acked[candidate.Hash]) - return -} - -// isAckOnlyPrecedings is a helper function to check if a block -// only contain acks to delivered blocks. -func (to *totalOrdering) isAckOnlyPrecedings(b *types.Block) bool { - for _, ack := range b.Acks { - if _, pending := to.pendings[ack]; pending { - return false - } - } - return true -} - -// output is a helper function to finish the delivery of -// deliverable preceding set. -func (to *totalOrdering) output( - precedings map[common.Hash]struct{}, - numChains uint32) (ret []*types.Block) { - for p := range precedings { - // Remove the first element from corresponding blockVector. - b := to.pendings[p] - chainID := b.Position.ChainID - // TODO(mission): This way to use slice makes it reallocate frequently. - to.globalVector.blocks[int(chainID)] = - to.globalVector.blocks[int(chainID)][1:] - ret = append(ret, b) - // Remove block relations. - to.clean(b) - to.dirtyChainIDs = append(to.dirtyChainIDs, int(chainID)) - } - sort.Sort(types.ByHash(ret)) - // Find new candidates from tip of globalVector of each chain. - // The complexity here is O(N^2logN). - // TODO(mission): only those tips that acking some blocks in - // the devliered set should be checked. This - // improvment related to the latency introduced by K. - for chainID, blocks := range to.globalVector.blocks[:numChains] { - if len(blocks) == 0 { - continue - } - if _, picked := to.candidateChainMapping[uint32(chainID)]; picked { - continue - } - if !to.isAckOnlyPrecedings(blocks[0]) { - continue - } - // Build totalOrderingCandidateInfo for new candidate. - to.prepareCandidate(blocks[0]) - } - return ret -} - -// generateDeliverSet would: -// - generate preceding set -// - check if the preceding set deliverable by checking potential function -func (to *totalOrdering) generateDeliverSet() ( - delivered map[common.Hash]struct{}, mode uint32) { - var ( - chainID, otherChainID uint32 - info, otherInfo *totalOrderingCandidateInfo - precedings = make(map[uint32]struct{}) - cfg = to.configs[to.curRound-to.configs[0].roundID] - ) - mode = TotalOrderingModeNormal - to.globalVector.updateCandidateInfo(to.dirtyChainIDs, to.objCache) - globalInfo := to.globalVector.cachedCandidateInfo - for _, chainID = range to.candidateChainIDs { - to.candidates[chainID].updateAckingHeightVector( - globalInfo, cfg.k, to.dirtyChainIDs, to.objCache) - } - // Update winning records for each candidate. - // TODO(mission): It's not reasonable to - // request one routine for each candidate, the context - // switch rate would be high. - var wg sync.WaitGroup - wg.Add(len(to.candidateChainIDs)) - for _, chainID := range to.candidateChainIDs { - info = to.candidates[chainID] - go func(can uint32, canInfo *totalOrderingCandidateInfo) { - for _, otherChainID := range to.candidateChainIDs { - if can == otherChainID { - continue - } - canInfo.updateWinRecord( - otherChainID, - to.candidates[otherChainID], - to.dirtyChainIDs, - to.objCache, - cfg.numChains) - } - wg.Done() - }(chainID, info) - } - wg.Wait() - // Reset dirty chains. - to.dirtyChainIDs = to.dirtyChainIDs[:0] - // TODO(mission): ANS should be bound by current numChains. - globalAnsLength := globalInfo.getAckingNodeSetLength( - globalInfo, cfg.k, cfg.numChains) -CheckNextCandidateLoop: - for _, chainID = range to.candidateChainIDs { - info = to.candidates[chainID] - for _, otherChainID = range to.candidateChainIDs { - if chainID == otherChainID { - continue - } - otherInfo = to.candidates[otherChainID] - // TODO(mission): grade should be bound by current numChains. - if otherInfo.winRecords[chainID].grade( - cfg.numChains, cfg.phi, globalAnsLength) != 0 { - continue CheckNextCandidateLoop - } - } - precedings[chainID] = struct{}{} - } - if len(precedings) == 0 { - return - } - // internal is a helper function to verify internal stability. - internal := func() bool { - var ( - isPreceding, beaten bool - p uint32 - ) - for _, chainID = range to.candidateChainIDs { - if _, isPreceding = precedings[chainID]; isPreceding { - continue - } - beaten = false - for p = range precedings { - // TODO(mission): grade should be bound by current numChains. - if beaten = to.candidates[p].winRecords[chainID].grade( - cfg.numChains, cfg.phi, globalAnsLength) == 1; beaten { - break - } - } - if !beaten { - return false - } - } - return true - } - // checkAHV is a helper function to verify external stability. - // It would make sure some preceding block is strong enough - // to lead the whole preceding set. - checkAHV := func() bool { - var ( - height, count uint64 - p uint32 - ) - for p = range precedings { - count = 0 - info = to.candidates[p] - for _, height = range info.cachedHeightVector { - if height != infinity { - count++ - if count > cfg.phi { - return true - } - } - } - } - return false - } - // checkANS is a helper function to verify external stability. - // It would make sure all preceding blocks are strong enough - // to be delivered. - checkANS := func() bool { - var chainAnsLength uint64 - for p := range precedings { - // TODO(mission): ANS should be bound by current numChains. - chainAnsLength = to.candidates[p].getAckingNodeSetLength( - globalInfo, cfg.k, cfg.numChains) - if uint64(chainAnsLength) < uint64(cfg.numChains)-cfg.phi { - return false - } - } - return true - } - // If all chains propose enough blocks, we should force - // to deliver since the whole picture of the DAG is revealed. - if globalAnsLength != uint64(cfg.numChains) { - // Check internal stability first. - if !internal() { - return - } - - // The whole picture is not ready, we need to check if - // exteranl stability is met, and we can deliver earlier. - if checkAHV() && checkANS() { - mode = TotalOrderingModeEarly - } else { - return - } - } - delivered = make(map[common.Hash]struct{}) - for p := range precedings { - delivered[to.candidates[p].hash] = struct{}{} - } - return -} - -// flushBlocks flushes blocks. -func (to *totalOrdering) flushBlocks( - b *types.Block) (flushed []*types.Block, mode uint32, err error) { - cfg := to.configs[to.curRound-to.configs[0].roundID] - mode = TotalOrderingModeFlush - if cfg.isValidLastBlock(b) { - to.flushReadyChains[b.Position.ChainID] = struct{}{} - } - // Flush blocks until last blocks from all chains are arrived. - if len(to.flushReadyChains) < int(cfg.numChains) { - return - } - if len(to.flushReadyChains) > int(cfg.numChains) { - // This line should never be reached. - err = ErrFutureRoundDelivered - return - } - // Dump all blocks in this round. - for { - if len(to.flushed) == int(cfg.numChains) { - break - } - // Dump all candidates without checking potential function. - flushedHashes := make(map[common.Hash]struct{}) - for _, chainID := range to.candidateChainIDs { - candidateBlock := to.pendings[to.candidates[chainID].hash] - if candidateBlock.Position.Round > to.curRound { - continue - } - flushedHashes[candidateBlock.Hash] = struct{}{} - } - if len(flushedHashes) == 0 { - err = ErrTotalOrderingHangs - return - } - flushedBlocks := to.output(flushedHashes, cfg.numChains) - for _, b := range flushedBlocks { - if !cfg.isValidLastBlock(b) { - continue - } - to.flushed[b.Position.ChainID] = struct{}{} - } - flushed = append(flushed, flushedBlocks...) - } - // Switch back to normal mode: delivered by DEXON total ordering algorithm. - to.duringFlush = false - to.flushed = make(map[uint32]struct{}) - to.flushReadyChains = make(map[uint32]struct{}) - // Clean all cached intermediate stats. - for idx := range to.candidates { - if to.candidates[idx] == nil { - continue - } - to.candidates[idx].recycle(to.objCache) - to.candidates[idx] = nil - } - to.dirtyChainIDs = nil - to.candidateChainMapping = make(map[uint32]common.Hash) - to.candidateChainIDs = nil - to.globalVector.cachedCandidateInfo = nil - to.switchRound() - // Force to pick new candidates. - numChains := to.configs[to.curRound-to.configs[0].roundID].numChains - to.output(map[common.Hash]struct{}{}, numChains) - return -} - -// deliverBlocks delivers blocks by DEXON total ordering algorithm. -func (to *totalOrdering) deliverBlocks() ( - delivered []*types.Block, mode uint32, err error) { - hashes, mode := to.generateDeliverSet() - cfg := to.configs[to.curRound-to.configs[0].roundID] - // output precedings - delivered = to.output(hashes, cfg.numChains) - // Check if any block in delivered set are the last block in this round - // of that chain. If yes, flush or round-switching would be performed. - for _, b := range delivered { - if b.Position.Round > to.curRound { - err = ErrFutureRoundDelivered - return - } - if !cfg.isValidLastBlock(b) { - continue - } - if cfg.isFlushRequired { - // Switch to flush mode. - to.duringFlush = true - to.flushReadyChains = make(map[uint32]struct{}) - to.flushed = make(map[uint32]struct{}) - } else { - // Switch round directly. - to.switchRound() - } - break - } - if to.duringFlush { - // Make sure last blocks from all chains are marked as 'flushed'. - for _, b := range delivered { - if !cfg.isValidLastBlock(b) { - continue - } - to.flushed[b.Position.ChainID] = struct{}{} - } - // Some last blocks for the round to be flushed might not be delivered - // yet. - for _, tip := range to.globalVector.tips[:cfg.numChains] { - if tip.Position.Round > to.curRound || cfg.isValidLastBlock(tip) { - to.flushReadyChains[tip.Position.ChainID] = struct{}{} - } - } - } - return -} - -// processBlock is the entry point of totalOrdering. -func (to *totalOrdering) processBlock( - b *types.Block) ([]*types.Block, uint32, error) { - // NOTE: I assume the block 'b' is already safe for total ordering. - // That means, all its acking blocks are during/after - // total ordering stage. - cfg := to.configs[to.curRound-to.configs[0].roundID] - to.pendings[b.Hash] = b - to.buildBlockRelation(b) - pos, err := to.updateVectors(b) - if err != nil { - return nil, uint32(0), err - } - // Mark the proposer of incoming block as dirty. - if b.Position.ChainID < cfg.numChains { - to.dirtyChainIDs = append(to.dirtyChainIDs, int(b.Position.ChainID)) - _, picked := to.candidateChainMapping[b.Position.ChainID] - if pos == 0 && !picked { - if to.isAckOnlyPrecedings(b) { - to.prepareCandidate(b) - } - } - } - if to.duringFlush { - return to.flushBlocks(b) - } - return to.deliverBlocks() -} |