dig_blockstore/encoding.rs
1//! Key encoding helpers for RocksDB keys (hash, height, epoch, metadata).
2//!
3//! **Normative**
4//! - [`KEY-001`](../docs/requirements/domains/key_encoding/specs/KEY-001_hash_keys.md) — hash keys
5//! - [`KEY-002`](../docs/requirements/domains/key_encoding/specs/KEY-002_height_keys.md) — height keys
6//! - [`KEY-003`](../docs/requirements/domains/key_encoding/specs/KEY-003_epoch_keys.md) — epoch keys
7//! - [`KEY-004`](../docs/requirements/domains/key_encoding/specs/KEY-004_metadata_keys.md) — metadata UTF-8 keys
8//! - Crate-root re-exports: [`STR-003`](../docs/requirements/domains/crate_structure/specs/STR-003.md)
9//!
10//! **Rationale:** Big-endian `u64` keys ([`height_key`], [`epoch_key`]) preserve lexicographic order
11//! equal to numeric order, enabling efficient range scans. Hash keys are raw [`Bytes32`] with no prefix
12//! ([`chia_protocol::Bytes32`]).
13
14use chia_protocol::Bytes32;
15
16/// Raw 32-byte RocksDB key for `CF_BLOCKS`, `CF_HEADERS`, and `CF_ATTESTED` ([`KEY-001`](../docs/requirements/domains/key_encoding/specs/KEY-001_hash_keys.md)).
17///
18/// **Contract:** The returned array is **exactly** the [`Bytes32`] octets — no length prefix,
19/// type tag, or endian reinterpretation. This matches NORMATIVE `key = block_hash.as_ref() → [u8; 32]`.
20///
21/// **Usage:** Pass `hash_key(h).as_slice()` to RocksDB APIs expecting `&[u8]` ([`crate::store::BlockStore`]).
22///
23/// **Zero-copy:** The slice borrows the same 32 bytes held inside `Bytes32` (fixed-size wire type from Chia / DIG stack).
24#[must_use]
25pub fn hash_key(hash: &Bytes32) -> &[u8; 32] {
26 hash.as_ref()
27 .try_into()
28 .expect("Bytes32 must be exactly 32 bytes (KEY-001)")
29}
30
31/// Encode a chain height as an 8-byte **big-endian** key for [`crate::constants::CF_CANONICAL`] ([`KEY-002`](../docs/requirements/domains/key_encoding/specs/KEY-002_height_keys.md)).
32///
33/// **Sort invariant:** For `a < b`, `height_key(a).as_slice() < height_key(b).as_slice()` in bytewise order, so
34/// RocksDB’s default comparator iterates heights in ascending numeric order (required for range scans and reorg walks).
35///
36/// **Decode:** Use [`decode_height_key`] after reads (symmetric to [`decode_epoch_key`] for checkpoints).
37///
38/// **Fixed width:** Always exactly 8 bytes — no VLQ or length prefix.
39#[must_use]
40pub fn height_key(height: u64) -> [u8; 8] {
41 height.to_be_bytes()
42}
43
44/// Decode a height key produced by [`height_key`] ([`KEY-002`](../docs/requirements/domains/key_encoding/specs/KEY-002_height_keys.md)).
45#[must_use]
46pub fn decode_height_key(key: &[u8; 8]) -> u64 {
47 u64::from_be_bytes(*key)
48}
49
50/// Encode an epoch number as an 8-byte **big-endian** key for [`crate::constants::CF_CHECKPOINTS`]
51/// ([`KEY-003`](../docs/requirements/domains/key_encoding/specs/KEY-003_epoch_keys.md),
52/// [`NORMATIVE` §KEY-003](../docs/requirements/domains/key_encoding/NORMATIVE.md#key-003-epoch-keys-8-bytes-big-endian)).
53///
54/// **Wire shape:** Identical octets to [`height_key`] for the same `u64` ([`KEY-002`](../docs/requirements/domains/key_encoding/specs/KEY-002_height_keys.md));
55/// the separate name documents call-site intent (checkpoint epochs vs canonical heights) and leaves room for future newtypes.
56///
57/// **Sort invariant:** For `a < b`, `epoch_key(a).as_slice() < epoch_key(b).as_slice()` in bytewise order, matching RocksDB’s
58/// default comparator — required for epoch-range scans (e.g. future [`CKP-004`](../docs/requirements/domains/checkpoint_storage/specs/CKP-004_get_checkpoints_in_range.md)).
59///
60/// **Decode:** Use [`decode_epoch_key`] after reads (symmetric to [`decode_height_key`] for canonical heights).
61///
62/// **Fixed width:** Always exactly 8 bytes — no VLQ or length prefix.
63#[must_use]
64pub fn epoch_key(epoch: u64) -> [u8; 8] {
65 epoch.to_be_bytes()
66}
67
68/// Decode an epoch key produced by [`epoch_key`] ([`KEY-003`](../docs/requirements/domains/key_encoding/specs/KEY-003_epoch_keys.md)).
69///
70/// **Contract:** Input MUST be exactly the 8 bytes returned by [`epoch_key`] for some `u64`; this is the inverse of
71/// `u64::to_be_bytes` / `u64::from_be_bytes` and matches [`decode_height_key`]’s numeric interpretation.
72#[must_use]
73pub fn decode_epoch_key(key: &[u8; 8]) -> u64 {
74 u64::from_be_bytes(*key)
75}
76
77/// UTF-8 bytes for a metadata key name in [`crate::constants::CF_METADATA`]
78/// ([`KEY-004`](../docs/requirements/domains/key_encoding/specs/KEY-004_metadata_keys.md),
79/// [`NORMATIVE` §KEY-004](../docs/requirements/domains/key_encoding/NORMATIVE.md#key-004-metadata-keys-variable-utf-8)).
80///
81/// **Contract:** Returns `name.as_bytes()` — the exact UTF-8 encoding of `name`. No length prefix, no type tag,
82/// no NUL terminator. Key length equals the UTF-8 byte length (variable; unlike fixed-width hash/height/epoch keys).
83///
84/// **Well-known keys:** Prefer [`crate::constants::META_TIP`], [`crate::constants::META_GENESIS_HASH`],
85/// [`crate::constants::META_MIN_HEIGHT`], [`crate::constants::META_SCHEMA_VERSION`],
86/// [`crate::constants::META_ZSTD_DICT`] at call sites so metadata names stay centralized ([`TYP-002`](../docs/requirements/domains/storage_types/specs/TYP-002.md)).
87///
88/// **Usage:** Pass `metadata_key(name)` (or `META_*.as_bytes()`) to RocksDB `get_cf` / `put_cf` for `CF_METADATA`
89/// ([`crate::store::BlockStore`]). Human-readable ASCII names aid `ldb` inspection per KEY-004 implementation notes.
90///
91/// **Sort order:** Unlike canonical height keys, metadata rows are looked up by **exact key**; lexicographic order is
92/// not part of the storage contract for this family.
93#[must_use]
94pub fn metadata_key(name: &str) -> &[u8] {
95 name.as_bytes()
96}