diff --git a/beacon-chain/db/kv/blocks.go b/beacon-chain/db/kv/blocks.go index 3b1226cce1ce..7cf787769e0e 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 ErrDeleteFinalized + return ErrDeleteJustifiedAndFinalized } 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 384e9533ecd7..67235b2aef41 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), ErrDeleteFinalized) + require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteJustifiedAndFinalized) } 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), ErrDeleteFinalized) + require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteJustifiedAndFinalized) } 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), ErrDeleteFinalized) + require.ErrorIs(t, db.DeleteBlock(ctx, root), ErrDeleteJustifiedAndFinalized) } 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 957b1735796b..6d79723cc7a7 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" -// ErrDeleteFinalized is raised when we attempt to delete a finalized block/state -var ErrDeleteFinalized = errors.New("cannot delete finalized block or state") +// ErrDeleteJustifiedAndFinalized is raised when we attempt to delete a finalized block/state +var ErrDeleteJustifiedAndFinalized = 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 2d655889a112..3376abc8ef40 100644 --- a/beacon-chain/db/kv/finalized_block_roots.go +++ b/beacon-chain/db/kv/finalized_block_roots.go @@ -5,6 +5,7 @@ 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" @@ -28,90 +29,139 @@ 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 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. +// 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. func (s *Store) updateFinalizedBlockRoots(ctx context.Context, tx *bolt.Tx, checkpoint *ethpb.Checkpoint) error { ctx, span := trace.StartSpan(ctx, "BeaconDB.updateFinalizedBlockRoots") defer span.End() - finalizedBkt := tx.Bucket(finalizedBlockRootsIndexBucket) + 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. previousFinalizedCheckpoint := ðpb.Checkpoint{} - if b := finalizedBkt.Get(previousFinalizedCheckpointKey); b != nil { + if b := bkt.Get(previousFinalizedCheckpointKey); b != nil { if err := decode(ctx, b, previousFinalizedCheckpoint); err != nil { tracing.AnnotateError(span, err) return err } } - // 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) + 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)) if err != nil { tracing.AnnotateError(span, err) return err } - if err = finalizedBkt.Put(checkpoint.Root, enc); err != nil { + if err := blocks.BeaconBlockIsNil(signedBlock); err != nil { tracing.AnnotateError(span, err) return err } - return updatePrevFinalizedCheckpoint(ctx, span, finalizedBkt, checkpoint) - } + block := signedBlock.Block() + + 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 { + enc, err := encode(ctx, container) + if err != nil { + tracing.AnnotateError(span, err) + return err + } + if err := bkt.Put(root, enc); err != nil { + tracing.AnnotateError(span, err) 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] + // 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 } - 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] + parent.ChildRoot = root + enc, err := encode(ctx, parent) + if err != nil { + tracing.AnnotateError(span, err) + return err } - default: - container = ðpb.FinalizedBlockRootContainer{ - ParentRoot: finalized[i-1], - ChildRoot: finalized[i+1], + if err := bkt.Put(pr[:], enc); err != nil { + tracing.AnnotateError(span, err) + return err } + break } + previousRoot = root + root = pr[:] + } - enc, err := encode(ctx, container) - if err != nil { - tracing.AnnotateError(span, err) - return err + // 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 = finalizedBkt.Put(r, enc); err != nil { + if err := bkt.Put(root, containerFinalizedButNotCanonical); err != nil { tracing.AnnotateError(span, err) return err } } - return updatePrevFinalizedCheckpoint(ctx, span, finalizedBkt, checkpoint) + // Update previous checkpoint + enc, err := encode(ctx, checkpoint) + if err != nil { + tracing.AnnotateError(span, err) + return err + } + + return bkt.Put(previousFinalizedCheckpointKey, enc) } // BackfillFinalizedIndex updates the finalized index for a contiguous chain of blocks that are the ancestors of the @@ -192,6 +242,8 @@ 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() @@ -244,53 +296,3 @@ 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 b5f45906e3bd..a9bd7780c34a 100644 --- a/beacon-chain/db/kv/finalized_block_roots_test.go +++ b/beacon-chain/db/kv/finalized_block_roots_test.go @@ -26,30 +26,38 @@ func TestStore_IsFinalizedBlock(t *testing.T) { ctx := context.Background() require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) - blks := makeBlocks(t, 0, slotsPerEpoch*2, 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) + 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)) - for i := uint64(0); i <= slotsPerEpoch; i++ { - root, err = blks[i].Block().HashTreeRoot() + // 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() require.NoError(t, err) - assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Block at index %d was not considered finalized", i) + assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Block at index %d was not considered finalized in the index", i) } - for i := slotsPerEpoch + 1; i < uint64(len(blks)); i++ { - root, err = blks[i].Block().HashTreeRoot() + for i := slotsPerEpoch * 3; 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, but should not have", i) + assert.Equal(t, false, db.IsFinalizedBlock(ctx, root), "Block at index %d was considered finalized in the index, but should not have", i) } } -func TestStore_IsFinalizedGenesisBlock(t *testing.T) { +func TestStore_IsFinalizedBlockGenesis(t *testing.T) { db := setupDB(t) ctx := context.Background() @@ -61,114 +69,136 @@ func TestStore_IsFinalizedGenesisBlock(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)) + assert.Equal(t, true, db.IsFinalizedBlock(ctx, root), "Finalized genesis block doesn't exist in db") } -func TestStore_IsFinalizedChildBlock(t *testing.T) { +// 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) - ctx := context.Background() + 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)) - 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{ + // First checkpoint + checkpoint1 := ðpb.Checkpoint{ + Root: sszRootOrDie(t, blocks1[0]), 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) - 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) - } -} -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() + st, err := util.NewBeaconState() require.NoError(t, err) - cp := ðpb.Checkpoint{ - Epoch: 1, - Root: root[:], + // 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) } - require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) - root2, err := blks[slotsPerEpoch*2].Block().HashTreeRoot() - require.NoError(t, err) - cp = ðpb.Checkpoint{ + + // Second checkpoint + checkpoint2 := ðpb.Checkpoint{ + Root: sszRootOrDie(t, blocks2[0]), 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 - })) + // 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) + } + } } -func TestStore_OrphanedBlockIsNotFinalized(t *testing.T) { +func TestStore_IsFinalizedChildBlock(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)) + 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) - root, err := blks[slotsPerEpoch].Block().HashTreeRoot() - require.NoError(t, err) - cp := ðpb.Checkpoint{ - Epoch: 1, - Root: root[:], - } - require.NoError(t, db.SaveFinalizedCheckpoint(ctx, cp)) + cp := ðpb.Checkpoint{ + Epoch: 1, + Root: root[:], + } - for i := uint64(0); i <= slotsPerEpoch; i++ { - root, err = blks[i].Block().HashTreeRoot() + st, err := util.NewBeaconState() 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) + // 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.") + } } } + + setup := func(t testing.TB) *Store { + db := setupDB(t) + require.NoError(t, db.SaveGenesisBlockRoot(ctx, genesisBlockRoot)) + + return db + } + + 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() + require.NoError(t, err) + return root[:] } func makeBlocks(t *testing.T, i, n uint64, previousRoot [32]byte) []interfaces.ReadOnlySignedBeaconBlock { @@ -189,6 +219,24 @@ 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 2e5f988945b1..56a4a2d31de8 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 ErrDeleteFinalized + return ErrDeleteJustifiedAndFinalized } // 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 bf66c8f96335..9d8f3c489647 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, 9, roots[len(roots)-1]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 8, 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, 6, roots[5]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 5, 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, 8, roots[7]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 7, 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, 4, roots[6]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 3, 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, 3, roots[2]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 2, 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, 3, roots[1]) + filteredBlocks, err := s.loadBlocks(ctx, 0, 2, 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, 6, roots[8]) + _, err = s.loadBlocks(ctx, 0, 5, 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 = 1 + b0.Block.Slot = 0 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b1 := util.NewBeaconBlock() - b1.Block.Slot = 2 + b1.Block.Slot = 1 b1.Block.ParentRoot = r0[:] r1, err := b1.Block.HashTreeRoot() if err != nil { return nil, nil, err } b2 := util.NewBeaconBlock() - b2.Block.Slot = 3 + b2.Block.Slot = 2 b2.Block.ParentRoot = r1[:] r2, err := b2.Block.HashTreeRoot() if err != nil { return nil, nil, err } b3 := util.NewBeaconBlock() - b3.Block.Slot = 4 + b3.Block.Slot = 3 b3.Block.ParentRoot = r1[:] r3, err := b3.Block.HashTreeRoot() if err != nil { return nil, nil, err } b4 := util.NewBeaconBlock() - b4.Block.Slot = 5 + b4.Block.Slot = 4 b4.Block.ParentRoot = r2[:] r4, err := b4.Block.HashTreeRoot() if err != nil { return nil, nil, err } b5 := util.NewBeaconBlock() - b5.Block.Slot = 6 + b5.Block.Slot = 5 b5.Block.ParentRoot = r3[:] r5, err := b5.Block.HashTreeRoot() if err != nil { return nil, nil, err } b6 := util.NewBeaconBlock() - b6.Block.Slot = 7 + b6.Block.Slot = 6 b6.Block.ParentRoot = r4[:] r6, err := b6.Block.HashTreeRoot() if err != nil { return nil, nil, err } b7 := util.NewBeaconBlock() - b7.Block.Slot = 8 + b7.Block.Slot = 7 b7.Block.ParentRoot = r6[:] r7, err := b7.Block.HashTreeRoot() if err != nil { return nil, nil, err } b8 := util.NewBeaconBlock() - b8.Block.Slot = 9 + b8.Block.Slot = 8 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 = 1 + b0.Block.Slot = 0 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b1 := util.NewBeaconBlock() - b1.Block.Slot = 2 + b1.Block.Slot = 1 b1.Block.ParentRoot = r0[:] r1, err := b1.Block.HashTreeRoot() if err != nil { return nil, nil, err } b21 := util.NewBeaconBlock() - b21.Block.Slot = 3 + b21.Block.Slot = 2 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 = 3 + b22.Block.Slot = 2 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 = 3 + b23.Block.Slot = 2 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 = 3 + b24.Block.Slot = 2 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 = 4 + b3.Block.Slot = 3 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 = 1 + b0.Block.Slot = 0 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b1 := util.NewBeaconBlock() - b1.Block.Slot = 2 + b1.Block.Slot = 1 b1.Block.ParentRoot = r0[:] r1, err := b1.Block.HashTreeRoot() if err != nil { return nil, nil, err } b21 := util.NewBeaconBlock() - b21.Block.Slot = 3 + b21.Block.Slot = 2 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 = 3 + b22.Block.Slot = 2 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 = 3 + b23.Block.Slot = 2 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 = 3 + b24.Block.Slot = 2 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 = 1 + b0.Block.Slot = 0 b0.Block.ParentRoot = genesisRoot r0, err := b0.Block.HashTreeRoot() if err != nil { return nil, nil, err } b21 := util.NewBeaconBlock() - b21.Block.Slot = 3 + b21.Block.Slot = 2 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 = 3 + b22.Block.Slot = 2 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 = 3 + b23.Block.Slot = 2 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 = 3 + b24.Block.Slot = 2 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, gRoot)) + require.NoError(t, beaconDB.SaveGenesisBlockRoot(ctx, [32]byte{})) roots, _, err := tree1(t, beaconDB, gRoot[:]) require.NoError(t, err) - filteredBlocks, err := s.loadFinalizedBlocks(ctx, 0, 9) + filteredBlocks, err := s.loadFinalizedBlocks(ctx, 0, 8) require.NoError(t, err) - require.Equal(t, 1, len(filteredBlocks)) + require.Equal(t, 0, 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, 9) + filteredBlocks, err = s.loadFinalizedBlocks(ctx, 0, 8) require.NoError(t, err) - require.Equal(t, 7, len(filteredBlocks)) + require.Equal(t, 10, len(filteredBlocks)) }