From 1694390a58294b97fad3f33d40324d31dafba190 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Thu, 17 Sep 2026 16:50:34 -0400 Subject: [PATCH 1/2] feat(snapshots): add archive proofs preset Amp-Thread-ID: https://ampcode.com/threads/T-01a0b11a-5059-7341-91bc-349cd900352f Co-authored-by: Amp --- app/snapshots/SnapshotsClient.tsx | 59 +++---------------- app/snapshots/data.test.ts | 41 ++++++++++++- app/snapshots/data.ts | 97 +++++++++++++++++++++++++++---- 3 files changed, 133 insertions(+), 64 deletions(-) diff --git a/app/snapshots/SnapshotsClient.tsx b/app/snapshots/SnapshotsClient.tsx index f9e60f7..c8f0eba 100644 --- a/app/snapshots/SnapshotsClient.tsx +++ b/app/snapshots/SnapshotsClient.tsx @@ -20,10 +20,13 @@ import { } from '../analytics/events'; import { + applyComponentDependencies, + buildDownloadCommand, CHAIN_NAME_BY_NETWORK, formatBytes, formatDate, formatNumber, + hasCompleteArchive, presetSize, PresetName, PRESETS, @@ -53,46 +56,6 @@ function InlineCommand({ command, onCopy }: { command: string; onCopy?: () => vo ); } -// Builds the `base-reth-node download` command from the current selection, -// mirroring the CLI's component flags and preset/archive shortcuts. -function buildDownloadCommand( - chainName: string, - preset: PresetName | null, - selectedComponents: string[], -): string { - const matchingPreset = PRESETS.find( - (p) => - p.components.length === selectedComponents.length && - p.components.every((c) => selectedComponents.includes(c)), - )?.name; - const archiveWithoutRocksDb = - PRESETS.find((p) => p.name === 'archive')?.components.filter((c) => c !== 'rocksdb_indices') ?? - []; - const isArchiveWithoutRocksDb = - archiveWithoutRocksDb.length === selectedComponents.length && - archiveWithoutRocksDb.every((c) => selectedComponents.includes(c)); - const effectivePreset = preset ?? matchingPreset; - const args: string[] = []; - - if (effectivePreset) { - args.push(`--${effectivePreset}`, '--resumable'); - } else if (isArchiveWithoutRocksDb) { - args.push('--archive', '--without-rocksdb'); - } else { - if (selectedComponents.includes('transactions')) args.push('--with-txs'); - if (selectedComponents.includes('transaction_senders')) args.push('--with-senders'); - if (selectedComponents.includes('receipts')) args.push('--with-receipts'); - if ( - selectedComponents.includes('account_changesets') || - selectedComponents.includes('storage_changesets') - ) { - args.push('--with-state-history'); - } - } - - return `base-reth-node download --chain ${chainName}${args.length ? ` ${args.join(' ')}` : ''}`; -} - type SnapshotsClientProps = { /** Non-empty; the server renders the error state when there is nothing to show. */ snapshots: Snapshot[]; @@ -143,16 +106,9 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) { next.push(name); } - // Enforce component dependencies: senders need transactions; the rocksdb - // indices need transactions, receipts, and state history. - const withTransactions = next.includes('transactions'); - const withReceipts = next.includes('receipts'); - const withStateHistory = - next.includes('account_changesets') && next.includes('storage_changesets'); - if (!withTransactions) next = next.filter((c) => c !== 'transaction_senders'); - if (!withTransactions || !withReceipts || !withStateHistory) { - next = next.filter((c) => c !== 'rocksdb_indices'); - } + // Enforce component dependencies: proofs require the complete archive; + // senders and RocksDB indices have narrower requirements of their own. + next = applyComponentDependencies(next); if (next.length === 0) return; @@ -280,7 +236,7 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) { selectPreset(next as PresetName); }} aria-label="Configure Snapshot" - className="grid-cols-1 lg:grid-cols-3" + className="grid-cols-1 lg:grid-cols-4" > {PRESETS.map((p) => { const size = presetSize(activeSnapshot.components, p); @@ -333,6 +289,7 @@ export function SnapshotsClient({ snapshots }: SnapshotsClientProps) { (c.name === 'state_history' ? withStateHistory : selectedComponents.includes(c.name)); const isDisabled = REQUIRED_COMPONENTS.has(c.name) || + (c.name === 'proofs' && !hasCompleteArchive(selectedComponents)) || (c.name === 'transaction_senders' && !withTransactions) || (c.name === 'rocksdb_indices' && (!withTransactions || !withReceipts || !withStateHistory)); diff --git a/app/snapshots/data.test.ts b/app/snapshots/data.test.ts index 35c7226..2cb58c6 100644 --- a/app/snapshots/data.test.ts +++ b/app/snapshots/data.test.ts @@ -1,4 +1,10 @@ -import { PRESETS, presetSize, type SnapshotComponent } from './data'; +import { + applyComponentDependencies, + buildDownloadCommand, + PRESETS, + presetSize, + type SnapshotComponent, +} from './data'; const component = (name: string, size: number, fullSize?: number): SnapshotComponent => ({ name, @@ -18,6 +24,7 @@ describe('presetSize', () => { component('account_changesets', 60, 6), component('storage_changesets', 50, 5), component('rocksdb_indices', 40), + component('proofs', 25), ]; it('uses all state and headers plus the history window of full static-file components', () => { @@ -41,9 +48,41 @@ describe('presetSize', () => { expect(presetSize(components, minimal)).toBe(120); }); + it('adds the proofs component to the complete archive size', () => { + const archiveWithProofs = PRESETS.find((preset) => preset.name === 'archive-proofs')!; + + expect(presetSize(components, archiveWithProofs)).toBe(455); + }); + it('falls back to the complete size when older API data has no tail size', () => { const full = PRESETS.find((preset) => preset.name === 'full')!; expect(presetSize([component('transactions', 80)], full)).toBe(80); }); }); + +describe('proofs configuration', () => { + const archive = PRESETS.find((preset) => preset.name === 'archive')!; + const archiveWithProofs = PRESETS.find((preset) => preset.name === 'archive-proofs')!; + + it('builds the archive and proofs command from a custom selection', () => { + expect(buildDownloadCommand('base', null, archiveWithProofs.components)).toBe( + 'base-reth-node download --chain base --archive --proofs', + ); + }); + + it('builds the same command when the Archive + Proofs preset is selected', () => { + expect(buildDownloadCommand('base', 'archive-proofs', archiveWithProofs.components)).toBe( + 'base-reth-node download --chain base --archive --proofs', + ); + }); + + it('removes proofs when any archive component is removed', () => { + const withoutReceipts = archiveWithProofs.components.filter( + (component) => component !== 'receipts', + ); + + expect(applyComponentDependencies(withoutReceipts)).not.toContain('proofs'); + expect(applyComponentDependencies(archive.components)).toEqual(archive.components); + }); +}); diff --git a/app/snapshots/data.ts b/app/snapshots/data.ts index 61a26ad..3a67fc0 100644 --- a/app/snapshots/data.ts +++ b/app/snapshots/data.ts @@ -29,7 +29,7 @@ export type Snapshot = { manifestUrl: string; }; -export type PresetName = 'minimal' | 'full' | 'archive'; +export type PresetName = 'minimal' | 'full' | 'archive' | 'archive-proofs'; export type Preset = { name: PresetName; @@ -39,21 +39,30 @@ export type Preset = { capabilities: string[]; }; +const ARCHIVE_COMPONENTS = [ + 'state', + 'headers', + 'transactions', + 'transaction_senders', + 'receipts', + 'account_changesets', + 'storage_changesets', + 'rocksdb_indices', +]; + export const PRESETS: Preset[] = [ + { + name: 'archive-proofs', + displayName: 'Archive + Proofs', + description: 'Everything in Archive, plus execution proofs.', + components: [...ARCHIVE_COMPONENTS, 'proofs'], + capabilities: ['Sync', 'Validate', 'Query', 'Trace', 'Debug', 'Index', 'Prove'], + }, { name: 'archive', displayName: 'Archive', - description: 'Everything included. Full historical data for indexers and RPC providers.', - components: [ - 'state', - 'headers', - 'transactions', - 'transaction_senders', - 'receipts', - 'account_changesets', - 'storage_changesets', - 'rocksdb_indices', - ], + description: 'Full historical data for indexers and RPC providers.', + components: ARCHIVE_COMPONENTS, capabilities: ['Sync', 'Validate', 'Query', 'Trace', 'Debug', 'Index'], }, { @@ -92,6 +101,66 @@ export function presetSize(components: SnapshotComponent[], preset: Preset): num ); } +export function hasCompleteArchive(components: string[]): boolean { + return ARCHIVE_COMPONENTS.every((component) => components.includes(component)); +} + +export function applyComponentDependencies(components: string[]): string[] { + let next = [...components]; + const withTransactions = next.includes('transactions'); + const withReceipts = next.includes('receipts'); + const withStateHistory = + next.includes('account_changesets') && next.includes('storage_changesets'); + + if (!withTransactions) next = next.filter((component) => component !== 'transaction_senders'); + if (!withTransactions || !withReceipts || !withStateHistory) { + next = next.filter((component) => component !== 'rocksdb_indices'); + } + if (!hasCompleteArchive(next)) next = next.filter((component) => component !== 'proofs'); + + return next; +} + +export function buildDownloadCommand( + chainName: string, + preset: PresetName | null, + selectedComponents: string[], +): string { + const matchingPreset = PRESETS.find( + (candidate) => + candidate.components.length === selectedComponents.length && + candidate.components.every((component) => selectedComponents.includes(component)), + )?.name; + const archiveWithoutRocksDb = ARCHIVE_COMPONENTS.filter( + (component) => component !== 'rocksdb_indices', + ); + const isArchiveWithoutRocksDb = + archiveWithoutRocksDb.length === selectedComponents.length && + archiveWithoutRocksDb.every((component) => selectedComponents.includes(component)); + const effectivePreset = preset ?? matchingPreset; + const args: string[] = []; + + if (effectivePreset === 'archive-proofs') { + args.push('--archive', '--proofs'); + } else if (effectivePreset) { + args.push(`--${effectivePreset}`, '--resumable'); + } else if (isArchiveWithoutRocksDb) { + args.push('--archive', '--without-rocksdb'); + } else { + if (selectedComponents.includes('transactions')) args.push('--with-txs'); + if (selectedComponents.includes('transaction_senders')) args.push('--with-senders'); + if (selectedComponents.includes('receipts')) args.push('--with-receipts'); + if ( + selectedComponents.includes('account_changesets') || + selectedComponents.includes('storage_changesets') + ) { + args.push('--with-state-history'); + } + } + + return `base-reth-node download --chain ${chainName}${args.length ? ` ${args.join(' ')}` : ''}`; +} + export const CHAIN_NAME_BY_NETWORK: Record = { mainnet: 'base', sepolia: 'base-sepolia', @@ -139,6 +208,7 @@ export const COMPONENT_META: Record Date: Thu, 17 Sep 2026 16:54:30 -0400 Subject: [PATCH 2/2] fix(snapshots): exclude proofs from homepage archive size Amp-Thread-ID: https://ampcode.com/threads/T-01a0b11a-5059-7341-91bc-349cd900352f Co-authored-by: Amp --- app/snapshots/SnapshotDownloadBox.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/snapshots/SnapshotDownloadBox.tsx b/app/snapshots/SnapshotDownloadBox.tsx index 3344d09..e0cada6 100644 --- a/app/snapshots/SnapshotDownloadBox.tsx +++ b/app/snapshots/SnapshotDownloadBox.tsx @@ -2,7 +2,7 @@ import { Card } from '../components/ui/Card'; import { CommandLine } from '../components/ui/CommandLine'; import { Text } from '../components/ui/Text'; -import { formatBytes, formatDate, formatNumber, type Snapshot } from './data'; +import { formatBytes, formatDate, formatNumber, PRESETS, presetSize, type Snapshot } from './data'; // The archive preset built by the snapshots configurator, hoisted to the // homepage so a node operator can copy-and-go without opening the full page. @@ -13,8 +13,10 @@ const ARCHIVE_COMMAND = 'base-reth-node download --chain base --archive --resuma // the ready-to-run download command, with a link out to the configurator for // anything smaller than a full archive. export function SnapshotDownloadBox({ snapshot }: { snapshot: Snapshot }) { + const archivePreset = PRESETS.find((preset) => preset.name === 'archive'); + const archiveSize = archivePreset ? presetSize(snapshot.components, archivePreset) : snapshot.size; const stats = [ - { label: 'Total Size', value: formatBytes(snapshot.size) }, + { label: 'Total Size', value: formatBytes(archiveSize) }, { label: 'Block', value: formatNumber(snapshot.block) }, { label: 'Version', value: snapshot.rethVersion }, { label: 'Updated', value: formatDate(snapshot.date) },