From 32fb18339281dc9adfb0a1de67370b7baab1a0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Kapka?= Date: Fri, 22 Mar 2024 06:09:56 +0900 Subject: [PATCH] Modify the algorithm of `updateFinalizedBlockRoots` (#13486) * rename error var * new algo * replay_test * add comment * review * fill out parent root * handle edge cases * review --- beacon-chain/db/kv/blocks.go | 2 +- beacon-chain/db/kv/blocks_test.go | 6 +- beacon-chain/db/kv/error.go | 4 +- beacon-chain/db/kv/finalized_block_roots.go | 208 +++++++------- .../db/kv/finalized_block_roots_test.go | 256 +++++++----------- beacon-chain/db/kv/state.go | 2 +- beacon-chain/state/stategen/replay_test.go | 78 +++--- 7 files changed, 253 insertions(+), 303 deletions(-) diff --git a/beacon-chain/db/kv/blocks.go b/beacon-chain/db/kv/blocks.go index 7cf787769..3b1226cce 100644 --- a/beacon-chain/db/kv/blocks.go +++ b/beacon-chain/db/kv/blocks.go @@ -224,7 +224,7 @@ func (s *Store) DeleteBlock(ctx context.Context, root [32]byte) error { return s.db.Update(func(tx *bolt.Tx) error { bkt := tx.Bucket(finalizedBlockRootsIndexBucket) if b := bkt.Get(root[:]); b != nil { - return ErrDeleteJustifiedAndFinalized + return ErrDeleteFinalized } if err := tx.Bucket(blocksBucket).Delete(root[:]); err != nil { diff --git a/beacon-chain/db/kv/blocks_test.go b/beacon-chain/db/kv/blocks_test.go index 67235b2ae..384e9533e 100644 --- a/beacon-chain/db/kv/blocks_test.go +++ b/beacon-chain/db/kv/blocks_test.go @@ -289,7 +289,7 @@ func TestStore_DeleteBlock(t *testing.T) { require.Equal(t, b, nil) require.Equal(t, false, db.HasStateSummary(ctx, root2)) - require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteJustifiedAndFinalized) + require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteFinalized) } func TestStore_DeleteJustifiedBlock(t *testing.T) { @@ -309,7 +309,7 @@ func TestStore_DeleteJustifiedBlock(t *testing.T) { require.NoError(t, db.SaveBlock(ctx, blk)) require.NoError(t, db.SaveState(ctx, st, root)) require.NoError(t, db.SaveJustifiedCheckpoint(ctx, cp)) - require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteJustifiedAndFinalized) + require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteFinalized) } func TestStore_DeleteFinalizedBlock(t *testing.T) { @@ -329,7 +329,7 @@ func TestStore_DeleteFinalizedBlock(t *testing.T) { require.NoError(t, db.SaveState(ctx, st, root)) require.NoError(t, db.SaveGenesisBlockRoot(ctx, root)) require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) - require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteJustifiedAndFinalized) + require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteFinalized) } func TestStore_GenesisBlock(t *testing.T) { db := setupDB(t) diff --git a/beacon-chain/db/kv/error.go b/beacon-chain/db/kv/error.go index 6d79723cc..957b17357 100644 --- a/beacon-chain/db/kv/error.go +++ b/beacon-chain/db/kv/error.go @@ -2,8 +2,8 @@ package kv import "github.com/pkg/errors" -// ErrDeleteJustifiedAndFinalized is raised when we attempt to delete a finalized block/state -var ErrDeleteJustifiedAndFinalized = errors.New("cannot delete finalized block or state") +// ErrDeleteFinalized is raised when we attempt to delete a finalized block/state +var ErrDeleteFinalized = errors.New("cannot delete finalized block or state") // ErrNotFound can be used directly, or as a wrapped DBError, whenever a db method needs to // indicate that a value couldn't be found. diff --git a/beacon-chain/db/kv/finalized_block_roots.go b/beacon-chain/db/kv/finalized_block_roots.go index 3376abc8e..2d655889a 100644 --- a/beacon-chain/db/kv/finalized_block_roots.go +++ b/beacon-chain/db/kv/finalized_block_roots.go @@ -5,7 +5,6 @@ import ( "context" "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/db/filters" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" @@ -29,72 +28,76 @@ var containerFinalizedButNotCanonical = []byte("recent block needs reindexing to // beacon block chain using the finalized root alone as this would exclude all other blocks in the // finalized epoch from being indexed as "final and canonical". // -// The algorithm for building the index works as follows: -// - De-index all finalized beacon block roots from previous_finalized_epoch to -// new_finalized_epoch. (I.e. delete these roots from the index, to be re-indexed.) -// - Build the canonical finalized chain by walking up the ancestry chain from the finalized block -// root until a parent is found in the index, or the parent is genesis or the origin checkpoint. -// - Add all block roots in the database where epoch(block.slot) == checkpoint.epoch. -// -// This method ensures that all blocks from the current finalized epoch are considered "final" while -// maintaining only canonical and finalized blocks older than the current finalized epoch. +// The main part of the algorithm traverses parent->child block relationships in the +// `blockParentRootIndicesBucket` bucket to find the path between the last finalized checkpoint +// and the current finalized checkpoint. It relies on the invariant that there is a unique path +// between two finalized checkpoints. func (s *Store) updateFinalizedBlockRoots(ctx context.Context, tx *bolt.Tx, checkpoint *ethpb.Checkpoint) error { ctx, span := trace.StartSpan(ctx, "BeaconDB.updateFinalizedBlockRoots") defer span.End() - bkt := tx.Bucket(finalizedBlockRootsIndexBucket) - - root := checkpoint.Root - var previousRoot []byte - genesisRoot := tx.Bucket(blocksBucket).Get(genesisBlockRootKey) - initCheckpointRoot := tx.Bucket(blocksBucket).Get(originCheckpointBlockRootKey) - - // De-index recent finalized block roots, to be re-indexed. + finalizedBkt := tx.Bucket(finalizedBlockRootsIndexBucket) previousFinalizedCheckpoint := ðpb.Checkpoint{} - if b := bkt.Get(previousFinalizedCheckpointKey); b != nil { + if b := finalizedBkt.Get(previousFinalizedCheckpointKey); b != nil { if err := decode(ctx, b, previousFinalizedCheckpoint); err != nil { tracing.AnnotateError(span, err) return err } } - blockRoots, err := s.BlockRoots(ctx, filters.NewFilter(). - SetStartEpoch(previousFinalizedCheckpoint.Epoch). - SetEndEpoch(checkpoint.Epoch+1), - ) - if err != nil { - tracing.AnnotateError(span, err) - return err - } - for _, root := range blockRoots { - if err := bkt.Delete(root[:]); err != nil { - tracing.AnnotateError(span, err) - return err - } - } - - // Walk up the ancestry chain until we reach a block root present in the finalized block roots - // index bucket or genesis block root. - for { - if bytes.Equal(root, genesisRoot) { - break - } - - signedBlock, err := s.Block(ctx, bytesutil.ToBytes32(root)) + // Handle the case of checkpoint sync. + if previousFinalizedCheckpoint.Root == nil && bytes.Equal(checkpoint.Root, tx.Bucket(blocksBucket).Get(originCheckpointBlockRootKey)) { + container := ðpb.FinalizedBlockRootContainer{} + enc, err := encode(ctx, container) if err != nil { tracing.AnnotateError(span, err) return err } - if err := blocks.BeaconBlockIsNil(signedBlock); err != nil { + if err = finalizedBkt.Put(checkpoint.Root, enc); err != nil { tracing.AnnotateError(span, err) return err } - block := signedBlock.Block() + return updatePrevFinalizedCheckpoint(ctx, span, finalizedBkt, checkpoint) + } - parentRoot := block.ParentRoot() - container := ðpb.FinalizedBlockRootContainer{ - ParentRoot: parentRoot[:], - ChildRoot: previousRoot, + var finalized [][]byte + if previousFinalizedCheckpoint.Root == nil { + genesisRoot := tx.Bucket(blocksBucket).Get(genesisBlockRootKey) + _, finalized = pathToFinalizedCheckpoint(ctx, [][]byte{genesisRoot}, checkpoint.Root, tx) + } else { + if err := updateChildOfPrevFinalizedCheckpoint( + ctx, + span, + finalizedBkt, + tx.Bucket(blockParentRootIndicesBucket), previousFinalizedCheckpoint.Root, + ); err != nil { + return err + } + _, finalized = pathToFinalizedCheckpoint(ctx, [][]byte{previousFinalizedCheckpoint.Root}, checkpoint.Root, tx) + } + + for i, r := range finalized { + var container *ethpb.FinalizedBlockRootContainer + switch i { + case 0: + container = ðpb.FinalizedBlockRootContainer{ + ParentRoot: previousFinalizedCheckpoint.Root, + } + if len(finalized) > 1 { + container.ChildRoot = finalized[i+1] + } + case len(finalized) - 1: + // We don't know the finalized child of the new finalized checkpoint. + // It will be filled out in the next function call. + container = ðpb.FinalizedBlockRootContainer{} + if len(finalized) > 1 { + container.ParentRoot = finalized[i-1] + } + default: + container = ðpb.FinalizedBlockRootContainer{ + ParentRoot: finalized[i-1], + ChildRoot: finalized[i+1], + } } enc, err := encode(ctx, container) @@ -102,66 +105,13 @@ func (s *Store) updateFinalizedBlockRoots(ctx context.Context, tx *bolt.Tx, chec tracing.AnnotateError(span, err) return err } - if err := bkt.Put(root, enc); err != nil { - tracing.AnnotateError(span, err) - return err - } - - // breaking here allows the initial checkpoint root to be correctly inserted, - // but stops the loop from trying to search for its parent. - if bytes.Equal(root, initCheckpointRoot) { - break - } - - // Found parent, loop exit condition. - pr := block.ParentRoot() - if parentBytes := bkt.Get(pr[:]); parentBytes != nil { - parent := ðpb.FinalizedBlockRootContainer{} - if err := decode(ctx, parentBytes, parent); err != nil { - tracing.AnnotateError(span, err) - return err - } - parent.ChildRoot = root - enc, err := encode(ctx, parent) - if err != nil { - tracing.AnnotateError(span, err) - return err - } - if err := bkt.Put(pr[:], enc); err != nil { - tracing.AnnotateError(span, err) - return err - } - break - } - previousRoot = root - root = pr[:] - } - - // Upsert blocks from the current finalized epoch. - roots, err := s.BlockRoots(ctx, filters.NewFilter().SetStartEpoch(checkpoint.Epoch).SetEndEpoch(checkpoint.Epoch+1)) - if err != nil { - tracing.AnnotateError(span, err) - return err - } - for _, root := range roots { - root := root[:] - if bytes.Equal(root, checkpoint.Root) || bkt.Get(root) != nil { - continue - } - if err := bkt.Put(root, containerFinalizedButNotCanonical); err != nil { + if err = finalizedBkt.Put(r, enc); err != nil { tracing.AnnotateError(span, err) return err } } - // Update previous checkpoint - enc, err := encode(ctx, checkpoint) - if err != nil { - tracing.AnnotateError(span, err) - return err - } - - return bkt.Put(previousFinalizedCheckpointKey, enc) + return updatePrevFinalizedCheckpoint(ctx, span, finalizedBkt, checkpoint) } // BackfillFinalizedIndex updates the finalized index for a contiguous chain of blocks that are the ancestors of the @@ -242,8 +192,6 @@ func (s *Store) BackfillFinalizedIndex(ctx context.Context, blocks []blocks.ROBl // IsFinalizedBlock returns true if the block root is present in the finalized block root index. // A beacon block root contained exists in this index if it is considered finalized and canonical. -// Note: beacon blocks from the latest finalized epoch return true, whether or not they are -// considered canonical in the "head view" of the beacon node. func (s *Store) IsFinalizedBlock(ctx context.Context, blockRoot [32]byte) bool { _, span := trace.StartSpan(ctx, "BeaconDB.IsFinalizedBlock") defer span.End() @@ -296,3 +244,53 @@ func (s *Store) FinalizedChildBlock(ctx context.Context, blockRoot [32]byte) (in tracing.AnnotateError(span, err) return blk, err } + +func pathToFinalizedCheckpoint(ctx context.Context, roots [][]byte, checkpointRoot []byte, tx *bolt.Tx) (bool, [][]byte) { + if len(roots) == 0 || (len(roots) == 1 && roots[0] == nil) { + return false, nil + } + + for _, r := range roots { + if bytes.Equal(r, checkpointRoot) { + return true, [][]byte{r} + } + children := lookupValuesForIndices(ctx, map[string][]byte{string(blockParentRootIndicesBucket): r}, tx) + if len(children) == 0 { + children = [][][]byte{nil} + } + isPath, path := pathToFinalizedCheckpoint(ctx, children[0], checkpointRoot, tx) + if isPath { + return true, append([][]byte{r}, path...) + } + } + + return false, nil +} + +func updatePrevFinalizedCheckpoint(ctx context.Context, span *trace.Span, finalizedBkt *bolt.Bucket, checkpoint *ethpb.Checkpoint) error { + enc, err := encode(ctx, checkpoint) + if err != nil { + tracing.AnnotateError(span, err) + return err + } + return finalizedBkt.Put(previousFinalizedCheckpointKey, enc) +} + +func updateChildOfPrevFinalizedCheckpoint(ctx context.Context, span *trace.Span, finalizedBkt, parentBkt *bolt.Bucket, checkpointRoot []byte) error { + container := ðpb.FinalizedBlockRootContainer{} + if err := decode(ctx, finalizedBkt.Get(checkpointRoot), container); err != nil { + tracing.AnnotateError(span, err) + return err + } + container.ChildRoot = parentBkt.Get(checkpointRoot) + enc, err := encode(ctx, container) + if err != nil { + tracing.AnnotateError(span, err) + return err + } + if err = finalizedBkt.Put(checkpointRoot, enc); err != nil { + tracing.AnnotateError(span, err) + return err + } + return nil +} diff --git a/beacon-chain/db/kv/finalized_block_roots_test.go b/beacon-chain/db/kv/finalized_block_roots_test.go index a9bd7780c..b5f45906e 100644 --- a/beacon-chain/db/kv/finalized_block_roots_test.go +++ b/beacon-chain/db/kv/finalized_block_roots_test.go @@ -26,38 +26,30 @@ func TestStore_IsFinalizedBlock(t *testing.T) { ctx := context.Background() require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) - - blks := makeBlocks(t, 0, slotsPerEpoch*3, genesisBlockRoot) + blks := makeBlocks(t, 0, slotsPerEpoch*2, genesisBlockRoot) require.NoError(t, db.SaveBlocks(ctx, blks)) root, err := blks[slotsPerEpoch].Block().HashTreeRoot() require.NoError(t, err) - cp := ðpb.Checkpoint{ Epoch: 1, Root: root[:], } - - st, err := util.NewBeaconState() - require.NoError(t, err) - // a state is required to save checkpoint - require.NoError(t, db.SaveState(ctx, st, root)) require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) - // All blocks up to slotsPerEpoch*2 should be in the finalized index. - for i := uint64(0); i < slotsPerEpoch*2; i++ { - root, err := blks[i].Block().HashTreeRoot() + for i := uint64(0); i <= slotsPerEpoch; i++ { + root, err = blks[i].Block().HashTreeRoot() require.NoError(t, err) - assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Block at index %d was not considered finalized in the index", i) + assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Block at index %d was not considered finalized", i) } - for i := slotsPerEpoch * 3; i < uint64(len(blks)); i++ { - root, err := blks[i].Block().HashTreeRoot() + for i := slotsPerEpoch + 1; i < uint64(len(blks)); i++ { + root, err = blks[i].Block().HashTreeRoot() require.NoError(t, err) - assert.Equal(t, false, db.IsFinalizedBlock(ctx, root), "Block at index %d was considered finalized in the index, but should not have", i) + assert.Equal(t, false, db.IsFinalizedBlock(ctx, root), "Block at index %d was considered finalized, but should not have", i) } } -func TestStore_IsFinalizedBlockGenesis(t *testing.T) { +func TestStore_IsFinalizedGenesisBlock(t *testing.T) { db := setupDB(t) ctx := context.Background() @@ -69,136 +61,114 @@ func TestStore_IsFinalizedBlockGenesis(t *testing.T) { require.NoError(t, err) require.NoError(t, db.SaveBlock(ctx, wsb)) require.NoError(t, db.SaveGenesisBlockRoot(ctx, root)) - assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Finalized genesis block doesn't exist in db") -} - -// This test scenario is to test a specific edge case where the finalized block root is not part of -// the finalized and canonical chain. -// -// Example: -// 0 1 2 3 4 5 6 slot -// a <- b <-- d <- e <- f <- g roots -// -// ^- c -// -// Imagine that epochs are 2 slots and that epoch 1, 2, and 3 are finalized. Checkpoint roots would -// be c, e, and g. In this scenario, c was a finalized checkpoint root but no block built upon it so -// it should not be considered "final and canonical" in the view at slot 6. -func TestStore_IsFinalized_ForkEdgeCase(t *testing.T) { - slotsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch) - blocks0 := makeBlocks(t, slotsPerEpoch*0, slotsPerEpoch, genesisBlockRoot) - blocks1 := append( - makeBlocks(t, slotsPerEpoch*1, 1, bytesutil.ToBytes32(sszRootOrDie(t, blocks0[len(blocks0)-1]))), // No block builds off of the first block in epoch. - makeBlocks(t, slotsPerEpoch*1+1, slotsPerEpoch-1, bytesutil.ToBytes32(sszRootOrDie(t, blocks0[len(blocks0)-1])))..., - ) - blocks2 := makeBlocks(t, slotsPerEpoch*2, slotsPerEpoch, bytesutil.ToBytes32(sszRootOrDie(t, blocks1[len(blocks1)-1]))) - - db := setupDB(t) - ctx := context.Background() - - require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) - require.NoError(t, db.SaveBlocks(ctx, blocks0)) - require.NoError(t, db.SaveBlocks(ctx, blocks1)) - require.NoError(t, db.SaveBlocks(ctx, blocks2)) - - // First checkpoint - checkpoint1 := ðpb.Checkpoint{ - Root: sszRootOrDie(t, blocks1[0]), - Epoch: 1, - } - - st, err := util.NewBeaconState() - require.NoError(t, err) - // A state is required to save checkpoint - require.NoError(t, db.SaveState(ctx, st, bytesutil.ToBytes32(checkpoint1.Root))) - require.NoError(t, db.SaveFinalizedCheckpoint(ctx, checkpoint1)) - // All blocks in blocks0 and blocks1 should be finalized and canonical. - for i, block := range append(blocks0, blocks1...) { - root := sszRootOrDie(t, block) - assert.Equal(t, true, db.IsFinalizedBlock(ctx, bytesutil.ToBytes32(root)), "%d - Expected block %#x to be finalized", i, root) - } - - // Second checkpoint - checkpoint2 := ðpb.Checkpoint{ - Root: sszRootOrDie(t, blocks2[0]), - Epoch: 2, - } - // A state is required to save checkpoint - require.NoError(t, db.SaveState(ctx, st, bytesutil.ToBytes32(checkpoint2.Root))) - require.NoError(t, db.SaveFinalizedCheckpoint(ctx, checkpoint2)) - // All blocks in blocks0 and blocks2 should be finalized and canonical. - for i, block := range append(blocks0, blocks2...) { - root := sszRootOrDie(t, block) - assert.Equal(t, true, db.IsFinalizedBlock(ctx, bytesutil.ToBytes32(root)), "%d - Expected block %#x to be finalized", i, root) - } - // All blocks in blocks1 should be finalized and canonical, except blocks1[0]. - for i, block := range blocks1 { - root := sszRootOrDie(t, block) - if db.IsFinalizedBlock(ctx, bytesutil.ToBytes32(root)) == (i == 0) { - t.Errorf("Expected db.IsFinalizedBlock(ctx, blocks1[%d]) to be %v", i, i != 0) - } - } + assert.Equal(t, true, db.IsFinalizedBlock(ctx, root)) } func TestStore_IsFinalizedChildBlock(t *testing.T) { slotsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch) ctx := context.Background() + db := setupDB(t) + require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) - eval := func(t testing.TB, ctx context.Context, db *Store, blks []interfaces.ReadOnlySignedBeaconBlock) { - require.NoError(t, db.SaveBlocks(ctx, blks)) - root, err := blks[slotsPerEpoch].Block().HashTreeRoot() - require.NoError(t, err) - - cp := ðpb.Checkpoint{ - Epoch: 1, - Root: root[:], - } - - st, err := util.NewBeaconState() - require.NoError(t, err) - // a state is required to save checkpoint - require.NoError(t, db.SaveState(ctx, st, root)) - require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) - - // All blocks up to slotsPerEpoch should have a finalized child block. - for i := uint64(0); i < slotsPerEpoch; i++ { - root, err := blks[i].Block().HashTreeRoot() - require.NoError(t, err) - assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Block at index %d was not considered finalized in the index", i) - blk, err := db.FinalizedChildBlock(ctx, root) - assert.NoError(t, err) - if blk == nil { - t.Error("Child block doesn't exist for valid finalized block.") - } - } + blks := makeBlocks(t, 0, slotsPerEpoch*2, genesisBlockRoot) + require.NoError(t, db.SaveBlocks(ctx, blks)) + root, err := blks[slotsPerEpoch].Block().HashTreeRoot() + require.NoError(t, err) + cp := ðpb.Checkpoint{ + Epoch: 1, + Root: root[:], } + require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) - setup := func(t testing.TB) *Store { - db := setupDB(t) - require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) - - return db + for i := uint64(0); i < slotsPerEpoch; i++ { + root, err = blks[i].Block().HashTreeRoot() + require.NoError(t, err) + assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Block at index %d was not considered finalized", i) + blk, err := db.FinalizedChildBlock(ctx, root) + assert.NoError(t, err) + assert.Equal(t, false, blk == nil, "Child block at index %d was not considered finalized", i) } - - t.Run("phase0", func(t *testing.T) { - db := setup(t) - - blks := makeBlocks(t, 0, slotsPerEpoch*3, genesisBlockRoot) - eval(t, ctx, db, blks) - }) - - t.Run("altair", func(t *testing.T) { - db := setup(t) - - blks := makeBlocksAltair(t, 0, slotsPerEpoch*3, genesisBlockRoot) - eval(t, ctx, db, blks) - }) } -func sszRootOrDie(t *testing.T, block interfaces.ReadOnlySignedBeaconBlock) []byte { - root, err := block.Block().HashTreeRoot() +func TestStore_ChildRootOfPrevFinalizedCheckpointIsUpdated(t *testing.T) { + slotsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch) + ctx := context.Background() + db := setupDB(t) + require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) + + blks := makeBlocks(t, 0, slotsPerEpoch*3, genesisBlockRoot) + require.NoError(t, db.SaveBlocks(ctx, blks)) + root, err := blks[slotsPerEpoch].Block().HashTreeRoot() require.NoError(t, err) - return root[:] + cp := ðpb.Checkpoint{ + Epoch: 1, + Root: root[:], + } + require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) + root2, err := blks[slotsPerEpoch*2].Block().HashTreeRoot() + require.NoError(t, err) + cp = ðpb.Checkpoint{ + Epoch: 2, + Root: root2[:], + } + require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) + + require.NoError(t, db.db.View(func(tx *bolt.Tx) error { + container := ðpb.FinalizedBlockRootContainer{} + f := tx.Bucket(finalizedBlockRootsIndexBucket).Get(root[:]) + require.NoError(t, decode(ctx, f, container)) + r, err := blks[slotsPerEpoch+1].Block().HashTreeRoot() + require.NoError(t, err) + assert.DeepEqual(t, r[:], container.ChildRoot) + return nil + })) +} + +func TestStore_OrphanedBlockIsNotFinalized(t *testing.T) { + slotsPerEpoch := uint64(params.BeaconConfig().SlotsPerEpoch) + db := setupDB(t) + ctx := context.Background() + + require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) + blk0 := util.NewBeaconBlock() + blk0.Block.ParentRoot = genesisBlockRoot[:] + blk0Root, err := blk0.Block.HashTreeRoot() + require.NoError(t, err) + blk1 := util.NewBeaconBlock() + blk1.Block.Slot = 1 + blk1.Block.ParentRoot = blk0Root[:] + blk2 := util.NewBeaconBlock() + blk2.Block.Slot = 2 + // orphan block at index 1 + blk2.Block.ParentRoot = blk0Root[:] + blk2Root, err := blk2.Block.HashTreeRoot() + require.NoError(t, err) + sBlk0, err := consensusblocks.NewSignedBeaconBlock(blk0) + require.NoError(t, err) + sBlk1, err := consensusblocks.NewSignedBeaconBlock(blk1) + require.NoError(t, err) + sBlk2, err := consensusblocks.NewSignedBeaconBlock(blk2) + require.NoError(t, err) + blks := append([]interfaces.ReadOnlySignedBeaconBlock{sBlk0, sBlk1, sBlk2}, makeBlocks(t, 3, slotsPerEpoch*2-3, blk2Root)...) + require.NoError(t, db.SaveBlocks(ctx, blks)) + + root, err := blks[slotsPerEpoch].Block().HashTreeRoot() + require.NoError(t, err) + cp := ðpb.Checkpoint{ + Epoch: 1, + Root: root[:], + } + require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) + + for i := uint64(0); i <= slotsPerEpoch; i++ { + root, err = blks[i].Block().HashTreeRoot() + require.NoError(t, err) + if i == 1 { + assert.Equal(t, false, db.IsFinalizedBlock(ctx, root), "Block at index 1 was considered finalized, but should not have") + } else { + assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Block at index %d was not considered finalized", i) + } + } } func makeBlocks(t *testing.T, i, n uint64, previousRoot [32]byte) []interfaces.ReadOnlySignedBeaconBlock { @@ -219,24 +189,6 @@ func makeBlocks(t *testing.T, i, n uint64, previousRoot [32]byte) []interfaces.R return ifaceBlocks } -func makeBlocksAltair(t *testing.T, startIdx, num uint64, previousRoot [32]byte) []interfaces.ReadOnlySignedBeaconBlock { - blocks := make([]*ethpb.SignedBeaconBlockAltair, num) - ifaceBlocks := make([]interfaces.ReadOnlySignedBeaconBlock, num) - for j := startIdx; j < num+startIdx; j++ { - parentRoot := make([]byte, fieldparams.RootLength) - copy(parentRoot, previousRoot[:]) - blocks[j-startIdx] = util.NewBeaconBlockAltair() - blocks[j-startIdx].Block.Slot = primitives.Slot(j + 1) - blocks[j-startIdx].Block.ParentRoot = parentRoot - var err error - previousRoot, err = blocks[j-startIdx].Block.HashTreeRoot() - require.NoError(t, err) - ifaceBlocks[j-startIdx], err = consensusblocks.NewSignedBeaconBlock(blocks[j-startIdx]) - require.NoError(t, err) - } - return ifaceBlocks -} - func TestStore_BackfillFinalizedIndexSingle(t *testing.T) { db := setupDB(t) ctx := context.Background() diff --git a/beacon-chain/db/kv/state.go b/beacon-chain/db/kv/state.go index 56a4a2d31..2e5f98894 100644 --- a/beacon-chain/db/kv/state.go +++ b/beacon-chain/db/kv/state.go @@ -458,7 +458,7 @@ func (s *Store) DeleteState(ctx context.Context, blockRoot [32]byte) error { bkt = tx.Bucket(stateBucket) // Safeguard against deleting genesis, finalized, head state. if bytes.Equal(blockRoot[:], finalized.Root) || bytes.Equal(blockRoot[:], genesisBlockRoot) || bytes.Equal(blockRoot[:], justified.Root) { - return ErrDeleteJustifiedAndFinalized + return ErrDeleteFinalized } // Nothing to delete if state doesn't exist. diff --git a/beacon-chain/state/stategen/replay_test.go b/beacon-chain/state/stategen/replay_test.go index 9d8f3c489..bf66c8f96 100644 --- a/beacon-chain/state/stategen/replay_test.go +++ b/beacon-chain/state/stategen/replay_test.go @@ -189,7 +189,7 @@ func TestLoadBlocks_FirstBranch(t *testing.T) { roots, savedBlocks, err := tree1(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32)) require.NoError(t, err) - filteredBlocks, err := s.loadBlocks(ctx, 0, 8, roots[len(roots)-1]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 9, roots[len(roots)-1]) require.NoError(t, err) wanted := []*ethpb.SignedBeaconBlock{ @@ -220,7 +220,7 @@ func TestLoadBlocks_SecondBranch(t *testing.T) { roots, savedBlocks, err := tree1(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32)) require.NoError(t, err) - filteredBlocks, err := s.loadBlocks(ctx, 0, 5, roots[5]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 6, roots[5]) require.NoError(t, err) wanted := []*ethpb.SignedBeaconBlock{ @@ -249,7 +249,7 @@ func TestLoadBlocks_ThirdBranch(t *testing.T) { roots, savedBlocks, err := tree1(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32)) require.NoError(t, err) - filteredBlocks, err := s.loadBlocks(ctx, 0, 7, roots[7]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 8, roots[7]) require.NoError(t, err) wanted := []*ethpb.SignedBeaconBlock{ @@ -280,7 +280,7 @@ func TestLoadBlocks_SameSlots(t *testing.T) { roots, savedBlocks, err := tree2(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32)) require.NoError(t, err) - filteredBlocks, err := s.loadBlocks(ctx, 0, 3, roots[6]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 4, roots[6]) require.NoError(t, err) wanted := []*ethpb.SignedBeaconBlock{ @@ -309,7 +309,7 @@ func TestLoadBlocks_SameEndSlots(t *testing.T) { roots, savedBlocks, err := tree3(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32)) require.NoError(t, err) - filteredBlocks, err := s.loadBlocks(ctx, 0, 2, roots[2]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 3, roots[2]) require.NoError(t, err) wanted := []*ethpb.SignedBeaconBlock{ @@ -337,7 +337,7 @@ func TestLoadBlocks_SameEndSlotsWith2blocks(t *testing.T) { roots, savedBlocks, err := tree4(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32)) require.NoError(t, err) - filteredBlocks, err := s.loadBlocks(ctx, 0, 2, roots[1]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 3, roots[1]) require.NoError(t, err) wanted := []*ethpb.SignedBeaconBlock{ @@ -363,7 +363,7 @@ func TestLoadBlocks_BadStart(t *testing.T) { roots, _, err := tree1(t, beaconDB, bytesutil.PadTo([]byte{'A'}, 32)) require.NoError(t, err) - _, err = s.loadBlocks(ctx, 0, 5, roots[8]) + _, err = s.loadBlocks(ctx, 0, 6, roots[8]) assert.ErrorContains(t, "end block roots don't match", err) } @@ -374,63 +374,63 @@ func TestLoadBlocks_BadStart(t *testing.T) { // \- B7 func tree1(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) { b0 := util.NewBeaconBlock() - b0.Block.Slot = 0 + b0.Block.Slot = 1 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b1 := util.NewBeaconBlock() - b1.Block.Slot = 1 + b1.Block.Slot = 2 b1.Block.ParentRoot = r0[:] r1, err := b1.Block.HashTreeRoot() if err != nil { return nil, nil, err } b2 := util.NewBeaconBlock() - b2.Block.Slot = 2 + b2.Block.Slot = 3 b2.Block.ParentRoot = r1[:] r2, err := b2.Block.HashTreeRoot() if err != nil { return nil, nil, err } b3 := util.NewBeaconBlock() - b3.Block.Slot = 3 + b3.Block.Slot = 4 b3.Block.ParentRoot = r1[:] r3, err := b3.Block.HashTreeRoot() if err != nil { return nil, nil, err } b4 := util.NewBeaconBlock() - b4.Block.Slot = 4 + b4.Block.Slot = 5 b4.Block.ParentRoot = r2[:] r4, err := b4.Block.HashTreeRoot() if err != nil { return nil, nil, err } b5 := util.NewBeaconBlock() - b5.Block.Slot = 5 + b5.Block.Slot = 6 b5.Block.ParentRoot = r3[:] r5, err := b5.Block.HashTreeRoot() if err != nil { return nil, nil, err } b6 := util.NewBeaconBlock() - b6.Block.Slot = 6 + b6.Block.Slot = 7 b6.Block.ParentRoot = r4[:] r6, err := b6.Block.HashTreeRoot() if err != nil { return nil, nil, err } b7 := util.NewBeaconBlock() - b7.Block.Slot = 7 + b7.Block.Slot = 8 b7.Block.ParentRoot = r6[:] r7, err := b7.Block.HashTreeRoot() if err != nil { return nil, nil, err } b8 := util.NewBeaconBlock() - b8.Block.Slot = 8 + b8.Block.Slot = 9 b8.Block.ParentRoot = r6[:] r8, err := b8.Block.HashTreeRoot() if err != nil { @@ -466,21 +466,21 @@ func tree1(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, // \- B2 -- B3 func tree2(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) { b0 := util.NewBeaconBlock() - b0.Block.Slot = 0 + b0.Block.Slot = 1 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b1 := util.NewBeaconBlock() - b1.Block.Slot = 1 + b1.Block.Slot = 2 b1.Block.ParentRoot = r0[:] r1, err := b1.Block.HashTreeRoot() if err != nil { return nil, nil, err } b21 := util.NewBeaconBlock() - b21.Block.Slot = 2 + b21.Block.Slot = 3 b21.Block.ParentRoot = r1[:] b21.Block.StateRoot = bytesutil.PadTo([]byte{'A'}, 32) r21, err := b21.Block.HashTreeRoot() @@ -488,7 +488,7 @@ func tree2(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b22 := util.NewBeaconBlock() - b22.Block.Slot = 2 + b22.Block.Slot = 3 b22.Block.ParentRoot = r1[:] b22.Block.StateRoot = bytesutil.PadTo([]byte{'B'}, 32) r22, err := b22.Block.HashTreeRoot() @@ -496,7 +496,7 @@ func tree2(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b23 := util.NewBeaconBlock() - b23.Block.Slot = 2 + b23.Block.Slot = 3 b23.Block.ParentRoot = r1[:] b23.Block.StateRoot = bytesutil.PadTo([]byte{'C'}, 32) r23, err := b23.Block.HashTreeRoot() @@ -504,7 +504,7 @@ func tree2(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b24 := util.NewBeaconBlock() - b24.Block.Slot = 2 + b24.Block.Slot = 3 b24.Block.ParentRoot = r1[:] b24.Block.StateRoot = bytesutil.PadTo([]byte{'D'}, 32) r24, err := b24.Block.HashTreeRoot() @@ -512,7 +512,7 @@ func tree2(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b3 := util.NewBeaconBlock() - b3.Block.Slot = 3 + b3.Block.Slot = 4 b3.Block.ParentRoot = r24[:] r3, err := b3.Block.HashTreeRoot() if err != nil { @@ -549,21 +549,21 @@ func tree2(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, // \- B2 func tree3(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) { b0 := util.NewBeaconBlock() - b0.Block.Slot = 0 + b0.Block.Slot = 1 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b1 := util.NewBeaconBlock() - b1.Block.Slot = 1 + b1.Block.Slot = 2 b1.Block.ParentRoot = r0[:] r1, err := b1.Block.HashTreeRoot() if err != nil { return nil, nil, err } b21 := util.NewBeaconBlock() - b21.Block.Slot = 2 + b21.Block.Slot = 3 b21.Block.ParentRoot = r1[:] b21.Block.StateRoot = bytesutil.PadTo([]byte{'A'}, 32) r21, err := b21.Block.HashTreeRoot() @@ -571,7 +571,7 @@ func tree3(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b22 := util.NewBeaconBlock() - b22.Block.Slot = 2 + b22.Block.Slot = 3 b22.Block.ParentRoot = r1[:] b22.Block.StateRoot = bytesutil.PadTo([]byte{'B'}, 32) r22, err := b22.Block.HashTreeRoot() @@ -579,7 +579,7 @@ func tree3(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b23 := util.NewBeaconBlock() - b23.Block.Slot = 2 + b23.Block.Slot = 3 b23.Block.ParentRoot = r1[:] b23.Block.StateRoot = bytesutil.PadTo([]byte{'C'}, 32) r23, err := b23.Block.HashTreeRoot() @@ -587,7 +587,7 @@ func tree3(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b24 := util.NewBeaconBlock() - b24.Block.Slot = 2 + b24.Block.Slot = 3 b24.Block.ParentRoot = r1[:] b24.Block.StateRoot = bytesutil.PadTo([]byte{'D'}, 32) r24, err := b24.Block.HashTreeRoot() @@ -626,14 +626,14 @@ func tree3(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, // \- B2 func tree4(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, []*ethpb.SignedBeaconBlock, error) { b0 := util.NewBeaconBlock() - b0.Block.Slot = 0 + b0.Block.Slot = 1 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b21 := util.NewBeaconBlock() - b21.Block.Slot = 2 + b21.Block.Slot = 3 b21.Block.ParentRoot = r0[:] b21.Block.StateRoot = bytesutil.PadTo([]byte{'A'}, 32) r21, err := b21.Block.HashTreeRoot() @@ -641,7 +641,7 @@ func tree4(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b22 := util.NewBeaconBlock() - b22.Block.Slot = 2 + b22.Block.Slot = 3 b22.Block.ParentRoot = r0[:] b22.Block.StateRoot = bytesutil.PadTo([]byte{'B'}, 32) r22, err := b22.Block.HashTreeRoot() @@ -649,7 +649,7 @@ func tree4(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b23 := util.NewBeaconBlock() - b23.Block.Slot = 2 + b23.Block.Slot = 3 b23.Block.ParentRoot = r0[:] b23.Block.StateRoot = bytesutil.PadTo([]byte{'C'}, 32) r23, err := b23.Block.HashTreeRoot() @@ -657,7 +657,7 @@ func tree4(t *testing.T, beaconDB db.Database, genesisRoot []byte) ([][32]byte, return nil, nil, err } b24 := util.NewBeaconBlock() - b24.Block.Slot = 2 + b24.Block.Slot = 3 b24.Block.ParentRoot = r0[:] b24.Block.StateRoot = bytesutil.PadTo([]byte{'D'}, 32) r24, err := b24.Block.HashTreeRoot() @@ -697,17 +697,17 @@ func TestLoadFinalizedBlocks(t *testing.T) { gRoot, err := gBlock.Block.HashTreeRoot() require.NoError(t, err) util.SaveBlock(t, ctx, beaconDB, gBlock) - require.NoError(t, beaconDB.SaveGenesisBlockRoot(ctx, [32]byte{})) + require.NoError(t, beaconDB.SaveGenesisBlockRoot(ctx, gRoot)) roots, _, err := tree1(t, beaconDB, gRoot[:]) require.NoError(t, err) - filteredBlocks, err := s.loadFinalizedBlocks(ctx, 0, 8) + filteredBlocks, err := s.loadFinalizedBlocks(ctx, 0, 9) require.NoError(t, err) - require.Equal(t, 0, len(filteredBlocks)) + require.Equal(t, 1, len(filteredBlocks)) require.NoError(t, beaconDB.SaveStateSummary(ctx, ðpb.StateSummary{Root: roots[8][:]})) require.NoError(t, s.beaconDB.SaveFinalizedCheckpoint(ctx, ðpb.Checkpoint{Root: roots[8][:]})) - filteredBlocks, err = s.loadFinalizedBlocks(ctx, 0, 8) + filteredBlocks, err = s.loadFinalizedBlocks(ctx, 0, 9) require.NoError(t, err) - require.Equal(t, 10, len(filteredBlocks)) + require.Equal(t, 7, len(filteredBlocks)) }