mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-10 05:47:59 -05:00
Compare commits
2 Commits
kzg-verifi
...
e2e-test-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
812129e44a | ||
|
|
32bc5e6a76 |
@@ -642,8 +642,25 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// E2E TEST: Log the dependent root result for verification
|
||||
// The returned root should be from epoch-1, not from epoch or later
|
||||
returnedEpoch := slots.ToEpoch(node.slot)
|
||||
if returnedEpoch >= epoch {
|
||||
// BUG DETECTED: returning a root from the wrong epoch
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"requestedEpoch": epoch,
|
||||
"returnedSlot": node.slot,
|
||||
"returnedEpoch": returnedEpoch,
|
||||
"returnedRoot": fmt.Sprintf("%#x", node.root),
|
||||
}).Error("E2E_DEPENDENT_ROOT_BUG: dependent root is from wrong epoch")
|
||||
}
|
||||
return node.root, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
3
changelog/potuz_finalized_deproot.md
Normal file
3
changelog/potuz_finalized_deproot.md
Normal file
@@ -0,0 +1,3 @@
|
||||
### Added
|
||||
|
||||
- Track the dependent root of the latest finalized checkpoint in forkchoice.
|
||||
@@ -94,6 +94,7 @@ go_test(
|
||||
"component_handler_test.go",
|
||||
"endtoend_setup_test.go",
|
||||
"endtoend_test.go",
|
||||
"minimal_dependent_root_e2e_test.go",
|
||||
"minimal_e2e_test.go",
|
||||
"minimal_slashing_e2e_test.go",
|
||||
"slasher_simulator_e2e_test.go",
|
||||
|
||||
@@ -6,6 +6,7 @@ go_library(
|
||||
srcs = [
|
||||
"builder.go",
|
||||
"data.go",
|
||||
"dependent_root.go",
|
||||
"execution_engine.go",
|
||||
"fee_recipient.go",
|
||||
"finality.go",
|
||||
|
||||
95
testing/endtoend/evaluators/dependent_root.go
Normal file
95
testing/endtoend/evaluators/dependent_root.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package evaluators
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
||||
e2e "github.com/OffchainLabs/prysm/v7/testing/endtoend/params"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/endtoend/policies"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/endtoend/types"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// DependentRootEvaluators returns evaluators for testing dependent root handling.
|
||||
// These evaluators verify that the dependent root bug does NOT occur:
|
||||
// - When a block at the first slot of an epoch becomes finalized and its parent pruned
|
||||
// - The dependent root query should still return correct data (from the previous epoch)
|
||||
//
|
||||
// The evaluator checks for "E2E_DEPENDENT_ROOT_BUG" error log which indicates
|
||||
// a dependent root was returned from the wrong epoch.
|
||||
//
|
||||
// WITH the fix: No bug log appears → test PASSES
|
||||
// WITHOUT the fix: Bug log appears (wrong epoch data) → test FAILS
|
||||
func DependentRootEvaluators(afterEpoch primitives.Epoch) []types.Evaluator {
|
||||
return []types.Evaluator{
|
||||
{
|
||||
Name: "no_dependent_root_bug_epoch_%d",
|
||||
Policy: policies.AfterNthEpoch(afterEpoch + 2), // Allow time for finalization
|
||||
Evaluation: checkNoDependentRootBug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// checkNoDependentRootBug scans beacon node logs to verify that the dependent root
|
||||
// bug did NOT occur. The bug would manifest as returning a dependent root from
|
||||
// the wrong epoch (current epoch instead of previous epoch).
|
||||
//
|
||||
// The beacon node logs "E2E_DEPENDENT_ROOT_BUG" when it detects this condition.
|
||||
//
|
||||
// WITH the fix: No bug log appears → test PASSES
|
||||
// WITHOUT the fix: Bug log appears → test FAILS
|
||||
func checkNoDependentRootBug(_ *types.EvaluationContext, _ ...*grpc.ClientConn) error {
|
||||
// This log message indicates the bug was triggered
|
||||
bugLogMessage := "E2E_DEPENDENT_ROOT_BUG"
|
||||
|
||||
for i := 0; i < e2e.TestParams.BeaconNodeCount; i++ {
|
||||
logFile := path.Join(e2e.TestParams.LogPath, fmt.Sprintf(e2e.BeaconNodeLogFileName, i))
|
||||
found, err := searchLogForMessages(logFile, []string{bugLogMessage})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to search beacon node %d log file", i)
|
||||
}
|
||||
if found {
|
||||
// Bug was detected - test FAILS
|
||||
return errors.New("E2E_DEPENDENT_ROOT_BUG detected: dependent root returned from wrong epoch - the fix is not working or not present")
|
||||
}
|
||||
}
|
||||
|
||||
// No bug detected - test PASSES
|
||||
log.Info("No dependent root bug detected - fix is working correctly")
|
||||
return nil
|
||||
}
|
||||
|
||||
// searchLogForMessages searches a log file for any of the given messages.
|
||||
func searchLogForMessages(logPath string, messages []string) (bool, error) {
|
||||
file, err := os.Open(logPath) // #nosec G304 -- test code only
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to open log file")
|
||||
}
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
log.WithError(err).Error("Failed to close log file")
|
||||
}
|
||||
}()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
for _, msg := range messages {
|
||||
if strings.Contains(line, msg) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return false, errors.Wrap(err, "error scanning log file")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
37
testing/endtoend/minimal_dependent_root_e2e_test.go
Normal file
37
testing/endtoend/minimal_dependent_root_e2e_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package endtoend
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/OffchainLabs/prysm/v7/config/params"
|
||||
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
||||
ev "github.com/OffchainLabs/prysm/v7/testing/endtoend/evaluators"
|
||||
"github.com/OffchainLabs/prysm/v7/testing/endtoend/types"
|
||||
)
|
||||
|
||||
// TestEndToEnd_MinimalConfig_DependentRoot tests that the beacon node correctly
|
||||
// handles the dependent root bug scenario where:
|
||||
// 1. A block at the first slot of an epoch becomes finalized
|
||||
// 2. The parent of that block is pruned during finalization
|
||||
// 3. Dependent root queries for that epoch would return wrong data without the fix
|
||||
//
|
||||
// The test adds verification logging that detects when a dependent root is
|
||||
// returned from the wrong epoch (E2E_DEPENDENT_ROOT_BUG).
|
||||
//
|
||||
// WITH the fix: No bug is detected → test PASSES
|
||||
// WITHOUT the fix: Bug is detected → test FAILS
|
||||
func TestEndToEnd_MinimalConfig_DependentRoot(t *testing.T) {
|
||||
r := e2eMinimalDependentRoot(t, types.InitForkCfg(version.Electra, version.Electra, params.E2ETestConfig()))
|
||||
r.run()
|
||||
}
|
||||
|
||||
func e2eMinimalDependentRoot(t *testing.T, cfg *params.BeaconChainConfig) *testRunner {
|
||||
// Use the standard e2eMinimal setup with the dependent root evaluators
|
||||
r := e2eMinimal(t, cfg,
|
||||
func(c *types.E2EConfig) {
|
||||
// Add the dependent root evaluators to check for the bug scenario
|
||||
c.Evaluators = append(c.Evaluators, ev.DependentRootEvaluators(2)...)
|
||||
},
|
||||
)
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user