eth/downloader, cmd: add --snap.exactpivot for exact-head snap sync

Add an opt-in --snap.exactpivot flag that pins the snap sync pivot to the
exact announced head instead of head-fsMinFullBlocks, so the reconstructed
state root equals stateRoot(head).

Snap sync normally pivots at head-64 and full-processes the last 64 blocks to
reach the head, which requires the serving peer to retain the state at
head-64. A peer that only holds a single, fixed head state (for example a
frozen / snapshot-restored source used for reproducible benchmarking) cannot
serve head-64: the pivot account-range request fails and the only peer is
dropped. With the flag set the pivot is the head itself, which such a peer
does serve.

Pinning the pivot to the exact head is only safe against a trusted peer that
serves a fixed, non-reorging head; on a live network the head can reorg
beneath the pivot. The flag is therefore intended for state-growth
benchmarking and trusted-peer setups, defaults to false, and leaves default
behaviour unchanged. This mirrors the static snap pivot Nethermind already
supports.
This commit is contained in:
AnkushinDaniil 2026-06-30 19:12:41 +04:00
parent 7f3afa1b71
commit b9e6aef8ab
8 changed files with 81 additions and 1 deletions

View file

@ -76,6 +76,7 @@ var (
utils.BlobPoolDataCapFlag,
utils.BlobPoolPriceBumpFlag,
utils.SyncModeFlag,
utils.SnapExactPivotFlag,
utils.SyncTargetFlag,
utils.ExitWhenSyncedFlag,
utils.GCModeFlag,

View file

@ -280,6 +280,15 @@ var (
Value: ethconfig.Defaults.SyncMode.String(),
Category: flags.StateCategory,
}
SnapExactPivotFlag = &cli.BoolFlag{
Name: "snap.exactpivot",
Usage: "Pin the snap sync pivot to the exact announced head instead of head-64, so the downloaded " +
"state root equals stateRoot(head). Only safe against a trusted peer that serves a fixed, " +
"non-reorging head (e.g. a frozen / snapshot-restored source); on a live network the head can reorg " +
"beneath the pivot and corrupt the synced state. Intended for state-growth benchmarking and " +
"trusted-peer setups, not general mainnet operation.",
Category: flags.StateCategory,
}
GCModeFlag = &cli.StringFlag{
Name: "gcmode",
Usage: `Blockchain garbage collection mode ("full", "archive")`,
@ -1757,6 +1766,15 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
}
}
if ctx.IsSet(SnapExactPivotFlag.Name) {
cfg.SnapExactPivot = ctx.Bool(SnapExactPivotFlag.Name)
if cfg.SnapExactPivot {
log.Warn("Snap sync pivot pinned to the exact head via --snap.exactpivot: only safe against a " +
"trusted peer serving a fixed, non-reorging head. On a live network a reorg beneath the pivot " +
"can corrupt the synced state. Use only for benchmarking / trusted-peer setups.")
}
}
if ctx.IsSet(ChainHistoryFlag.Name) {
value := ctx.String(ChainHistoryFlag.Name)
if err := cfg.HistoryMode.UnmarshalText([]byte(value)); err != nil {

View file

@ -352,6 +352,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
TxPool: eth.txPool,
Network: networkID,
Sync: config.SyncMode,
SnapExactPivot: config.SnapExactPivot,
BloomCache: uint64(cacheLimit),
RequiredBlocks: config.RequiredBlocks,
SnapV2: config.SnapV2,

View file

@ -306,6 +306,9 @@ func (d *Downloader) fetchHeaders(from uint64) error {
// Retrieve the next pivot header, either from skeleton chain
// or the filled chain
number := head.Number.Uint64() - uint64(fsMinFullBlocks)
if d.exactPivot {
number = head.Number.Uint64()
}
log.Warn("Pivot seemingly stale, moving", "old", d.pivotHeader.Number, "new", number)
if d.pivotHeader = d.skeleton.Header(number); d.pivotHeader == nil {

View file

@ -141,6 +141,7 @@ type Downloader struct {
// State sync
pivotHeader *types.Header // Pivot block header to dynamically push the syncing state root
pivotLock sync.RWMutex // Lock protecting pivot header reads from updates
exactPivot bool // Pin the snap pivot to the exact head instead of head-fsMinFullBlocks
snapSyncer snap.Syncer // snap/1 or snap/2 state syncer, selected at construction
stateSyncStart chan *stateSync
@ -261,6 +262,13 @@ func New(stateDb ethdb.Database, mode ethconfig.SyncMode, chain BlockChain, drop
return dl
}
// SetExactPivot pins the snap sync pivot to the exact announced head instead of
// head-fsMinFullBlocks, so the downloaded state root equals stateRoot(head).
// Must be set before synchronisation starts.
func (d *Downloader) SetExactPivot(v bool) {
d.exactPivot = v
}
// Progress retrieves the synchronisation boundaries, specifically the origin
// block where synchronisation started at (may have failed/suspended); the block
// or header sync is currently at; and the latest known block which the sync targets.
@ -472,7 +480,12 @@ func (d *Downloader) syncToHead() (err error) {
if err != nil {
return err
}
if latest.Number.Uint64() > uint64(fsMinFullBlocks) {
if d.exactPivot {
// With --snap.exactpivot, pin the pivot to the exact announced head
// instead of head-fsMinFullBlocks, so the synced state root equals
// stateRoot(head). Safe only against a fixed, non-reorging head.
pivot = latest
} else if latest.Number.Uint64() > uint64(fsMinFullBlocks) {
number := latest.Number.Uint64() - uint64(fsMinFullBlocks)
// Retrieve the pivot header from the skeleton chain segment but

View file

@ -683,6 +683,43 @@ func testBeaconSync(t *testing.T, protocol uint, mode SyncMode) {
}
}
// TestBeaconSyncSnapExactPivot verifies that SetExactPivot pins the snap sync
// pivot to the exact announced head instead of head-fsMinFullBlocks, so the
// synced state root equals stateRoot(head).
func TestBeaconSyncSnapExactPivot(t *testing.T) {
success := make(chan struct{})
tester := newTesterWithNotification(t, SnapSync, func() {
close(success)
})
defer tester.terminate()
tester.downloader.SetExactPivot(true)
chain := testChainBase.shorten(blockCacheMaxItems - 15)
tester.newPeer("peer", eth.ETH69, chain.blocks[1:])
head := chain.blocks[len(chain.blocks)-1].Header()
if err := tester.downloader.BeaconSync(head, nil); err != nil {
t.Fatalf("failed to beacon sync chain: %v", err)
}
select {
case <-success:
if bs := int(tester.chain.CurrentBlock().Number.Uint64()) + 1; bs != len(chain.blocks) {
t.Fatalf("synchronised blocks mismatch: have %v, want %v", bs, len(chain.blocks))
}
// With the exact-pivot flag the snap pivot must equal the head, not
// head-fsMinFullBlocks as in the default beacon snap sync.
pivot := rawdb.ReadLastPivotNumber(tester.downloader.stateDB)
if pivot == nil {
t.Fatalf("no snap pivot recorded")
}
if *pivot != head.Number.Uint64() {
t.Fatalf("pivot mismatch: have %d, want %d (exact head)", *pivot, head.Number.Uint64())
}
case <-time.NewTimer(time.Second * 3).C:
t.Fatalf("failed to sync chain in three seconds")
}
}
// Tests that synchronisation progress (origin block number, current block number
// and highest block number) is tracked and updated correctly.
func TestSyncProgressFull(t *testing.T) { testSyncProgress(t, eth.ETH69, FullSync) }

View file

@ -94,6 +94,11 @@ type Config struct {
NetworkId uint64
SyncMode SyncMode
// SnapExactPivot pins the snap sync pivot to the exact announced head
// instead of head-fsMinFullBlocks, so the downloaded state root equals
// stateRoot(head). Intended for syncing a fixed state from a trusted peer.
SnapExactPivot bool
// HistoryMode configures chain history retention.
HistoryMode history.HistoryMode

View file

@ -106,6 +106,7 @@ type handlerConfig struct {
TxPool txPool // Transaction pool to propagate from
Network uint64 // Network identifier to advertise
Sync ethconfig.SyncMode // Whether to snap or full sync
SnapExactPivot bool // Pin the snap pivot to the exact head instead of head-fsMinFullBlocks
BloomCache uint64 // Megabytes to alloc for snap sync bloom
RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges
SnapV2 bool // Whether to advertise and sync via the snap/2 protocol
@ -158,6 +159,7 @@ func newHandler(config *handlerConfig) (*handler, error) {
}
// Construct the downloader (long sync)
h.downloader = downloader.New(config.Database, config.Sync, h.chain, h.removePeer, h.enableSyncedFeatures, config.SnapV2)
h.downloader.SetExactPivot(config.SnapExactPivot)
// If snap sync is requested but snapshots are disabled, fail loudly
if h.downloader.ConfigSyncMode() == ethconfig.SnapSync && (config.Chain.Snapshots() == nil && config.Chain.TrieDB().Scheme() == rawdb.HashScheme) {