Skip to main content

bal_archive/
keys.rs

1//! Key layouts. The byte layout IS the schema version: change it here, bump
2//! `SCHEMA_VERSION`, and never write a migration in a hurry.
3//!
4//! ```text
5//! SLOTS     addr(20) || slot(32) || block(8, BE) || index(4, BE)  ->  tag(1) || value (minimal big-endian, 0..=32 bytes)
6//! BLOCKIDX  addr(20) || block(8, BE)                              ->  slot(32) || slot(32) || ...  (every slot written in that block)
7//! BOOT      addr(20) || slot(32)                                  ->  state(1) || first_seen(8, BE)
8//! ```
9//!
10//! Big-endian block/index make lexicographic order equal numeric order, which
11//! is what makes "seek to (addr, slot, block, MAX) and step back" a single
12//! ordered-KV operation.
13
14use alloy_primitives::{Address, B256};
15use bal_codec::BlockAccessIndex;
16
17/// Key/value layout version written into `meta:schema_version`. Bump when
18/// any byte layout in this module changes.
19pub const SCHEMA_VERSION: u32 = 3;
20
21/// Oldest on-disk version this build upgrades in place.
22/// - v1 -> v2: tables v2 adds (`created`) and states it writes (`Done` without
23///   a proof record, settled by creation); v1 data is valid v2 data.
24/// - v2 -> v3: the block index is regrouped (one entry per address and block
25///   instead of one per slot) and values are stored minimal; v2 values still
26///   decode, the index is rewritten on open.
27pub const OLDEST_UPGRADABLE: u32 = 1;
28
29pub const SLOT_KEY_LEN: usize = 20 + 32 + 8 + 4;
30pub const SLOT_PREFIX_LEN: usize = 20 + 32;
31pub const BLOCKIDX_KEY_LEN: usize = 20 + 8;
32/// v1/v2 block-index key: addr || block || slot, one per slot.
33pub const LEGACY_BLOCKIDX_KEY_LEN: usize = 20 + 8 + 32;
34
35/// Where a stored value came from. Stored as the first byte of every value so
36/// provenance can never drift from the data.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38#[repr(u8)]
39pub enum Provenance {
40    /// From a BAL verified against `header.block_access_list_hash`.
41    Bal = 0,
42    /// From `eth_getProof` verified against `header.state_root`.
43    Proof = 1,
44    /// Imported, unverified. Only ever written by an explicit import command.
45    Imported = 2,
46    /// From a BAL that could not be checked because the header carried no
47    /// hash. Only written with `allow_unverified`; never mistaken for `Bal`.
48    Unverified = 3,
49}
50
51impl Provenance {
52    /// Inverse of `self as u8`; `None` for tags this build does not know.
53    pub fn from_byte(b: u8) -> Option<Self> {
54        match b {
55            0 => Some(Self::Bal),
56            1 => Some(Self::Proof),
57            2 => Some(Self::Imported),
58            3 => Some(Self::Unverified),
59            _ => None,
60        }
61    }
62
63    /// `true` for values anchored to a block header (BAL hash or state root).
64    pub fn is_verified(self) -> bool {
65        matches!(self, Self::Bal | Self::Proof)
66    }
67}
68
69pub(crate) fn slot_key(
70    addr: Address,
71    slot: B256,
72    block: u64,
73    index: BlockAccessIndex,
74) -> [u8; SLOT_KEY_LEN] {
75    let mut k = [0u8; SLOT_KEY_LEN];
76    k[..20].copy_from_slice(addr.as_slice());
77    k[20..52].copy_from_slice(slot.as_slice());
78    k[52..60].copy_from_slice(&block.to_be_bytes());
79    k[60..64].copy_from_slice(&index.to_be_bytes());
80    k
81}
82
83pub fn slot_prefix(addr: Address, slot: B256) -> [u8; SLOT_PREFIX_LEN] {
84    let mut k = [0u8; SLOT_PREFIX_LEN];
85    k[..20].copy_from_slice(addr.as_slice());
86    k[20..].copy_from_slice(slot.as_slice());
87    k
88}
89
90pub fn parse_slot_key(k: &[u8]) -> Option<(Address, B256, u64, BlockAccessIndex)> {
91    if k.len() != SLOT_KEY_LEN {
92        return None;
93    }
94    Some((
95        Address::from_slice(&k[..20]),
96        B256::from_slice(&k[20..52]),
97        u64::from_be_bytes(k[52..60].try_into().ok()?),
98        u32::from_be_bytes(k[60..64].try_into().ok()?),
99    ))
100}
101
102pub fn blockidx_key(addr: Address, block: u64) -> [u8; BLOCKIDX_KEY_LEN] {
103    let mut k = [0u8; BLOCKIDX_KEY_LEN];
104    k[..20].copy_from_slice(addr.as_slice());
105    k[20..].copy_from_slice(&block.to_be_bytes());
106    k
107}
108
109pub fn parse_blockidx_key(k: &[u8]) -> Option<(Address, u64)> {
110    if k.len() != BLOCKIDX_KEY_LEN {
111        return None;
112    }
113    Some((
114        Address::from_slice(&k[..20]),
115        u64::from_be_bytes(k[20..28].try_into().ok()?),
116    ))
117}
118
119/// v1/v2 key -> (addr, block, slot), for the on-open migration.
120pub fn parse_legacy_blockidx_key(k: &[u8]) -> Option<(Address, u64, B256)> {
121    if k.len() != LEGACY_BLOCKIDX_KEY_LEN {
122        return None;
123    }
124    Some((
125        Address::from_slice(&k[..20]),
126        u64::from_be_bytes(k[20..28].try_into().ok()?),
127        B256::from_slice(&k[28..]),
128    ))
129}
130
131/// The block index value: the written slots, concatenated.
132pub fn encode_slots(slots: &[B256]) -> Vec<u8> {
133    let mut out = Vec::with_capacity(slots.len() * 32);
134    for s in slots {
135        out.extend_from_slice(s.as_slice());
136    }
137    out
138}
139
140pub fn decode_slots(v: &[u8]) -> Option<Vec<B256>> {
141    if !v.len().is_multiple_of(32) {
142        return None;
143    }
144    Some(v.as_chunks::<32>().0.iter().map(B256::from).collect())
145}
146
147/// tag || value with leading zero bytes stripped: a counter costs 1 byte,
148/// an address 20, a full hash 32. Zero is the tag alone.
149pub fn encode_value(p: Provenance, v: B256) -> Vec<u8> {
150    let first = v.iter().position(|b| *b != 0).unwrap_or(32);
151    let mut out = Vec::with_capacity(1 + 32 - first);
152    out.push(p as u8);
153    out.extend_from_slice(&v[first..]);
154    out
155}
156
157/// Accepts both the minimal form and the v1/v2 fixed 33-byte form (a 32-byte
158/// minimal value is byte-identical to it).
159pub fn decode_value(v: &[u8]) -> Option<(Provenance, B256)> {
160    if v.is_empty() || v.len() > 33 {
161        return None;
162    }
163    let mut word = [0u8; 32];
164    let body = &v[1..];
165    word[32 - body.len()..].copy_from_slice(body);
166    Some((Provenance::from_byte(v[0])?, B256::from(word)))
167}
168
169/// Bootstrap bookkeeping per (addr, slot).
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub enum BootState {
172    /// Pre-value obtained and stored (with `Provenance::Proof`).
173    Done,
174    /// First seen at `first_seen`; proof at `first_seen - 1` not yet obtained.
175    Pending {
176        /// Block of the first recorded change.
177        first_seen: u64,
178    },
179    /// `first_seen - 1` fell out of the node's state window before a proof was
180    /// obtained. Terminal: reads in `[start, first_seen)` are unavailable.
181    Lost {
182        /// Block of the first recorded change.
183        first_seen: u64,
184    },
185}
186
187pub fn encode_boot(s: BootState) -> [u8; 9] {
188    let mut out = [0u8; 9];
189    let (tag, n) = match s {
190        BootState::Done => (0u8, 0u64),
191        BootState::Pending { first_seen } => (1, first_seen),
192        BootState::Lost { first_seen } => (2, first_seen),
193    };
194    out[0] = tag;
195    out[1..].copy_from_slice(&n.to_be_bytes());
196    out
197}
198
199pub fn decode_boot(v: &[u8]) -> Option<BootState> {
200    if v.len() != 9 {
201        return None;
202    }
203    let n = u64::from_be_bytes(v[1..].try_into().ok()?);
204    match v[0] {
205        0 => Some(BootState::Done),
206        1 => Some(BootState::Pending { first_seen: n }),
207        2 => Some(BootState::Lost { first_seen: n }),
208        _ => None,
209    }
210}
211
212/// Exclusive upper bound for a prefix range: prefix + 1 in big-endian.
213/// Returns `None` if the prefix is all 0xFF (then the range is unbounded).
214pub fn prefix_end(prefix: &[u8]) -> Option<Vec<u8>> {
215    let mut end = prefix.to_vec();
216    for i in (0..end.len()).rev() {
217        if end[i] != 0xFF {
218            end[i] += 1;
219            end.truncate(i + 1);
220            return Some(end);
221        }
222    }
223    None
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn keys_order_numerically() {
232        let a = Address::repeat_byte(1);
233        let s = B256::repeat_byte(2);
234        assert!(slot_key(a, s, 255, 0) < slot_key(a, s, 256, 0));
235        assert!(slot_key(a, s, 256, 1) < slot_key(a, s, 256, 2));
236        assert!(slot_key(a, s, 256, u32::MAX) < slot_key(a, s, 257, 0));
237        assert_eq!(parse_slot_key(&slot_key(a, s, 7, 9)), Some((a, s, 7, 9)));
238    }
239
240    #[test]
241    fn prefix_end_increments() {
242        assert_eq!(prefix_end(&[1, 2, 3]), Some(vec![1, 2, 4]));
243        assert_eq!(prefix_end(&[1, 0xFF]), Some(vec![2]));
244        assert_eq!(prefix_end(&[0xFF, 0xFF]), None);
245    }
246
247    #[test]
248    fn value_roundtrip() {
249        let v = B256::repeat_byte(9);
250        assert_eq!(
251            decode_value(&encode_value(Provenance::Proof, v)),
252            Some((Provenance::Proof, v))
253        );
254        // Minimal encoding: small numbers are short, zero is the tag alone,
255        // and the old fixed form still decodes.
256        let one = B256::from(alloy_primitives::U256::from(1u8).to_be_bytes::<32>());
257        assert_eq!(encode_value(Provenance::Bal, one), vec![0, 1]);
258        assert_eq!(encode_value(Provenance::Bal, B256::ZERO), vec![0]);
259        assert_eq!(decode_value(&[0, 1]), Some((Provenance::Bal, one)));
260        let mut fixed = vec![1u8];
261        fixed.extend_from_slice(v.as_slice());
262        assert_eq!(decode_value(&fixed), Some((Provenance::Proof, v)));
263        assert_eq!(decode_slots(&encode_slots(&[v, one])), Some(vec![v, one]));
264        assert_eq!(decode_slots(&[1, 2, 3]), None);
265        assert_eq!(
266            decode_boot(&encode_boot(BootState::Pending { first_seen: 5 })),
267            Some(BootState::Pending { first_seen: 5 })
268        );
269    }
270}