// 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 // . package test import ( "bytes" "fmt" "sync" "time" "github.com/dexon-foundation/dexon-consensus/common" "github.com/dexon-foundation/dexon-consensus/core/types" "github.com/dexon-foundation/dexon-consensus/core/utils" ) var ( // ErrEmptyDeliverSequence means there is no delivery event in this App // instance. ErrEmptyDeliverSequence = fmt.Errorf("empty deliver sequence") // ErrMismatchBlockHashSequence means the delivering sequence between two App // instances are different. ErrMismatchBlockHashSequence = fmt.Errorf("mismatch block hash sequence") // ErrMismatchRandomness means the randomness between two blocks with the // same hash from two App instances are different. ErrMismatchRandomness = fmt.Errorf("mismatch randomness") // ErrApplicationIntegrityFailed means the internal datum in a App instance // is not integrated. ErrApplicationIntegrityFailed = fmt.Errorf("application integrity failed") // ErrTimestampOutOfOrder means the later delivered block has timestamp // older than previous block. ErrTimestampOutOfOrder = fmt.Errorf("timestamp out of order") // ErrHeightOutOfOrder means the later delivered block has height not equal // to height of previous block plus one. ErrHeightOutOfOrder = fmt.Errorf("height out of order") // ErrDeliveredBlockNotConfirmed means some block delivered (confirmed) but // not confirmed. ErrDeliveredBlockNotConfirmed = fmt.Errorf("delivered block not confirmed") // ErrAckingBlockNotDelivered means the delivered sequence not forming a // DAG. ErrAckingBlockNotDelivered = fmt.Errorf("acking block not delivered") // ErrLowerPendingHeight raised when lastPendingHeight is lower than the // height to be prepared. ErrLowerPendingHeight = fmt.Errorf( "last pending height < consensus height") // ErrConfirmedHeightNotIncreasing raise when the height of the confirmed // block doesn't follow previous confirmed block on the same chain. ErrConfirmedHeightNotIncreasing = fmt.Errorf( "confirmed height not increasing") // ErrParentBlockNotDelivered raised when the parent block is not seen by // this app. ErrParentBlockNotDelivered = fmt.Errorf("parent block not delivered") // ErrMismatchDeliverPosition raised when the block hash and position are // mismatched when calling BlockDelivered. ErrMismatchDeliverPosition = fmt.Errorf("mismatch deliver position") // ErrEmptyRandomness raised when the block contains empty randomness. ErrEmptyRandomness = fmt.Errorf("empty randomness") // ErrInvalidHeight refers to invalid value for block height. ErrInvalidHeight = fmt.Errorf("invalid height") ) // AppDeliveredRecord caches information when this application received // a block delivered notification. type AppDeliveredRecord struct { Rand []byte When time.Time Pos types.Position } // App implements Application interface for testing purpose. type App struct { Confirmed map[common.Hash]*types.Block LastConfirmedHeight uint64 confirmedLock sync.RWMutex Delivered map[common.Hash]*AppDeliveredRecord DeliverSequence common.Hashes deliveredLock sync.RWMutex state *State gov *Governance rEvt *utils.RoundEvent hEvt *common.Event roundToNotify uint64 } // NewApp constructs a TestApp instance. func NewApp(initRound uint64, gov *Governance, rEvt *utils.RoundEvent) ( app *App) { app = &App{ Confirmed: make(map[common.Hash]*types.Block), Delivered: make(map[common.Hash]*AppDeliveredRecord), DeliverSequence: common.Hashes{}, gov: gov, rEvt: rEvt, hEvt: common.NewEvent(), roundToNotify: initRound, } if gov != nil { app.state = gov.State() } if rEvt != nil { rEvt.Register(func(evts []utils.RoundEventParam) { app.hEvt.RegisterHeight( evts[len(evts)-1].NextRoundValidationHeight(), utils.RoundEventRetryHandlerGenerator(rEvt, app.hEvt), ) }) rEvt.TriggerInitEvent() } return app } // PreparePayload implements Application interface. func (app *App) PreparePayload(position types.Position) ([]byte, error) { if app.state == nil { return []byte{}, nil } return app.state.PackRequests() } // PrepareWitness implements Application interface. func (app *App) PrepareWitness(height uint64) (types.Witness, error) { // Although we only perform reading operations here, to make sure what we // prepared unique under concurrent access to this method, writer lock is // used. app.deliveredLock.Lock() defer app.deliveredLock.Unlock() hash, lastRec := app.LastDeliveredRecordNoLock() if lastRec == nil { return types.Witness{}, nil } if lastRec.Pos.Height < height { return types.Witness{}, ErrLowerPendingHeight } return types.Witness{ Height: lastRec.Pos.Height, Data: hash.Bytes(), }, nil } // VerifyBlock implements Application interface. func (app *App) VerifyBlock(block *types.Block) types.BlockVerifyStatus { // Make sure we can handle the witness carried by this block. app.deliveredLock.RLock() defer app.deliveredLock.RUnlock() _, rec := app.LastDeliveredRecordNoLock() if rec != nil && rec.Pos.Height < block.Witness.Height { return types.VerifyRetryLater } // Confirm if the consensus height matches corresponding block hash. var h common.Hash copy(h[:], block.Witness.Data) app.confirmedLock.RLock() defer app.confirmedLock.RUnlock() if block.Witness.Height >= types.GenesisHeight { // Make sure the hash and height are matched. confirmed, exist := app.Confirmed[h] if !exist || block.Witness.Height != confirmed.Position.Height { return types.VerifyInvalidBlock } } if block.Position.Height != types.GenesisHeight { // This check is copied from fullnode, below is quoted from coresponding // comment: // // Check if target block is the next height to be verified, we can only // verify the next block in a given chain. if app.LastConfirmedHeight+1 != block.Position.Height { return types.VerifyRetryLater } } return types.VerifyOK } // BlockConfirmed implements Application interface. func (app *App) BlockConfirmed(b types.Block) { app.confirmedLock.Lock() defer app.confirmedLock.Unlock() app.Confirmed[b.Hash] = &b if app.LastConfirmedHeight+1 != b.Position.Height { panic(ErrConfirmedHeightNotIncreasing) } app.LastConfirmedHeight = b.Position.Height } // ClearUndeliveredBlocks -- func (app *App) ClearUndeliveredBlocks() { app.deliveredLock.RLock() defer app.deliveredLock.RUnlock() app.confirmedLock.Lock() defer app.confirmedLock.Unlock() app.LastConfirmedHeight = uint64(len(app.DeliverSequence)) } // BlockDelivered implements Application interface. func (app *App) BlockDelivered(blockHash common.Hash, pos types.Position, rand []byte) { func() { app.deliveredLock.Lock() defer app.deliveredLock.Unlock() app.Delivered[blockHash] = &AppDeliveredRecord{ Rand: common.CopyBytes(rand), When: time.Now().UTC(), Pos: pos, } if len(app.DeliverSequence) > 0 { // Make sure parent block also delivered. lastHash := app.DeliverSequence[len(app.DeliverSequence)-1] d, exists := app.Delivered[lastHash] if !exists { panic(ErrParentBlockNotDelivered) } if d.Pos.Height+1 != pos.Height { panic(ErrHeightOutOfOrder) } } app.DeliverSequence = append(app.DeliverSequence, blockHash) }() // Apply packed state change requests in payload. func() { if app.state == nil { return } app.confirmedLock.RLock() defer app.confirmedLock.RUnlock() b, exists := app.Confirmed[blockHash] if !exists { panic(ErrDeliveredBlockNotConfirmed) } if !b.Position.Equal(pos) { panic(ErrMismatchDeliverPosition) } if err := app.state.Apply(b.Payload); err != nil { if err != ErrDuplicatedChange { panic(err) } } if app.roundToNotify == pos.Round { if app.gov != nil { app.gov.NotifyRound(app.roundToNotify, pos.Height) app.roundToNotify++ } } }() app.hEvt.NotifyHeight(pos.Height) } // GetLatestDeliveredPosition would return the latest position of delivered // block seen by this application instance. func (app *App) GetLatestDeliveredPosition() types.Position { app.deliveredLock.RLock() defer app.deliveredLock.RUnlock() app.confirmedLock.RLock() defer app.confirmedLock.RUnlock() if len(app.DeliverSequence) == 0 { return types.Position{} } return app.Confirmed[app.DeliverSequence[len(app.DeliverSequence)-1]].Position } // Compare performs these checks against another App instance // and return erros if not passed: // - deliver sequence by comparing block hashes. // - consensus timestamp of each block are equal. func (app *App) Compare(other *App) (err error) { app.WithLock(func(app *App) { other.WithLock(func(other *App) { minLength := len(app.DeliverSequence) if minLength > len(other.DeliverSequence) { minLength = len(other.DeliverSequence) } if minLength == 0 { err = ErrEmptyDeliverSequence return } // Here we assumes both Apps begin from the same height. for idx, h := range app.DeliverSequence[:minLength] { hOther := other.DeliverSequence[idx] if hOther != h { err = ErrMismatchBlockHashSequence return } if bytes.Compare(app.Delivered[h].Rand, other.Delivered[h].Rand) != 0 { err = ErrMismatchRandomness return } } }) }) return } // Verify checks the integrity of date received by this App instance. func (app *App) Verify() error { app.confirmedLock.RLock() defer app.confirmedLock.RUnlock() app.deliveredLock.RLock() defer app.deliveredLock.RUnlock() if len(app.DeliverSequence) == 0 { return ErrEmptyDeliverSequence } if len(app.DeliverSequence) != len(app.Delivered) { return ErrApplicationIntegrityFailed } expectHeight := uint64(1) prevTime := time.Time{} for _, h := range app.DeliverSequence { _, exist := app.Confirmed[h] if !exist { return ErrDeliveredBlockNotConfirmed } _, exist = app.Delivered[h] if !exist { return ErrApplicationIntegrityFailed } b, exist := app.Confirmed[h] if !exist { return ErrApplicationIntegrityFailed } // Make sure the consensus time is incremental. if prevTime.After(b.Timestamp) { return ErrTimestampOutOfOrder } prevTime = b.Timestamp // Make sure the consensus height is incremental. rec, exist := app.Delivered[h] if !exist { return ErrApplicationIntegrityFailed } if len(rec.Rand) == 0 { return ErrEmptyRandomness } // Make sure height is valid. if b.Position.Height < types.GenesisHeight { return ErrInvalidHeight } if expectHeight != rec.Pos.Height { return ErrHeightOutOfOrder } expectHeight++ } return nil } // BlockReceived implements interface Debug. func (app *App) BlockReceived(hash common.Hash) {} // BlockReady implements interface Debug. func (app *App) BlockReady(hash common.Hash) {} // WithLock provides a backdoor to check status of App with reader lock. func (app *App) WithLock(function func(*App)) { app.confirmedLock.RLock() defer app.confirmedLock.RUnlock() app.deliveredLock.RLock() defer app.deliveredLock.RUnlock() function(app) } // LastDeliveredRecordNoLock returns the latest AppDeliveredRecord under lock. func (app *App) LastDeliveredRecordNoLock() (common.Hash, *AppDeliveredRecord) { var hash common.Hash if len(app.DeliverSequence) == 0 { return hash, nil } hash = app.DeliverSequence[len(app.DeliverSequence)-1] return hash, app.Delivered[hash] }