Skip to main content

thruput_sdk/storage/
keypath.rs

1use crate::hasher::Hasher;
2use crate::types::StorageKey;
3use xdrs::{WriteXDR, cxs::ConstWriter};
4
5/// Path identifying a storage slot.
6///
7/// A keypath is a sequence of length-framed components absorbed into one of
8/// the VM's incremental hash streams. Each call to [`child`](Self::child)
9/// appends `CXS(byte_len) ‖ CXS(value)`: the component value CXS-encoded,
10/// prefixed by the CXS-encoded byte length of that encoding. The storage key
11/// is the SHA-256 of the concatenated frames; leaf storage cells derive it
12/// once at construction and hold it as a [`LeafKey`].
13///
14/// The keypath IS its midstate: it holds the VM stream that has absorbed
15/// exactly its frames. [`child`](Self::child) forks the parent's stream — a
16/// constant-time `SHA256Copy`, which is what that syscall exists for — and
17/// absorbs only its own frame, so a shared parent prefix is absorbed once
18/// ever; [`generate_key`](Self::generate_key) forks once more and finalizes.
19/// No preimage bytes are kept guest-side.
20///
21/// The self-delimiting `CXS(len)` prefix makes the preimage
22/// `child(v1) ‖ child(v2) ‖ …` uniquely decodable: read the length, then read
23/// exactly that many value bytes. This holds for *any* value encoding —
24/// including fixed opaque `[u8; N]` keys that do not self-delimit — so
25/// distinct component sequences can never share a preimage.
26///
27/// Like a [`Hasher`], a keypath is meaningful only in the current contract
28/// invocation, so it can't be stored. Live keypaths, user hashers, and one
29/// transient per key derivation share the VM's live-stream budget (the
30/// chain's `max_sha256_buffers` network config parameter); the VM refuses
31/// streams past it and the refusing
32/// syscall's error is thrown. A stream absorbs at most
33/// `kSHA256BufferMaxSize` (4096) preimage bytes, also VM-enforced. A single
34/// component's encoding is capped at the SDK's fixed encode-buffer size.
35#[derive(Debug, Default)]
36pub struct Keypath {
37    hasher: Hasher,
38    exclusive: bool,
39}
40
41impl Clone for Keypath {
42    fn clone(&self) -> Self {
43        Self {
44            hasher: self.hasher.fork(),
45            exclusive: self.exclusive,
46        }
47    }
48}
49
50impl Keypath {
51    /// Root keypath for a contract state tree, seeded with the state type's
52    /// compile-time-framed storage domain. The seed bytes are exactly what
53    /// [`child`](Self::child) would append for the domain string, so the
54    /// domain remains "the root's first child frame". Called by
55    /// [`contract_state_root`](crate::storage::contract_state_root); not
56    /// intended for direct use. For a bare, undomained keypath (tests,
57    /// hand-rolled raw storage) use `Keypath::default()`.
58    #[doc(hidden)]
59    pub fn root(domain: FramedDomain) -> Self {
60        let mut kp = Self::default();
61        kp.hasher.update(&domain.as_bytes());
62        kp
63    }
64
65    /// Child keyed by an encodable value.
66    ///
67    /// Appends `CXS(byte_len) ‖ CXS(value)`, where `CXS(value)` is the value's
68    /// CXS encoding and `byte_len` is its byte length, also CXS-encoded (the
69    /// same compact codec, so small lengths cost a single byte). The
70    /// self-delimiting length prefix keeps the preimage uniquely decodable for
71    /// any value bytes.
72    ///
73    /// A single value's encoding is capped at the SDK's fixed encode-buffer
74    /// size; oversized values throw `LibraryExn::StorageSerialization`.
75    ///
76    /// Slot identity is the sequence of `child` values from the root: there is
77    /// no per-component "structural vs. user-key" tag, so two children of the
78    /// *same* node built from the same value address the same slot regardless
79    /// of intent. SDK-generated code never mixes schemes under one node (a
80    /// struct keys children by field position, a `Map` by user key, a `Vector`
81    /// by region index then element index one level down), so distinct slots
82    /// stay distinct. A hand-written `From<Keypath>` must uphold the same rule:
83    /// give each child of a node a distinct `value`.
84    pub fn child<I: WriteXDR + ?Sized>(&self, value: &I) -> Keypath {
85        let mut child = self.clone();
86        // Encode the component into the reused scratch in place (no fresh
87        // stack buffer), then absorb the frame into the forked stream.
88        crate::call::with_scratch(|buf| {
89            crate::internal::encode_scratch(buf, value).unwrap_or_else(|_| {
90                crate::exn::library_throw(
91                    crate::exn::LibraryExn::StorageSerialization,
92                )
93            });
94            let bytes: &[u8] = buf.as_slice();
95            // CXS-encode the byte length through the SAME compact codec the
96            // value uses (small lengths cost a single byte). Encode it into a
97            // separate stack buffer so we don't disturb `buf`, which still
98            // holds the value bytes we're about to append. A `u32` CXS encoding
99            // is at most 5 bytes.
100            let len: u32 = bytes.len().try_into().unwrap_or_else(|_| {
101                crate::exn::library_throw(
102                    crate::exn::LibraryExn::StorageKeypathOverflow,
103                )
104            });
105            let mut len_buf = [core::mem::MaybeUninit::<u8>::uninit(); 5];
106            let written = xdrs::cxs::encode_into(&len, &mut len_buf)
107                .unwrap_or_else(|_| {
108                    crate::exn::library_throw(
109                        crate::exn::LibraryExn::StorageSerialization,
110                    )
111                });
112            // Safety: `encode_into` initialized exactly `written` bytes.
113            let len_bytes: &[u8] =
114                unsafe { len_buf[..written].assume_init_ref() };
115            child.hasher.update(&len_bytes);
116            child.hasher.update(&bytes);
117        });
118        child
119    }
120
121    /// Child keyed by a component pre-framed at compile time.
122    ///
123    /// Appends `frame`'s bytes verbatim — byte-identical to what
124    /// [`child`](Self::child) would append for the same component value,
125    /// with no runtime encoding and no scratch-buffer checkout. Called by
126    /// `derive(Storage)`-generated field derivation and fixed-slot SDK
127    /// containers; not intended for direct use.
128    #[doc(hidden)]
129    pub fn child_framed(&self, frame: &'static FramedComponent) -> Keypath {
130        let mut child = self.clone();
131        child.hasher.update(&frame.as_bytes());
132        child
133    }
134
135    pub fn generate_key(&self) -> StorageKey {
136        self.hasher.fork().finish().into()
137    }
138
139    /// Consume the keypath and return its storage key, finalizing its own
140    /// stream directly — no transient fork.
141    pub fn into_key(self) -> StorageKey {
142        self.hasher.finish().into()
143    }
144
145    /// Whether `self` and `other` address the same storage slot.
146    ///
147    /// Slot identity is the keypath preimage; SHA-256 is collision
148    /// resistant, so equality of the derived keys decides it. The
149    /// `exclusive` access-mode flag is not part of slot identity.
150    pub fn addresses_same_slot(&self, other: &Keypath) -> bool {
151        self.generate_key() == other.generate_key()
152    }
153
154    pub fn is_exclusive(&self) -> bool {
155        self.exclusive
156    }
157
158    pub fn set_exclusive(&mut self) {
159        self.exclusive = true;
160    }
161}
162
163/// Compile-time buffer bound for domain framing (and the domain-length
164/// assert in [`framed_domain_len`]): far above any sane domain name, far
165/// below the VM's 4096-byte stream input cap.
166const DOMAIN_FRAME_MAX: usize = 256;
167
168/// Byte length of [`frame_domain`]'s output for `domain`.
169///
170/// Const mirror of the framing [`Keypath::child`] applies to a `&str`
171/// component: `CXS(byte_len) ‖ CXS(domain)`. Kept honest by the
172/// `const_frame_matches_runtime_child` test and by [`frame_domain`]'s
173/// exact-fill assertion.
174pub const fn framed_domain_len(domain: &str) -> usize {
175    // 10 = two CXS(u32) length prefixes at their 5-byte maximum. Framed
176    // domains share the keypath preimage budget; a domain anywhere near
177    // this cap is a bug, so fail const-eval with a readable message.
178    assert!(
179        domain.len() + 10 <= DOMAIN_FRAME_MAX,
180        "storage domain too long; shorten it with #[storage_domain = \"...\"]"
181    );
182    let mut inner = ConstWriter::<DOMAIN_FRAME_MAX>::new();
183    inner.write_opaque_variable(domain.as_bytes());
184    let inner_len = inner.pos();
185    let mut outer = ConstWriter::<DOMAIN_FRAME_MAX>::new();
186    outer.write_u32(inner_len as u32);
187    outer.pos() + inner_len
188}
189
190/// Compile-time framing of a storage-domain string: the exact bytes
191/// [`Keypath::child`] would append for `domain` at runtime.
192///
193/// `N` must equal [`framed_domain_len`]`(domain)`; `into_bytes` asserts the
194/// exact fill at const-eval time, so any mismatch is a compile error rather
195/// than a wrong storage key.
196pub const fn frame_domain<const N: usize>(domain: &str) -> [u8; N] {
197    let mut probe = ConstWriter::<DOMAIN_FRAME_MAX>::new();
198    probe.write_opaque_variable(domain.as_bytes());
199    let inner_len = probe.pos();
200    let mut w = ConstWriter::<N>::new();
201    w.write_u32(inner_len as u32);
202    w.write_opaque_variable(domain.as_bytes());
203    w.into_bytes()
204}
205
206/// Capacity of a [`FramedComponent`]: `CXS(byte_len) ‖ CXS(v)` for a `u32`
207/// is at most 1 (length prefix — a CXS `u32` is at most 5 bytes, so its
208/// length always encodes in one byte) + 5 (value) bytes.
209const MAX_FRAMED_U32: usize = 6;
210
211/// A `u32` keypath component pre-framed at compile time: the exact bytes
212/// [`Keypath::child`] would append for the same value, built through the
213/// same CXS codec.
214///
215/// The only constructor is [`from_u32`](Self::from_u32), which does the
216/// framing itself — malformed or aliased frames are unrepresentable, so
217/// [`Keypath::child_framed`] cannot be handed raw bytes.
218#[derive(Clone, Copy, Debug)]
219pub struct FramedComponent {
220    /// Frame bytes at `buf[..len]`; the tail is zero padding.
221    buf: [u8; MAX_FRAMED_U32],
222    len: u8,
223}
224
225impl FramedComponent {
226    /// Frame `v` exactly as [`Keypath::child`]`(&v)` would at runtime:
227    /// `CXS(byte_len) ‖ CXS(v)`. Kept honest by the
228    /// `const_u32_frame_matches_runtime_child` test. Called by
229    /// `derive(Storage)`-generated code and fixed-slot SDK containers; not
230    /// intended (but safe) for direct use.
231    #[doc(hidden)]
232    pub const fn from_u32(v: u32) -> Self {
233        let mut probe = ConstWriter::<5>::new();
234        probe.write_u32(v);
235        let inner_len = probe.pos();
236        let mut w = ConstWriter::<MAX_FRAMED_U32>::new();
237        w.write_u32(inner_len as u32);
238        w.write_u32(v);
239        let len = w.pos() as u8;
240        // Pad to capacity so `into_bytes`' exact-fill assertion holds;
241        // `len` marks the real frame end.
242        while w.pos() < MAX_FRAMED_U32 {
243            w.write_opaque_fixed(&[0]);
244        }
245        Self {
246            buf: w.into_bytes(),
247            len,
248        }
249    }
250
251    fn as_bytes(&self) -> &[u8] {
252        &self.buf[..self.len as usize]
253    }
254}
255
256/// A storage domain pre-framed at compile time (see [`frame_domain`]).
257///
258/// Newtype so a domain-seeded [`Keypath`] root cannot be handed raw,
259/// unframed bytes. Constructed only by `derive(Storage)`-generated code
260/// (via [`frame_domain`]); not intended for direct use. The domain's name
261/// string is not carried — host collision diagnostics identify colliding
262/// roots by type path.
263#[derive(Clone, Copy, Debug)]
264pub struct FramedDomain(&'static [u8]);
265
266impl FramedDomain {
267    #[doc(hidden)]
268    pub const fn new(framed: &'static [u8]) -> Self {
269        Self(framed)
270    }
271
272    pub(crate) const fn as_bytes(&self) -> &'static [u8] {
273        self.0
274    }
275}
276
277#[cfg(test)]
278mod test_keypath {
279    use super::*;
280
281    /// Key oracle: sha2 over an expected framed preimage, computed
282    /// natively with no syscall. With no guest-side preimage bytes to
283    /// inspect, drift guards compare stream-derived keys against this.
284    fn oracle_key(preimage: &[u8]) -> StorageKey {
285        use sha2::Digest;
286        let digest: [u8; 32] = sha2::Sha256::digest(preimage).into();
287        crate::SHA256Hash::new(digest).into()
288    }
289
290    crate::contract_test!(exclusive_flag_propagates_through_clone {
291        let mut kp = Keypath::default().child("some bytes");
292        kp.set_exclusive();
293        assert!(kp.clone().is_exclusive());
294    });
295
296    crate::contract_test!(distinct_paths_distinct_keys {
297        let root = Keypath::default();
298        // boundary-shift case: [AA BB]+[CC] vs [AA]+[BB CC].
299        // Fixed opaque [u8; N] keys do NOT self-delimit, so without the
300        // CXS(len) prefix these two sequences would share the preimage
301        // AA BB CC. The length prefix keeps them distinct.
302        let a = root.child(&[0xAAu8, 0xBB]).child(&[0xCCu8]);
303        let b = root.child(&[0xAAu8]).child(&[0xBBu8, 0xCC]);
304        assert!(!a.addresses_same_slot(&b));
305        // Concretely: child(&[AA,BB]) = [02 AA BB], child(&[CC]) = [01 CC];
306        // child(&[AA]) = [01 AA], child(&[BB,CC]) = [02 BB CC].
307        assert_eq!(
308            a.generate_key(),
309            oracle_key(&[0x02, 0xAA, 0xBB, 0x01, 0xCC])
310        );
311        assert_eq!(
312            b.generate_key(),
313            oracle_key(&[0x01, 0xAA, 0x02, 0xBB, 0xCC])
314        );
315    });
316
317    crate::contract_test!(clone_copies_state_and_diverges {
318        // Differential pin, the deleted buffer tests' role: a clone copies
319        // the absorbed state, the original and clone then extend
320        // independently, and each key still equals the sha2 of its exact
321        // framed preimage — so a divergence on either side shows up.
322        let base = Keypath::default().child(&[1u8, 2, 3]);
323        let a = base.child(&[0xAAu8, 0xBB]);
324        let b = base.child(&[0xCCu8]);
325        assert_eq!(
326            a.generate_key(),
327            oracle_key(&[0x03, 1, 2, 3, 0x02, 0xAA, 0xBB])
328        );
329        assert_eq!(
330            b.generate_key(),
331            oracle_key(&[0x03, 1, 2, 3, 0x01, 0xCC])
332        );
333        // The shared parent is untouched by its children's extensions.
334        assert_eq!(base.generate_key(), oracle_key(&[0x03, 1, 2, 3]));
335    });
336
337    crate::contract_test!(addresses_same_slot_matches_preimage_equality {
338        let root = Keypath::default();
339        let a = root.child(&3u32).child(&42u64);
340        let same = root.child(&3u32).child(&42u64);
341        let different = root.child(&3u32).child(&43u64);
342        assert!(a.addresses_same_slot(&same));
343        assert!(!a.addresses_same_slot(&different));
344    });
345
346    crate::contract_test!(addresses_same_slot_ignores_exclusive_flag {
347        // The exclusive flag is an access mode, not part of slot identity:
348        // two otherwise-identical keypaths still address the same slot.
349        let base = Keypath::default().child(&7u32);
350        let mut exclusive = base.clone();
351        exclusive.set_exclusive();
352        assert!(exclusive.is_exclusive());
353        assert!(!base.is_exclusive());
354        assert!(base.addresses_same_slot(&exclusive));
355        assert!(exclusive.addresses_same_slot(&base));
356    });
357
358    crate::contract_test!(derivation_is_deterministic {
359        let k1 = Keypath::default().child(&3u32).child(&42u64);
360        let k2 = Keypath::default().child(&3u32).child(&42u64);
361        assert_eq!(k1.generate_key(), k2.generate_key());
362    });
363
364    crate::contract_test!(golden_key_vector {
365        // Preimage: child(&1u32) ‖ child(&0u32), each = CXS(len) ‖ CXS(value).
366        // CXS encodes small uints in 1 byte, so 1u32 -> [01], 0u32 -> [00],
367        // each with byte-length 1 -> CXS(1) = [01]. Locks the on-chain key:
368        // sha256([01 01 01 00]).
369        let kp = Keypath::default().child(&1u32).child(&0u32);
370        let expected: StorageKey = crate::SHA256Hash::new(hex_literal::hex!(
371            "f896c3a5f9841b6e1f0a22bd35a6a1bc5efb28aaa23b66301ec8098ce57cf99a"
372        ))
373        .into();
374        assert_eq!(kp.generate_key(), expected, "golden key changed");
375    });
376
377    crate::contract_test!(domain_frame_is_distinct_and_length_framed {
378        let root = Keypath::default();
379        // Two roots with different domains diverge.
380        let a = root.child("Token").child(&0u32);
381        let b = root.child("Vault").child(&0u32);
382        assert!(!a.addresses_same_slot(&b));
383        // The domain is just the root's first `child` — no special framing.
384        // CXS("AB") = [02 41 42] (opaque-variable: CXS(len=2) ‖ "AB"); the
385        // outer child wraps it with CXS(len=3) = [03]. Golden sha256 of the
386        // domain preimage locks the on-chain layout.
387        let dom = root.child("AB");
388        assert_eq!(dom.generate_key(), oracle_key(&[0x03, 0x02, b'A', b'B']));
389        let expected: StorageKey = crate::SHA256Hash::new(hex_literal::hex!(
390            "801d3bfb551c298e3b7a2dfef59a178ce0c8d3f41fec0f3d8c308035c678e516"
391        ))
392        .into();
393        assert_eq!(dom.generate_key(), expected, "golden domain key changed");
394    });
395
396    crate::contract_test!(const_u32_frame_matches_runtime_child {
397        // Load-bearing drift guard, same contract as
398        // const_frame_matches_runtime_child: const u32 framing must hash to
399        // the key the runtime CXS codec derives, or const-derived field
400        // keys diverge from runtime-derived ones.
401        macro_rules! check {
402            ($v:expr) => {{
403                const FRAMED: FramedComponent = FramedComponent::from_u32($v);
404                let runtime = Keypath::default().child(&$v);
405                assert_eq!(
406                    runtime.generate_key(),
407                    oracle_key(FRAMED.as_bytes()),
408                    "const u32 framing drifted from runtime child() for {}",
409                    $v
410                );
411            }};
412        }
413        // Every CXS u32 width boundary: 1-byte header (0..=62), 2-byte
414        // (63..=255), 3-byte (256..=65535), 5-byte (65536..).
415        check!(0u32);
416        check!(1u32);
417        check!(62u32);
418        check!(63u32);
419        check!(255u32);
420        check!(256u32);
421        check!(65535u32);
422        check!(65536u32);
423        check!(u32::MAX);
424
425        // Golden bytes: child(&1u32) appends CXS(len=1) ‖ CXS(1) = [01 01]
426        // (the same layout golden_key_vector locks for the runtime path).
427        const F1: FramedComponent = FramedComponent::from_u32(1u32);
428        assert_eq!(F1.as_bytes(), &[0x01, 0x01]);
429    });
430
431    crate::contract_test!(child_framed_matches_child {
432        const FRAME: FramedComponent = FramedComponent::from_u32(7u32);
433        // A non-empty parent (domain + one level) so the append lands on a
434        // realistic preimage.
435        let parent = Keypath::default().child("Domain").child(&3u32);
436        let via_const = parent.child_framed(&FRAME);
437        let via_runtime = parent.child(&7u32);
438        assert!(via_const.addresses_same_slot(&via_runtime));
439        // It is a child, not the parent.
440        assert!(!via_const.addresses_same_slot(&parent));
441    });
442
443    crate::contract_test!(child_framed_preserves_exclusive_flag {
444        const FRAME: FramedComponent = FramedComponent::from_u32(0u32);
445        let mut parent = Keypath::default().child("Domain");
446        parent.set_exclusive();
447        // child_framed clones the parent, so access mode propagates exactly
448        // like child() (which also clones).
449        assert!(parent.child_framed(&FRAME).is_exclusive());
450    });
451
452    crate::contract_test!(const_frame_matches_runtime_child {
453        // THE load-bearing test: const framing must hash to the key the
454        // runtime CXS codec derives, or const-derived storage keys diverge
455        // from runtime-derived ones.
456        macro_rules! check {
457            ($domain:literal) => {{
458                const N: usize = framed_domain_len($domain);
459                const FRAMED: [u8; N] = frame_domain::<N>($domain);
460                let runtime = Keypath::default().child($domain);
461                assert_eq!(
462                    runtime.generate_key(),
463                    oracle_key(&FRAMED),
464                    "const framing drifted from runtime child() for {:?}",
465                    $domain
466                );
467            }};
468        }
469        check!("A");
470        check!("AB");
471        check!("ERC20State");
472        check!("a_rather_long_storage_domain_name_for_framing");
473        // > 62 bytes: both CXS length prefixes take the 2-byte header form.
474        check!(
475            "this_domain_is_deliberately_longer_than_sixty_three_bytes_to_exercise_the_two_byte_cxs_length_header"
476        );
477        check!("日本語ドメイン");
478
479        // Golden bytes for "AB" (same vector as
480        // domain_frame_is_distinct_and_length_framed).
481        const N_AB: usize = framed_domain_len("AB");
482        const AB: [u8; N_AB] = frame_domain::<N_AB>("AB");
483        assert_eq!(&AB[..], &[0x03, 0x02, b'A', b'B']);
484    });
485}
486
487pub use leaf_key::*;
488mod leaf_key {
489    use super::Keypath;
490    use crate::types::StorageKey;
491    use core::ops::Deref;
492
493    /// The derived storage key a leaf storage cell holds, plus its access
494    /// mode. Construction derives the key immediately and releases the
495    /// keypath's VM hash stream, so leaves hold no stream — only containers
496    /// that keep deriving child slots (maps, vectors) retain a [`Keypath`].
497    #[derive(Debug)]
498    pub struct LeafKey {
499        key: StorageKey,
500        exclusive: bool,
501    }
502
503    impl From<Keypath> for LeafKey {
504        fn from(keypath: Keypath) -> Self {
505            let exclusive = keypath.is_exclusive();
506            LeafKey {
507                key: keypath.into_key(),
508                exclusive,
509            }
510        }
511    }
512
513    impl LeafKey {
514        pub fn get(&self) -> &StorageKey {
515            &self.key
516        }
517
518        pub fn is_exclusive(&self) -> bool {
519            self.exclusive
520        }
521    }
522
523    impl Deref for LeafKey {
524        type Target = StorageKey;
525
526        fn deref(&self) -> &Self::Target {
527            &self.key
528        }
529    }
530}