From 062c0d907e9ce9bfe7b8116d368b8cb13a81b393 Mon Sep 17 00:00:00 2001 From: Paolo Galli Date: Wed, 4 Sep 2024 11:43:41 +0200 Subject: [PATCH] Justified revision (#802) * feature: add justified revision for block * fix: missing err declaration * docs: add justified revision to thor.yaml * feat: storing justified blockId in engine.data * a non-persist way to implement jutified * use store point to restore quality * refactor: rename CommitLevel interface * refactor: change approach to non-persist the justified block * bft: add justified tests * fix: rename file name and simplified error check in test * improve bft package * add comments --------- Co-authored-by: tony Co-authored-by: otherview Co-authored-by: Darren Kelly --- api/accounts/accounts.go | 4 +- api/api.go | 2 +- api/blocks/blocks.go | 4 +- api/blocks/blocks_test.go | 12 ++ api/debug/debug.go | 6 +- api/doc/thor.yaml | 5 +- api/utils/revisions.go | 14 +- api/utils/revisions_test.go | 10 ++ bft/engine.go | 81 +++++++++- bft/engine_test.go | 284 +++++++++++++++++++++++++++++------ cmd/thor/node/packer_loop.go | 2 +- cmd/thor/solo/types.go | 6 + test/datagen/address.go | 19 +++ test/datagen/hash.go | 19 +++ 14 files changed, 399 insertions(+), 69 deletions(-) create mode 100644 test/datagen/address.go create mode 100644 test/datagen/hash.go diff --git a/api/accounts/accounts.go b/api/accounts/accounts.go index e8cd6cf90..a6d1df3ee 100644 --- a/api/accounts/accounts.go +++ b/api/accounts/accounts.go @@ -31,7 +31,7 @@ type Accounts struct { stater *state.Stater callGasLimit uint64 forkConfig thor.ForkConfig - bft bft.Finalizer + bft bft.Committer } func New( @@ -39,7 +39,7 @@ func New( stater *state.Stater, callGasLimit uint64, forkConfig thor.ForkConfig, - bft bft.Finalizer, + bft bft.Committer, ) *Accounts { return &Accounts{ repo, diff --git a/api/api.go b/api/api.go index 7e2f915be..ea33a0c16 100644 --- a/api/api.go +++ b/api/api.go @@ -38,7 +38,7 @@ func New( stater *state.Stater, txPool *txpool.TxPool, logDB *logdb.LogDB, - bft bft.Finalizer, + bft bft.Committer, nw node.Network, forkConfig thor.ForkConfig, allowedOrigins string, diff --git a/api/blocks/blocks.go b/api/blocks/blocks.go index 3062138c7..bddb3ac12 100644 --- a/api/blocks/blocks.go +++ b/api/blocks/blocks.go @@ -19,10 +19,10 @@ import ( type Blocks struct { repo *chain.Repository - bft bft.Finalizer + bft bft.Committer } -func New(repo *chain.Repository, bft bft.Finalizer) *Blocks { +func New(repo *chain.Repository, bft bft.Committer) *Blocks { return &Blocks{ repo, bft, diff --git a/api/blocks/blocks_test.go b/api/blocks/blocks_test.go index 2bad4f203..d8ef60771 100644 --- a/api/blocks/blocks_test.go +++ b/api/blocks/blocks_test.go @@ -20,6 +20,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/vechain/thor/v2/api/blocks" "github.com/vechain/thor/v2/block" "github.com/vechain/thor/v2/chain" @@ -52,6 +53,7 @@ func TestBlock(t *testing.T) { "testGetBlockByHeight": testGetBlockByHeight, "testGetBestBlock": testGetBestBlock, "testGetFinalizedBlock": testGetFinalizedBlock, + "testGetJustifiedBlock": testGetJustifiedBlock, "testGetBlockWithRevisionNumberTooHigh": testGetBlockWithRevisionNumberTooHigh, } { t.Run(name, tt) @@ -99,6 +101,16 @@ func testGetFinalizedBlock(t *testing.T) { assert.Equal(t, genesisBlock.Header().ID(), finalized.ID) } +func testGetJustifiedBlock(t *testing.T) { + res, statusCode := httpGet(t, ts.URL+"/blocks/justified") + justified := new(blocks.JSONCollapsedBlock) + require.NoError(t, json.Unmarshal(res, &justified)) + + assert.Equal(t, http.StatusOK, statusCode) + assert.Equal(t, uint32(0), justified.Number) + assert.Equal(t, genesisBlock.Header().ID(), justified.ID) +} + func testGetBlockById(t *testing.T) { res, statusCode := httpGet(t, ts.URL+"/blocks/"+blk.Header().ID().String()) rb := new(blocks.JSONCollapsedBlock) diff --git a/api/debug/debug.go b/api/debug/debug.go index ab2724e23..5e6c83b58 100644 --- a/api/debug/debug.go +++ b/api/debug/debug.go @@ -43,8 +43,8 @@ type Debug struct { forkConfig thor.ForkConfig callGasLimit uint64 allowCustomTracer bool - bft bft.Finalizer allowedTracers map[string]interface{} + bft bft.Committer skipPoA bool } @@ -54,7 +54,7 @@ func New( forkConfig thor.ForkConfig, callGaslimit uint64, allowCustomTracer bool, - bft bft.Finalizer, + bft bft.Committer, allowedTracers map[string]interface{}, soloMode bool) *Debug { return &Debug{ @@ -63,8 +63,8 @@ func New( forkConfig, callGaslimit, allowCustomTracer, - bft, allowedTracers, + bft, soloMode, } } diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml index 24d62c3da..85d6a0209 100644 --- a/api/doc/thor.yaml +++ b/api/doc/thor.yaml @@ -2252,7 +2252,7 @@ components: RevisionInQuery: name: revision in: query - description: Specify either `best`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed. + description: Specify either `best`, `justified`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed. schema: type: string @@ -2260,7 +2260,7 @@ components: name: revision in: query description: | - Specify either `best`,`next`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed. + Specify either `best`, `next`, `justified`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed. If the `next` block is specified, the call code will be executed on the next block, with the following: - The block number is the `best` block number plus one. @@ -2279,6 +2279,7 @@ components: - a block ID (hex string) - a block number (integer) - `best` stands for latest block + - `justified` stands for the justified block - `finalized` stands for the finalized block required: true schema: diff --git a/api/utils/revisions.go b/api/utils/revisions.go index c4cf668cb..de64473aa 100644 --- a/api/utils/revisions.go +++ b/api/utils/revisions.go @@ -21,6 +21,7 @@ const ( revBest int64 = -1 revFinalized int64 = -2 revNext int64 = -3 + revJustified int64 = -4 ) type Revision struct { @@ -41,6 +42,10 @@ func ParseRevision(revision string, allowNext bool) (*Revision, error) { return &Revision{revFinalized}, nil } + if revision == "justified" { + return &Revision{revJustified}, nil + } + if revision == "next" { if !allowNext { return nil, errors.New("invalid revision: next is not allowed") @@ -67,7 +72,7 @@ func ParseRevision(revision string, allowNext bool) (*Revision, error) { // GetSummary returns the block summary for the given revision, // revision required to be a deterministic block other than "next". -func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Finalizer) (sum *chain.BlockSummary, err error) { +func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Committer) (sum *chain.BlockSummary, err error) { var id thor.Bytes32 switch rev := rev.val.(type) { case thor.Bytes32: @@ -83,6 +88,11 @@ func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Finalizer) (sum * id = repo.BestBlockSummary().Header.ID() case revFinalized: id = bft.Finalized() + case revJustified: + id, err = bft.Justified() + if err != nil { + return nil, err + } } } if id.IsZero() { @@ -97,7 +107,7 @@ func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Finalizer) (sum * // GetSummaryAndState returns the block summary and state for the given revision, // this function supports the "next" revision. -func GetSummaryAndState(rev *Revision, repo *chain.Repository, bft bft.Finalizer, stater *state.Stater) (*chain.BlockSummary, *state.State, error) { +func GetSummaryAndState(rev *Revision, repo *chain.Repository, bft bft.Committer, stater *state.Stater) (*chain.BlockSummary, *state.State, error) { if rev.IsNext() { best := repo.BestBlockSummary() diff --git a/api/utils/revisions_test.go b/api/utils/revisions_test.go index 3af996693..76a4bbfdc 100644 --- a/api/utils/revisions_test.go +++ b/api/utils/revisions_test.go @@ -41,6 +41,11 @@ func TestParseRevision(t *testing.T) { err: nil, expected: &Revision{revBest}, }, + { + revision: "justified", + err: nil, + expected: &Revision{revJustified}, + }, { revision: "finalized", err: nil, @@ -122,6 +127,11 @@ func TestGetSummary(t *testing.T) { revision: &Revision{uint32(1234)}, err: errors.New("not found"), }, + { + name: "justified", + revision: &Revision{revJustified}, + err: nil, + }, { name: "finalized", revision: &Revision{revFinalized}, diff --git a/bft/engine.go b/bft/engine.go index 2e3f6a73f..925e469b2 100644 --- a/bft/engine.go +++ b/bft/engine.go @@ -25,8 +25,14 @@ const dataStoreName = "bft.engine" var finalizedKey = []byte("finalized") -type Finalizer interface { +type Committer interface { Finalized() thor.Bytes32 + Justified() (thor.Bytes32, error) +} + +type justified struct { + search thor.Bytes32 + value thor.Bytes32 } // BFTEngine tracks all votes of blocks, computes the finalized checkpoint. @@ -39,6 +45,7 @@ type BFTEngine struct { master thor.Address casts casts finalized atomic.Value + justified atomic.Value caches struct { state *lru.Cache quality *lru.Cache @@ -60,6 +67,7 @@ func NewEngine(repo *chain.Repository, mainDB *muxdb.MuxDB, forkConfig thor.Fork engine.caches.quality, _ = lru.New(16) engine.caches.justifier = cache.NewPrioCache(16) + // Restore finalized block, if any if val, err := engine.data.Get(finalizedKey); err != nil { if !engine.data.IsNotFound(err) { return nil, err @@ -77,6 +85,54 @@ func (engine *BFTEngine) Finalized() thor.Bytes32 { return engine.finalized.Load().(thor.Bytes32) } +// Justified returns the justified checkpoint. +func (engine *BFTEngine) Justified() (thor.Bytes32, error) { + head := engine.repo.BestBlockSummary().Header + finalized := engine.Finalized() + + // if head is in the first epoch and not concluded yet + if head.Number() < getCheckPoint(engine.forkConfig.FINALITY)+thor.CheckpointInterval-1 { + return finalized, nil + } + + // find the recent concluded checkpoint + concluded := getCheckPoint(head.Number()) + if head.Number() < getStorePoint(head.Number()) { + concluded -= thor.CheckpointInterval + } + + headChain := engine.repo.NewChain(head.ID()) + + // storeID is the block id where an epoch concluded + storeID, err := headChain.GetBlockID(getStorePoint(concluded)) + if err != nil { + return thor.Bytes32{}, err + } + + if val := engine.justified.Load(); val != nil && storeID == val.(justified).search { + return val.(justified).value, nil + } + + quality, err := engine.getQuality(storeID) + if err != nil { + return thor.Bytes32{}, err + } + + // if the quality is 0, then the epoch is not justified + // this is possible for the starting epochs + if quality == 0 { + return finalized, nil + } + + checkpoint, err := engine.findCheckpointByQuality(quality, finalized, storeID) + if err != nil { + return thor.Bytes32{}, err + } + + engine.justified.Store(justified{search: storeID, value: checkpoint}) + return checkpoint, nil +} + // Accepts checks if the given block is on the same branch of finalized checkpoint. func (engine *BFTEngine) Accepts(parentID thor.Bytes32) (bool, error) { finalized := engine.Finalized() @@ -123,7 +179,7 @@ func (engine *BFTEngine) CommitBlock(header *block.Header, isPacking bool) error engine.caches.quality.Add(header.ID(), state.Quality) if state.Committed && state.Quality > 1 { - id, err := engine.findCheckpointByQuality(state.Quality-1, engine.Finalized(), header.ParentID()) + id, err := engine.findCheckpointByQuality(state.Quality-1, engine.Finalized(), header.ID()) if err != nil { return err } @@ -182,17 +238,23 @@ func (engine *BFTEngine) ShouldVote(parentID thor.Bytes32) (bool, error) { headQuality := st.Quality finalized := engine.Finalized() + chain := engine.repo.NewChain(parentID) // most recent justified checkpoint var recentJC thor.Bytes32 if st.Justified { // if justified in this round, use this round's checkpoint - checkpoint, err := engine.repo.NewChain(parentID).GetBlockID(getCheckPoint(block.Number(parentID))) + checkpoint, err := chain.GetBlockID(getCheckPoint(block.Number(parentID))) if err != nil { return false, err } recentJC = checkpoint } else { - checkpoint, err := engine.findCheckpointByQuality(headQuality, finalized, parentID) + // if current round is not justified, find the most recent justified checkpoint + prev, err := chain.GetBlockID(getStorePoint(block.Number(parentID) - thor.CheckpointInterval)) + if err != nil { + return false, err + } + checkpoint, err := engine.findCheckpointByQuality(headQuality, finalized, prev) if err != nil { return false, err } @@ -223,7 +285,7 @@ func (engine *BFTEngine) ShouldVote(parentID thor.Bytes32) (bool, error) { return true, nil } -// computeState computes the bft state regarding the given block header. +// computeState computes the bft state regarding the given block header to the closest checkpoint. func (engine *BFTEngine) computeState(header *block.Header) (*bftState, error) { if cached, ok := engine.caches.state.Get(header.ID()); ok { return cached.(*bftState), nil @@ -278,7 +340,8 @@ func (engine *BFTEngine) computeState(header *block.Header) (*bftState, error) { } // findCheckpointByQuality finds the first checkpoint reaches the given quality. -func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, parentID thor.Bytes32) (blockID thor.Bytes32, err error) { +// It is caller's responsibility to ensure the epoch that headID belongs to is concluded. +func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, headID thor.Bytes32) (blockID thor.Bytes32, err error) { defer func() { if e := recover(); e != nil { err = e.(error) @@ -291,7 +354,7 @@ func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, paren searchStart = getCheckPoint(engine.forkConfig.FINALITY) } - c := engine.repo.NewChain(parentID) + c := engine.repo.NewChain(headID) get := func(i int) (uint32, error) { id, err := c.GetBlockID(getStorePoint(searchStart + uint32(i)*thor.CheckpointInterval)) if err != nil { @@ -300,7 +363,8 @@ func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, paren return engine.getQuality(id) } - n := int((block.Number(parentID) + 1 - searchStart) / thor.CheckpointInterval) + // sort.Search searches from [0, n) + n := int((block.Number(headID)-searchStart)/thor.CheckpointInterval) + 1 num := sort.Search(n, func(i int) bool { quality, err := get(i) if err != nil { @@ -310,6 +374,7 @@ func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, paren return quality >= target }) + // n means not found for sort.Search if num == n { return thor.Bytes32{}, errors.New("failed find the block by quality") } diff --git a/bft/engine_test.go b/bft/engine_test.go index 3d2eb4a3f..2480bbf79 100644 --- a/bft/engine_test.go +++ b/bft/engine_test.go @@ -5,17 +5,17 @@ package bft import ( - "crypto/rand" - "math" "testing" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/block" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/muxdb" "github.com/vechain/thor/v2/packer" "github.com/vechain/thor/v2/state" + "github.com/vechain/thor/v2/test/datagen" "github.com/vechain/thor/v2/thor" ) @@ -29,28 +29,13 @@ type TestBFT struct { const MaxBlockProposers = 11 -var devAccounts = genesis.DevAccounts() -var defaultFC = thor.ForkConfig{ - VIP191: math.MaxUint32, - ETH_CONST: math.MaxUint32, - BLOCKLIST: math.MaxUint32, - ETH_IST: math.MaxUint32, - VIP214: math.MaxUint32, - FINALITY: 0, -} - -func RandomAddress() thor.Address { - var addr thor.Address - - rand.Read(addr[:]) - return addr -} - -func RandomBytes32() thor.Bytes32 { - var b32 thor.Bytes32 +var ( + devAccounts = genesis.DevAccounts() + defaultFC = thor.NoFork +) - rand.Read(b32[:]) - return b32 +func init() { + defaultFC.FINALITY = 0 } func newTestBft(forkCfg thor.ForkConfig) (*TestBFT, error) { @@ -153,8 +138,10 @@ func (test *TestBFT) newBlock(parentSummary *chain.BlockSummary, master genesis. return nil, err } - if err = test.engine.CommitBlock(b.Header(), false); err != nil { - return nil, err + if b.Header().Number() >= test.fc.FINALITY { + if err = test.engine.CommitBlock(b.Header(), false); err != nil { + return nil, err + } } return test.repo.GetBlockSummary(b.Header().ID()) @@ -222,8 +209,10 @@ func (test *TestBFT) pack(parentID thor.Bytes32, shouldVote bool, best bool) (*c return nil, err } - if err := test.engine.CommitBlock(blk.Header, true); err != nil { - return nil, err + if blk.Header.Number() >= test.fc.FINALITY { + if err := test.engine.CommitBlock(blk.Header, true); err != nil { + return nil, err + } } if best { @@ -243,6 +232,10 @@ func TestNewEngine(t *testing.T) { genID := testBFT.repo.BestBlockSummary().Header.ID() assert.Equal(t, genID, testBFT.engine.Finalized()) + + j, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, genID, j) } func TestNewBlock(t *testing.T) { @@ -394,6 +387,16 @@ func TestFinalized(t *testing.T) { } assert.Equal(t, finalized, testBFT.engine.Finalized()) + + jc, err := testBFT.repo.NewBestChain().GetBlockID(thor.CheckpointInterval * 2) + if err != nil { + t.Fatal(err) + } + + j, err := testBFT.engine.Justified() + assert.NoError(t, err) + assert.Equal(t, jc, j) + assert.Equal(t, jc, testBFT.engine.justified.Load().(justified).value) } func TestAccepts(t *testing.T) { @@ -629,6 +632,20 @@ func TestGetVote(t *testing.T) { assert.Equal(t, false, v) }, + }, { + "test findCheckpointByQuality edge case, should not fail", func(t *testing.T) { + testBFT, err := newTestBft(defaultFC) + if err != nil { + t.Fatal(err) + } + + testBFT.fastForwardWithMinority(thor.CheckpointInterval * 3) + testBFT.fastForward(thor.CheckpointInterval*1 + 3) + _, err = testBFT.engine.ShouldVote(testBFT.repo.BestBlockSummary().Header.ID()) + if err != nil { + t.Fatal(err) + } + }, }, } @@ -713,8 +730,8 @@ func TestJustifier(t *testing.T) { var blkID thor.Bytes32 for i := 0; i <= MaxBlockProposers*2/3; i++ { - blkID = RandomBytes32() - vs.AddBlock(blkID, RandomAddress(), true) + blkID = datagen.RandomHash() + vs.AddBlock(blkID, datagen.RandomAddress(), true) } st := vs.Summarize() @@ -723,7 +740,7 @@ func TestJustifier(t *testing.T) { assert.True(t, st.Committed) // add vote after commits,commit/justify stays the same - vs.AddBlock(RandomBytes32(), RandomAddress(), true) + vs.AddBlock(datagen.RandomHash(), datagen.RandomAddress(), true) st = vs.Summarize() assert.Equal(t, uint32(3), st.Quality) assert.True(t, st.Justified) @@ -746,8 +763,8 @@ func TestJustifier(t *testing.T) { var blkID thor.Bytes32 for i := 0; i <= MaxBlockProposers*2/3; i++ { - blkID = RandomBytes32() - vs.AddBlock(blkID, RandomAddress(), false) + blkID = datagen.RandomHash() + vs.AddBlock(blkID, datagen.RandomAddress(), false) } st := vs.Summarize() @@ -773,13 +790,13 @@ func TestJustifier(t *testing.T) { var blkID thor.Bytes32 // vote times COM for i := 0; i < MaxBlockProposers*2/3; i++ { - blkID = RandomBytes32() - vs.AddBlock(blkID, RandomAddress(), true) + blkID = datagen.RandomHash() + vs.AddBlock(blkID, datagen.RandomAddress(), true) } - master := RandomAddress() + master := datagen.RandomAddress() // master votes WIT - blkID = RandomBytes32() + blkID = datagen.RandomHash() vs.AddBlock(blkID, master, false) // justifies but not committed @@ -787,7 +804,7 @@ func TestJustifier(t *testing.T) { assert.True(t, st.Justified) assert.False(t, st.Committed) - blkID = RandomBytes32() + blkID = datagen.RandomHash() // master votes COM vs.AddBlock(blkID, master, true) @@ -796,8 +813,8 @@ func TestJustifier(t *testing.T) { assert.False(t, st.Committed) // another master votes WIT - blkID = RandomBytes32() - vs.AddBlock(blkID, RandomAddress(), true) + blkID = datagen.RandomHash() + vs.AddBlock(blkID, datagen.RandomAddress(), true) st = vs.Summarize() assert.True(t, st.Committed) }, @@ -813,20 +830,20 @@ func TestJustifier(t *testing.T) { t.Fatal(err) } - master := RandomAddress() - vs.AddBlock(RandomBytes32(), master, true) + master := datagen.RandomAddress() + vs.AddBlock(datagen.RandomHash(), master, true) assert.Equal(t, true, vs.votes[master]) assert.Equal(t, uint64(1), vs.comVotes) - vs.AddBlock(RandomBytes32(), master, false) + vs.AddBlock(datagen.RandomHash(), master, false) assert.Equal(t, false, vs.votes[master]) assert.Equal(t, uint64(0), vs.comVotes) - vs.AddBlock(RandomBytes32(), master, true) + vs.AddBlock(datagen.RandomHash(), master, true) assert.Equal(t, false, vs.votes[master]) assert.Equal(t, uint64(0), vs.comVotes) - vs.AddBlock(RandomBytes32(), master, false) + vs.AddBlock(datagen.RandomHash(), master, false) assert.Equal(t, false, vs.votes[master]) assert.Equal(t, uint64(0), vs.comVotes) @@ -834,19 +851,19 @@ func TestJustifier(t *testing.T) { if err != nil { t.Fatal(err) } - vs.AddBlock(RandomBytes32(), master, false) + vs.AddBlock(datagen.RandomHash(), master, false) assert.Equal(t, false, vs.votes[master]) assert.Equal(t, uint64(0), vs.comVotes) - vs.AddBlock(RandomBytes32(), master, true) + vs.AddBlock(datagen.RandomHash(), master, true) assert.Equal(t, false, vs.votes[master]) assert.Equal(t, uint64(0), vs.comVotes) - vs.AddBlock(RandomBytes32(), master, true) + vs.AddBlock(datagen.RandomHash(), master, true) assert.Equal(t, false, vs.votes[master]) assert.Equal(t, uint64(0), vs.comVotes) - vs.AddBlock(RandomBytes32(), master, false) + vs.AddBlock(datagen.RandomHash(), master, false) assert.Equal(t, false, vs.votes[master]) assert.Equal(t, uint64(0), vs.comVotes) }, @@ -858,3 +875,174 @@ func TestJustifier(t *testing.T) { }) } } + +func TestJustified(t *testing.T) { + tests := []struct { + name string + testFunc func(*testing.T) + }{ + { + "first several rounds, never justified", func(t *testing.T) { + testBFT, err := newTestBft(defaultFC) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 3*thor.CheckpointInterval; i++ { + if err = testBFT.fastForwardWithMinority(1); err != nil { + t.Fatal(err) + } + + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), justified) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), testBFT.engine.Finalized()) + } + }, + }, { + "first several rounds, get justified", func(t *testing.T) { + testBFT, err := newTestBft(defaultFC) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 2*thor.CheckpointInterval-2; i++ { + if err = testBFT.fastForward(1); err != nil { + t.Fatal(err) + } + + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), justified) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), testBFT.engine.Finalized()) + } + + if err = testBFT.fastForward(1); err != nil { + t.Fatal(err) + } + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, uint32(thor.CheckpointInterval), block.Number(justified)) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), testBFT.engine.Finalized()) + }, + }, { + "first three not justified rounds, then justified", func(t *testing.T) { + testBFT, err := newTestBft(defaultFC) + if err != nil { + t.Fatal(err) + } + + if err = testBFT.fastForwardWithMinority(3*thor.CheckpointInterval - 1); err != nil { + t.Fatal(err) + } + + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), justified) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), testBFT.engine.Finalized()) + + if err = testBFT.fastForward(thor.CheckpointInterval); err != nil { + t.Fatal(err) + } + justified, err = testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, uint32(3*thor.CheckpointInterval), block.Number(justified)) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), testBFT.engine.Finalized()) + }, + }, { + "get finalized, then justified", func(t *testing.T) { + testBFT, err := newTestBft(defaultFC) + if err != nil { + t.Fatal(err) + } + + if err = testBFT.fastForward(3*thor.CheckpointInterval - 1); err != nil { + t.Fatal(err) + } + + assert.Equal(t, uint32(thor.CheckpointInterval), block.Number(testBFT.engine.Finalized())) + + if err = testBFT.fastForward(thor.CheckpointInterval - 1); err != nil { + t.Fatal(err) + } + + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + // current epoch is not concluded + assert.Equal(t, uint32(2*thor.CheckpointInterval), block.Number(justified)) + + if err = testBFT.fastForward(1); err != nil { + t.Fatal(err) + } + justified, err = testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, uint32(3*thor.CheckpointInterval), block.Number(justified)) + }, + }, { + "get finalized, not justified, then justified", func(t *testing.T) { + type tJustified = justified + testBFT, err := newTestBft(defaultFC) + if err != nil { + t.Fatal(err) + } + + if err = testBFT.fastForward(3*thor.CheckpointInterval - 1); err != nil { + t.Fatal(err) + } + + assert.Equal(t, uint32(thor.CheckpointInterval), block.Number(testBFT.engine.Finalized())) + + if err = testBFT.fastForwardWithMinority(thor.CheckpointInterval); err != nil { + t.Fatal(err) + } + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, uint32(2*thor.CheckpointInterval), block.Number(justified)) + + if err = testBFT.fastForward(thor.CheckpointInterval); err != nil { + t.Fatal(err) + } + justified, err = testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, uint32(4*thor.CheckpointInterval), block.Number(justified)) + // test cache + assert.Equal(t, justified, testBFT.engine.justified.Load().(tJustified).value) + }, + }, { + "fork in the middle, get justified", func(t *testing.T) { + fc := defaultFC + fc.FINALITY = thor.CheckpointInterval + + testBFT, err := newTestBft(fc) + if err != nil { + t.Fatal(err) + } + + for i := 0; i < 2*thor.CheckpointInterval-2; i++ { + if err = testBFT.fastForward(1); err != nil { + t.Fatal(err) + } + + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), justified) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), testBFT.engine.Finalized()) + } + + if err = testBFT.fastForward(1); err != nil { + t.Fatal(err) + } + justified, err := testBFT.engine.Justified() + assert.Nil(t, err) + assert.Equal(t, uint32(thor.CheckpointInterval), block.Number(justified)) + assert.Equal(t, testBFT.repo.GenesisBlock().Header().ID(), testBFT.engine.Finalized()) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunc(t) + }) + } +} diff --git a/cmd/thor/node/packer_loop.go b/cmd/thor/node/packer_loop.go index dfdc58b05..44e7f1ded 100644 --- a/cmd/thor/node/packer_loop.go +++ b/cmd/thor/node/packer_loop.go @@ -158,7 +158,7 @@ func (n *Node) pack(flow *packer.Flow) (err error) { // write logs if logEnabled { - if n.writeLogs(newBlock, receipts, oldBest.Header.ID()); err != nil { + if err := n.writeLogs(newBlock, receipts, oldBest.Header.ID()); err != nil { return errors.Wrap(err, "write logs") } } diff --git a/cmd/thor/solo/types.go b/cmd/thor/solo/types.go index a4f055122..c431ece0d 100644 --- a/cmd/thor/solo/types.go +++ b/cmd/thor/solo/types.go @@ -23,14 +23,20 @@ func (comm *Communicator) PeersStats() []*comm.PeerStats { // BFTEngine is a fake bft engine for solo. type BFTEngine struct { finalized thor.Bytes32 + justified thor.Bytes32 } func (engine *BFTEngine) Finalized() thor.Bytes32 { return engine.finalized } +func (engine *BFTEngine) Justified() (thor.Bytes32, error) { + return engine.justified, nil +} + func NewBFTEngine(repo *chain.Repository) *BFTEngine { return &BFTEngine{ finalized: repo.GenesisBlock().Header().ID(), + justified: repo.GenesisBlock().Header().ID(), } } diff --git a/test/datagen/address.go b/test/datagen/address.go new file mode 100644 index 000000000..882159f6e --- /dev/null +++ b/test/datagen/address.go @@ -0,0 +1,19 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package datagen + +import ( + "crypto/rand" + + "github.com/vechain/thor/v2/thor" +) + +func RandomAddress() thor.Address { + var addr thor.Address + + rand.Read(addr[:]) + return addr +} diff --git a/test/datagen/hash.go b/test/datagen/hash.go new file mode 100644 index 000000000..2b15709f0 --- /dev/null +++ b/test/datagen/hash.go @@ -0,0 +1,19 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package datagen + +import ( + "crypto/rand" + + "github.com/vechain/thor/v2/thor" +) + +func RandomHash() thor.Bytes32 { + var b32 thor.Bytes32 + + rand.Read(b32[:]) + return b32 +}