diff --git a/cl/persistence/beacon_indicies/indicies.go b/cl/persistence/beacon_indicies/indicies.go index dac453e0d96..b3994792b52 100644 --- a/cl/persistence/beacon_indicies/indicies.go +++ b/cl/persistence/beacon_indicies/indicies.go @@ -244,10 +244,15 @@ func WriteParentBlockRoot(ctx context.Context, tx kv.RwTx, blockRoot, parentRoot return tx.Put(kv.BlockRootToParentRoot, blockRoot[:], parentRoot[:]) } -func TruncateCanonicalChain(ctx context.Context, tx kv.RwTx, slot uint64) error { - return tx.ForEach(kv.CanonicalBlockRoots, base_encoding.Encode64ToBytes4(slot), func(k, _ []byte) error { +func TruncateCanonicalChain(ctx context.Context, tx kv.RwTx, slot uint64) (uint64, common.Hash, error) { + var highestSlot uint64 + var highestRoot common.Hash + err := tx.ForEach(kv.CanonicalBlockRoots, base_encoding.Encode64ToBytes4(slot), func(k, v []byte) error { + highestSlot = base_encoding.Decode64FromBytes4(k) + copy(highestRoot[:], v) return tx.Delete(kv.CanonicalBlockRoots, k) }) + return highestSlot, highestRoot, err } func PruneSignedHeaders(tx kv.RwTx, from uint64) error { diff --git a/cl/persistence/beacon_indicies/indicies_test.go b/cl/persistence/beacon_indicies/indicies_test.go index 2df26462d8c..2bed0c10df3 100644 --- a/cl/persistence/beacon_indicies/indicies_test.go +++ b/cl/persistence/beacon_indicies/indicies_test.go @@ -120,13 +120,57 @@ func TestTruncateCanonicalChain(t *testing.T) { require.NoError(t, err) require.Equal(t, common.Hash(blockRoot), canonicalRoot) - require.NoError(t, TruncateCanonicalChain(context.Background(), tx, 0)) + _, _, err = TruncateCanonicalChain(context.Background(), tx, 0) + require.NoError(t, err) canonicalRoot, err = ReadCanonicalBlockRoot(tx, block.Block.Slot) require.NoError(t, err) require.Equal(t, common.Hash{}, canonicalRoot) } +// TestTruncateCanonicalChainReturnsHighest verifies that TruncateCanonicalChain +// returns the slot and root of the highest entry it deleted, and preserves +// entries below the truncation point. +func TestTruncateCanonicalChainReturnsHighest(t *testing.T) { + db := setupTestDB(t) + defer db.Close() + tx, err := db.BeginRw(context.Background()) + require.NoError(t, err) + defer tx.Rollback() + + slots := []uint64{100, 101, 102, 103, 104} + roots := make([]common.Hash, len(slots)) + for i, slot := range slots { + block := cltypes.NewSignedBeaconBlock(&clparams.MainnetBeaconConfig, clparams.Phase0Version) + block.Block.Slot = slot + block.Block.ParentRoot = common.Hash{byte(i + 1)} + require.NoError(t, WriteBeaconBlockHeaderAndIndicies(context.Background(), tx, block.SignedBeaconBlockHeader(), true)) + root, err := block.Block.HashSSZ() + require.NoError(t, err) + roots[i] = common.Hash(root) + } + + // Truncate from slot 102: deletes 102/103/104, preserves 100/101. + highestSlot, highestRoot, err := TruncateCanonicalChain(context.Background(), tx, 102) + require.NoError(t, err) + require.Equal(t, uint64(104), highestSlot) + require.Equal(t, roots[4], highestRoot) + + // Slots 100, 101 must still be canonical. + for i, slot := range []uint64{100, 101} { + preserved, err := ReadCanonicalBlockRoot(tx, slot) + require.NoError(t, err) + require.Equal(t, roots[i], preserved) + } + + // Slots 102, 103, 104 must be gone. + for _, slot := range []uint64{102, 103, 104} { + gone, err := ReadCanonicalBlockRoot(tx, slot) + require.NoError(t, err) + require.Equal(t, common.Hash{}, gone) + } +} + func TestReadBeaconBlockHeader(t *testing.T) { db := setupTestDB(t) defer db.Close() diff --git a/cl/phase1/stages/forkchoice.go b/cl/phase1/stages/forkchoice.go index acbb8d3f1eb..5ec15961f37 100644 --- a/cl/phase1/stages/forkchoice.go +++ b/cl/phase1/stages/forkchoice.go @@ -107,18 +107,6 @@ func updateCanonicalChainInTheDatabase(ctx context.Context, tx kv.RwTx, headSlot return fmt.Errorf("failed to read canonical block root: %w", err) } - oldCanonical := common.Hash{} - // Guard against uint64 underflow: currentSlot=0 → currentSlot-1 = MaxUint64 → infinite loop. - for i := currentSlot; i > 1; i-- { - oldCanonical, err = beacon_indicies.ReadCanonicalBlockRoot(tx, i-1) - if err != nil { - return fmt.Errorf("failed to read canonical block root: %w", err) - } - if oldCanonical != (common.Hash{}) { - break - } - } - // List of new canonical chain entries reconnectionRoots := []canonicalEntry{{currentSlot, currentRoot}} @@ -151,8 +139,10 @@ func updateCanonicalChainInTheDatabase(ctx context.Context, tx kv.RwTx, headSlot reconnectionRoots = append(reconnectionRoots, canonicalEntry{currentSlot, currentRoot}) } - // Truncate the canonical chain at the current slot - if err := beacon_indicies.TruncateCanonicalChain(ctx, tx, currentSlot); err != nil { + // Truncate the canonical chain at the common ancestor and capture the + // previous canonical tip (highest slot/root deleted) for the chain_reorg event. + oldCanonicalSlot, oldCanonical, err := beacon_indicies.TruncateCanonicalChain(ctx, tx, currentSlot) + if err != nil { return fmt.Errorf("failed to truncate canonical chain: %w", err) } @@ -168,12 +158,9 @@ func updateCanonicalChainInTheDatabase(ctx context.Context, tx kv.RwTx, headSlot return fmt.Errorf("failed to mark root canonical: %w", err) } - // check reorg - parentRoot, err := beacon_indicies.ReadParentBlockRoot(ctx, tx, headRoot) - if err != nil { - return fmt.Errorf("failed to read parent block root: %w", err) - } - if parentRoot != oldCanonical { + // A reorg occurred if the previous canonical chain extended past the + // common ancestor. currentSlot is the common ancestor's slot here. + if oldCanonical != (common.Hash{}) && currentSlot < oldCanonicalSlot { log.Debug("cl reorg", "new_head_slot", headSlot, "fork_slot", currentSlot, "old_canonical", oldCanonical, "new_canonical", headRoot) oldStateRoot, err := beacon_indicies.ReadStateRootByBlockRoot(ctx, tx, oldCanonical) if err != nil { @@ -187,7 +174,7 @@ func updateCanonicalChainInTheDatabase(ctx context.Context, tx kv.RwTx, headSlot } reorgEvent := &beaconevents.ChainReorgData{ Slot: headSlot, - Depth: currentSlot - headSlot, + Depth: oldCanonicalSlot - currentSlot, OldHeadBlock: oldCanonical, NewHeadBlock: headRoot, OldHeadState: oldStateRoot,