Skip to content

[db] Give every snapshot stream a versioned header - #2463

Open
maxkozlovsky wants to merge 2 commits into
mainfrom
max/snapshot-stream-versioning
Open

[db] Give every snapshot stream a versioned header#2463
maxkozlovsky wants to merge 2 commits into
mainfrom
max/snapshot-stream-versioning

Conversation

@maxkozlovsky

@maxkozlovsky maxkozlovsky commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Problem

The snapshot format carries no version marker of any kind — no manifest, no magic, no version byte. A future change to any stream's layout would be read by an older loader as whatever the new bytes happened to look like. The per-file .blake3 checksums do not help: a well-formed file in an unexpected layout passes them and then misparses.

What this does

Opens each of the four per-shard streams (eth_header, account, storage, code) with an eight-byte header:

struct monad_snapshot_stream_header {
    uint32_t magic;    // "MSGS"
    uint8_t version;
    uint8_t kind;      // monad_snapshot_type
    uint8_t reserved;  // 0
    uint8_t guard;     // 0xff
};
  • An unrecognised version aborts with a clear message instead of misparsing, so a later revision of any stream can be introduced safely.
  • kind rejects a shard assembled from mismatched files rather than silently misreading it.
  • reserved is written as 0 and readers must accept any value, so a later revision can give it a per-kind meaning without a version bump.

Compatibility

The header is optional, and its absence is what identifies a snapshot predating it. Detection is structural and works for all four streams because the magic satisfies both constraints at once: its low byte 0x4d is below 0xc0, which the RLP list opening an account or eth_header stream cannot be; and the guard puts the eight bytes above 2^56 when read as the leading uint64 of a storage or code stream, where an account offset or a code length never reaches. That same property makes an older binary abort on a newer snapshot rather than misread it.

Account offsets still count from the first account record rather than the stream start, so they mean the same thing whether or not a header is present.

Testing

Both directions were checked end to end against an official mainnet snapshot (block 90045827, 11 GB, 256 shards, written before this header existed), alongside the new SnapshotStreamHeaders and HeaderlessSnapshotRestores unit tests:

  • Existing snapshot, this code. Restored into a slot-encoded target and reproduced exactly the state_root carried in the snapshot's own block header, 0x4aa9630d…f6d3fe11 — canonical mainnet state, not merely a self-consistent result. Restored into a page-encoded target and reproduced the root main produces from the same input.
  • New snapshot, older code. A snapshot dumped by this branch and handed to a main binary aborts in about a second, at db_snapshot.cpp:227 on the first shard's account stream, and leaves the target database untouched — the intended loud rejection instead of a misparse.

🤖 Generated with Claude Code

Copilot AI left a comment

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.

Pull request overview

Adds a minimal, versioned per-stream header to the on-disk DB snapshot format so loaders can reliably detect format revisions and mismatched shard files, while preserving backward compatibility with headerless snapshots already in the wild.

Changes:

  • Introduces monad_snapshot_stream_header (magic/version/kind/guard) and related constants in db_snapshot.h.
  • Updates snapshot dumping to write the header once per non-empty stream, and updates loading to optionally consume/validate the header before parsing records.
  • Extends snapshot tests to (a) assert headers are present/well-formed and (b) exercise the compatibility path by stripping headers and restoring successfully.

Verdict: CORRECT

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
category/execution/ethereum/test/test_db_snapshot.cpp Adds tests for stream headers and for restoring from headerless (legacy) snapshots; adds helpers to rewrite streams.
category/execution/ethereum/db/db_snapshot.h Defines the stream header layout and constants (magic/version/guard) and documents stream framing.
category/execution/ethereum/db/db_snapshot.cpp Writes headers on first write per stream and strips/validates headers on load while keeping offsets relative to the first account record.

🤖 Generated with Claude Code


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread category/execution/ethereum/test/test_db_snapshot.cpp
@maxkozlovsky
maxkozlovsky marked this pull request as draft July 28, 2026 19:36
@maxkozlovsky
maxkozlovsky force-pushed the max/snapshot-stream-versioning branch 2 times, most recently from d21a73f to 7da7e92 Compare July 28, 2026 22:15
@maxkozlovsky
maxkozlovsky marked this pull request as ready for review July 29, 2026 22:20
@maxkozlovsky
maxkozlovsky force-pushed the max/snapshot-stream-versioning branch from 7da7e92 to d2867f4 Compare July 31, 2026 18:49
kkuehlz
kkuehlz previously approved these changes Jul 31, 2026
The snapshot format carried no version marker of any kind: no manifest,
no magic, no version byte. Changing any stream's layout would therefore
have an older loader read the new bytes as whatever they happened to look
like, and the per-file checksums do not help — a well-formed file of an
unexpected layout passes them and then misparses. That the format has so
far never changed is the only reason this has not bitten.

Open each of the four streams with an eight-byte header holding a magic,
a format version, and the monad_snapshot_type the stream contains. An
unrecognised version now aborts with a clear message, and the kind field
means a shard assembled from mismatched files is rejected rather than
misparsed. Every record goes through one writer that opens a stream with
its header on first use, so a write site cannot leave a stream unframed.

Existing snapshots keep loading, because the header's absence is what
identifies them: the magic's low byte is below 0xc0, which the RLP list
opening an account or eth_header stream cannot be, and the guard byte
puts the eight bytes above 2^56, far beyond the account offset or code
length that opens a storage or code stream. Account offsets keep counting
from the first account record rather than the stream start, so they mean
the same thing whether or not a header is present.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maxkozlovsky
maxkozlovsky force-pushed the max/snapshot-stream-versioning branch from 2b56a9c to c9b11fa Compare July 31, 2026 22:17
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.

4 participants