This commit is contained in:
james-prysm
2025-12-05 18:44:10 -06:00
parent 32bc5e6a76
commit 812129e44a
5 changed files with 147 additions and 0 deletions

View File

@@ -649,6 +649,19 @@ func (f *ForkChoice) DependentRootForEpoch(root [32]byte, epoch primitives.Epoch
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
}

View File

@@ -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",

View File

@@ -6,6 +6,7 @@ go_library(
srcs = [
"builder.go",
"data.go",
"dependent_root.go",
"execution_engine.go",
"fee_recipient.go",
"finality.go",

View 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
}

View 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
}