Skip to content

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

Draft
meyer9 wants to merge 2 commits into
meyer9/snapshotter-stream-to-s3from
meyer9/snapshotter-proof-static-tables
Draft

feat(snapshotter): deduplicate RocksDB proofs SSTs#4915
meyer9 wants to merge 2 commits into
meyer9/snapshotter-stream-to-s3from
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 64796ec6 98e7839c 520d069b
Cobalt pass 725 0 14 64796ec6 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.

@meyer9
meyer9 force-pushed the meyer9/snapshotter-proof-static-tables branch from fd796cd to 8c3076e Compare September 8, 2026 15:12
@meyer9
meyer9 changed the base branch from main to meyer9/snapshotter-stream-to-s3 September 8, 2026 15:12
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Review Summary

This PR cleanly splits RocksDB proof snapshots into immutable SST archives (shared/deduplicated across runs) and per-run mutable metadata. The manifest extension (proofs_static), upload dedup logic, and download verification pipeline are well-structured. Validation of archive paths and output file paths is thorough against traversal attacks.

Existing Inline Findings (from prior review pass)

The four inline comments already posted cover the key issues:

  1. Blocking I/O on async runtime (download.rs:165-175) — verify_outputs (BLAKE3 hashing) and extract_tar_zst (full decompression) are synchronous and called directly from async run_from_manifest. The previous implementation wrapped extract_tar_zst in spawn_blocking. For production SST files that can be hundreds of MB, this will block the tokio runtime thread.

  2. Unvalidated tar entries vs declared output_files (download.rs) — archive.unpack(target_dir) extracts whatever the tar contains, but only declared output_files are verified and cleaned up. A corrupted or malicious archive could write undeclared files that persist silently.

  3. Duplicate ProofsStaticManifest type (download.rs:115) — The download crate defines its own Deserialize-only ProofsStaticManifest while base-reth-cli already exports a public ProofsStaticManifest with both Serialize and Deserialize. Worth consolidating if dependency constraints allow.

  4. Base URL fallback edge case (download.rs:228-230) — Minor nit on rsplit_once fallback behavior.

Additional Observations

  • Entry ordering is correct: Static SST tables are processed before mutable metadata in fetch_manifest_entries (line 231-236), so RocksDB references are satisfied before metadata that points to them.
  • Upload-side proof_static_keys mapping (upload.rs:729-743): The filter_map with strip_prefix("static_files/") silently drops entries whose file field does not start with that prefix. This works correctly because generate_manifest always constructs these paths with the static_files/ prefix, but a comment noting this coupling would help future maintenance.
  • Test coverage is strong: the new E2E test (generated_fake_rocksdb_proofs_snapshot_restores_end_to_end) exercises the full generate → upload → download → verify round trip through MinIO, and the downloads_metadata_and_static_tables_and_reuses_verified_sst test verifies the incremental reuse path.

Not Block-Production Sensitive

This PR only touches snapshot infrastructure (crates/infra/snapshotter, crates/execution/cli, crates/utilities/reth-cli). No block-production, builder, or execution paths are affected.

@meyer9
meyer9 force-pushed the meyer9/snapshotter-proof-static-tables branch 2 times, most recently from bfa11b5 to f65f76d Compare September 8, 2026 15:47
@meyer9
meyer9 force-pushed the meyer9/snapshotter-proof-static-tables branch from f65f76d to c6323f7 Compare September 10, 2026 20:19
Comment on lines +125 to +128
/// Parses the optional proofs extension from a manifest JSON document.
pub fn from_manifest_bytes(bytes: &[u8]) -> Result<Option<Self>> {
Self::from_named_manifest_bytes(bytes, "proofs_static")
}

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: from_manifest_bytes hardcodes "proofs_static" on a generic type

RocksDbStaticManifest::from_manifest_bytes always looks up the "proofs_static" key, but this struct also backs RocksdbStaticManifest (the main RocksDB index type). Calling RocksdbStaticManifest::from_manifest_bytes(bytes) would parse the proofs extension instead of "rocksdb_static" — silently returning the wrong data.

Since both ProofsStaticManifest and RocksdbStaticManifest are type aliases to the same struct, the compiler won't catch this at any call site.

Consider either:

  1. Removing the convenience method and requiring callers to use from_named_manifest_bytes with the explicit key, or
  2. Making ProofsStaticManifest and RocksdbStaticManifest distinct newtypes (or at least removing the convenience alias on the base struct)

@github-actions

Copy link
Copy Markdown
Contributor

<!-- CLAUDE_REVIEW_SUMMARY -->

Review Summary

This PR splits proof snapshots into immutable SST archives and per-run metadata, enabling deduplication of unchanged RocksDB SST tables across snapshot runs. The overall approach is sound — content-addressed archive names, BLAKE3 verification, and reuse of existing remote static files are well-designed.

New Findings

# Severity File Description
1 Issue snapshot_manifest.rs:125-128 RocksDbStaticManifest::from_manifest_bytes hardcodes "proofs_static" key, making it silently wrong when called through the RocksdbStaticManifest alias (both are the same type).

Prior Findings (from earlier review runs, still applicable)

# Severity File Description
2 Issue download.rs:165-172 verify_outputs and extract_tar_zst perform blocking I/O directly on the async runtime (previous spawn_blocking wrapper was removed).
3 Issue download.rs:172 extract_tar_zst uses archive.unpack() without validating entries against declared output_files, allowing undeclared files to be written to disk.
4 Nit download.rs:115-122 Duplicate ProofsStaticManifest struct definition (parallel to the one in base-reth-cli).
5 Nit download.rs:228-229 Fallback base URL from rsplit_once can silently produce wrong base when manifest URL has no path separator.

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