Skip to content

feat(snapshotter): deduplicate RocksDB proofs SSTs - #4915

Draft
meyer9 wants to merge 1 commit into
mainfrom
meyer9/snapshotter-proof-static-tables
Draft

feat(snapshotter): deduplicate RocksDB proofs SSTs#4915
meyer9 wants to merge 1 commit into
mainfrom
meyer9/snapshotter-proof-static-tables

Conversation

@meyer9

@meyer9 meyer9 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • split opt-in RocksDB proofs snapshots into immutable, shared SST archives and per-run metadata archives
  • publish a proofs_static manifest extension so existing SSTs are skipped during later snapshot runs
  • update base snapshot download --proofs to restore, verify, and reuse the incremental proof artifacts

Testing

  • cargo test -p base-snapshotter --lib --no-fail-fast
  • cargo test -p base-execution-cli commands::download::tests --no-fail-fast
  • cargo test -p base-execution-cli commands::download::tests::generated_fake_rocksdb_proofs_snapshot_restores_end_to_end -- --nocapture
  • targeted MinIO integration tests for proof generation/upload and existing snapshot upload flows
  • cargo clippy -p base-snapshotter --all-targets -- -D warnings
  • cargo clippy -p base-execution-cli --all-targets -- -D warnings

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@depot-code-access

depot-code-access Bot commented Sep 3, 2026

Copy link
Copy Markdown

✅ All benchmarks green — 14 within ±2% (deterministic instruction counts). View run

Benchmark details (14)
Benchmark Base (target) Head (this PR) Δ instructions
batch_queue/drain/drain_cached_span_batches 242,027 242,027 +0.0%
batch_transaction/encode_in_place/encode_in_place 4,199,759 4,199,759 +0.0%
batch_transaction/temporary_frame_buffers/temporary_frame_buffers 8,408,350 8,408,350 +0.0%
flashblock_decode/decode/brotli 3,296,484 3,296,484 +0.0%
flashblock_decode/decode/plain_json 2,280,194 2,280,194 +0.0%
flz/compress_len/real_contract_call 43,148 43,148 +0.0%
flz/compress_len/synthetic_0 38,205 38,205 +0.0%
flz/compress_len/synthetic_1 54,682 54,682 +0.0%
flz/compress_len/synthetic_2 147,976 147,976 +0.0%
flz/data_gas 43,059 43,059 +0.0%
flz/tx_estimated_size 43,056 43,056 +0.0%
frame_parse/decode/single_4kib 1,031 1,031 +0.0%
frame_parse/parse_frames/few_large 1,053,062 1,053,062 +0.0%
frame_parse/parse_frames/many_small 154,763 154,763 +0.0%

@meyer9
meyer9 force-pushed the meyer9/snapshotter-proof-static-tables branch from 1620b30 to bdeca99 Compare September 3, 2026 23:56
Comment on lines +164 to +182
for entry in entries {
if Self::verify_outputs(target_dir, &entry.output_files)? {
info!(target: "reth::cli", file = %entry.file_name, "Reusing verified proofs snapshot artifact");
continue;
}

Self::cleanup_outputs(target_dir, &entry.output_files);
let archive_path = Self::download_archive(&entry, &cache_dir).await?;
Self::extract_tar_zst(&archive_path, target_dir)?;
tokio::fs::remove_file(&archive_path).await.ok();

Self::extract_and_cleanup(&archive_path, target_dir, &cache_dir).await
if !Self::verify_outputs(target_dir, &entry.output_files)? {
Self::cleanup_outputs(target_dir, &entry.output_files);
eyre::bail!(
"proofs archive extracted but output verification failed: {}",
entry.file_name
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: Blocking I/O on the async runtime

Both verify_outputs (line 165, 175) and extract_tar_zst (line 172) perform synchronous file I/O — verify_outputs reads and BLAKE3-hashes every output file, and extract_tar_zst decompresses an entire tar.zst archive. Calling these directly from the async run_from_manifest method blocks the tokio runtime thread.

The previous implementation wrapped extract_tar_zst in tokio::task::spawn_blocking, which was removed in this refactor. For SST files that may be hundreds of megabytes, the blocking time is significant.

Consider wrapping these calls in spawn_blocking:

let target = target_dir.to_path_buf();
let outputs = entry.output_files.clone();
let verified = tokio::task::spawn_blocking(move || Self::verify_outputs(&target, &outputs)).await??;

and similarly for extract_tar_zst.

.get("size")
.and_then(|s| s.as_u64())
.ok_or_else(|| eyre::eyre!("proofs component missing 'size' field in manifest"))?;
let archive_base_url = manifest.base_url.as_deref().unwrap_or_else(|| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: fallback base URL from manifest_url can produce an invalid URL when the manifest path has no /

rsplit_once('/') returns None when the manifest URL has no / path separator, causing the fallback to be the entire manifest_url string (e.g. "http://example.com"). Then on line 245 the trailing-slash fixup and Url::join would work, but the intent of the fallback — stripping the manifest.json leaf — would silently produce the wrong base. This is an existing pre-PR edge case and practically unreachable, just noting it.

}

#[derive(Debug, Deserialize)]
struct ProofsDownloadManifest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Duplicate ProofsStaticManifest type

This file defines its own ProofsStaticManifest (Deserialize-only) at line 115, while base_snapshotter::snapshot already exports a public ProofsStaticManifest with both Serialize and Deserialize. The download crate already depends on base-snapshotter indirectly through reth_cli_commands.

If there's no dependency constraint preventing it, reusing the snapshotter's type would keep the manifest schema in one place. If these crates intentionally don't depend on each other, at minimum consider adding a comment noting the parallel definition.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Summary

The PR cleanly splits the monolithic proofs archive into immutable SST archives (content-addressed, shared across runs) and per-run mutable metadata. The manifest extension approach (proofs_static at the top level) is a sound design that preserves backward compatibility with stock Reth manifest readers. The download side correctly implements incremental reuse with BLAKE3 verification.

Findings

Blocking I/O on the async runtime (download.rs:164-182) — verify_outputs and extract_tar_zst perform synchronous file reads, BLAKE3 hashing, and decompression directly inside async fn run_from_manifest. The old code wrapped extract_tar_zst in tokio::task::spawn_blocking; this was removed in the refactor. For large SST files (potentially hundreds of MB), this blocks the tokio worker thread. Consider restoring spawn_blocking wrappers.

Duplicate type definition (download.rs:115) — ProofsStaticManifest is defined separately in base-execution-cli and base-snapshotter. If no dependency constraint prevents it, reusing the snapshotter's type keeps the manifest schema in one place.

Design Observations (non-blocking)

  • Path validation (validate_relative_archive_path, validate_output_files) is thorough — rejects traversal, absolute paths, and non-Normal components.
  • The upload refactor from upload_file (prefix + filename) to upload_file_to_key (full key) is cleaner and enables the flexible key structure needed for proofs SST archives.
  • Test coverage is good: the integration test verifies both initial download and reuse on second run, and the snapshotter tests verify skipping recompression of existing SSTs.

@depot-code-access

depot-code-access Bot commented Sep 4, 2026

Copy link
Copy Markdown

Base Std historical fork tests

Fork Result Passed Failed Skipped base/base base-anvil base-std
Beryl pass 616 0 13 61e5c05c 98e7839c 520d069b
Cobalt pass 725 0 14 61e5c05c 98e7839c 4571b325

View run

@meyer9
meyer9 force-pushed the meyer9/snapshotter-proof-static-tables branch from bdeca99 to 14e64ae Compare September 4, 2026 00:43

Self::cleanup_outputs(target_dir, &entry.output_files);
let archive_path = Self::download_archive(&entry, &cache_dir).await?;
Self::extract_tar_zst(&archive_path, target_dir)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: extract_tar_zst no longer validates tar entry paths against output_files

The previous implementation used a fixed, single-component filename validated before extraction. Now, extraction uses archive.unpack(target_dir) which writes whatever paths are in the tar, while output_files is only checked after extraction in verify_outputs.

A malicious or corrupted manifest could declare output_files as ["proofs/000001.sst"] but package a tar containing proofs/000001.sst plus additional unexpected files (e.g., proofs/LOCK or other files outside the declared set). These extra files would be written to disk but never verified or cleaned up by cleanup_outputs (which only removes declared output_files).

Consider either:

  1. Validating that extracted entries match output_files exactly (rejecting unexpected entries), or
  2. Extracting into a temporary directory first, verifying, then moving only the declared files.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Summary

This PR splits the monolithic RocksDB proofs snapshot into immutable SST table archives and per-run metadata archives, with BLAKE3-based deduplication on the download side and content-addressed SST deduplication on the upload side. The architecture is sound — immutable SSTs are shared across runs, and only mutable RocksDB metadata is re-downloaded.

Findings

1. Blocking I/O on the async runtime (existing finding, confirmed)
verify_outputs (BLAKE3 hashing of potentially large SST files) and extract_tar_zst (tar+zstd decompression) are synchronous operations called directly from the async run_from_manifest method. The previous implementation wrapped extract_tar_zst in spawn_blocking which was removed in this refactor. For production SST files (hundreds of MBs to GBs), this blocks the tokio runtime thread.

2. Tar extraction writes undeclared files without cleanup (new finding, inline comment posted)
extract_tar_zst uses archive.unpack(target_dir) which writes all entries from the tar to disk, but verify_outputs and cleanup_outputs only operate on the declared output_files. A manifest could declare specific output files while the tar contains additional entries — these would be written to disk but never verified or cleaned up.

3. Duplicate ProofsStaticManifest type (existing finding, confirmed)
The download crate defines its own ProofsStaticManifest (Deserialize-only) while base_snapshotter::snapshot already exports a public ProofsStaticManifest with both Serialize and Deserialize. base-snapshotter is already a dev-dependency but not a regular dependency.

Positive observations

  • SST tables are downloaded before metadata, ensuring RocksDB consistency if the process is interrupted
  • Path traversal validation is thorough — both archive paths and output file paths are validated for Normal components only and require the proofs/ prefix
  • The reuse-on-second-run path is well tested (downloads_metadata_and_static_tables_and_reuses_verified_sst)
  • The end-to-end test using SnapshotGenerator to produce a real manifest then restore it validates the full round-trip
  • Upload-side deduplication via content-addressed SST naming is clean

@meyer9
meyer9 force-pushed the meyer9/snapshotter-proof-static-tables branch from 14e64ae to fd796cd Compare September 4, 2026 00:48
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Summary — feat(snapshotter): deduplicate RocksDB proofs SSTs

Overall this is a well-structured change that splits proof snapshots into immutable SST archives (shared across runs) and per-run metadata, with content-addressed naming, BLAKE3 verification, and incremental reuse on both upload and download sides. The design is sound and well-tested with E2E coverage.

Findings from inline comments (4 issues raised)

  1. Blocking I/O on async runtime (download.rs:165-182) — verify_outputs and extract_tar_zst are synchronous and called directly from an async context. The previous code used spawn_blocking for extraction; this was removed in the refactor. For large SST files (hundreds of MB), this blocks the tokio runtime thread.

  2. Tar extraction does not validate entry paths against output_files (download.rs:172) — archive.unpack(target_dir) writes whatever the tar contains, while verification only checks declared output_files after the fact. Undeclared files in the tar are written but never cleaned up.

  3. Duplicate ProofsStaticManifest type (download.rs:115-118) — The download crate defines its own ProofsStaticManifest while the snapshotter crate exports a public one with Serialize + Deserialize. Consider reusing or documenting the intentional duplication.

  4. Fallback base URL edge case (download.rs:228) — Minor: when manifest_url has no / path, rsplit_once falls back to the full URL. Practically unreachable but worth noting.

All four findings were posted in a prior review pass and remain applicable. No new inline comments were needed — the prior findings cover the substantive issues.

Things that look correct

  • Metadata entry is pushed last in the entries list (line 236), so SSTs are always downloaded/extracted before metadata — correct for RocksDB consistency.
  • Content-addressed archive naming in proofs_table_archive_name uses both the path hash and content hash, preventing collisions.
  • Path validation (validate_relative_archive_path, validate_output_files) correctly rejects traversal attacks and enforces the proofs/ prefix.
  • Upload-side dedup via remote_static_files lookup prevents re-uploading and re-compressing existing SSTs.
  • The proofs_static extension is injected at the top level of the manifest, preserving compatibility with stock Reth v2 manifest readers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants