Compare commits

...

10 Commits

Author SHA1 Message Date
satushh
18cced70ec add bullet points in changelog file 2025-08-22 11:16:35 +05:30
satushh
196e457450 Merge branch 'develop' into flag-sync-from-genesis 2025-08-22 11:12:09 +05:30
satushh
00f441e7e2 changelog 2025-08-22 11:10:04 +05:30
satushh
6f7e7f5885 bazel run //:gazelle -- fix 2025-08-22 10:24:19 +05:30
satushh
bb666833c5 node: check for genesis block in validateSyncFlags 2025-08-21 21:41:35 +05:30
satushh
334eb40576 bazel run //:gazelle -- fix 2025-08-21 17:07:31 +05:30
satushh
097605b45d node: avoid duplicate registration in test 2025-08-21 15:28:18 +05:30
satushh
8f68e224d9 node: tests and usage 2025-08-21 11:32:13 +05:30
satushh
9b2ee0f720 node: make flags more robust 2025-08-20 20:43:21 +05:30
satushh
dcf9379dd2 flag: support sync-from-genesis flag 2025-08-19 20:47:45 +05:30
7 changed files with 200 additions and 0 deletions

View File

@@ -86,7 +86,9 @@ go_test(
"//beacon-chain/blockchain:go_default_library",
"//beacon-chain/builder:go_default_library",
"//beacon-chain/core/feed/state:go_default_library",
"//beacon-chain/db:go_default_library",
"//beacon-chain/db/filesystem:go_default_library",
"//beacon-chain/db/testing:go_default_library",
"//beacon-chain/execution:go_default_library",
"//beacon-chain/execution/testing:go_default_library",
"//beacon-chain/monitor:go_default_library",
@@ -99,6 +101,7 @@ go_test(
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"@com_github_ethereum_go_ethereum//common:go_default_library",
"@com_github_prometheus_client_golang//prometheus:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],

View File

@@ -550,6 +550,11 @@ func (b *BeaconNode) startDB(cliCtx *cli.Context, depositAddress string) error {
return errors.Wrap(err, "could not ensure embedded genesis")
}
// Validate sync options when starting with an empty database
if err := b.validateSyncFlags(); err != nil {
return err
}
if b.CheckpointInitializer != nil {
log.Info("Checkpoint sync - Downloading origin state and block")
if err := b.CheckpointInitializer.Initialize(b.ctx, b.db); err != nil {
@@ -564,6 +569,52 @@ func (b *BeaconNode) startDB(cliCtx *cli.Context, depositAddress string) error {
log.WithField("address", depositAddress).Info("Deposit contract")
return nil
}
// validateSyncFlags ensures that when starting with an empty database,
// the user has explicitly chosen either genesis sync or checkpoint sync.
func (b *BeaconNode) validateSyncFlags() error {
// Check if database has an origin checkpoint (indicating it's not empty)
_, err := b.db.OriginCheckpointBlockRoot(b.ctx)
if err == nil {
// Database is not empty, validation is not needed
return nil
}
if !errors.Is(err, db.ErrNotFoundOriginBlockRoot) {
// Some other error occurred
return errors.Wrap(err, "could not check origin checkpoint block root")
}
// if genesis exists, also consider DB non-empty.
if gb, err := b.db.GenesisBlock(b.ctx); err == nil && gb != nil && !gb.IsNil() {
return nil
}
// Database is empty, check if user has provided required flags
syncFromGenesis := b.cliCtx.Bool(flags.SyncFromGenesis.Name)
hasCheckpointSync := b.CheckpointInitializer != nil
if !syncFromGenesis && !hasCheckpointSync {
return errors.New("when starting with an empty database, you must specify either:\n" +
" --sync-from-genesis (to sync from genesis)\n" +
" --checkpoint-sync-url <url> (to sync from a remote beacon node)\n" +
" --checkpoint-state <path> and --checkpoint-block <path> (to sync from local files)\n\n" +
"Checkpoint sync is recommended for faster syncing.")
}
// Check for conflicting sync options
if syncFromGenesis && hasCheckpointSync {
return errors.New("conflicting sync options: cannot use both --sync-from-genesis and checkpoint sync flags. " +
"Please choose either genesis sync or checkpoint sync, not both.")
}
if syncFromGenesis {
log.Warn("Syncing from genesis is enabled. This will take a very long time and is not recommended. " +
"Consider using checkpoint sync instead with --checkpoint-sync-url.")
}
return nil
}
func (b *BeaconNode) startSlasherDB(cliCtx *cli.Context, clearer *dbClearer) error {
if !b.slasherEnabled {
return nil

View File

@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
@@ -14,15 +15,19 @@ import (
"github.com/OffchainLabs/prysm/v6/beacon-chain/blockchain"
"github.com/OffchainLabs/prysm/v6/beacon-chain/builder"
statefeed "github.com/OffchainLabs/prysm/v6/beacon-chain/core/feed/state"
"github.com/OffchainLabs/prysm/v6/beacon-chain/db"
"github.com/OffchainLabs/prysm/v6/beacon-chain/db/filesystem"
testDB "github.com/OffchainLabs/prysm/v6/beacon-chain/db/testing"
"github.com/OffchainLabs/prysm/v6/beacon-chain/execution"
mockExecution "github.com/OffchainLabs/prysm/v6/beacon-chain/execution/testing"
"github.com/OffchainLabs/prysm/v6/beacon-chain/monitor"
"github.com/OffchainLabs/prysm/v6/cmd"
"github.com/OffchainLabs/prysm/v6/cmd/beacon-chain/flags"
"github.com/OffchainLabs/prysm/v6/config/features"
"github.com/OffchainLabs/prysm/v6/runtime"
"github.com/OffchainLabs/prysm/v6/testing/assert"
"github.com/OffchainLabs/prysm/v6/testing/require"
"github.com/prometheus/client_golang/prometheus"
logTest "github.com/sirupsen/logrus/hooks/test"
"github.com/urfave/cli/v2"
)
@@ -49,6 +54,7 @@ func TestNodeClose_OK(t *testing.T) {
set.Bool("demo-config", true, "demo configuration")
set.String("deposit-contract", "0x0000000000000000000000000000000000000000", "deposit contract address")
set.String("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A", "fee recipient")
set.Bool("sync-from-genesis", true, "sync from genesis")
require.NoError(t, set.Set("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A"))
cmd.ValidatorMonitorIndicesFlag.Value = &cli.IntSlice{}
cmd.ValidatorMonitorIndicesFlag.Value.SetInt(1)
@@ -74,6 +80,7 @@ func TestNodeStart_Ok(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.String("datadir", tmp, "node data directory")
set.String("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A", "fee recipient")
set.Bool("sync-from-genesis", true, "sync from genesis")
require.NoError(t, set.Set("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A"))
ctx, cancel := newCliContextWithCancel(&app, set)
@@ -104,6 +111,7 @@ func TestNodeStart_SyncChecker(t *testing.T) {
set := flag.NewFlagSet("test", 0)
set.String("datadir", tmp, "node data directory")
set.String("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A", "fee recipient")
set.Bool("sync-from-genesis", true, "sync from genesis")
require.NoError(t, set.Set("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A"))
ctx, cancel := newCliContextWithCancel(&app, set)
@@ -143,6 +151,7 @@ func TestClearDB(t *testing.T) {
set.String("datadir", tmp, "node data directory")
set.Bool(cmd.ForceClearDB.Name, true, "force clear db")
set.String("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A", "fee recipient")
set.Bool("sync-from-genesis", true, "sync from genesis")
require.NoError(t, set.Set("suggested-fee-recipient", "0x6e35733c5af9B61374A128e6F85f553aF09ff89A"))
context, cancel := newCliContextWithCancel(&app, set)
@@ -262,3 +271,128 @@ func TestCORS(t *testing.T) {
})
}
}
// TestValidateSyncFlags tests the validateSyncFlags function with real database instances
func TestValidateSyncFlags(t *testing.T) {
tests := []struct {
expectWarning bool
expectError bool
hasCheckpointInitializer bool
syncFromGenesis bool
dbHasOriginCheckpoint bool
expectedErrorContains string
name string
}{
{
name: "Database not empty - validation skipped",
dbHasOriginCheckpoint: true,
syncFromGenesis: false,
expectError: false,
},
{
name: "Empty DB, no sync flags - should fail",
dbHasOriginCheckpoint: false,
syncFromGenesis: false,
expectError: true,
expectedErrorContains: "when starting with an empty database, you must specify either",
},
{
name: "Empty DB, sync from genesis - should succeed with warning",
dbHasOriginCheckpoint: false,
syncFromGenesis: true,
expectError: false,
expectWarning: true,
},
{
name: "Empty DB, checkpoint sync - should succeed",
dbHasOriginCheckpoint: false,
hasCheckpointInitializer: true,
expectError: false,
},
{
name: "Empty DB, conflicting sync options - should fail",
dbHasOriginCheckpoint: false,
syncFromGenesis: true,
hasCheckpointInitializer: true,
expectError: true,
expectedErrorContains: "conflicting sync options",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Isolate Prometheus metrics per subtest to avoid duplicate registration across DB setups.
reg := prometheus.NewRegistry()
prometheus.DefaultRegisterer = reg
prometheus.DefaultGatherer = reg
ctx := context.Background()
// Set up real database for testing (empty to start).
beaconDB := testDB.SetupDB(t)
// Populate database if needed (simulate "non-empty" via origin checkpoint).
if tt.dbHasOriginCheckpoint {
err := beaconDB.SaveOriginCheckpointBlockRoot(ctx, [32]byte{0x01})
require.NoError(t, err)
}
// Set up CLI flags
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.Bool(flags.SyncFromGenesis.Name, tt.syncFromGenesis, "")
app := cli.App{}
cliCtx := cli.NewContext(&app, flagSet, nil)
// Create BeaconNode with test setup
beaconNode := &BeaconNode{
ctx: ctx,
db: beaconDB,
cliCtx: cliCtx,
}
// Set CheckpointInitializer if needed
if tt.hasCheckpointInitializer {
beaconNode.CheckpointInitializer = &mockCheckpointInitializer{}
}
// Capture log output for warning detection
hook := logTest.NewGlobal()
defer hook.Reset()
// Call the function under test
err := beaconNode.validateSyncFlags()
// Validate results
if tt.expectError {
require.NotNil(t, err)
if tt.expectedErrorContains != "" {
require.ErrorContains(t, tt.expectedErrorContains, err)
}
} else {
require.NoError(t, err)
}
// Check for warning log if expected
if tt.expectWarning {
found := false
for _, entry := range hook.Entries {
if entry.Level.String() == "warning" &&
strings.Contains(entry.Message, "Syncing from genesis is enabled") {
found = true
break
}
}
require.Equal(t, true, found, "Expected warning log about genesis sync")
}
})
}
}
// mockCheckpointInitializer is a simple mock for testing
type mockCheckpointInitializer struct{}
func (m *mockCheckpointInitializer) Initialize(ctx context.Context, db db.Database) error {
return nil
}

View File

@@ -0,0 +1,3 @@
### Added
- A new flag to ensure you have to specify whether sync from genesis or checkpoint in case of empty DB.
- Addressing this issue: https://github.com/OffchainLabs/prysm/issues/13020

View File

@@ -271,6 +271,13 @@ var (
"If such a sync is not possible, the node will treat it as a critical and irrecoverable failure",
Value: "",
}
// SyncFromGenesis enables syncing from genesis when starting with an empty database.
SyncFromGenesis = &cli.BoolFlag{
Name: "sync-from-genesis",
Usage: "Explicitly enables syncing from genesis when starting with an empty database. " +
"Alternately you can checkpoint sync instead with " +
"--checkpoint-sync-url, --checkpoint-state, or --checkpoint-block flags.",
}
// MinPeersPerSubnet defines a flag to set the minimum number of peers that a node will attempt to peer with for a subnet.
MinPeersPerSubnet = &cli.Uint64Flag{
Name: "minimum-peers-per-subnet",

View File

@@ -69,6 +69,7 @@ var appFlags = []cli.Flag{
flags.ChainID,
flags.NetworkID,
flags.WeakSubjectivityCheckpoint,
flags.SyncFromGenesis,
flags.Eth1HeaderReqLimit,
flags.MinPeersPerSubnet,
flags.MaxConcurrentDials,

View File

@@ -148,6 +148,7 @@ var appHelpFlagGroups = []flagGroup{
checkpoint.BlockPath,
checkpoint.RemoteURL,
checkpoint.StatePath,
flags.SyncFromGenesis,
flags.WeakSubjectivityCheckpoint,
genesis.BeaconAPIURL,
genesis.StatePath,