eth/downloader, cmd: add --snap.skipreceipts for state-only snap sync

Add an opt-in --snap.skipreceipts flag that makes snap sync neither
schedule, fetch nor validate historical block receipts. The state trie is
still reconstructed and verified in full; only the receipt backfill is
skipped.

Receipts are execution outputs (re-derivable by re-executing blocks) and
are not required to reconstruct or validate state. Skipping them lets a
node snap-sync the state from a peer that does not retain historical
receipts (for example a snapshot-restored, state-only source), where the
default receipt backfill otherwise fails receipt-root validation and drops
the only peer. This mirrors the state-only snap behaviour Nethermind
already supports via its static snap pivot.

The flag defaults to false, so default behaviour is unchanged.
This commit is contained in:
AnkushinDaniil 2026-06-18 00:04:48 +04:00
parent 7f3afa1b71
commit 370e60bab6
8 changed files with 78 additions and 20 deletions

View file

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

View file

@ -280,6 +280,15 @@ var (
Value: ethconfig.Defaults.SyncMode.String(),
Category: flags.StateCategory,
}
SnapSkipReceiptsFlag = &cli.BoolFlag{
Name: "snap.skipreceipts",
Usage: "Skip the historical receipt backfill during snap sync, reconstructing state only. " +
"Lets a node snap-sync from a peer that does not retain historical receipts (e.g. a state-only / " +
"snapshot-restored source). The resulting node has no receipts for the pre-sync range, so " +
"eth_getLogs / eth_getTransactionReceipt / eth_getBlockReceipts return a pruned-history error there. " +
"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")`,
@ -1764,6 +1773,15 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) {
}
}
if ctx.IsSet(SnapSkipReceiptsFlag.Name) {
cfg.SnapSkipReceipts = ctx.Bool(SnapSkipReceiptsFlag.Name)
if cfg.SnapSkipReceipts {
log.Warn("Snap sync receipt backfill disabled via --snap.skipreceipts: state is reconstructed in full, " +
"but the node will have no receipts for the pre-sync range. eth_getLogs, eth_getTransactionReceipt and " +
"eth_getBlockReceipts will report pruned history there. Use only for benchmarking / trusted-peer setups.")
}
}
if ctx.IsSet(CacheFlag.Name) || ctx.IsSet(CacheDatabaseFlag.Name) {
cfg.DatabaseCache = ctx.Int(CacheFlag.Name) * ctx.Int(CacheDatabaseFlag.Name) / 100
}

View file

@ -346,15 +346,16 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
// Permit the downloader to use the trie cache allowance during fast sync
cacheLimit := options.TrieCleanLimit + options.TrieDirtyLimit + options.SnapshotLimit
if eth.handler, err = newHandler(&handlerConfig{
NodeID: eth.p2pServer.Self().ID(),
Database: chainDb,
Chain: eth.blockchain,
TxPool: eth.txPool,
Network: networkID,
Sync: config.SyncMode,
BloomCache: uint64(cacheLimit),
RequiredBlocks: config.RequiredBlocks,
SnapV2: config.SnapV2,
NodeID: eth.p2pServer.Self().ID(),
Database: chainDb,
Chain: eth.blockchain,
TxPool: eth.txPool,
Network: networkID,
Sync: config.SyncMode,
SnapSkipReceipts: config.SnapSkipReceipts,
BloomCache: uint64(cacheLimit),
RequiredBlocks: config.RequiredBlocks,
SnapV2: config.SnapV2,
}); err != nil {
return nil, err
}

View file

@ -261,6 +261,12 @@ func New(stateDb ethdb.Database, mode ethconfig.SyncMode, chain BlockChain, drop
return dl
}
// SetSkipReceipts skips the historical receipt backfill during snap sync,
// reconstructing state only. Must be set before synchronisation starts.
func (d *Downloader) SetSkipReceipts(v bool) {
d.queue.skipReceipts = 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.

View file

@ -150,6 +150,10 @@ type queue struct {
active *sync.Cond
closed bool
// skipReceipts skips scheduling and fetching receipts during snap sync;
// state is still reconstructed in full.
skipReceipts bool
logTime time.Time // Time instance when status was last reported
}
@ -272,7 +276,7 @@ func (q *queue) Schedule(headers []*types.Header, hashes []common.Hash, from uin
q.blockTaskQueue.Push(header, -int64(header.Number.Uint64()))
}
// Queue for receipt retrieval
if q.mode == ethconfig.SnapSync && !header.EmptyReceipts() {
if q.mode == ethconfig.SnapSync && !q.skipReceipts && !header.EmptyReceipts() {
if _, ok := q.receiptTaskPool[hash]; ok {
log.Warn("Header already scheduled for receipt fetch", "number", header.Number, "hash", hash)
} else {
@ -425,7 +429,7 @@ func (q *queue) reserveHeaders(p *peerConnection, count int, taskPool map[common
// we can ask the resultcache if this header is within the
// "prioritized" segment of blocks. If it is not, we need to throttle
stale, throttle, item, err := q.resultCache.AddFetch(header, q.mode == ethconfig.SnapSync)
stale, throttle, item, err := q.resultCache.AddFetch(header, q.mode == ethconfig.SnapSync && !q.skipReceipts)
if stale {
// Don't put back in the task queue, this item has already been
// delivered upstream

View file

@ -195,6 +195,28 @@ func TestBasics(t *testing.T) {
}
}
// TestSkipReceipts verifies that skipReceipts schedules bodies in full but no
// receipt tasks.
func TestSkipReceipts(t *testing.T) {
q := newQueue(10, 10)
q.skipReceipts = true
q.Prepare(1, SnapSync)
headers := chain.headers()
hashes := make([]common.Hash, len(headers))
for i, header := range headers {
hashes[i] = header.Hash()
}
q.Schedule(headers, hashes, 1)
if got, exp := q.PendingBodies(), chain.Len(); got != exp {
t.Errorf("wrong pending block count, got %d, exp %d", got, exp)
}
if got, exp := q.PendingReceipts(), 0; got != exp {
t.Errorf("wrong pending receipt count, got %d, exp %d", got, exp)
}
}
func TestEmptyBlocks(t *testing.T) {
numOfBlocks := len(emptyChain.blocks)

View file

@ -94,6 +94,10 @@ type Config struct {
NetworkId uint64
SyncMode SyncMode
// SnapSkipReceipts skips the historical receipt backfill during snap sync,
// reconstructing state only.
SnapSkipReceipts bool
// HistoryMode configures chain history retention.
HistoryMode history.HistoryMode

View file

@ -100,15 +100,16 @@ type txPool interface {
// handlerConfig is the collection of initialization parameters to create a full
// node network handler.
type handlerConfig struct {
NodeID enode.ID // P2P node ID used for tx propagation topology
Database ethdb.Database // Database for direct sync insertions
Chain *core.BlockChain // Blockchain to serve data from
TxPool txPool // Transaction pool to propagate from
Network uint64 // Network identifier to advertise
Sync ethconfig.SyncMode // Whether to snap or full sync
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
NodeID enode.ID // P2P node ID used for tx propagation topology
Database ethdb.Database // Database for direct sync insertions
Chain *core.BlockChain // Blockchain to serve data from
TxPool txPool // Transaction pool to propagate from
Network uint64 // Network identifier to advertise
Sync ethconfig.SyncMode // Whether to snap or full sync
SnapSkipReceipts bool // Skip historical receipt backfill during snap sync (state-only)
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
}
type handler struct {
@ -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.SetSkipReceipts(config.SnapSkipReceipts)
// 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) {