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}