// 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 test
import (
"fmt"
"sync"
"time"
"github.com/dexon-foundation/dexon-consensus/common"
"github.com/dexon-foundation/dexon-consensus/core/types"
)
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")
// ErrMismatchConsensusTime means the consensus timestamp between two blocks
// with the same hash from two App instances are different.
ErrMismatchConsensusTime = fmt.Errorf("mismatch consensus time")
// ErrApplicationIntegrityFailed means the internal datum in a App instance
// is not integrated.
ErrApplicationIntegrityFailed = fmt.Errorf("application integrity failed")
// ErrConsensusTimestampOutOfOrder means the later delivered block has
// consensus timestamp older than previous block.
ErrConsensusTimestampOutOfOrder = fmt.Errorf(
"consensus timestamp out of order")
// ErrConsensusHeightOutOfOrder means the later delivered block has
// consensus height not equal to height of previous block plus one.
ErrConsensusHeightOutOfOrder = fmt.Errorf(
"consensus height out of order")
// ErrDeliveredBlockNotConfirmed means some block delivered (confirmed) but
// not confirmed.
ErrDeliveredBlockNotConfirmed = fmt.Errorf("delivered block not confirmed")
// ErrMismatchTotalOrderingAndDelivered mean the sequence of total ordering
// and delivered are different.
ErrMismatchTotalOrderingAndDelivered = fmt.Errorf(
"mismatch total ordering and delivered sequence")
// 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")
)
// This definition is copied from core package.
const (
// TotalOrderingModeError returns mode error.
TotalOrderingModeError uint32 = iota
// TotalOrderingModeNormal returns mode normal.
TotalOrderingModeNormal
// TotalOrderingModeEarly returns mode early.
TotalOrderingModeEarly
// TotalOrderingModeFlush returns mode flush.
TotalOrderingModeFlush
)
// AppTotalOrderRecord caches information when this application received
// a total-ordering deliver notification.
type AppTotalOrderRecord struct {
BlockHashes common.Hashes
Mode uint32
When time.Time
}
// AppDeliveredRecord caches information when this application received
// a block delivered notification.
type AppDeliveredRecord struct {
Result types.FinalizationResult
When time.Time
Pos types.Position
}
// App implements Application interface for testing purpose.
type App struct {
Confirmed map[common.Hash]*types.Block
LastConfirmedHeights map[uint32]uint64
confirmedLock sync.RWMutex
TotalOrdered []*AppTotalOrderRecord
TotalOrderedByHash map[common.Hash]*AppTotalOrderRecord
totalOrderedLock sync.RWMutex
Delivered map[common.Hash]*AppDeliveredRecord
DeliverSequence common.Hashes
deliveredLock sync.RWMutex
state *State
gov *Governance
lastPendingHeightLock sync.RWMutex
LastPendingHeight uint64
roundToNotify uint64
}
// NewApp constructs a TestApp instance.
func NewApp(initRound uint64, gov *Governance) (app *App) {
app = &App{
Confirmed: make(map[common.Hash]*types.Block),
LastConfirmedHeights: make(map[uint32]uint64),
TotalOrdered: []*AppTotalOrderRecord{},
TotalOrderedByHash: make(map[common.Hash]*AppTotalOrderRecord),
Delivered: make(map[common.Hash]*AppDeliveredRecord),
DeliverSequence: common.Hashes{},
gov: gov,
roundToNotify: initRound,
}
if gov != nil {
app.state = gov.State()
}
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) {
pendingHeight := app.getLastPendingWitnessHeight()
if pendingHeight < height {
return types.Witness{}, ErrLowerPendingHeight
}
if pendingHeight == 0 {
return types.Witness{}, nil
}
hash := func() common.Hash {
app.deliveredLock.RLock()
defer app.deliveredLock.RUnlock()
// Our witness height starts from 1.
h := app.DeliverSequence[pendingHeight-1]
// Double confirm if the delivered record matches the pending height.
if app.Delivered[h].Result.Height != pendingHeight {
app.confirmedLock.RLock()
defer app.confirmedLock.RUnlock()
panic(fmt.Errorf("unmatched finalization record: %s, %v, %v",
app.Confirmed[h], pendingHeight,
app.Delivered[h].Result.Height))
}
return h
}()
return types.Witness{
Height: pendingHeight,
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.
pendingHeight := app.getLastPendingWitnessHeight()
if pendingHeight < 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.deliveredLock.RLock()
defer app.deliveredLock.RUnlock()
// This is the difference between test.App and fullnode, fullnode has the
// genesis block at height=0, we don't. Thus our reasonable witness starts
// from 1.
if block.Witness.Height > 0 {
if block.Witness.Height != app.Delivered[h].Result.Height {
return types.VerifyInvalidBlock
}
}
if block.Position.Height != 0 {
// 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.
app.confirmedLock.RLock()
defer app.confirmedLock.RUnlock()
if app.LastConfirmedHeights[block.Position.ChainID]+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 b.Position.Height != 0 {
if h, exists := app.LastConfirmedHeights[b.Position.ChainID]; exists {
if h+1 != b.Position.Height {
panic(ErrConfirmedHeightNotIncreasing)
}
}
}
app.LastConfirmedHeights[b.Position.ChainID] = b.Position.Height
}
// TotalOrderingDelivered implements Application interface.
func (app *App) TotalOrderingDelivered(blockHashes common.Hashes, mode uint32) {
app.totalOrderedLock.Lock()
defer app.totalOrderedLock.Unlock()
rec := &AppTotalOrderRecord{
BlockHashes: blockHashes,
Mode: mode,
When: time.Now().UTC(),
}
app.TotalOrdered = append(app.TotalOrdered, rec)
for _, h := range blockHashes {
if _, exists := app.TotalOrderedByHash[h]; exists {
panic(fmt.Errorf("deliver duplicated blocks from total ordering"))
}
app.TotalOrderedByHash[h] = rec
}
}
// BlockDelivered implements Application interface.
func (app *App) BlockDelivered(
blockHash common.Hash, pos types.Position, result types.FinalizationResult) {
func() {
app.deliveredLock.Lock()
defer app.deliveredLock.Unlock()
app.Delivered[blockHash] = &AppDeliveredRecord{
Result: result,
When: time.Now().UTC(),
Pos: pos,
}
app.DeliverSequence = append(app.DeliverSequence, blockHash)
// Make sure parent block also delivered.
if !result.ParentHash.Equal(common.Hash{}) {
d, exists := app.Delivered[result.ParentHash]
if !exists {
panic(ErrParentBlockNotDelivered)
}
if d.Result.Height+1 != result.Height {
panic(ErrConsensusHeightOutOfOrder)
}
}
app.lastPendingHeightLock.Lock()
defer app.lastPendingHeightLock.Unlock()
app.LastPendingHeight = result.Height
}()
// Apply packed state change requests in payload.
func() {
if app.state == nil {
return
}
app.confirmedLock.RLock()
defer app.confirmedLock.RUnlock()
b := app.Confirmed[blockHash]
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)
app.roundToNotify++
}
}
}()
}
// 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) error {
app.deliveredLock.RLock()
defer app.deliveredLock.RUnlock()
other.deliveredLock.RLock()
defer other.deliveredLock.RUnlock()
minLength := len(app.DeliverSequence)
if minLength > len(other.DeliverSequence) {
minLength = len(other.DeliverSequence)
}
if minLength == 0 {
return ErrEmptyDeliverSequence
}
for idx, h := range app.DeliverSequence[:minLength] {
hOther := other.DeliverSequence[idx]
if hOther != h {
return ErrMismatchBlockHashSequence
}
if app.Delivered[h].Result.Timestamp !=
other.Delivered[h].Result.Timestamp {
return ErrMismatchConsensusTime
}
}
return nil
}
// Verify checks the integrity of date received by this App instance.
func (app *App) Verify() error {
// TODO(mission): verify blocks' position when delivered.
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 {
_, exists := app.Confirmed[h]
if !exists {
return ErrDeliveredBlockNotConfirmed
}
rec, exists := app.Delivered[h]
if !exists {
return ErrApplicationIntegrityFailed
}
// Make sure the consensus time is incremental.
ok := prevTime.Before(rec.Result.Timestamp) ||
prevTime.Equal(rec.Result.Timestamp)
if !ok {
return ErrConsensusTimestampOutOfOrder
}
prevTime = rec.Result.Timestamp
// Make sure the consensus height is incremental.
if expectHeight != rec.Result.Height {
return ErrConsensusHeightOutOfOrder
}
expectHeight++
}
// Check causality.
revealedDAG := make(map[common.Hash]struct{})
for _, toDeliver := range app.TotalOrdered {
for _, h := range toDeliver.BlockHashes {
b, exists := app.Confirmed[h]
if !exists {
return ErrDeliveredBlockNotConfirmed
}
for _, ack := range b.Acks {
if _, ackingBlockExists := revealedDAG[ack]; !ackingBlockExists {
return ErrAckingBlockNotDelivered
}
}
if toDeliver.Mode == TotalOrderingModeFlush {
// For blocks delivered by flushing, the acking relations would
// exist in one deliver set, however, only later block would
// ack previous block, not backward.
revealedDAG[h] = struct{}{}
}
}
// For blocks not delivered by flushing, the acking relations only exist
// between deliver sets.
if toDeliver.Mode != TotalOrderingModeFlush {
for _, h := range toDeliver.BlockHashes {
revealedDAG[h] = struct{}{}
}
}
}
// Make sure the order of delivered and total ordering are the same by
// comparing the concated string.
app.totalOrderedLock.RLock()
defer app.totalOrderedLock.RUnlock()
hashSequenceIdx := 0
Loop:
for _, rec := range app.TotalOrdered {
for _, h := range rec.BlockHashes {
if hashSequenceIdx >= len(app.DeliverSequence) {
break Loop
}
if h != app.DeliverSequence[hashSequenceIdx] {
return ErrMismatchTotalOrderingAndDelivered
}
hashSequenceIdx++
}
}
if hashSequenceIdx != len(app.DeliverSequence) {
// The count of delivered blocks should be larger than those delivered
// by total ordering.
return ErrMismatchTotalOrderingAndDelivered
}
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.totalOrderedLock.RLock()
defer app.totalOrderedLock.RUnlock()
app.deliveredLock.RLock()
defer app.deliveredLock.RUnlock()
app.lastPendingHeightLock.RLock()
defer app.lastPendingHeightLock.RUnlock()
function(app)
}
func (app *App) getLastPendingWitnessHeight() uint64 {
app.lastPendingHeightLock.RLock()
defer app.lastPendingHeightLock.RUnlock()
return app.LastPendingHeight
}