From 32bc5e6a7612fbb78f9ef565cdfe9525430843f6 Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 5 Dec 2025 15:45:42 -0300 Subject: [PATCH] Track the dependent root of the latest finalized checkpoint This PR adds the dependent root of the latest finalized checkpoint to forkchoice since this node will be typically pruned upon finalization. --- .../forkchoice/doubly-linked-tree/forkchoice.go | 8 ++++++-- .../forkchoice/doubly-linked-tree/store.go | 3 +++ .../forkchoice/doubly-linked-tree/store_test.go | 17 ++++++++++++++--- .../forkchoice/doubly-linked-tree/types.go | 1 + changelog/potuz_finalized_deproot.md | 3 +++ 5 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 changelog/potuz_finalized_deproot.md diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go index 04c6794b52f8..9c948d52ccea 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go @@ -642,8 +642,12 @@ func (f *ForkChoice) DependentRootForEpoch(root [32]byte, epoch primitives.Epoch if !ok || node == nil { return [32]byte{}, ErrNilNode } - if slots.ToEpoch(node.slot) >= epoch && node.parent != nil { - node = node.parent + if slots.ToEpoch(node.slot) >= epoch { + if node.parent != nil { + node = node.parent + } else { + return f.store.finalizedDependentRoot, nil + } } return node.root, nil } diff --git a/beacon-chain/forkchoice/doubly-linked-tree/store.go b/beacon-chain/forkchoice/doubly-linked-tree/store.go index d645ab9f3567..ac163d71df29 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/store.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/store.go @@ -212,6 +212,9 @@ func (s *Store) prune(ctx context.Context) error { return nil } + // Save the new finalized dependent root because it will be pruned + s.finalizedDependentRoot = finalizedNode.parent.root + // Prune nodeByRoot starting from root if err := s.pruneFinalizedNodeByRootMap(ctx, s.treeRootNode, finalizedNode); err != nil { return err diff --git a/beacon-chain/forkchoice/doubly-linked-tree/store_test.go b/beacon-chain/forkchoice/doubly-linked-tree/store_test.go index ec1f8404fadc..eb5515f2e232 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/store_test.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/store_test.go @@ -465,6 +465,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) { ctx := t.Context() f := setup(1, 1) + // Insert a block in slot 32 state, blk, err := prepareForkchoiceState(ctx, params.BeaconConfig().SlotsPerEpoch, [32]byte{'a'}, params.BeaconConfig().ZeroHash, params.BeaconConfig().ZeroHash, 1, 1) require.NoError(t, err) require.NoError(t, f.InsertNode(ctx, state, blk)) @@ -475,6 +476,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) { require.NoError(t, err) require.Equal(t, dependent, [32]byte{}) + // Insert a block in slot 33 state, blk1, err := prepareForkchoiceState(ctx, params.BeaconConfig().SlotsPerEpoch+1, [32]byte{'b'}, blk.Root(), params.BeaconConfig().ZeroHash, 1, 1) require.NoError(t, err) require.NoError(t, f.InsertNode(ctx, state, blk1)) @@ -488,7 +490,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) { require.NoError(t, err) require.Equal(t, dependent, [32]byte{}) - // Insert a block for the next epoch (missed slot 0) + // Insert a block for the next epoch (missed slot 0), slot 65 state, blk2, err := prepareForkchoiceState(ctx, 2*params.BeaconConfig().SlotsPerEpoch+1, [32]byte{'c'}, blk1.Root(), params.BeaconConfig().ZeroHash, 1, 1) require.NoError(t, err) @@ -509,6 +511,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) { require.NoError(t, err) require.Equal(t, dependent, blk1.Root()) + // Insert a block at slot 66 state, blk3, err := prepareForkchoiceState(ctx, 2*params.BeaconConfig().SlotsPerEpoch+2, [32]byte{'d'}, blk2.Root(), params.BeaconConfig().ZeroHash, 1, 1) require.NoError(t, err) require.NoError(t, f.InsertNode(ctx, state, blk3)) @@ -533,8 +536,11 @@ func TestStore_TargetRootForEpoch(t *testing.T) { dependent, err = f.DependentRoot(1) require.NoError(t, err) require.Equal(t, [32]byte{}, dependent) + dependent, err = f.DependentRoot(2) + require.NoError(t, err) + require.Equal(t, blk1.Root(), dependent) - // Insert a block for next epoch (slot 0 present) + // Insert a block for the next epoch, slot 96 (descends from finalized at slot 33) state, blk4, err := prepareForkchoiceState(ctx, 3*params.BeaconConfig().SlotsPerEpoch, [32]byte{'e'}, blk1.Root(), params.BeaconConfig().ZeroHash, 1, 1) require.NoError(t, err) require.NoError(t, f.InsertNode(ctx, state, blk4)) @@ -551,6 +557,7 @@ func TestStore_TargetRootForEpoch(t *testing.T) { require.NoError(t, err) require.Equal(t, dependent, blk1.Root()) + // Insert a block at slot 97 state, blk5, err := prepareForkchoiceState(ctx, 3*params.BeaconConfig().SlotsPerEpoch+1, [32]byte{'f'}, blk4.Root(), params.BeaconConfig().ZeroHash, 1, 1) require.NoError(t, err) require.NoError(t, f.InsertNode(ctx, state, blk5)) @@ -600,12 +607,16 @@ func TestStore_TargetRootForEpoch(t *testing.T) { require.NoError(t, err) require.Equal(t, target, blk1.Root()) - // Prune finalization + // Prune finalization, finalize the block at slot 96 s.finalizedCheckpoint.Root = blk4.Root() require.NoError(t, s.prune(ctx)) target, err = f.TargetRootForEpoch(blk4.Root(), 3) require.NoError(t, err) require.Equal(t, blk4.Root(), target) + // Dependent root for the finalized block should be the root of the pruned block at slot 33 + dependent, err = f.DependentRootForEpoch(blk4.Root(), 3) + require.NoError(t, err) + require.Equal(t, blk1.Root(), dependent) } func TestStore_DependentRootForEpoch(t *testing.T) { diff --git a/beacon-chain/forkchoice/doubly-linked-tree/types.go b/beacon-chain/forkchoice/doubly-linked-tree/types.go index 3db93944af80..094c47be1657 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/types.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/types.go @@ -31,6 +31,7 @@ type Store struct { proposerBoostRoot [fieldparams.RootLength]byte // latest block root that was boosted after being received in a timely manner. previousProposerBoostRoot [fieldparams.RootLength]byte // previous block root that was boosted after being received in a timely manner. previousProposerBoostScore uint64 // previous proposer boosted root score. + finalizedDependentRoot [fieldparams.RootLength]byte // dependent root at finalized checkpoint. committeeWeight uint64 // tracks the total active validator balance divided by the number of slots per Epoch. treeRootNode *Node // the root node of the store tree. headNode *Node // last head Node diff --git a/changelog/potuz_finalized_deproot.md b/changelog/potuz_finalized_deproot.md new file mode 100644 index 000000000000..bbf33bbb58a7 --- /dev/null +++ b/changelog/potuz_finalized_deproot.md @@ -0,0 +1,3 @@ +### Added + +- Track the dependent root of the latest finalized checkpoint in forkchoice.