Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/snapshots/SnapshotDownloadBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) },
Expand Down
59 changes: 8 additions & 51 deletions app/snapshots/SnapshotsClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ import {
} from '../analytics/events';

import {
applyComponentDependencies,
buildDownloadCommand,
CHAIN_NAME_BY_NETWORK,
formatBytes,
formatDate,
formatNumber,
hasCompleteArchive,
presetSize,
PresetName,
PRESETS,
Expand Down Expand Up @@ -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[];
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down
41 changes: 40 additions & 1 deletion app/snapshots/data.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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', () => {
Expand All @@ -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);
});
});
97 changes: 85 additions & 12 deletions app/snapshots/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'],
},
{
Expand Down Expand Up @@ -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<string, string> = {
mainnet: 'base',
sepolia: 'base-sepolia',
Expand Down Expand Up @@ -139,6 +208,7 @@ export const COMPONENT_META: Record<string, { displayName: string; description:
description: 'Historical storage slot changes.',
},
rocksdb_indices: { displayName: 'Indices', description: 'Database indices for fast lookups.' },
proofs: { displayName: 'Proofs', description: 'Execution proofs for archived blocks.' },
};

export const COMPONENT_ORDER = Object.keys(COMPONENT_META);
Expand Down Expand Up @@ -196,6 +266,7 @@ export const SAMPLE_SNAPSHOTS: Snapshot[] = [
account_changesets: 310,
storage_changesets: 680,
rocksdb_indices: 240,
proofs: 842,
},
{
transactions: 42,
Expand All @@ -218,6 +289,7 @@ export const SAMPLE_SNAPSHOTS: Snapshot[] = [
account_changesets: 40,
storage_changesets: 90,
rocksdb_indices: 35,
proofs: 339,
},
{
transactions: 9,
Expand All @@ -242,6 +314,7 @@ export const SAMPLE_SNAPSHOTS: Snapshot[] = [
account_changesets: 2,
storage_changesets: 5,
rocksdb_indices: 2,
proofs: 6,
},
{
transactions: 1,
Expand Down
Loading