This document defines the storage versioning, schema compatibility, and migration policy for bitcoin-rs.
This policy applies to all persistent storage surfaces in bitcoin-rs:
- Key-Value database backends (
crates/storage/src/trait_.rs,crates/storage/src/column_families.rs). - Append-only flat block files (
crates/storage/src/block_file.rs). - Binary UTXO snapshot artifacts (
crates/utxo/src/snapshot.rs). - Chainstate checkpoint directories and metadata (
crates/node/src/checkpoint.rs).
bitcoin-rs splits persistent data across four distinct storage mechanisms:
| Storage Surface | Implementation File | On-Disk Location | Version Indicator |
|---|---|---|---|
| Key-Value Storage | crates/storage/src/column_families.rs |
Datadir root (fjall, rocksdb, mdbx, redb) |
None (See Gap) |
| Flat Block Files | crates/storage/src/block_file.rs |
blocks/blkNNNNN.dat |
Magic BRSB (0x42525342), no version field |
| UTXO Snapshot | crates/utxo/src/snapshot.rs |
utxo-v4.dat inside checkpoint directory |
Magic 0x5554584F, header version 4 |
| Chainstate Checkpoint | crates/node/src/checkpoint.rs |
chainstate-checkpoints/CURRENT & gen-N/ |
Manifest 1, Headers 1, UTXO 4, CoinStats 1 |
KvStore (crates/storage/src/trait_.rs) abstracts backend storage over twelve fixed column families defined in ColumnFamily (crates/storage/src/column_families.rs):
TxConfirmed(0)TxMempool(1)BlockHeaders(2)Funding(3)Spending(4)Filters(5)FilterHeaders(6)Coinstats(7)BlockTree(8)UtxoMeta(9)BlockBodies(10)UndoData(11)
Storage backends (FjallStore in fjall_impl.rs, RocksDbStore in rocksdb_impl.rs, MdbxStore in mdbx_impl.rs, and RedbStore in redb_impl.rs) create or open these twelve tables on startup using ColumnFamily::ALL.
UndoData holds the per-block records a reorg needs to disconnect a block, keyed by bitcoin_rs_pruning::block_undo_key (height and block hash). It is canonical persistent state, not a cache: without a block's undo record the node cannot disconnect that block, so any schema or resync decision that discards it costs the ability to reorganise below the point it was discarded.
IDENTIFIED GAP: The
KvStoreinterface and database backends store no schema version metadata in database headers or dedicated version rows. Database engines open existing keyspaces or tables by string name (ColumnFamily::name()) without checking schema compatibility.
FlatFileBlockStore (crates/storage/src/block_file.rs) writes raw block bodies to blocks/blkNNNNN.dat. Each record begins with a 4-byte magic (BLOCK_FILE_MAGIC = *b"BRSB"), followed by record length and metadata. Individual files cap at 128 MiB (BLOCK_FILE_MAX_BYTES).
IDENTIFIED GAP: Flat block files carry a record magic header, but lack a format version field.
crates/utxo/src/snapshot.rs serializes and deserializes the UTXO set.
SnapshotHeaderstoresmagic(0x55_54_58_4f),version(4),tip_hash(32 bytes),height(u32), andrecord_count(u64).- Writer function
write_snapshotemits version4(SNAPSHOT_WRITE_VERSION). - Reader function
read_snapshotdecodes legacy versions2,3, and4. - Reader function
read_snapshot_strict_v4decodes version4exclusively, enforcing strict end-of-file validation and requiring a 384-byteMuHash3072trailer (MUHASH_TRAILER_LEN).
crates/node/src/checkpoint.rs persists full chainstate checkpoints under chainstate-checkpoints/.
CURRENTfile containsCurrentV1JSON referencing an active generation directory (e.g.gen-0000000000000001/).manifest-v1.json(CheckpointManifestV1) records component versions and codec identifier strings:headers: version1, codec"bitcoin-rs-canonical-headers".utxo: version4, codec"bitcoin-rs-utxo-spendable-v1".coinstats: version1, codec"bitcoin-rs-coinstats".
A database or artifact change is breaking when an existing binary cannot parse data written by a different version.
The following modifications break key-value store compatibility:
- Adding, removing, or reordering variants in
ColumnFamily. - Changing the string name returned by
ColumnFamily::name(). - Altering the binary serialization layout of keys or values within any column family.
- Changing key prefixes or integer endianness.
The following modifications break UTXO snapshot compatibility:
- Changing fields or layout in
SnapshotHeader,SnapshotRecordHeaderV4, orSnapshotVoutHeader. - Changing the varint encoding or output serialization order.
- Modifying
MUHASH_TRAILER_LEN(384 bytes) or trailer calculation rules.
When changing the binary layout of the UTXO snapshot:
- Increment
SNAPSHOT_WRITE_VERSIONincrates/utxo/src/snapshot.rs. - Increment
UTXO_VERSIONincrates/node/src/checkpoint.rs. - Update
UTXO_CODECincrates/node/src/checkpoint.rsto reflect the new layout. - Define a new record header struct in
crates/utxo/src/snapshot.rs(e.g.,SnapshotRecordHeaderV5). - Add a reading function for the new version in
read_snapshot_with_policy_observed. - Retain older read decoders in
read_snapshotonly if backward read support is explicitly required by project maintainers; otherwise, remove legacy decoders to enforce a clean cutover.
load_checkpoint_from_dir in crates/node/src/checkpoint.rs inspects on-disk checkpoint metadata during node startup. Startup outcomes follow strict precedence:
| Condition | Internal Function Return | Node Action |
|---|---|---|
Mismatched or unsupported headers or CoinStats version |
Err(IncompatibleCheckpoint::UnsupportedVersion) |
Fatal Abort. Node stops immediately with an explicit error. |
Mismatched utxo.version or unexpected utxo/coinstats codec |
Ok(CheckpointLoad::HeadersOnly) |
Fallback Resync. Node retains validated BlockTree headers, drops UTXO payload, and resynchronizes chainstate. |
Missing CURRENT file, invalid JSON, or missing generation directory |
Ok(CheckpointLoad::Cold) |
Cold Start. Node starts initial block download from genesis (height 0). |
| Valid manifest, headers, UTXO snapshot, and CoinStats | Ok(CheckpointLoad::Complete) |
Normal Startup. Node loads full chainstate and resumes operation. |
For raw Key-Value database stores (fjall, rocksdb, mdbx, redb):
- Because KV stores lack on-disk schema headers, opening a datadir with incompatible KV key/value layouts causes runtime read failures or database panics.
bitcoin-rs does not support in-place database migrations.
The codebase contains zero schema transformation scripts, database version tables, or in-place record converters.
Undo records in UndoData carry their own format version as the first byte
(crates/utxo/src/undo_codec.rs), and each record is bound to the height AND
block hash of the block it undoes, so a record from an abandoned branch cannot
be replayed against a different block at the same height.
The codec has no reader for a version other than the current one, by design: a record it cannot decode is a record the node refuses to disconnect against, which is the safe direction. So a change to the encoding is a breaking schema change under section 6.2 and needs the same treatment as a column-family change. It is not covered by the UTXO snapshot version rules, which govern a different format.
Undo records are also the one persistent structure a resync cannot rebuild lazily: they exist to disconnect blocks already applied. Discarding them costs the ability to reorganise below the discard point until those blocks are re-applied.
When a pull request alters key-value column family key/value formats, column family enum definitions, or storage engine layouts:
- Do not write in-place conversion code or compatibility adapters.
- Update the component version or codec string in
crates/node/src/checkpoint.rswhere the manifest covers the component. It covers headers, the UTXO snapshot, and CoinStats. It does NOT cover the key-value column families, the flat block files, or the undo codec, and there is no schema metadata in the KV stores either (see the gap noted in section 2.1). For those, a bump records intent but detects nothing: the node will open the old keyspace by name and read it as if current. Step 3 is the actual safeguard, not step 2. - Require users to wipe the local datadir and resync from genesis or load a fresh UTXO snapshot.
- Allow the node's native
HeadersOnlyorColdfallback mechanisms to rebuild incompatible state cleanly.