diff --git a/.github/workflows/qa-rpc-integration-tests-remote.yml b/.github/workflows/qa-rpc-integration-tests-remote.yml index 7a56d0f398d..8b2c7481c7c 100644 --- a/.github/workflows/qa-rpc-integration-tests-remote.yml +++ b/.github/workflows/qa-rpc-integration-tests-remote.yml @@ -86,7 +86,7 @@ jobs: run: | echo "Starting Erigon..." - ./erigon --datadir $ERIGON_REFERENCE_DATA_DIR --persist.receipts --prune.experimental.include-commitment-history --nodiscover --maxpeers 0 --no-downloader --prune.mode=archive --http=false > erigon.log 2>&1 & + ./erigon --datadir $ERIGON_REFERENCE_DATA_DIR --prune.include-receipts --prune.experimental.include-commitment-history --nodiscover --maxpeers 0 --no-downloader --prune.mode=archive --http=false > erigon.log 2>&1 & ERIGON_PID=$! ERIGON_EXIT_STATUS=$? diff --git a/ChangeLog.md b/ChangeLog.md index 113b797b096..ce2e25dc26c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -2,9 +2,12 @@ ### Breaking Changes -#### `--persist.receipts`: historical receipts cache now off by default in all prune modes +#### `--prune.include-receipts`: historical receipts cache now off by default in all prune modes -The historical ("fat") receipts cache is no longer enabled by default on non-archive nodes. Previously `--persist.receipts` defaulted on for every prune mode except `archive`; it now defaults off everywhere. The consensus layer was the consumer that justified retaining these receipts on pruned nodes, and it no longer needs them ([#21617](https://github.com/erigontech/erigon/issues/21617)). +The historical ("fat") receipts cache is no longer enabled by default on non-archive nodes. Previously +`--prune.include-receipts` (formerly `--persist.receipts`, still accepted as an alias) defaulted on for every prune mode +except `archive`; it now defaults off everywhere. The consensus layer was the consumer that justified retaining these +receipts on pruned nodes, and it no longer needs them ([#21617](https://github.com/erigontech/erigon/issues/21617)). **What changed:** @@ -15,14 +18,38 @@ The historical ("fat") receipts cache is no longer enabled by default on non-arc | `blocks` | on | off | | `minimal` | on | off | -Receipts and logs stay available within a node's retention window regardless: without the cache they are re-executed on demand from state history, so `eth_getLogs` and `eth_getBlockReceipts` keep working, at higher latency. For `full` and `minimal` nodes the availability window is unchanged (receipts follow the state-history window either way). For `blocks` nodes the cache previously made receipts and logs queryable back to genesis; without it they follow the state-history window (last 262,144 blocks) — pass `--persist.receipts` if you rely on full-range `eth_getLogs`. +Receipts and logs stay available within a node's retention window regardless: without the cache they are re-executed on +demand from state history, so `eth_getLogs` and `eth_getBlockReceipts` keep working, at higher latency. For `full` and +`minimal` nodes the availability window is unchanged (receipts follow the state-history window either way). For `blocks` +nodes the cache previously made receipts and logs queryable back to genesis; without it they follow the state-history +window (last 262,144 blocks) — pass `--prune.include-receipts` if you rely on full-range `eth_getLogs`. -**Migration:** existing datadirs are unaffected — `--persist.receipts` is recorded at datadir creation and the stored value wins, so a node already syncing with the cache keeps it. Such a node now logs a startup notice that `--persist.receipts` differs from the value stored in the datadir; pass `--persist.receipts` explicitly to silence it. Only newly-created `full`/`minimal`/`blocks` datadirs start without the cache; pass `--persist.receipts` on a fresh datadir to opt back in. +**Migration:** existing datadirs are unaffected — the receipts-cache setting is recorded at datadir creation and the +stored value wins, so a node already syncing with the cache keeps it. Such a node now logs a startup notice that +`--prune.include-receipts` differs from the value stored in the datadir; pass `--prune.include-receipts` explicitly to +silence it. Only newly-created `full`/`minimal`/`blocks` datadirs start without the cache; pass +`--prune.include-receipts` on a fresh datadir to opt back in. (#22296) — by @yperbasis --- +#### CLI: receipts and commitment-history pruning flags moved under `--prune.*` + +The receipt cache and commitment history now share the `--prune.*` naming used by the rest of the pruning flags. All +former names keep working as aliases, and stored datadir settings are unaffected. + +- `--persist.receipts` → `--prune.include-receipts` (alias: `--persist.receipts`, `--experiment.persist.receipts.v2`). +- New `--prune.receipts.distance` (alias: `--persist.receipts.distance`) bounds how far back the receipt cache is kept: + a block count, `keep-all`, or empty/`0` (default) to follow the state-history window. Requires + `--prune.include-receipts`. Snapshots older than the window are skipped at download time. +- `--prune.commitment-history.distance` now also accepts `keep-all` (in addition to a block count); empty or `0` still + keeps everything. + +(#22349) — by @AskAlexSharov + +--- + #### JSON-RPC: block-number strings must use the `0x` hex format Quoted decimal strings (e.g., `"3"`) are no longer accepted as block-number diff --git a/cmd/erigon/node/config_snapshot_test.go b/cmd/erigon/node/config_snapshot_test.go index 05e2689fa9b..64090a0082a 100644 --- a/cmd/erigon/node/config_snapshot_test.go +++ b/cmd/erigon/node/config_snapshot_test.go @@ -191,8 +191,9 @@ func TestConfigWithFlags(t *testing.T) { } // TestPersistReceiptsDefaultByMode pins that the historical receipts cache -// (--persist.receipts) defaults to off in every prune mode, and is enabled only -// when the operator sets the flag explicitly. +// (--prune.include-receipts) defaults to off in every prune mode, and is enabled +// only when the operator sets the flag explicitly. The legacy --persist.receipts +// alias must keep working. func TestPersistReceiptsDefaultByMode(t *testing.T) { tests := []struct { name string @@ -204,9 +205,10 @@ func TestPersistReceiptsDefaultByMode(t *testing.T) { {"full", []string{"--prune.mode=full"}, false}, {"minimal", []string{"--prune.mode=minimal"}, false}, {"blocks", []string{"--prune.mode=blocks"}, false}, - {"explicit on with full", []string{"--prune.mode=full", "--persist.receipts"}, true}, - {"explicit on with archive", []string{"--prune.mode=archive", "--persist.receipts"}, true}, - {"explicit off with full", []string{"--prune.mode=full", "--persist.receipts=false"}, false}, + {"explicit on with full", []string{"--prune.mode=full", "--prune.include-receipts"}, true}, + {"explicit on with archive", []string{"--prune.mode=archive", "--prune.include-receipts"}, true}, + {"explicit off with full", []string{"--prune.mode=full", "--prune.include-receipts=false"}, false}, + {"legacy persist.receipts alias", []string{"--prune.mode=full", "--persist.receipts"}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 2492745ac80..bed97e028cf 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -100,9 +100,9 @@ var ( Value: ethconfig.Defaults.NetworkID, } PersistReceiptsV2Flag = cli.BoolFlag{ - Name: "persist.receipts", - Aliases: []string{"experiment.persist.receipts.v2"}, - Usage: "Download historical Receipts. If disabled: using state-history to re-exec transactions and generate Receipts - all RPC: eth_getLogs, eth_getBlockReceipts will work (just higher latency)", + Name: "prune.include-receipts", + Aliases: []string{"experiment.persist.receipts.v2", "persist.receipts"}, + Usage: "Download historical Receipts (stored on disk as the rcache domain: snapshots/history/*rcache*.v). If disabled: using state-history to re-exec transactions and generate Receipts - all RPC: eth_getLogs, eth_getBlockReceipts will work (just higher latency)", Value: ethconfig.Defaults.PersistReceiptsCacheV2, } DevValidatorSeedFlag = cli.StringFlag{ @@ -1113,9 +1113,14 @@ var ( Usage: "Enables blazing fast eth_getProof for executed block", Aliases: []string{"experimental.commitment-history", "prune.experimental.include-commitment-history"}, } - CommitmentHistoryDistanceFlag = cli.Uint64Flag{ + CommitmentHistoryDistanceFlag = cli.StringFlag{ Name: "prune.commitment-history.distance", - Usage: "Keep commitment history only for the latest N blocks. Older snapshots are skipped at download time. 0 (default) keeps everything. Requires --prune.include-commitment-history.", + Usage: "Keep commitment history only for the latest N blocks, or \"keep-all\". Older snapshots are skipped at download time. Empty or 0 (default) keeps everything. Requires --prune.include-commitment-history", + } + PersistReceiptsDistanceFlag = cli.StringFlag{ + Name: "prune.receipts.distance", + Aliases: []string{"persist.receipts.distance"}, + Usage: "Keep the receipt cache only for the latest N blocks, or \"keep-all\" to keep it all. Empty or 0 (default) follows the state-history window (NOT keep-all). Older snapshots are skipped at download time. Requires --prune.include-receipts", } AlwaysGenerateChangesetsFlag = cli.BoolFlag{ Name: "experimental.always-generate-changesets", diff --git a/db/kv/prune/distance.go b/db/kv/prune/distance.go index c99118eef81..3c18f5c13f7 100644 --- a/db/kv/prune/distance.go +++ b/db/kv/prune/distance.go @@ -18,7 +18,6 @@ package prune import ( "fmt" - "math" "strconv" "strings" ) @@ -28,11 +27,9 @@ const ( historyDistanceAliasHint = `a block count or "keep-all"` ) -// ParseBlocksDistance parses a --prune.distance.blocks value. It accepts a -// decimal block count, the named sentinels ("keep-post-merge" → -// KeepPostMergeBlocksPruneMode, "keep-all" → KeepAllBlocksPruneMode), or an -// empty string (returns 0, meaning unset). -func ParseBlocksDistance(s string) (uint64, error) { +// ParseBlocksDistance parses a block-distance value: a decimal count, +// "keep-post-merge", "keep-all", or empty ("" → 0, unset). +func ParseBlocksDistance(s, flagName string) (uint64, error) { switch normalizeDistanceAlias(s) { case "": return 0, nil @@ -41,22 +38,40 @@ func ParseBlocksDistance(s string) (uint64, error) { case "keep-all": return uint64(KeepAllBlocksPruneMode), nil default: - return parseDistanceNumber(s, "--prune.distance.blocks", blocksDistanceAliasHint) + return parseDistanceNumber(s, "--"+flagName, blocksDistanceAliasHint) } } -// ParseHistoryDistance parses a --prune.distance value. It accepts a decimal -// block count, the "keep-all" alias (keep all state history), or an empty -// string (returns 0, meaning unset). The block-only "keep-post-merge" policy -// is rejected: chain history-expiry has no meaning for state history. -func ParseHistoryDistance(s string) (uint64, error) { +// ParseHistoryDistance parses a state-history distance value: a decimal count, +// "keep-all", or empty ("" → 0, unset). +func ParseHistoryDistance(s, flagName string) (uint64, error) { + return parseStateHistoryDistance(s, "--"+flagName, KeepPostMergeBlocksPruneMode) +} + +// ParseCommitmentHistoryDistance parses a commitment-history distance value; +// "keep-all" maps to KeepAllBlocksPruneMode (its default). +func ParseCommitmentHistoryDistance(s, flagName string) (uint64, error) { + return parseStateHistoryDistance(s, "--"+flagName, KeepAllBlocksPruneMode) +} + +// ParseReceiptsDistance parses a receipts distance value; "keep-all" maps to +// KeepAllReceiptsPruneMode, distinct from the KeepAllBlocksPruneMode follow-history default. +func ParseReceiptsDistance(s, flagName string) (uint64, error) { + return parseStateHistoryDistance(s, "--"+flagName, KeepAllReceiptsPruneMode) +} + +// parseStateHistoryDistance parses a state-history-style distance flag: a decimal +// block count, "keep-all" (→ keepAll), or an empty string (→ 0, unset). Unlike +// Blocks, "keep-post-merge" is rejected — chain history-expiry is meaningless for +// state. +func parseStateHistoryDistance(s, flag string, keepAll Distance) (uint64, error) { switch normalizeDistanceAlias(s) { case "": return 0, nil case "keep-all": - return math.MaxUint64, nil + return uint64(keepAll), nil default: - return parseDistanceNumber(s, "--prune.distance", historyDistanceAliasHint) + return parseDistanceNumber(s, flag, historyDistanceAliasHint) } } diff --git a/db/kv/prune/distance_test.go b/db/kv/prune/distance_test.go index ac3864c0e0f..2e6082ee1e7 100644 --- a/db/kv/prune/distance_test.go +++ b/db/kv/prune/distance_test.go @@ -56,7 +56,7 @@ func TestParseBlocksDistance(t *testing.T) { } for _, tc := range cases { t.Run(tc.in, func(t *testing.T) { - got, err := ParseBlocksDistance(tc.in) + got, err := ParseBlocksDistance(tc.in, "prune.distance.blocks") if tc.wantErr { require.Error(t, err) return @@ -88,7 +88,7 @@ func TestParseHistoryDistance(t *testing.T) { } for _, tc := range cases { t.Run(tc.in, func(t *testing.T) { - got, err := ParseHistoryDistance(tc.in) + got, err := ParseHistoryDistance(tc.in, "prune.distance") if tc.wantErr { require.Error(t, err) return @@ -108,11 +108,84 @@ func TestBlocksDistanceCLIValue(t *testing.T) { // A keep-post-merge Blocks sentinel must render with its readable alias rather // than the raw MaxUint64 magic number. func TestModeString_BlocksSentinelAlias(t *testing.T) { - blockDist, err := ParseBlocksDistance("keep-post-merge") + blockDist, err := ParseBlocksDistance("keep-post-merge", "prune.distance.blocks") require.NoError(t, err) - mode, err := FromCli(archiveModeStr, 0, blockDist, 0) + mode, err := FromCli(archiveModeStr, 0, blockDist, 0, 0) require.NoError(t, err) assert.Equal(t, KeepPostMergeBlocksPruneMode, mode.Blocks) assert.Equal(t, "archive --prune.distance.blocks=keep-post-merge", mode.String()) } + +func TestParseCommitmentHistoryDistance(t *testing.T) { + cases := []struct { + in string + want uint64 + wantErr bool + }{ + {in: "", want: 0}, + {in: "100000", want: 100_000}, + {in: "keep-all", want: uint64(KeepAllBlocksPruneMode)}, + {in: "Keep-All", want: uint64(KeepAllBlocksPruneMode)}, + {in: "0x186a0", want: 100_000}, + {in: "keep-post-merge", wantErr: true}, + {in: "garbage", wantErr: true}, + } + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + got, err := ParseCommitmentHistoryDistance(tc.in, "prune.commitment-history.distance") + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestParseReceiptsDistance(t *testing.T) { + cases := []struct { + in string + want uint64 + wantErr bool + }{ + {in: "", want: 0}, + {in: "100000", want: 100_000}, + // keep-all maps to KeepAllReceiptsPruneMode, distinct from the + // KeepAllBlocksPruneMode follow-history default. + {in: "keep-all", want: uint64(KeepAllReceiptsPruneMode)}, + {in: "Keep-All", want: uint64(KeepAllReceiptsPruneMode)}, + {in: "keep-post-merge", wantErr: true}, + {in: "garbage", wantErr: true}, + } + for _, tc := range cases { + t.Run(tc.in, func(t *testing.T) { + got, err := ParseReceiptsDistance(tc.in, "prune.receipts.distance") + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +// Explicit receipts keep-all must round-trip through FromCli, be distinct from +// the follow-history default, and render with its alias (not the magic number). +func TestModeString_ReceiptsKeepAll(t *testing.T) { + dist, err := ParseReceiptsDistance("keep-all", "prune.receipts.distance") + require.NoError(t, err) + + mode, err := FromCli(minimalModeStr, 0, 0, 0, dist) + require.NoError(t, err) + assert.Equal(t, KeepAllReceiptsPruneMode, mode.Receipts) + assert.False(t, mode.ReceiptsFollowHistory(), "explicit keep-all is not the follow-history default") + assert.Contains(t, mode.String(), "--prune.receipts.distance=keep-all") + + def, err := FromCli(minimalModeStr, 0, 0, 0, 0) + require.NoError(t, err) + assert.True(t, def.ReceiptsFollowHistory(), "unset receipts follows history") + assert.NotContains(t, def.String(), "prune.receipts.distance") +} diff --git a/db/kv/prune/storage_mode.go b/db/kv/prune/storage_mode.go index 6c82ccce266..7fdee5aca76 100644 --- a/db/kv/prune/storage_mode.go +++ b/db/kv/prune/storage_mode.go @@ -35,24 +35,28 @@ var ( History: Distance(math.MaxUint64), Blocks: KeepAllBlocksPruneMode, CommitmentHistory: KeepAllBlocksPruneMode, + Receipts: KeepAllBlocksPruneMode, } FullMode = Mode{ Initialised: true, Blocks: Distance(config3.DefaultPruneDistance), History: Distance(config3.DefaultPruneDistance), CommitmentHistory: KeepAllBlocksPruneMode, + Receipts: KeepAllBlocksPruneMode, } BlocksMode = Mode{ Initialised: true, Blocks: KeepAllBlocksPruneMode, History: Distance(config3.DefaultPruneDistance), CommitmentHistory: KeepAllBlocksPruneMode, + Receipts: KeepAllBlocksPruneMode, } MinimalMode = Mode{ Initialised: true, Blocks: Distance(config3.MinimalPruneDistance), History: Distance(config3.MinimalPruneDistance), CommitmentHistory: KeepAllBlocksPruneMode, + Receipts: KeepAllBlocksPruneMode, } DefaultMode = ArchiveMode @@ -61,6 +65,7 @@ var ( History: Distance(math.MaxUint64), Blocks: Distance(math.MaxUint64), CommitmentHistory: KeepAllBlocksPruneMode, + Receipts: KeepAllBlocksPruneMode, } ErrUnknownPruneMode = fmt.Errorf("--prune.mode must be one of %s, %s, %s, %s", fullModeStr, archiveModeStr, minimalModeStr, blockModeStr) @@ -78,6 +83,7 @@ type Mode struct { History BlockAmount Blocks BlockAmount CommitmentHistory BlockAmount + Receipts BlockAmount } // String renders m in the shape an operator would type on the CLI: the named @@ -121,6 +127,7 @@ func (m Mode) String() string { fmt.Fprintf(&sb, " --prune.distance=%d", m.History.toValue()) } appendCommitmentHistory(&sb, m) + appendReceipts(&sb, m) return sb.String() } if m.Blocks == KeepAllBlocksPruneMode && m.History.Enabled() { @@ -132,6 +139,7 @@ func (m Mode) String() string { fmt.Fprintf(&sb, " --prune.distance=%d", m.History.toValue()) } appendCommitmentHistory(&sb, m) + appendReceipts(&sb, m) return sb.String() } @@ -148,22 +156,37 @@ func (m Mode) String() string { fmt.Fprintf(&sb, " --prune.distance.blocks=%s", blocksDistanceCLIValue(m.Blocks.toValue())) } appendCommitmentHistory(&sb, m) + appendReceipts(&sb, m) return sb.String() } func modeEquals(a, b Mode) bool { return a.History.toValue() == b.History.toValue() && a.Blocks.toValue() == b.Blocks.toValue() && - commitmentHistoryOrDefault(a.CommitmentHistory).toValue() == commitmentHistoryOrDefault(b.CommitmentHistory).toValue() + commitmentHistoryOrDefault(a.CommitmentHistory).toValue() == commitmentHistoryOrDefault(b.CommitmentHistory).toValue() && + receiptsOrDefault(a.Receipts).toValue() == receiptsOrDefault(b.Receipts).toValue() } func appendCommitmentHistory(sb *strings.Builder, m Mode) { if m.CommitmentHistory != nil && m.CommitmentHistory.toValue() != KeepAllBlocksPruneMode.toValue() { - fmt.Fprintf(sb, " --prune.commitment-history.older=%d", m.CommitmentHistory.toValue()) + fmt.Fprintf(sb, " --prune.commitment-history.distance=%d", m.CommitmentHistory.toValue()) } } -func FromCli(pruneMode string, distanceHistory, distanceBlocks, commitmentHistoryOlder uint64) (Mode, error) { +func appendReceipts(sb *strings.Builder, m Mode) { + if m.Receipts == nil { + return + } + switch m.Receipts.toValue() { + case KeepAllBlocksPruneMode.toValue(): // follow-history default — nothing to render + case KeepAllReceiptsPruneMode.toValue(): + sb.WriteString(" --prune.receipts.distance=keep-all") + default: + fmt.Fprintf(sb, " --prune.receipts.distance=%d", m.Receipts.toValue()) + } +} + +func FromCli(pruneMode string, distanceHistory, distanceBlocks, commitmentHistoryOlder, receiptsDistance uint64) (Mode, error) { var mode Mode switch pruneMode { case archiveModeStr, "": @@ -189,6 +212,9 @@ func FromCli(pruneMode string, distanceHistory, distanceBlocks, commitmentHistor if commitmentHistoryOlder > 0 { mode.CommitmentHistory = Distance(commitmentHistoryOlder) } + if receiptsDistance > 0 { + mode.Receipts = Distance(receiptsDistance) + } return mode, nil } @@ -201,7 +227,7 @@ func (m Mode) Validate() error { return nil } if commitment, history := commitmentHistory.toValue(), m.History.toValue(); commitment > history { - return fmt.Errorf("--prune.commitment-history.older=%d exceeds --prune.distance=%d; commitment history older than state-history retention cannot serve eth_getProof", commitment, history) + return fmt.Errorf("--prune.commitment-history.distance=%d exceeds --prune.distance=%d; commitment history older than state-history retention cannot serve eth_getProof", commitment, history) } return nil } @@ -234,12 +260,25 @@ func Get(db kv.Getter) (Mode, error) { prune.CommitmentHistory = blockAmount } + blockAmount, err = get(db, kv.PruneReceipts) + if err != nil { + return prune, err + } + if blockAmount != nil { + prune.Receipts = blockAmount + } + return prune, nil } const ( KeepPostMergeBlocksPruneMode = Distance(math.MaxUint64) // Use chain-specific history pruning (aka. history-expiry) KeepAllBlocksPruneMode = Distance(math.MaxUint64 - 1) // Keep all history + // KeepAllReceiptsPruneMode forces the receipt cache to be kept in full. + // It is distinct from KeepAllBlocksPruneMode, which for receipts is the + // unset default meaning "follow the state-history window" rather than + // "keep all" — so an operator needs a separate value to override it. + KeepAllReceiptsPruneMode = Distance(math.MaxUint64 - 2) ) type BlockAmount interface { @@ -259,11 +298,12 @@ type BlockAmount interface { type Distance uint64 // Enabled reports whether p actively drives distance-based pruning. It is -// false for the two sentinel values that select a different policy shape +// false for the sentinel values that select a different policy shape // (KeepPostMergeBlocksPruneMode → chain history-expiry; KeepAllBlocksPruneMode → -// retain forever) and true for every finite Distance. +// retain forever / follow-history for receipts; KeepAllReceiptsPruneMode → +// force keep-all receipts) and true for every finite Distance. func (p Distance) Enabled() bool { - return p != KeepPostMergeBlocksPruneMode && p != KeepAllBlocksPruneMode + return p != KeepPostMergeBlocksPruneMode && p != KeepAllBlocksPruneMode && p != KeepAllReceiptsPruneMode } func (p Distance) toValue() uint64 { return uint64(p) } func (p Distance) dbType() []byte { return kv.PruneTypeOlder } @@ -279,6 +319,7 @@ func (p Distance) PruneTo(stageHead uint64) uint64 { func EnsureNotChanged(tx kv.GetPut, pruneMode Mode) (Mode, error) { if pruneMode.Initialised { pruneMode.CommitmentHistory = commitmentHistoryOrDefault(pruneMode.CommitmentHistory) + pruneMode.Receipts = receiptsOrDefault(pruneMode.Receipts) if err := pruneMode.Validate(); err != nil { return pruneMode, err } @@ -345,7 +386,8 @@ func EnsureNotChanged(tx kv.GetPut, pruneMode Mode) (Mode, error) { func isRetentionWindowChange(persisted, requested Mode) bool { if persisted.History == requested.History && persisted.Blocks == requested.Blocks && - persisted.CommitmentHistory == requested.CommitmentHistory { + persisted.CommitmentHistory == requested.CommitmentHistory && + persisted.Receipts == requested.Receipts { return false } historyOK := persisted.History == requested.History || @@ -354,7 +396,9 @@ func isRetentionWindowChange(persisted, requested Mode) bool { (isBlocksRetentionPolicy(persisted.Blocks) && isBlocksRetentionPolicy(requested.Blocks)) commitmentOK := persisted.CommitmentHistory == requested.CommitmentHistory || (isCommitmentHistoryRetentionPolicy(persisted.CommitmentHistory) && isCommitmentHistoryRetentionPolicy(requested.CommitmentHistory)) - return historyOK && blocksOK && commitmentOK + receiptsOK := persisted.Receipts == requested.Receipts || + (isReceiptsRetentionPolicy(persisted.Receipts) && isReceiptsRetentionPolicy(requested.Receipts)) + return historyOK && blocksOK && commitmentOK && receiptsOK } // isCommitmentHistoryRetentionPolicy reports whether b expresses a @@ -369,6 +413,18 @@ func isCommitmentHistoryRetentionPolicy(b BlockAmount) bool { return isFiniteDistance(b) } +// isReceiptsRetentionPolicy reports whether b expresses a receipt-cache +// retention policy the shim will let operators move between. Finite Distance, +// KeepAllBlocksPruneMode (follow-history default) and KeepAllReceiptsPruneMode +// (force keep-all) all qualify in either direction; KeepPostMergeBlocksPruneMode +// is meaningless here. +func isReceiptsRetentionPolicy(b BlockAmount) bool { + if b == KeepAllBlocksPruneMode || b == KeepAllReceiptsPruneMode { + return true + } + return isFiniteDistance(b) +} + // isBlocksRetentionPolicy reports whether b expresses a block-data retention // policy that the shim will let operators move between. Finite Distance values // and KeepPostMergeBlocksPruneMode (chain-history-expiry) both qualify; @@ -420,6 +476,30 @@ func (m Mode) CommitmentHistoryAmount() BlockAmount { return commitmentHistoryOrDefault(m.CommitmentHistory) } +// receiptsOrDefault resolves an unset Receipts to KeepAllBlocksPruneMode — the +// follow-history default (not force keep-all, which is KeepAllReceiptsPruneMode) +// — guarding the persistence layer against a nil BlockAmount. +func receiptsOrDefault(b BlockAmount) BlockAmount { + if b == nil { + return KeepAllBlocksPruneMode + } + return b +} + +// ReceiptsAmount returns the receipt-cache retention, resolving an unset (nil) +// field to the follow-history default so callers can query it without a nil check. +func (m Mode) ReceiptsAmount() BlockAmount { + return receiptsOrDefault(m.Receipts) +} + +// ReceiptsFollowHistory reports whether receipt-cache retention uses the +// follow-history default — no explicit --prune.receipts.distance, so the +// cache tracks the general retention window rather than a finite window or +// KeepAllReceiptsPruneMode. +func (m Mode) ReceiptsFollowHistory() bool { + return receiptsOrDefault(m.Receipts) == KeepAllBlocksPruneMode +} + func overwriteStoredMode(db kv.GetPut, pm Mode) error { if err := writeBlockAmount(db, kv.PruneHistory, pm.History); err != nil { return err @@ -427,7 +507,10 @@ func overwriteStoredMode(db kv.GetPut, pm Mode) error { if err := writeBlockAmount(db, kv.PruneBlocks, pm.Blocks); err != nil { return err } - return writeBlockAmount(db, kv.PruneCommitmentHistory, commitmentHistoryOrDefault(pm.CommitmentHistory)) + if err := writeBlockAmount(db, kv.PruneCommitmentHistory, commitmentHistoryOrDefault(pm.CommitmentHistory)); err != nil { + return err + } + return writeBlockAmount(db, kv.PruneReceipts, receiptsOrDefault(pm.Receipts)) } func setIfNotExist(db kv.GetPut, pm Mode) error { @@ -440,7 +523,10 @@ func setIfNotExist(db kv.GetPut, pm Mode) error { if err := setOnEmpty(db, kv.PruneBlocks, pm.Blocks); err != nil { return err } - return setOnEmpty(db, kv.PruneCommitmentHistory, commitmentHistoryOrDefault(pm.CommitmentHistory)) + if err := setOnEmpty(db, kv.PruneCommitmentHistory, commitmentHistoryOrDefault(pm.CommitmentHistory)); err != nil { + return err + } + return setOnEmpty(db, kv.PruneReceipts, receiptsOrDefault(pm.Receipts)) } func createBlockAmount(pruneType []byte, v []byte) (BlockAmount, error) { diff --git a/db/kv/prune/storage_mode_test.go b/db/kv/prune/storage_mode_test.go index 302fd065444..f02baa40479 100644 --- a/db/kv/prune/storage_mode_test.go +++ b/db/kv/prune/storage_mode_test.go @@ -71,14 +71,14 @@ func TestModeEqualsComparesCommitmentHistory(t *testing.T) { func TestModeString_CommitmentHistory(t *testing.T) { m := ArchiveMode m.CommitmentHistory = Distance(100_000) - assert.Equal(t, "archive --prune.commitment-history.older=100000", m.String()) + assert.Equal(t, "archive --prune.commitment-history.distance=100000", m.String()) // Default (keep-all) commitment history adds no clause. assert.Equal(t, "archive", ArchiveMode.String()) // Legacy blocks shape with a bounded commitment window. legacyBlocks := Mode{Initialised: true, History: Distance(100_000), Blocks: KeepAllBlocksPruneMode, CommitmentHistory: Distance(80_000)} - assert.Equal(t, "blocks --prune.distance=100000 --prune.commitment-history.older=80000", legacyBlocks.String()) + assert.Equal(t, "blocks --prune.distance=100000 --prune.commitment-history.distance=80000", legacyBlocks.String()) } func TestModeString_LegacyShapes(t *testing.T) { @@ -107,14 +107,14 @@ func TestModeString_LegacyShapes(t *testing.T) { func TestParseCLIMode(t *testing.T) { t.Run("full", func(t *testing.T) { - mode, err := FromCli(fullModeStr, 0, 0, 0) + mode, err := FromCli(fullModeStr, 0, 0, 0, 0) assert.NoError(t, err) assert.Equal(t, FullMode, mode) assert.Equal(t, "full", mode.String()) }) t.Run("archive", func(t *testing.T) { - mode, err := FromCli(archiveModeStr, 0, 0, 0) + mode, err := FromCli(archiveModeStr, 0, 0, 0, 0) assert.NoError(t, err) assert.Equal(t, ArchiveMode, mode) assert.Equal(t, archiveModeStr, mode.String()) @@ -129,23 +129,23 @@ func TestParseCLIMode(t *testing.T) { exp.Blocks = Distance(100500) exp.History = Distance(400500) - mode, err := FromCli(archiveModeStr, exp.History.toValue(), exp.Blocks.toValue(), 0) + mode, err := FromCli(archiveModeStr, exp.History.toValue(), exp.Blocks.toValue(), 0, 0) assert.NoError(t, err) assert.Equal(t, exp, mode) assert.Equal(t, "archive --prune.distance=400500 --prune.distance.blocks=100500", mode.String()) }) t.Run("minimal", func(t *testing.T) { - mode, err := FromCli(minimalModeStr, 0, 0, 0) + mode, err := FromCli(minimalModeStr, 0, 0, 0, 0) assert.NoError(t, err) assert.Equal(t, MinimalMode, mode) assert.Equal(t, minimalModeStr, mode.String()) }) t.Run("garbage", func(t *testing.T) { - _, err := FromCli("garb", 1, 2, 0) + _, err := FromCli("garb", 1, 2, 0, 0) assert.ErrorIs(t, err, ErrUnknownPruneMode) }) t.Run("empty", func(t *testing.T) { - mode, err := FromCli("", 0, 0, 0) + mode, err := FromCli("", 0, 0, 0, 0) assert.NoError(t, err) assert.Equal(t, DefaultMode, mode) @@ -155,20 +155,81 @@ func TestParseCLIMode(t *testing.T) { func TestFromCli_CommitmentHistory(t *testing.T) { t.Run("zero-keeps-all", func(t *testing.T) { - // --prune.commitment-history.older=0 is the "unlimited" spelling. - mode, err := FromCli(archiveModeStr, 0, 0, 0) + // --prune.commitment-history.distance=0 is the "unlimited" spelling. + mode, err := FromCli(archiveModeStr, 0, 0, 0, 0) require.NoError(t, err) assert.Equal(t, KeepAllBlocksPruneMode, mode.CommitmentHistory) assert.False(t, mode.CommitmentHistory.Enabled()) }) t.Run("bounded-set", func(t *testing.T) { - mode, err := FromCli(archiveModeStr, 0, 0, 100_000) + mode, err := FromCli(archiveModeStr, 0, 0, 100_000, 0) require.NoError(t, err) assert.Equal(t, Distance(100_000), mode.CommitmentHistory) assert.True(t, mode.CommitmentHistory.Enabled()) }) } +func TestNamedModesKeepAllReceipts(t *testing.T) { + for _, m := range []Mode{ArchiveMode, FullMode, BlocksMode, MinimalMode, MockMode, DefaultMode} { + assert.Equal(t, KeepAllBlocksPruneMode, m.Receipts, "named modes must leave Receipts at the follow-history default (KeepAllBlocksPruneMode, not the explicit keep-all sentinel KeepAllReceiptsPruneMode)") + assert.False(t, m.Receipts.Enabled()) + } +} + +func TestModeEqualsComparesReceipts(t *testing.T) { + a := Mode{Initialised: true, History: Distance(100), Blocks: Distance(100), Receipts: Distance(50)} + b := a + assert.True(t, modeEquals(a, b)) + b.Receipts = Distance(60) + assert.False(t, modeEquals(a, b)) +} + +func TestModeString_Receipts(t *testing.T) { + m := ArchiveMode + m.Receipts = Distance(100_000) + assert.Equal(t, "archive --prune.receipts.distance=100000", m.String()) + + // Default (keep-all) receipts adds no clause. + assert.Equal(t, "archive", ArchiveMode.String()) +} + +func TestFromCli_Receipts(t *testing.T) { + t.Run("zero-keeps-all", func(t *testing.T) { + mode, err := FromCli(archiveModeStr, 0, 0, 0, 0) + require.NoError(t, err) + assert.Equal(t, KeepAllBlocksPruneMode, mode.Receipts) + assert.False(t, mode.Receipts.Enabled()) + }) + t.Run("bounded-set", func(t *testing.T) { + mode, err := FromCli(archiveModeStr, 0, 0, 0, 100_000) + require.NoError(t, err) + assert.Equal(t, Distance(100_000), mode.Receipts) + assert.True(t, mode.Receipts.Enabled()) + }) +} + +func TestGet_ReceiptsLegacyDefaultsToKeepAll(t *testing.T) { + // Legacy datadir: only History/Blocks keys were ever written. The missing + // receipts key must resolve to KeepAllBlocksPruneMode. + _, tx := memdb.NewTestTx(t) + require.NoError(t, setOnEmpty(tx, kv.PruneHistory, Distance(100_000))) + require.NoError(t, setOnEmpty(tx, kv.PruneBlocks, Distance(100_000))) + + got, err := Get(tx) + require.NoError(t, err) + assert.Equal(t, KeepAllBlocksPruneMode, got.Receipts) +} + +func TestGet_ReceiptsRoundTrips(t *testing.T) { + _, tx := memdb.NewTestTx(t) + m := Mode{Initialised: true, History: Distance(100_000), Blocks: Distance(100_000), Receipts: Distance(80_000)} + require.NoError(t, overwriteStoredMode(tx, m)) + + got, err := Get(tx) + require.NoError(t, err) + assert.Equal(t, Distance(80_000), got.Receipts) +} + func TestModeValidate(t *testing.T) { finite := func(chOlder uint64) Mode { m := Mode{Initialised: true, History: Distance(100_000), Blocks: Distance(100_000)} @@ -436,7 +497,7 @@ func TestEnsureNotChanged_LegacyMinimalNoOp(t *testing.T) { // initialized before the rescope has identical persisted state and starts // without warning or DB rewrite. _, tx := memdb.NewTestTx(t) - legacy := Mode{Initialised: true, History: Distance(100_000), Blocks: Distance(100_000), CommitmentHistory: KeepAllBlocksPruneMode} + legacy := Mode{Initialised: true, History: Distance(100_000), Blocks: Distance(100_000), CommitmentHistory: KeepAllBlocksPruneMode, Receipts: KeepAllBlocksPruneMode} initStoredMode(t, tx, legacy) got, err := EnsureNotChanged(tx, MinimalMode) @@ -490,10 +551,10 @@ func TestEnsureNotChanged_BlocksFiniteToDefaultAccepted(t *testing.T) { // transition so the chain-history-expiry policy can be restored without // manual DB intervention or a re-sync. _, tx := memdb.NewTestTx(t) - persisted := Mode{Initialised: true, History: Distance(262_144), Blocks: Distance(262_144), CommitmentHistory: KeepAllBlocksPruneMode} + persisted := Mode{Initialised: true, History: Distance(262_144), Blocks: Distance(262_144), CommitmentHistory: KeepAllBlocksPruneMode, Receipts: KeepAllBlocksPruneMode} initStoredMode(t, tx, persisted) - requested := Mode{Initialised: true, History: Distance(100_000), Blocks: KeepPostMergeBlocksPruneMode, CommitmentHistory: KeepAllBlocksPruneMode} + requested := Mode{Initialised: true, History: Distance(100_000), Blocks: KeepPostMergeBlocksPruneMode, CommitmentHistory: KeepAllBlocksPruneMode, Receipts: KeepAllBlocksPruneMode} got, err := EnsureNotChanged(tx, requested) require.NoError(t, err) assert.Equal(t, requested, got) @@ -509,10 +570,10 @@ func TestEnsureNotChanged_BlocksKeepAllToFiniteRejected(t *testing.T) { // that the operator must opt into explicitly (e.g., by switching modes // from a fresh datadir). _, tx := memdb.NewTestTx(t) - persisted := Mode{Initialised: true, History: Distance(100_000), Blocks: KeepAllBlocksPruneMode, CommitmentHistory: KeepAllBlocksPruneMode} + persisted := Mode{Initialised: true, History: Distance(100_000), Blocks: KeepAllBlocksPruneMode, CommitmentHistory: KeepAllBlocksPruneMode, Receipts: KeepAllBlocksPruneMode} initStoredMode(t, tx, persisted) - requested := Mode{Initialised: true, History: Distance(262_144), Blocks: Distance(262_144), CommitmentHistory: KeepAllBlocksPruneMode} + requested := Mode{Initialised: true, History: Distance(262_144), Blocks: Distance(262_144), CommitmentHistory: KeepAllBlocksPruneMode, Receipts: KeepAllBlocksPruneMode} got, err := EnsureNotChanged(tx, requested) require.Error(t, err) assert.Equal(t, persisted, got) @@ -533,7 +594,7 @@ func TestEnsureNotChanged_ArbitraryDistanceChangeAccepted(t *testing.T) { _, tx := memdb.NewTestTx(t) initStoredMode(t, tx, MinimalMode) - custom := Mode{Initialised: true, History: Distance(500_000), Blocks: Distance(500_000), CommitmentHistory: KeepAllBlocksPruneMode} + custom := Mode{Initialised: true, History: Distance(500_000), Blocks: Distance(500_000), CommitmentHistory: KeepAllBlocksPruneMode, Receipts: KeepAllBlocksPruneMode} got, err := EnsureNotChanged(tx, custom) require.NoError(t, err) assert.Equal(t, custom, got) diff --git a/db/kv/tables.go b/db/kv/tables.go index 0d951e2c87f..3f26a832e56 100644 --- a/db/kv/tables.go +++ b/db/kv/tables.go @@ -294,6 +294,7 @@ var ( PruneHistory = []byte("pruneHistory") PruneBlocks = []byte("pruneBlocks") PruneCommitmentHistory = []byte("pruneCommitmentHistory") + PruneReceipts = []byte("pruneReceipts") DBSchemaVersionKey = []byte("dbVersion") GenesisKey = []byte("genesis") diff --git a/db/snapshotsync/snapshotsync.go b/db/snapshotsync/snapshotsync.go index 30a159ece29..469b70a3ad3 100644 --- a/db/snapshotsync/snapshotsync.go +++ b/db/snapshotsync/snapshotsync.go @@ -100,24 +100,18 @@ func isStateHistory(name string) bool { return strings.HasPrefix(name, "idx") || strings.HasPrefix(name, "history") || strings.HasPrefix(name, "accessor") } func canSnapshotBePruned(name string) bool { - return (isStateHistory(name) || strings.Contains(name, "transactions")) && !strings.Contains(name, "rcache") + return isStateHistory(name) || strings.Contains(name, "transactions") } -// buildBlackListForPruning returns the set of preverified snapshot names that -// should be skipped at download time according to pruneMode: -// - state history files (idx/history/accessor): blacklisted when stepPrune -// reaches their To and pruneMode.History is enabled. -// - commitment-domain state history files: additionally blacklisted when -// minCommitmentHistoryStep >= their To, independent of pruneMode.History. -// - transaction segments: blacklisted by distance when pruneMode.Blocks is a -// finite Distance (res.To <= blockPrune), or by chain history-expiry when -// pruneMode.Blocks is KeepPostMergeBlocksPruneMode and cc has MergeHeight set -// (cc.IsPreMerge(res.From)). KeepAllBlocksPruneMode leaves tx segments alone. -// - bodies, headers, rcache, domain files: never blacklisted here. +// buildBlackListForPruning returns preverified snapshot names to skip at +// download time: state history past the History window; commitment and rcache +// history past their own windows (independent of History); tx segments past the +// Blocks window, or pre-merge under chain history-expiry. Bodies, headers and +// domain files are never blacklisted. func buildBlackListForPruning( pruneMode prune.Mode, cc *chain.Config, - historyStepPrune, minCommitmentHistoryStep kv.Step, minBlockToDownload, blockPrune uint64, + historyStepPrune, minCommitmentHistoryStep, minReceiptsStep kv.Step, minBlockToDownload, blockPrune uint64, preverified snapcfg.Preverified, ) (map[string]struct{}, error) { @@ -126,9 +120,10 @@ func buildBlackListForPruning( historyEnabled := pruneMode.History.Enabled() blocksEnabled := pruneMode.Blocks.Enabled() commitmentHistoryEnabled := minCommitmentHistoryStep > 0 + receiptsEnabled := minReceiptsStep > 0 applyChainHistoryExpiry := pruneMode.Blocks == prune.KeepPostMergeBlocksPruneMode && cc != nil && cc.MergeHeight != nil - if !historyEnabled && !blocksEnabled && !commitmentHistoryEnabled && !applyChainHistoryExpiry { + if !historyEnabled && !blocksEnabled && !commitmentHistoryEnabled && !receiptsEnabled && !applyChainHistoryExpiry { return blackList, nil } @@ -149,8 +144,14 @@ func buildBlackListForPruning( if !ok { return blackList, errors.New("invalid state snapshot name") } - // Commitment-history filter runs independently of History pruning so - // commitment-only configs still skip old commitment segments. + // rcache history: pruned by its own receipts window, not History. + if isStateHistory(name) && strings.Contains(name, kv.RCacheDomain.String()) { + if receiptsEnabled && minReceiptsStep >= kv.Step(res.To) { + blackList[name] = struct{}{} + } + continue + } + // commitment history: pruned by its own window, not History. if commitmentHistoryEnabled && isStateHistory(name) && strings.Contains(name, kv.CommitmentDomain.String()) && minCommitmentHistoryStep >= kv.Step(res.To) { blackList[name] = struct{}{} continue @@ -164,7 +165,7 @@ func buildBlackListForPruning( blackList[name] = struct{}{} continue } - // Block segment (transactions only — canSnapshotBePruned filters bodies/headers/rcache). + // Block segment (transactions only — canSnapshotBePruned filters bodies/headers/domains). // e.g 'v1.0-000000-000100-transactions.seg' // parse "from" (000000) and "to" (000100) from the name. 100 is 100'000 blocks res, _, ok := snaptype.ParseFileName("", name) @@ -208,8 +209,8 @@ func getMinimumBlocksToDownload( blockReader blockReader, tx kv.Getter, maxStateStep, stepSize uint64, - stateHistoryPruneTo, commitmentHistoryPruneTo uint64, -) (minBlockToDownload uint64, minHistoryStep, minCommitmentHistoryStep kv.Step, err error) { + stateHistoryPruneTo, commitmentHistoryPruneTo, receiptsHistoryPruneTo uint64, +) (minBlockToDownload uint64, minHistoryStep, minCommitmentHistoryStep, minReceiptsStep kv.Step, err error) { started := time.Now() var iterations int64 defer func() { @@ -222,6 +223,7 @@ func getMinimumBlocksToDownload( minToDownload := uint64(math.MaxUint64) minHistoryStep = kv.Step(math.MaxUint32) minCommitmentHistoryStep = kv.Step(math.MaxUint32) + minReceiptsStep = kv.Step(math.MaxUint32) stateTxNum := maxStateStep * stepSize if err := blockReader.IterateFrozenBodies(tx, func(blockNum, baseTxNum, txAmount uint64) error { if iterations%1e6 == 0 { @@ -236,6 +238,9 @@ func getMinimumBlocksToDownload( if blockNum == commitmentHistoryPruneTo { minCommitmentHistoryStep = stepAtTxNum(baseTxNum, stepSize) } + if blockNum == receiptsHistoryPruneTo { + minReceiptsStep = stepAtTxNum(baseTxNum, stepSize) + } if stateTxNum <= baseTxNum { // only consider the block if it return nil } @@ -248,10 +253,24 @@ func getMinimumBlocksToDownload( } return nil }); err != nil { - return 0, 0, 0, err + return 0, 0, 0, 0, err } - return frozenBlocks - minToDownload, minHistoryStep, minCommitmentHistoryStep, nil + // A prune-to boundary below the first frozen body is never hit above, leaving + // its step at the MaxUint32 sentinel. Resolve it to 0 (disable the filter, + // keep everything) so an unfound cutoff can't blacklist every matching file. + minHistoryStep = clearUnsetStep(minHistoryStep) + minCommitmentHistoryStep = clearUnsetStep(minCommitmentHistoryStep) + minReceiptsStep = clearUnsetStep(minReceiptsStep) + + return frozenBlocks - minToDownload, minHistoryStep, minCommitmentHistoryStep, minReceiptsStep, nil +} + +func clearUnsetStep(s kv.Step) kv.Step { + if s == kv.Step(math.MaxUint32) { + return 0 + } + return s } func getMaxStepRangeInSnapshots(preverified snapcfg.Preverified) (uint64, error) { @@ -297,7 +316,7 @@ func computeBlocksToPrune(blockReader blockReader, p prune.Mode) (blocksToPrune // chain-history-expiry for blocks. The KeepPostMergeBlocksPruneMode + MergeHeight // branch covers that. func downloadFilteringApplies(pruneMode prune.Mode, cc *chain.Config) bool { - if pruneMode.History.Enabled() || pruneMode.Blocks.Enabled() || pruneMode.CommitmentHistoryAmount().Enabled() { + if pruneMode.History.Enabled() || pruneMode.Blocks.Enabled() || pruneMode.CommitmentHistoryAmount().Enabled() || pruneMode.ReceiptsAmount().Enabled() { return true } return pruneMode.Blocks == prune.KeepPostMergeBlocksPruneMode && cc != nil && cc.MergeHeight != nil @@ -329,16 +348,29 @@ func blocksRetentionCutoff(pruneMode prune.Mode, cc *chain.Config, head uint64) } } -// isReceiptsSegmentPruned reports whether a receipt-related preverified -// segment (rcache, logaddrs, logtopics) should be skipped at download time. -// It mirrors buildBlackListForPruning's per-mode handling for tx segments, -// but operates on block height (converted to txNum/step) because receipts -// are step-aligned in storage. -func isReceiptsSegmentPruned(ctx context.Context, tx kv.RwTx, txNumsReader rawdbv3.TxNumsReader, cc *chain.Config, pruneMode prune.Mode, head uint64, p snapcfg.PreverifiedItem, stepSize uint64) bool { +func historyRetentionCutoff(pruneMode prune.Mode, head uint64) uint64 { + if pruneMode.History == nil || !pruneMode.History.Enabled() { + return 0 + } + return pruneMode.History.PruneTo(head) +} + +// receiptsSegmentRetentionCutoff picks the download cutoff for a receipt-related +// segment: rcache history follows state history (so download agrees with rcache +// retirement in historyRetireCutoffs), log indexes follow block data. +func receiptsSegmentRetentionCutoff(pruneMode prune.Mode, cc *chain.Config, head uint64, name string) uint64 { + if pruneMode.ReceiptsFollowHistory() && strings.Contains(name, kv.RCacheDomain.String()) { + return historyRetentionCutoff(pruneMode, head) + } + return blocksRetentionCutoff(pruneMode, cc, head) +} + +// isReceiptsSegmentPruned reports whether a receipt-related segment predates +// pruneHeight (0 = keep everything) and should be skipped at download time. +func isReceiptsSegmentPruned(ctx context.Context, tx kv.RwTx, txNumsReader rawdbv3.TxNumsReader, p snapcfg.PreverifiedItem, stepSize, pruneHeight uint64) bool { if strings.Contains(p.Name, "domain") { return false // domain snapshots are never pruned } - pruneHeight := blocksRetentionCutoff(pruneMode, cc, head) if pruneHeight == 0 { return false } @@ -454,8 +486,9 @@ func SyncSnapshots( return err } commitmentHistoryPrune := prune.CommitmentHistoryAmount().PruneTo(frozenBlocks) - minBlockToDownload, minHistoryStep, minCommitmentHistoryStep, err := getMinimumBlocksToDownload( - ctx, blockReader, tx, maxStateStep, stepSize, historyPrune, commitmentHistoryPrune) + receiptsPrune := prune.ReceiptsAmount().PruneTo(frozenBlocks) + minBlockToDownload, minHistoryStep, minCommitmentHistoryStep, minReceiptsStep, err := getMinimumBlocksToDownload( + ctx, blockReader, tx, maxStateStep, stepSize, historyPrune, commitmentHistoryPrune, receiptsPrune) if err != nil { return err } @@ -468,8 +501,17 @@ func SyncSnapshots( "pruneToBlock", commitmentHistoryPrune, "minStep", minCommitmentHistoryStep) } + // Receipts filtering is opt-in (requires --prune.include-receipts); a zero + // step disables it in buildBlackListForPruning. + if !prune.ReceiptsAmount().Enabled() { + minReceiptsStep = 0 + } else if minReceiptsStep > 0 { + log.Info(fmt.Sprintf("[%s] Filtering old receipt-cache segments", logPrefix), + "pruneToBlock", receiptsPrune, + "minStep", minReceiptsStep) + } - blackListForPruning, err = buildBlackListForPruning(prune, cc, minHistoryStep, minCommitmentHistoryStep, minBlockToDownload, blockPrune, preverifiedBlockSnapshots) + blackListForPruning, err = buildBlackListForPruning(prune, cc, minHistoryStep, minCommitmentHistoryStep, minReceiptsStep, minBlockToDownload, blockPrune, preverifiedBlockSnapshots) if err != nil { return err } @@ -510,12 +552,18 @@ func SyncSnapshots( continue } - isRcacheRelatedSegment := strings.Contains(p.Name, kv.RCacheDomain.String()) || - strings.Contains(p.Name, kv.LogAddrIdx.String()) || + // rcache reaches this filter only in the follow-history default; + // explicit receipts windows are handled by the blacklist above. + isLogIndexSegment := strings.Contains(p.Name, kv.LogAddrIdx.String()) || strings.Contains(p.Name, kv.LogTopicIdx.String()) - - if isRcacheRelatedSegment && isReceiptsSegmentPruned(ctx, tx, txNumsReader, cc, prune, frozenBlocks, p, stepSize) { - continue + isRcacheHistorySegment := prune.ReceiptsFollowHistory() && strings.Contains(p.Name, kv.RCacheDomain.String()) + if isLogIndexSegment || isRcacheHistorySegment { + cutoff := receiptsSegmentRetentionCutoff(prune, cc, frozenBlocks, p.Name) + if isReceiptsSegmentPruned(ctx, tx, txNumsReader, p, stepSize, cutoff) { + log.Debug(fmt.Sprintf("[%s] skipping expired receipt-related segment", logPrefix), + "name", p.Name, "cutoffBlock", cutoff, "rcacheHistory", isRcacheHistorySegment) + continue + } } if _, ok := blackListForPruning[p.Name]; ok { diff --git a/db/snapshotsync/snapshotsync_test.go b/db/snapshotsync/snapshotsync_test.go index bf00b7d6e41..9d14332094e 100644 --- a/db/snapshotsync/snapshotsync_test.go +++ b/db/snapshotsync/snapshotsync_test.go @@ -100,7 +100,7 @@ func TestBlackListForPruning(t *testing.T) { // effectiveCutoff mirrors the internal adjustBlockPrune clamp; without // it the assertion accepts segments above the cutoff the function actually used. effectiveCutoff := adjustBlockPrune(blockPrune, minBlockToDownload) - blackList, err := buildBlackListForPruning(prune.MinimalMode, nil, stepPrune, 0, minBlockToDownload, blockPrune, preverified) + blackList, err := buildBlackListForPruning(prune.MinimalMode, nil, stepPrune, 0, 0, minBlockToDownload, blockPrune, preverified) if err != nil { t.Fatal(err) } @@ -155,7 +155,7 @@ func TestBlackListForPruning_BlocksModeKeepsAllTransactions(t *testing.T) { // range so at least some history files land in the blacklist; the exact // number depends on the bundled preverified set. const stepPrune = 5000 - blackList, err := buildBlackListForPruning(prune.BlocksMode, nil, stepPrune, 0, 100_000, 0, preverified) + blackList, err := buildBlackListForPruning(prune.BlocksMode, nil, stepPrune, 0, 0, 100_000, 0, preverified) if err != nil { t.Fatal(err) } @@ -222,6 +222,14 @@ func TestDownloadFilteringApplies(t *testing.T) { cc: ccMainnet, want: true, }, + { + // Receipts-only config: History/Blocks unlimited but a bounded + // receipts window. Filtering must still apply (regression guard). + name: "receipts-only bounded", + mode: prune.Mode{Initialised: true, History: prune.KeepAllBlocksPruneMode, Blocks: prune.KeepAllBlocksPruneMode, CommitmentHistory: prune.KeepAllBlocksPruneMode, Receipts: prune.Distance(100_000)}, + cc: ccMainnet, + want: true, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -253,7 +261,7 @@ func TestBlackListForPruning_ChainHistoryExpiry(t *testing.T) { Blocks: prune.KeepPostMergeBlocksPruneMode, } - blackList, err := buildBlackListForPruning(legacyFull, cc, 64, 0, 100_000, 0, preverified) + blackList, err := buildBlackListForPruning(legacyFull, cc, 64, 0, 0, 100_000, 0, preverified) if err != nil { t.Fatal(err) } @@ -298,7 +306,7 @@ func TestBuildBlackListForPruning_CommitmentHistory(t *testing.T) { // Archive keeps History/Blocks unlimited; only the commitment-history filter // should fire (regression guard for commitment-only configs). const minCommitmentHistoryStep = 16 - blackList, err := buildBlackListForPruning(prune.ArchiveMode, nil, 0, minCommitmentHistoryStep, 0, 0, preverified) + blackList, err := buildBlackListForPruning(prune.ArchiveMode, nil, 0, minCommitmentHistoryStep, 0, 0, 0, preverified) if err != nil { t.Fatal(err) } @@ -331,7 +339,7 @@ func TestBuildBlackListForPruning_CommitmentHistoryDisabled(t *testing.T) { preverified := snapcfg.Preverified{Items: snapcfg.PreverifiedItems{ {Name: "history/v1.0-commitment.0-16.v"}, }} - blackList, err := buildBlackListForPruning(prune.ArchiveMode, nil, 0, 0, 0, 0, preverified) + blackList, err := buildBlackListForPruning(prune.ArchiveMode, nil, 0, 0, 0, 0, 0, preverified) if err != nil { t.Fatal(err) } @@ -340,9 +348,70 @@ func TestBuildBlackListForPruning_CommitmentHistoryDisabled(t *testing.T) { } } -// TestGetMinimumBlocksToDownload_TwoCutoffs verifies the single pass resolves the -// history and commitment-history cutoffs to their respective steps independently. -func TestGetMinimumBlocksToDownload_TwoCutoffs(t *testing.T) { +// TestBuildBlackListForPruning_Receipts locks down the receipt-cache filter +// folded into buildBlackListForPruning: rcache-domain state-history files +// (idx/history/accessor) with To <= minReceiptsStep are blacklisted, while the +// rcache domain file, non-rcache history, and transaction segments are left +// alone. Filtering runs even when History pruning is off (the archive + +// --prune.receipts.distance config). +func TestBuildBlackListForPruning_Receipts(t *testing.T) { + preverified := snapcfg.Preverified{Items: snapcfg.PreverifiedItems{ + {Name: "history/v1.0-rcache.0-16.v"}, + {Name: "idx/v1.0-rcache.0-16.ef"}, + {Name: "accessor/v1.0-rcache.0-16.vi"}, + {Name: "history/v1.0-rcache.16-32.v"}, + {Name: "history/v1.0-accounts.0-16.v"}, + {Name: "domain/v1.0-rcache.0-16.kv"}, + {Name: "v1.0-000000-000100-transactions.seg"}, + }} + + const minReceiptsStep = 16 + blackList, err := buildBlackListForPruning(prune.ArchiveMode, nil, 0, 0, minReceiptsStep, 0, 0, preverified) + if err != nil { + t.Fatal(err) + } + + want := []string{ + "history/v1.0-rcache.0-16.v", + "idx/v1.0-rcache.0-16.ef", + "accessor/v1.0-rcache.0-16.vi", + } + for _, name := range want { + if _, ok := blackList[name]; !ok { + t.Errorf("expected %s to be blacklisted (To <= minReceiptsStep=%d)", name, minReceiptsStep) + } + } + for _, keep := range []string{ + "history/v1.0-rcache.16-32.v", // above the window + "history/v1.0-accounts.0-16.v", // not rcache domain + "domain/v1.0-rcache.0-16.kv", // domain files are never filtered + "v1.0-000000-000100-transactions.seg", + } { + if _, ok := blackList[keep]; ok { + t.Errorf("%s must not be blacklisted", keep) + } + } +} + +// TestBuildBlackListForPruning_ReceiptsDisabled verifies that a zero +// minReceiptsStep disables receipt-cache filtering entirely. +func TestBuildBlackListForPruning_ReceiptsDisabled(t *testing.T) { + preverified := snapcfg.Preverified{Items: snapcfg.PreverifiedItems{ + {Name: "history/v1.0-rcache.0-16.v"}, + }} + blackList, err := buildBlackListForPruning(prune.ArchiveMode, nil, 0, 0, 0, 0, 0, preverified) + if err != nil { + t.Fatal(err) + } + if len(blackList) != 0 { + t.Errorf("receipt-cache filtering must be disabled at minReceiptsStep=0, got %v", blackList) + } +} + +// TestGetMinimumBlocksToDownload_ThreeCutoffs verifies the single pass resolves the +// history, commitment-history and receipts cutoffs to their respective steps +// independently. +func TestGetMinimumBlocksToDownload_ThreeCutoffs(t *testing.T) { const stepSize = 100 // blockNum -> baseTxNum. step(baseTxNum) = floor((baseTxNum-(stepSize-1))/stepSize). br := &fakeBlockReader{ @@ -356,14 +425,16 @@ func TestGetMinimumBlocksToDownload_TwoCutoffs(t *testing.T) { // maxStateStep=150 → stateTxNum=15_000. Only block 100 (baseTxNum 10_000) is // below the cutoff, so minToDownload=1000-100=900 and minBlock=1000-900=100. tx := beginTestRoTx(t) - minBlock, historyStep, commitmentStep, err := getMinimumBlocksToDownload(context.Background(), br, tx, 150, stepSize, 100, 300) + minBlock, historyStep, commitmentStep, receiptsStep, err := getMinimumBlocksToDownload(context.Background(), br, tx, 150, stepSize, 100, 300, 200) if err != nil { t.Fatal(err) } assert.Equal(t, uint64(100), minBlock) - // step(10_000) = (10_000-99)/100 = 99 ; step(30_000) = (30_000-99)/100 = 299. + // step(10_000) = (10_000-99)/100 = 99 ; step(30_000) = (30_000-99)/100 = 299 ; + // step(20_000) = (20_000-99)/100 = 199. assert.Equal(t, kv.Step(99), historyStep) assert.Equal(t, kv.Step(299), commitmentStep) + assert.Equal(t, kv.Step(199), receiptsStep) } // TestGetMinimumBlocksToDownload_MinBlock pins the minBlockToDownload computation and @@ -381,12 +452,58 @@ func TestGetMinimumBlocksToDownload_MinBlock(t *testing.T) { // maxStateStep=150 → stateTxNum=15_000. Only block 100 (baseTxNum 10_000) is // below the cutoff, so minToDownload=300-100=200 and minBlock=300-200=100. tx := beginTestRoTx(t) - minBlock, historyStep, commitmentStep, err := getMinimumBlocksToDownload(context.Background(), br, tx, 150, stepSize, 200, 200) + minBlock, historyStep, commitmentStep, receiptsStep, err := getMinimumBlocksToDownload(context.Background(), br, tx, 150, stepSize, 200, 200, 100) if err != nil { t.Fatal(err) } assert.Equal(t, uint64(100), minBlock) - // step(20_000) = (20_000-99)/100 = 199. + // step(20_000) = (20_000-99)/100 = 199 ; step(10_000) = (10_000-99)/100 = 99. assert.Equal(t, kv.Step(199), historyStep) assert.Equal(t, kv.Step(199), commitmentStep) + assert.Equal(t, kv.Step(99), receiptsStep) +} + +// TestGetMinimumBlocksToDownload_CutoffBelowFrozenBodies pins sentinel +// normalization: when a prune-to boundary block is not visited during the +// frozen-body scan (it falls below the first frozen body), the corresponding +// step must resolve to 0 — disabling that filter so nothing is blacklisted — +// rather than staying at the MaxUint32 sentinel, which would blacklist every +// matching history file and skip downloading data the node needs. +func TestGetMinimumBlocksToDownload_CutoffBelowFrozenBodies(t *testing.T) { + const stepSize = 100 + br := &fakeBlockReader{ + frozenMax: 1000, + bodies: []frozenBody{ + {blockNum: 500, baseTxNum: 50_000}, + {blockNum: 600, baseTxNum: 60_000}, + {blockNum: 700, baseTxNum: 70_000}, + }, + } + tx := beginTestRoTx(t) + // All three prune-to boundaries (50/60/70) sit below the first frozen body + // (500), so none is hit during iteration and each step stays unset. + _, historyStep, commitmentStep, receiptsStep, err := getMinimumBlocksToDownload(context.Background(), br, tx, 600, stepSize, 50, 60, 70) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, kv.Step(0), historyStep) + assert.Equal(t, kv.Step(0), commitmentStep) + assert.Equal(t, kv.Step(0), receiptsStep) +} + +// TestReceiptsSegmentRetentionCutoff: when blocks are kept but state history is +// pruned, rcache history must follow state history (so download agrees with +// retirement) while log indexes follow block data; the windows coincide otherwise. +func TestReceiptsSegmentRetentionCutoff(t *testing.T) { + const head = 1_000_000 + rcacheSeg := "history/v1.0-" + kv.RCacheDomain.String() + ".0-64.v" + logIdxSeg := "idx/v1.0-" + kv.LogAddrIdx.String() + ".0-64.ef" + + blocksHistory := historyRetentionCutoff(prune.BlocksMode, head) + assert.NotZero(t, blocksHistory, "blocks-mode history window must be finite") + assert.Equal(t, uint64(0), blocksRetentionCutoff(prune.BlocksMode, nil, head), "blocks-mode blocks window is keep-all") + assert.Equal(t, blocksHistory, receiptsSegmentRetentionCutoff(prune.BlocksMode, nil, head, rcacheSeg)) + assert.Equal(t, uint64(0), receiptsSegmentRetentionCutoff(prune.BlocksMode, nil, head, logIdxSeg)) + + assert.Equal(t, blocksRetentionCutoff(prune.MinimalMode, nil, head), receiptsSegmentRetentionCutoff(prune.MinimalMode, nil, head, rcacheSeg)) } diff --git a/docs/site/docs/fundamentals/configuring-erigon.mdx b/docs/site/docs/fundamentals/configuring-erigon.mdx index b468680a04e..19fb3d822ca 100644 --- a/docs/site/docs/fundamentals/configuring-erigon.mdx +++ b/docs/site/docs/fundamentals/configuring-erigon.mdx @@ -59,8 +59,8 @@ These flags cover the general behavior and configuration of the Erigon client. * Default: `false` * `--ethstats value`: The reporting URL for an ethstats service. * `--trusted-setup-file value`: Absolute path to a `trusted_setup.json` file. -* `--persist.receipts, --experiment.persist.receipts.v2`: Downloads historical receipts. - * Default: `true` for minimal and full nodes, `false` for archive nodes +* `--prune.include-receipts, --persist.receipts, --experiment.persist.receipts.v2`: Downloads and caches historical receipts. Without it, receipts and logs are re-derived on demand from state history, so the related RPC calls still work, just with higher latency. + * Default: `false` (disabled) in every prune mode ### Database and Caching @@ -100,6 +100,10 @@ Flags for managing how old chain data is handled and stored. See [Snapshots Mana * Default: unset (retention follows `--prune.mode`) * `--prune.include-commitment-history, --prune.experimental.include-commitment-history, --experimental.commitment-history`: Enables fast `eth_getProof` for executed blocks by storing commitment history. Requires +32 GB RAM. See [`eth_getProof`](/interacting-with-erigon/eth#eth_getproof). * Default: `false` +* `--prune.commitment-history.distance value`: Keeps commitment history for the latest `N` blocks, or `keep-all`. Empty or `0` (default) keeps everything. Requires `--prune.include-commitment-history`. + * Default: unset (empty / `0`, keeps everything) +* `--prune.receipts.distance, --persist.receipts.distance value`: Keeps the receipt cache for the latest `N` blocks, or `keep-all`. Empty or `0` (default) follows the state-history window. Requires `--prune.include-receipts`. + * Default: unset (empty / `0`, follows the state-history window) * `--snap.skip-state-snapshot-download`: Skips state download and starts from genesis. * Default: `false` * `--snap.keepblocks`: Workaround/debug — keeps ancient blocks in the DB instead of moving them into snapshots (useful for debugging). diff --git a/docs/site/docs/fundamentals/database.md b/docs/site/docs/fundamentals/database.md index d8febc52718..2080c5b4473 100644 --- a/docs/site/docs/fundamentals/database.md +++ b/docs/site/docs/fundamentals/database.md @@ -59,7 +59,10 @@ Snapshots are organised into several subdirectories. The main ones are: - You can replay a single historical transaction without re-executing its block. - If an account changes V1 → V2 → V1 within one block, `debug_getModifiedAccountsByNumber` correctly returns it. -- Erigon stores compact per-transaction receipt *metadata* — cumulative gas used, blob gas used, log index — in a **required** receipt domain. Full receipts (with logs) live in a separate cache domain that is **off by default** in every prune mode (opt in with `--persist.receipts`). When a full receipt isn't cached, it is reconstructed on demand, re-deriving logs by re-execution. +- Erigon stores compact per-transaction receipt *metadata* — cumulative gas used, blob gas used, log index — in a + **required** receipt domain. Full receipts (with logs) live in a separate cache domain that is **off by default** in + every prune mode (opt in with `--prune.include-receipts`). When a full receipt isn't cached, it is reconstructed on + demand, re-deriving logs by re-execution. ## What does it cost on disk? diff --git a/docs/site/docs/fundamentals/pruning-modes.md b/docs/site/docs/fundamentals/pruning-modes.md index f3c4dfeb3dd..039f574d185 100644 --- a/docs/site/docs/fundamentals/pruning-modes.md +++ b/docs/site/docs/fundamentals/pruning-modes.md @@ -23,7 +23,11 @@ In order to switch type of node, you must first delete the `/chaindata` folder i :::tip **Persisting receipts**, which are pre-calculated receipts, increase the requests-per-second (RPS) and improve the latency and throughput of all receipts and logs-related RPC calls. -As of v3.6 they are disabled by default in every pruning mode (previously they were enabled by default for all modes except Archive); enable them with the flag `--persist.receipts`. Without them, receipts and logs are re-derived on demand from state history, so the related RPC calls keep working within the node's state-history window, just with higher latency. On a Historical Blocks node, persisted receipts additionally extend receipts and logs availability from the state-history window back to genesis. +As of v3.6 they are disabled by default in every pruning mode (previously they were enabled by default for all modes +except Archive); enable them with the flag `--prune.include-receipts` (the former `--persist.receipts` still works as an +alias). Without them, receipts and logs are re-derived on demand from state history, so the related RPC calls keep +working within the node's state-history window, just with higher latency. On a Historical Blocks node, persisted +receipts additionally extend receipts and logs availability from the state-history window back to genesis. ::: :::note[Breaking change in v3.5] @@ -50,4 +54,12 @@ The Minimal Node configuration (`--prune.mode=minimal`) is the smallest possible ## Blocks node -The Blocks Node configuration (`--prune.mode=blocks`) keeps the **full block and transaction history** — every block back to genesis — while pruning **state history**. It retains state only within the EIP-8252 window (the last 262,144 blocks), the same state-retention as a Full Node, but unlike a Full Node it never prunes older blocks. This suits users who need complete historical **block and transaction data** — for research, indexing, or block explorers — without paying the disk cost of an archive node's full historical **state**. For full-range **receipts and logs** (`eth_getLogs` / `eth_getBlockReceipts` back to genesis), add `--persist.receipts`; without it they are served only within the state-history window (the last 262,144 blocks). +The Blocks Node configuration (`--prune.mode=blocks`) keeps the **full block and transaction history** — every block +back to genesis — while pruning **state history**. It retains state only within the EIP-8252 window (the last 262,144 +blocks), the same state-retention as a Full Node, but unlike a Full Node it never prunes older blocks. This suits users +who need complete historical **block and transaction data** — for research, indexing, or block explorers — without +paying the disk cost of an archive node's full historical **state**. For full-range **receipts and logs** +(`eth_getLogs` / `eth_getBlockReceipts` back to genesis), add +`--prune.include-receipts --prune.receipts.distance=keep-all`; with `--prune.include-receipts` alone the receipt cache +follows the state-history window (the last 262,144 blocks), and without it receipts are re-derived from state history +within that same window. diff --git a/docs/site/static/llms-full.txt b/docs/site/static/llms-full.txt index 4bef2bc76f5..b6f11a56379 100644 --- a/docs/site/static/llms-full.txt +++ b/docs/site/static/llms-full.txt @@ -1568,7 +1568,10 @@ Snapshots are organised into several subdirectories. The main ones are: - You can replay a single historical transaction without re-executing its block. - If an account changes V1 → V2 → V1 within one block, `debug_getModifiedAccountsByNumber` correctly returns it. -- Erigon stores compact per-transaction receipt *metadata* — cumulative gas used, blob gas used, log index — in a **required** receipt domain. Full receipts (with logs) live in a separate cache domain that is **off by default** in every prune mode (opt in with `--persist.receipts`). When a full receipt isn't cached, it is reconstructed on demand, re-deriving logs by re-execution. +- Erigon stores compact per-transaction receipt *metadata* — cumulative gas used, blob gas used, log index — in a + **required** receipt domain. Full receipts (with logs) live in a separate cache domain that is **off by default** in + every prune mode (opt in with `--prune.include-receipts`). When a full receipt isn't cached, it is reconstructed on + demand, re-deriving logs by re-execution. ## What does it cost on disk? @@ -1648,7 +1651,11 @@ In order to switch type of node, you must first delete the `/chaindata` folder i :::tip **Persisting receipts**, which are pre-calculated receipts, increase the requests-per-second (RPS) and improve the latency and throughput of all receipts and logs-related RPC calls. -As of v3.6 they are disabled by default in every pruning mode (previously they were enabled by default for all modes except Archive); enable them with the flag `--persist.receipts`. Without them, receipts and logs are re-derived on demand from state history, so the related RPC calls keep working within the node's state-history window, just with higher latency. On a Historical Blocks node, persisted receipts additionally extend receipts and logs availability from the state-history window back to genesis. +As of v3.6 they are disabled by default in every pruning mode (previously they were enabled by default for all modes +except Archive); enable them with the flag `--prune.include-receipts` (the former `--persist.receipts` still works as an +alias). Without them, receipts and logs are re-derived on demand from state history, so the related RPC calls keep +working within the node's state-history window, just with higher latency. On a Historical Blocks node, persisted +receipts additionally extend receipts and logs availability from the state-history window back to genesis. ::: :::note[Breaking change in v3.5] @@ -1675,7 +1682,15 @@ The Minimal Node configuration (`--prune.mode=minimal`) is the smallest possible ## Blocks node -The Blocks Node configuration (`--prune.mode=blocks`) keeps the **full block and transaction history** — every block back to genesis — while pruning **state history**. It retains state only within the EIP-8252 window (the last 262,144 blocks), the same state-retention as a Full Node, but unlike a Full Node it never prunes older blocks. This suits users who need complete historical **block and transaction data** — for research, indexing, or block explorers — without paying the disk cost of an archive node's full historical **state**. For full-range **receipts and logs** (`eth_getLogs` / `eth_getBlockReceipts` back to genesis), add `--persist.receipts`; without it they are served only within the state-history window (the last 262,144 blocks). +The Blocks Node configuration (`--prune.mode=blocks`) keeps the **full block and transaction history** — every block +back to genesis — while pruning **state history**. It retains state only within the EIP-8252 window (the last 262,144 +blocks), the same state-retention as a Full Node, but unlike a Full Node it never prunes older blocks. This suits users +who need complete historical **block and transaction data** — for research, indexing, or block explorers — without +paying the disk cost of an archive node's full historical **state**. For full-range **receipts and logs** +(`eth_getLogs` / `eth_getBlockReceipts` back to genesis), add +`--prune.include-receipts --prune.receipts.distance=keep-all`; with `--prune.include-receipts` alone the receipt cache +follows the state-history window (the last 262,144 blocks), and without it receipts are re-derived from state history +within that same window. --- @@ -1924,8 +1939,8 @@ These flags cover the general behavior and configuration of the Erigon client. * Default: `false` * `--ethstats value`: The reporting URL for an ethstats service. * `--trusted-setup-file value`: Absolute path to a `trusted_setup.json` file. -* `--persist.receipts, --experiment.persist.receipts.v2`: Downloads historical receipts. - * Default: `true` for minimal and full nodes, `false` for archive nodes +* `--prune.include-receipts, --persist.receipts, --experiment.persist.receipts.v2`: Downloads and caches historical receipts. Without it, receipts and logs are re-derived on demand from state history, so the related RPC calls still work, just with higher latency. + * Default: `false` (disabled) in every prune mode ### Database and Caching @@ -1965,6 +1980,10 @@ Flags for managing how old chain data is handled and stored. See [Snapshots Mana * Default: unset (retention follows `--prune.mode`) * `--prune.include-commitment-history, --prune.experimental.include-commitment-history, --experimental.commitment-history`: Enables fast `eth_getProof` for executed blocks by storing commitment history. Requires +32 GB RAM. See [`eth_getProof`](/interacting-with-erigon/eth#eth_getproof). * Default: `false` +* `--prune.commitment-history.distance value`: Keeps commitment history for the latest `N` blocks, or `keep-all`. Empty or `0` (default) keeps everything. Requires `--prune.include-commitment-history`. + * Default: unset (empty / `0`, keeps everything) +* `--prune.receipts.distance, --persist.receipts.distance value`: Keeps the receipt cache for the latest `N` blocks, or `keep-all`. Empty or `0` (default) follows the state-history window. Requires `--prune.include-receipts`. + * Default: unset (empty / `0`, follows the state-history window) * `--snap.skip-state-snapshot-download`: Skips state download and starts from genesis. * Default: `false` * `--snap.keepblocks`: Workaround/debug — keeps ancient blocks in the DB instead of moving them into snapshots (useful for debugging). diff --git a/execution/stagedsync/stage_custom_trace.go b/execution/stagedsync/stage_custom_trace.go index 18b705d043a..052fc707b69 100644 --- a/execution/stagedsync/stage_custom_trace.go +++ b/execution/stagedsync/stage_custom_trace.go @@ -109,7 +109,7 @@ func StageCustomTraceCfg(produce []string, db kv.TemporalRwDB, dirs datadir.Dirs func SpawnCustomTrace(cfg CustomTraceCfg, ctx context.Context, logger log.Logger) error { if cfg.Produce.RCacheDomain { if err := cfg.db.View(context.Background(), func(tx kv.Tx) error { - return kvcfg.PersistReceipts.MustBeEnabled(tx, "you must enable `--persist.receipts` flag in db. remove chaindata and start erigon with this flag") + return kvcfg.PersistReceipts.MustBeEnabled(tx, "you must enable `--prune.include-receipts` flag in db. remove chaindata and start erigon with this flag") }); err != nil { panic(err) } diff --git a/execution/stagedsync/stage_execute.go b/execution/stagedsync/stage_execute.go index 3b6acafc9f6..0bcfa1baf8d 100644 --- a/execution/stagedsync/stage_execute.go +++ b/execution/stagedsync/stage_execute.go @@ -617,7 +617,9 @@ func PruneExecutionStage(ctx context.Context, s *PruneState, tx kv.TemporalRwTx, // historyRetireCutoffs maps the prune mode to per-domain retirement cutoffs, in // txNum — the aggregator floors each to its file step. CommitmentDomain uses its -// own --prune.commitment-history.distance window. +// own --prune.commitment-history.distance window; RCacheDomain follows the +// general history window by default, or its own --prune.receipts.distance +// window when set (keep-all retires nothing). func historyRetireCutoffs(ctx context.Context, tx kv.Tx, blockReader services.FullBlockReader, pm prune.Mode, forwardProgress uint64) (cutoffs kv.RetireCutoffs, err error) { historyTxNum, err := blockAmountRetireCutoffTxNum(ctx, tx, blockReader, pm.History, forwardProgress) if err != nil { @@ -627,7 +629,16 @@ func historyRetireCutoffs(ctx context.Context, tx kv.Tx, blockReader services.Fu if err != nil { return kv.RetireCutoffs{}, err } - rcacheTxNum := historyTxNum // TODO: in future PR add cli flag to manage rcache distance + rcacheTxNum := historyTxNum + switch receipts := pm.ReceiptsAmount(); { + case receipts == prune.KeepAllReceiptsPruneMode: + rcacheTxNum = 0 // explicit keep-all overrides the follow-history default + case receipts.Enabled(): + rcacheTxNum, err = blockAmountRetireCutoffTxNum(ctx, tx, blockReader, receipts, forwardProgress) + if err != nil { + return kv.RetireCutoffs{}, err + } + } return kv.RetireCutoffs{ Default: historyTxNum, PerDomain: map[kv.Domain]uint64{ diff --git a/execution/stagedsync/stage_execute_prune_test.go b/execution/stagedsync/stage_execute_prune_test.go index e043ba6507a..66b504a0b4e 100644 --- a/execution/stagedsync/stage_execute_prune_test.go +++ b/execution/stagedsync/stage_execute_prune_test.go @@ -37,8 +37,9 @@ import ( // TestRetireCutoffs_ConvertsBlockDistanceToTxNum pins the per-domain // block-distance-to-txNum conversion: Default drives most domains, CommitmentDomain -// gets its own window, RCacheDomain follows the history window. The aggregator -// floors txNum to its file step, so this layer stays in txNum. +// gets its own window, RCacheDomain follows the history window by default or its +// own receipts window when set. The aggregator floors txNum to its file step, so +// this layer stays in txNum. func TestRetireCutoffs_ConvertsBlockDistanceToTxNum(t *testing.T) { logger := log.New() dirs := datadir.New(t.TempDir()) @@ -105,4 +106,35 @@ func TestRetireCutoffs_ConvertsBlockDistanceToTxNum(t *testing.T) { require.Equal(t, uint64(0), cutoffs.Default) require.Equal(t, uint64(100), cutoffs.PerDomain[kv.CommitmentDomain]) }) + + t.Run("finite history + finite receipts -> rcache uses its own window", func(t *testing.T) { + // history Distance(10) -> block 20, txNum 200; receipts Distance(5) -> + // block 25, txNum 250 (a narrower window is retired more aggressively). + pm := prune.Mode{Initialised: true, History: prune.Distance(10), Receipts: prune.Distance(5)} + cutoffs, err := historyRetireCutoffs(ctx, tx, br, pm, forward) + require.NoError(t, err) + require.False(t, cutoffs.IsNoop()) + require.Equal(t, uint64(200), cutoffs.Default) + require.Equal(t, uint64(250), cutoffs.PerDomain[kv.RCacheDomain], "rcache uses its own receipts window, not the history one") + }) + + t.Run("finite history + keep-all receipts -> rcache retires nothing", func(t *testing.T) { + // history Distance(10) -> block 20, txNum 200; receipts keep-all overrides + // the follow-history default so rcache is kept in full (cutoff 0). + pm := prune.Mode{Initialised: true, History: prune.Distance(10), Receipts: prune.KeepAllReceiptsPruneMode} + cutoffs, err := historyRetireCutoffs(ctx, tx, br, pm, forward) + require.NoError(t, err) + require.Equal(t, uint64(200), cutoffs.Default) + require.Equal(t, uint64(0), cutoffs.PerDomain[kv.RCacheDomain], "explicit keep-all overrides follow-history") + }) + + t.Run("archive history + finite receipts -> rcache only", func(t *testing.T) { + // History keep-all -> Default 0; receipts Distance(20) -> block 10, txNum 100. + pm := prune.Mode{Initialised: true, History: prune.KeepAllBlocksPruneMode, Receipts: prune.Distance(20)} + cutoffs, err := historyRetireCutoffs(ctx, tx, br, pm, forward) + require.NoError(t, err) + require.False(t, cutoffs.IsNoop()) + require.Equal(t, uint64(0), cutoffs.Default) + require.Equal(t, uint64(100), cutoffs.PerDomain[kv.RCacheDomain]) + }) } diff --git a/llms-full.txt b/llms-full.txt index 4bef2bc76f5..b6f11a56379 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1568,7 +1568,10 @@ Snapshots are organised into several subdirectories. The main ones are: - You can replay a single historical transaction without re-executing its block. - If an account changes V1 → V2 → V1 within one block, `debug_getModifiedAccountsByNumber` correctly returns it. -- Erigon stores compact per-transaction receipt *metadata* — cumulative gas used, blob gas used, log index — in a **required** receipt domain. Full receipts (with logs) live in a separate cache domain that is **off by default** in every prune mode (opt in with `--persist.receipts`). When a full receipt isn't cached, it is reconstructed on demand, re-deriving logs by re-execution. +- Erigon stores compact per-transaction receipt *metadata* — cumulative gas used, blob gas used, log index — in a + **required** receipt domain. Full receipts (with logs) live in a separate cache domain that is **off by default** in + every prune mode (opt in with `--prune.include-receipts`). When a full receipt isn't cached, it is reconstructed on + demand, re-deriving logs by re-execution. ## What does it cost on disk? @@ -1648,7 +1651,11 @@ In order to switch type of node, you must first delete the `/chaindata` folder i :::tip **Persisting receipts**, which are pre-calculated receipts, increase the requests-per-second (RPS) and improve the latency and throughput of all receipts and logs-related RPC calls. -As of v3.6 they are disabled by default in every pruning mode (previously they were enabled by default for all modes except Archive); enable them with the flag `--persist.receipts`. Without them, receipts and logs are re-derived on demand from state history, so the related RPC calls keep working within the node's state-history window, just with higher latency. On a Historical Blocks node, persisted receipts additionally extend receipts and logs availability from the state-history window back to genesis. +As of v3.6 they are disabled by default in every pruning mode (previously they were enabled by default for all modes +except Archive); enable them with the flag `--prune.include-receipts` (the former `--persist.receipts` still works as an +alias). Without them, receipts and logs are re-derived on demand from state history, so the related RPC calls keep +working within the node's state-history window, just with higher latency. On a Historical Blocks node, persisted +receipts additionally extend receipts and logs availability from the state-history window back to genesis. ::: :::note[Breaking change in v3.5] @@ -1675,7 +1682,15 @@ The Minimal Node configuration (`--prune.mode=minimal`) is the smallest possible ## Blocks node -The Blocks Node configuration (`--prune.mode=blocks`) keeps the **full block and transaction history** — every block back to genesis — while pruning **state history**. It retains state only within the EIP-8252 window (the last 262,144 blocks), the same state-retention as a Full Node, but unlike a Full Node it never prunes older blocks. This suits users who need complete historical **block and transaction data** — for research, indexing, or block explorers — without paying the disk cost of an archive node's full historical **state**. For full-range **receipts and logs** (`eth_getLogs` / `eth_getBlockReceipts` back to genesis), add `--persist.receipts`; without it they are served only within the state-history window (the last 262,144 blocks). +The Blocks Node configuration (`--prune.mode=blocks`) keeps the **full block and transaction history** — every block +back to genesis — while pruning **state history**. It retains state only within the EIP-8252 window (the last 262,144 +blocks), the same state-retention as a Full Node, but unlike a Full Node it never prunes older blocks. This suits users +who need complete historical **block and transaction data** — for research, indexing, or block explorers — without +paying the disk cost of an archive node's full historical **state**. For full-range **receipts and logs** +(`eth_getLogs` / `eth_getBlockReceipts` back to genesis), add +`--prune.include-receipts --prune.receipts.distance=keep-all`; with `--prune.include-receipts` alone the receipt cache +follows the state-history window (the last 262,144 blocks), and without it receipts are re-derived from state history +within that same window. --- @@ -1924,8 +1939,8 @@ These flags cover the general behavior and configuration of the Erigon client. * Default: `false` * `--ethstats value`: The reporting URL for an ethstats service. * `--trusted-setup-file value`: Absolute path to a `trusted_setup.json` file. -* `--persist.receipts, --experiment.persist.receipts.v2`: Downloads historical receipts. - * Default: `true` for minimal and full nodes, `false` for archive nodes +* `--prune.include-receipts, --persist.receipts, --experiment.persist.receipts.v2`: Downloads and caches historical receipts. Without it, receipts and logs are re-derived on demand from state history, so the related RPC calls still work, just with higher latency. + * Default: `false` (disabled) in every prune mode ### Database and Caching @@ -1965,6 +1980,10 @@ Flags for managing how old chain data is handled and stored. See [Snapshots Mana * Default: unset (retention follows `--prune.mode`) * `--prune.include-commitment-history, --prune.experimental.include-commitment-history, --experimental.commitment-history`: Enables fast `eth_getProof` for executed blocks by storing commitment history. Requires +32 GB RAM. See [`eth_getProof`](/interacting-with-erigon/eth#eth_getproof). * Default: `false` +* `--prune.commitment-history.distance value`: Keeps commitment history for the latest `N` blocks, or `keep-all`. Empty or `0` (default) keeps everything. Requires `--prune.include-commitment-history`. + * Default: unset (empty / `0`, keeps everything) +* `--prune.receipts.distance, --persist.receipts.distance value`: Keeps the receipt cache for the latest `N` blocks, or `keep-all`. Empty or `0` (default) follows the state-history window. Requires `--prune.include-receipts`. + * Default: unset (empty / `0`, follows the state-history window) * `--snap.skip-state-snapshot-download`: Skips state download and starts from genesis. * Default: `false` * `--snap.keepblocks`: Workaround/debug — keeps ancient blocks in the DB instead of moving them into snapshots (useful for debugging). diff --git a/node/cli/default_flags.go b/node/cli/default_flags.go index 61c934544ac..01fc2960402 100644 --- a/node/cli/default_flags.go +++ b/node/cli/default_flags.go @@ -167,6 +167,7 @@ var DefaultFlags = []cli.Flag{ &utils.VMEnableDebugFlag, &utils.NetworkIdFlag, &utils.PersistReceiptsV2Flag, + &utils.PersistReceiptsDistanceFlag, &utils.FakePoWFlag, &utils.GpoBlocksFlag, &utils.GpoPercentileFlag, diff --git a/node/cli/flags.go b/node/cli/flags.go index 75a521831ef..03a157656d5 100644 --- a/node/cli/flags.go +++ b/node/cli/flags.go @@ -237,19 +237,17 @@ func applyRemainingEthFlags(ctx *cli.Command, cfg *ethconfig.Config, logger log. } _ = chainId - blockDistance, err := prune.ParseBlocksDistance(ctx.String(PruneBlocksDistanceFlag.Name)) - if err != nil { - utils.Fatalf("%v", err) - } - distance, err := prune.ParseHistoryDistance(ctx.String(PruneDistanceFlag.Name)) - if err != nil { - utils.Fatalf("%v", err) - } + blockDistance := mustDistance(prune.ParseBlocksDistance(ctx.String(PruneBlocksDistanceFlag.Name), PruneBlocksDistanceFlag.Name)) + distance := mustDistance(prune.ParseHistoryDistance(ctx.String(PruneDistanceFlag.Name), PruneDistanceFlag.Name)) cfg.PersistReceiptsCacheV2 = ctx.Bool(utils.PersistReceiptsV2Flag.Name) - commitmentHistoryOlder := ctx.Uint64(utils.CommitmentHistoryDistanceFlag.Name) - mode, err := prune.FromCli(ctx.String(PruneModeFlag.Name), distance, blockDistance, commitmentHistoryOlder) + commitmentHistoryOlder := mustDistance(prune.ParseCommitmentHistoryDistance(ctx.String(utils.CommitmentHistoryDistanceFlag.Name), utils.CommitmentHistoryDistanceFlag.Name)) + receiptsDistance := mustDistance(prune.ParseReceiptsDistance(ctx.String(utils.PersistReceiptsDistanceFlag.Name), utils.PersistReceiptsDistanceFlag.Name)) + if ctx.IsSet(utils.PersistReceiptsDistanceFlag.Name) && !cfg.PersistReceiptsCacheV2 { + utils.Fatalf("--%s requires --%s", utils.PersistReceiptsDistanceFlag.Name, utils.PersistReceiptsV2Flag.Name) + } + mode, err := prune.FromCli(ctx.String(PruneModeFlag.Name), distance, blockDistance, commitmentHistoryOlder, receiptsDistance) if err != nil { utils.Fatalf(fmt.Sprintf("error while parsing mode: %v", err)) } @@ -327,18 +325,16 @@ func applyRemainingEthFlags(ctx *cli.Command, cfg *ethconfig.Config, logger log. func ApplyFlagsForEthConfigCobra(f *pflag.FlagSet, cfg *ethconfig.Config) { pruneMode := cobraStringValueOrDefault(f, PruneModeFlag.Name, PruneModeFlag.Value) - pruneBlockDistance, err := prune.ParseBlocksDistance(cobraStringValueOrDefault(f, PruneBlocksDistanceFlag.Name, PruneBlocksDistanceFlag.Value)) - if err != nil { - utils.Fatalf("%v", err) - } - pruneDistance, err := prune.ParseHistoryDistance(cobraStringValueOrDefault(f, PruneDistanceFlag.Name, PruneDistanceFlag.Value)) - if err != nil { - utils.Fatalf("%v", err) - } + pruneBlockDistance := mustDistance(prune.ParseBlocksDistance(cobraStringValueOrDefault(f, PruneBlocksDistanceFlag.Name, PruneBlocksDistanceFlag.Value), PruneBlocksDistanceFlag.Name)) + pruneDistance := mustDistance(prune.ParseHistoryDistance(cobraStringValueOrDefault(f, PruneDistanceFlag.Name, PruneDistanceFlag.Value), PruneDistanceFlag.Name)) - commitmentHistoryOlder := cobraUint64ValueOrDefault(f, utils.CommitmentHistoryDistanceFlag.Name, 0) + commitmentHistoryOlder := mustDistance(prune.ParseCommitmentHistoryDistance(cobraStringValueOrDefault(f, utils.CommitmentHistoryDistanceFlag.Name, ""), utils.CommitmentHistoryDistanceFlag.Name)) + receiptsDistance := mustDistance(prune.ParseReceiptsDistance(cobraStringValueOrDefault(f, utils.PersistReceiptsDistanceFlag.Name, ""), utils.PersistReceiptsDistanceFlag.Name)) + if f.Changed(utils.PersistReceiptsDistanceFlag.Name) && !cobraBoolValueOrDefault(f, utils.PersistReceiptsV2Flag.Name, utils.PersistReceiptsV2Flag.Value) { + utils.Fatalf("--%s requires --%s", utils.PersistReceiptsDistanceFlag.Name, utils.PersistReceiptsV2Flag.Name) + } - mode, err := prune.FromCli(pruneMode, pruneDistance, pruneBlockDistance, commitmentHistoryOlder) + mode, err := prune.FromCli(pruneMode, pruneDistance, pruneBlockDistance, commitmentHistoryOlder, receiptsDistance) if err != nil { utils.Fatalf(fmt.Sprintf("error while parsing mode: %v", err)) } @@ -385,24 +381,21 @@ func cobraStringValueOrDefault(f *pflag.FlagSet, name, fallback string) string { return v } -func cobraUint64ValueOrDefault(f *pflag.FlagSet, name string, fallback uint64) uint64 { +func cobraBoolValueOrDefault(f *pflag.FlagSet, name string, fallback bool) bool { if f.Lookup(name) == nil { return fallback } - v, err := f.GetUint64(name) + v, err := f.GetBool(name) if err != nil { utils.Fatalf("failed to read --%s: %v", name, err) } return v } -func cobraBoolValueOrDefault(f *pflag.FlagSet, name string, fallback bool) bool { - if f.Lookup(name) == nil { - return fallback - } - v, err := f.GetBool(name) +// mustDistance fatals on a prune-distance parse error and returns the value. +func mustDistance(v uint64, err error) uint64 { if err != nil { - utils.Fatalf("failed to read --%s: %v", name, err) + utils.Fatalf("%v", err) } return v } diff --git a/node/eth/backend.go b/node/eth/backend.go index bd2ad09ac18..2bc7e2c2ef9 100644 --- a/node/eth/backend.go +++ b/node/eth/backend.go @@ -290,7 +290,7 @@ func New(ctx context.Context, stack *node.Node, config *ethconfig.Config, logger return err } if !notChanged { - logger.Warn("--persist.receipts differs from the value stored in the datadir; using the stored value (changing it requires a fresh datadir)", "inDB", config.PersistReceiptsCacheV2, "inConfig", inConfig) + logger.Warn("--prune.include-receipts differs from the value stored in the datadir; using the stored value (changing it requires a fresh datadir)", "inDB", config.PersistReceiptsCacheV2, "inConfig", inConfig) } if config.PersistReceiptsCacheV2 { statecfg.EnableHistoricalRCache() diff --git a/rpc/jsonrpc/eth_api.go b/rpc/jsonrpc/eth_api.go index 22cc2572224..01e375fb428 100644 --- a/rpc/jsonrpc/eth_api.go +++ b/rpc/jsonrpc/eth_api.go @@ -445,7 +445,7 @@ func (api *BaseAPI) checkPruneField(tx kv.Tx, block uint64, field func(*prune.Mo } // checkReceiptsAvailable checks if receipts are available for the given block. -// In case --persist.receipts which makes all historical receipts available even when state history is pruned. +// In case --prune.include-receipts which makes all historical receipts available even when state history is pruned. func (api *BaseAPI) checkReceiptsAvailable(ctx context.Context, tx kv.Tx, block uint64) error { persistReceipts, err := kvcfg.PersistReceipts.Enabled(tx) if err != nil { diff --git a/rpc/jsonrpc/eth_system.go b/rpc/jsonrpc/eth_system.go index 627d57d5cbc..14e20b3bd5b 100644 --- a/rpc/jsonrpc/eth_system.go +++ b/rpc/jsonrpc/eth_system.go @@ -159,7 +159,7 @@ func (api *APIImpl) Capabilities(ctx context.Context) (*CapabilitiesResult, erro var receiptsField CapabilityField if persistReceipts { - // --persist.receipts widens past state-history pruning (receipts are written to + // --prune.include-receipts widens past state-history pruning (receipts are written to // RCacheDomain at execution time, not re-derived from state). The remaining bound // is block-body availability: eth_getBlockReceipts walks block.Transactions(), and // getLogsV3 reads log indexes whose snapshots follow prune.Blocks (see @@ -167,7 +167,7 @@ func (api *APIImpl) Capabilities(ctx context.Context) (*CapabilitiesResult, erro // blocksOldest with the same DeleteStrategy as blocks. receiptsField = avail(blocksOldest, pruneMode.Blocks) } else { - // Without --persist.receipts, receipts are re-executed on demand, requiring both state + // Without --prune.include-receipts, receipts are re-executed on demand, requiring both state // history and the block body. Use the more restrictive of the two oldest-block bounds. if blocksOldest > stateOldest { receiptsField = avail(blocksOldest, pruneMode.Blocks) diff --git a/rpc/jsonrpc/eth_system_test.go b/rpc/jsonrpc/eth_system_test.go index 32883099a08..24b0a004638 100644 --- a/rpc/jsonrpc/eth_system_test.go +++ b/rpc/jsonrpc/eth_system_test.go @@ -222,7 +222,7 @@ func TestCapabilities(t *testing.T) { result, err := api.Capabilities(t.Context()) require.NoError(t, err) pruned := head - testPruneDistance - // --persist.receipts: receipts and logs available from genesis, not limited by state prune window. + // --prune.include-receipts: receipts and logs available from genesis, not limited by state prune window. require.Equal(t, uint64(0), oldest(t, result.Receipts)) require.Nil(t, result.Receipts.DeleteStrategy) require.Equal(t, uint64(0), oldest(t, result.Logs)) @@ -270,7 +270,7 @@ func TestCapabilities(t *testing.T) { }) // Post-#21342 production FullMode: Blocks is a finite Distance (EIP-8252 retention window), - // not the KeepPostMergeBlocksPruneMode sentinel. Without --persist.receipts, receipts/logs are + // not the KeepPostMergeBlocksPruneMode sentinel. Without --prune.include-receipts, receipts/logs are // bounded by max(stateOldest, blocksOldest) — equal here, so both report the prune window. t.Run("full_eip8252_no_persist", func(t *testing.T) { t.Parallel() @@ -290,7 +290,7 @@ func TestCapabilities(t *testing.T) { require.Equal(t, testPruneDistance, window(t, result.Logs)) }) - // full (EIP-8252) + --persist.receipts: persist.receipts widens past state history, but + // full (EIP-8252) + --prune.include-receipts: it widens past state history, but // block bodies and log indexes are still pruned at prune.Blocks, so receipts/logs are // bounded by blocksOldest, NOT genesis. This is the common pruned-archive config and the // case a routing layer would misroute if oldestBlock were reported as 0. @@ -309,8 +309,8 @@ func TestCapabilities(t *testing.T) { require.Equal(t, pruned, oldest(t, result.Blocks)) }) - // minimal + --persist.receipts: block bodies and log indexes are still pruned at the same - // distance as state, so persist.receipts cannot widen receipts/logs past blocksOldest — + // minimal + --prune.include-receipts: block bodies and log indexes are still pruned at the same + // distance as state, so it cannot widen receipts/logs past blocksOldest — // eth_getBlockReceipts needs the body and getLogsV3 needs the log indexes. t.Run("minimal_persist_receipts", func(t *testing.T) { t.Parallel() @@ -360,7 +360,7 @@ func TestCapabilities(t *testing.T) { require.Nil(t, result.Logs.DeleteStrategy) }) - // full mode + --persist.receipts on a merge chain: pre-merge blocks were never downloaded, + // full mode + --prune.include-receipts on a merge chain: pre-merge blocks were never downloaded, // so their receipts were never persisted. receipts/logs.oldestBlock must reflect the merge point. t.Run("full_persist_receipts_merge_height", func(t *testing.T) { t.Parallel() diff --git a/scripts/fusefilter-bench/fuse_compare.sh b/scripts/fusefilter-bench/fuse_compare.sh index 8f401b3277b..3f49c44fde8 100755 --- a/scripts/fusefilter-bench/fuse_compare.sh +++ b/scripts/fusefilter-bench/fuse_compare.sh @@ -95,7 +95,7 @@ COLLECT_TABLE_SIZES_FREQUENCY=3s nohup "$BIN" \ --nat=stun \ --torrent.download.rate 10G --torrent.upload.rate=1k \ --pprof --pprof.port=6062 \ - --prune.mode=archive --persist.receipts \ + --prune.mode=archive --prune.include-receipts \ --db.pagesize=4k \ --sync.loop.block.limit=10_000_000 \ --batchSize=512m --http=false \