Skip to main content

truthlinked_sdk/
manifest.rs

1//! Contract manifest generation for parallel execution optimization.
2//!
3//! Manifests declare which storage slots a contract will read/write, enabling
4//! the TruthLinked runtime to execute non-conflicting transactions in parallel.
5//!
6//! # What is a Manifest?
7//!
8//! A manifest is metadata attached to a contract that declares:
9//! - **Read slots**: Storage slots the contract reads from
10//! - **Write slots**: Storage slots the contract writes to
11//! - **Commutative keys**: Slots that support commutative operations (Add, Max, etc.)
12//! - **Key specs**: Dynamic slot extraction rules from calldata
13//!
14//! # Why Manifests?
15//!
16//! Manifests enable **parallel execution**:
17//! - Transactions with disjoint read/write sets execute in parallel
18//! - Commutative operations on the same slot can be batched
19//! - Runtime can detect conflicts before execution
20//!
21//! # Usage
22//!
23//! ## Automatic (Recommended)
24//!
25//! Use the `#[derive(Manifest)]` macro:
26//!
27//! ```ignore
28//! #[derive(Manifest)]
29//! #[manifest(
30//!     read_derived(namespace = "balances", key = "alice"),
31//!     write_derived(namespace = "balances", key = "bob"),
32//!     key_spec(offset = 4, len = 32)  // Extract account from calldata
33//! )]
34//! struct TransferManifest;
35//! ```
36//!
37//! ## Manual (Advanced)
38//!
39//! Use `ManifestBuilder` for programmatic construction:
40//!
41//! ```ignore
42//! use truthlinked_sdk::manifest::ManifestBuilder;
43//!
44//! let manifest = ManifestBuilder::new()
45//!     .read_slot([0x01; 32])
46//!     .write_slot([0x02; 32])
47//!     .commutative_slot([0x03; 32])
48//!     .storage_key_spec(4, 32)
49//!     .build();
50//! ```
51//!
52//! # Manifest Hash
53//!
54//! The manifest hash is computed as:
55//! ```text
56//! BLAKE3(bytecode || sorted(reads) || sorted(writes) || sorted(commutative))
57//! ```
58//!
59//! This hash is stored on-chain and verified at deployment/upgrade.
60
61extern crate alloc;
62
63use alloc::string::String;
64use alloc::vec::Vec;
65use blake3::Hasher;
66
67use crate::codec::Codec32;
68use crate::collections::{StorageBlob, StorageMap, StorageVec};
69
70/// Trait for types that can generate a contract manifest.
71///
72/// Typically implemented via `#[derive(Manifest)]` macro.
73///
74/// # Example
75///
76/// ```ignore
77/// #[derive(Manifest)]
78/// #[manifest(read_slot = "0x01...")]
79/// struct MyManifest;
80///
81/// let manifest = MyManifest::manifest();
82/// ```
83pub trait Manifest {
84    /// Returns the contract manifest.
85    fn manifest() -> ContractManifest;
86}
87
88/// Helper function to get a manifest from a type implementing `Manifest`.
89pub fn manifest_of<M: Manifest>() -> ContractManifest {
90    M::manifest()
91}
92
93/// Specification for extracting storage keys from calldata.
94///
95/// Key specs enable dynamic slot extraction at runtime. For example,
96/// extracting an account ID from calldata to compute a balance slot.
97///
98/// # Example
99///
100/// ```ignore
101/// // Extract 32-byte account ID starting at byte 4 of calldata
102/// let spec = StorageKeySpec { offset: 4, len: 32 };
103/// ```
104#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
105pub struct StorageKeySpec {
106    /// Byte offset in calldata where the key starts.
107    pub offset: usize,
108    /// Length of the key in bytes.
109    pub len: usize,
110}
111
112/// A contract manifest declaring storage access patterns.
113///
114/// Manifests enable parallel execution by declaring which storage slots
115/// a contract will access. The runtime uses this information to:
116/// - Execute non-conflicting transactions in parallel
117/// - Batch commutative operations
118/// - Detect conflicts before execution
119///
120/// # Fields
121///
122/// - `declared_reads`: Slots the contract reads from
123/// - `declared_writes`: Slots the contract writes to
124/// - `commutative_keys`: Slots supporting commutative operations
125/// - `storage_key_specs`: Rules for extracting dynamic keys from calldata
126///
127/// # Example
128///
129/// ```ignore
130/// let mut manifest = ContractManifest::new();
131/// manifest.add_read_slot([0x01; 32]);
132/// manifest.add_write_slot([0x02; 32]);
133/// manifest.normalize(); // Sort and deduplicate
134///
135/// let hash = manifest.manifest_hash(&bytecode);
136/// let json = manifest.to_json_pretty();
137/// ```
138#[derive(Clone, Debug, PartialEq, Eq, Default)]
139pub struct ContractManifest {
140    /// Storage slots the contract reads from.
141    pub declared_reads: Vec<[u8; 32]>,
142    /// Storage slots the contract writes to.
143    pub declared_writes: Vec<[u8; 32]>,
144    /// Storage slots that support commutative operations (Add, Max, Or, Append).
145    pub commutative_keys: Vec<[u8; 32]>,
146    /// Rules for extracting dynamic storage keys from calldata.
147    pub storage_key_specs: Vec<StorageKeySpec>,
148}
149
150impl ContractManifest {
151    /// Creates a new empty manifest.
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Adds a read slot to the manifest.
157    ///
158    /// Duplicates are automatically removed during normalization.
159    pub fn add_read_slot(&mut self, slot: [u8; 32]) -> &mut Self {
160        push_unique_slot(&mut self.declared_reads, slot);
161        self
162    }
163
164    /// Adds a write slot to the manifest.
165    ///
166    /// Duplicates are automatically removed during normalization.
167    pub fn add_write_slot(&mut self, slot: [u8; 32]) -> &mut Self {
168        push_unique_slot(&mut self.declared_writes, slot);
169        self
170    }
171
172    /// Adds a commutative key to the manifest.
173    ///
174    /// Commutative keys support operations like Add, Max, Or, Append
175    /// that can be executed in any order.
176    pub fn add_commutative_key(&mut self, slot: [u8; 32]) -> &mut Self {
177        push_unique_slot(&mut self.commutative_keys, slot);
178        self
179    }
180
181    /// Adds a storage key specification for dynamic slot extraction.
182    ///
183    /// # Arguments
184    ///
185    /// * `spec` - Specification defining offset and length in calldata
186    pub fn add_storage_key_spec(&mut self, spec: StorageKeySpec) -> &mut Self {
187        if !self.storage_key_specs.contains(&spec) {
188            self.storage_key_specs.push(spec);
189        }
190        self
191    }
192
193    /// Sorts and deduplicates all manifest entries.
194    ///
195    /// This ensures canonical representation for hashing and comparison.
196    pub fn normalize(&mut self) {
197        self.declared_reads.sort();
198        self.declared_reads.dedup();
199        self.declared_writes.sort();
200        self.declared_writes.dedup();
201        self.commutative_keys.sort();
202        self.commutative_keys.dedup();
203        self.storage_key_specs.sort();
204        self.storage_key_specs.dedup();
205    }
206
207    /// Returns a normalized copy of this manifest.
208    ///
209    /// Equivalent to cloning and calling `normalize()`.
210    pub fn normalized(mut self) -> Self {
211        self.normalize();
212        self
213    }
214
215    /// Computes the manifest hash for on-chain storage.
216    ///
217    /// The hash is computed as:
218    /// ```text
219    /// BLAKE3(bytecode || sorted(reads) || sorted(writes) || sorted(commutative))
220    /// ```
221    ///
222    /// This hash is stored on-chain and verified at deployment/upgrade.
223    ///
224    /// # Arguments
225    ///
226    /// * `bytecode` - The contract WASM bytecode
227    ///
228    /// # Returns
229    ///
230    /// A 32-byte BLAKE3 hash of the manifest and bytecode.
231    pub fn manifest_hash(&self, bytecode: &[u8]) -> [u8; 32] {
232        let canonical = self.clone().normalized();
233        let mut hasher = Hasher::new();
234        hasher.update(bytecode);
235        for slot in &canonical.declared_reads {
236            hasher.update(slot);
237        }
238        for slot in &canonical.declared_writes {
239            hasher.update(slot);
240        }
241        for slot in &canonical.commutative_keys {
242            hasher.update(slot);
243        }
244        *hasher.finalize().as_bytes()
245    }
246
247    /// Exports the manifest as pretty-printed JSON.
248    ///
249    /// The output format is compatible with the CLI's manifest file format.
250    ///
251    /// # Example Output
252    ///
253    /// ```json
254    /// {
255    ///   "declared_reads": [
256    ///     "0101010101010101010101010101010101010101010101010101010101010101"
257    ///   ],
258    ///   "declared_writes": [
259    ///     "0202020202020202020202020202020202020202020202020202020202020202"
260    ///   ],
261    ///   "commutative_keys": [],
262    ///   "storage_key_specs": [
263    ///     { "offset": 4, "len": 32 }
264    ///   ]
265    /// }
266    /// ```
267    pub fn to_json_pretty(&self) -> String {
268        let canonical = self.clone().normalized();
269        let mut out = String::new();
270        out.push_str("{\n");
271        push_slot_list(&mut out, "declared_reads", &canonical.declared_reads, true);
272        push_slot_list(
273            &mut out,
274            "declared_writes",
275            &canonical.declared_writes,
276            true,
277        );
278        push_slot_list(
279            &mut out,
280            "commutative_keys",
281            &canonical.commutative_keys,
282            true,
283        );
284        push_specs_list(&mut out, &canonical.storage_key_specs);
285        out.push_str("}\n");
286        out
287    }
288}
289
290/// Builder for programmatically constructing contract manifests.
291///
292/// `ManifestBuilder` provides a fluent API for building manifests with
293/// helper methods for common patterns (maps, vecs, blobs).
294///
295/// # Example
296///
297/// ```ignore
298/// use truthlinked_sdk::manifest::ManifestBuilder;
299/// use truthlinked_sdk::collections::{Namespace, StorageMap};
300///
301/// const NS_BALANCES: Namespace = Namespace([1u8; 32]);
302/// let balances = StorageMap::<u64>::new(NS_BALANCES);
303///
304/// let manifest = ManifestBuilder::new()
305///     .read_map_get(&balances, b"alice")
306///     .write_map_set(&balances, b"bob")
307///     .storage_key_spec(4, 32)
308///     .build();
309/// ```
310#[derive(Clone, Debug, Default)]
311pub struct ManifestBuilder {
312    manifest: ContractManifest,
313}
314
315impl ManifestBuilder {
316    /// Creates a new empty manifest builder.
317    pub fn new() -> Self {
318        Self::default()
319    }
320
321    /// Adds a read slot (fluent API).
322    pub fn read_slot(mut self, slot: [u8; 32]) -> Self {
323        self.manifest.add_read_slot(slot);
324        self
325    }
326
327    /// Adds a write slot (fluent API).
328    pub fn write_slot(mut self, slot: [u8; 32]) -> Self {
329        self.manifest.add_write_slot(slot);
330        self
331    }
332
333    /// Adds a commutative slot (fluent API).
334    pub fn commutative_slot(mut self, slot: [u8; 32]) -> Self {
335        self.manifest.add_commutative_key(slot);
336        self
337    }
338
339    /// Adds a storage key specification (fluent API).
340    pub fn storage_key_spec(mut self, offset: usize, len: usize) -> Self {
341        self.manifest
342            .add_storage_key_spec(StorageKeySpec { offset, len });
343        self
344    }
345
346    /// Declares reading a map's existence check slot.
347    pub fn read_map_contains<V: Codec32>(mut self, map: &StorageMap<V>, key: &[u8]) -> Self {
348        self.manifest.add_read_slot(map.exists_slot_for(key));
349        self
350    }
351
352    /// Declares reading a map entry (both existence and value slots).
353    pub fn read_map_get<V: Codec32>(mut self, map: &StorageMap<V>, key: &[u8]) -> Self {
354        let (exists, value) = map.slots_for_key(key);
355        self.manifest.add_read_slot(exists).add_read_slot(value);
356        self
357    }
358
359    /// Declares writing a map entry (both existence and value slots).
360    pub fn write_map_set<V: Codec32>(mut self, map: &StorageMap<V>, key: &[u8]) -> Self {
361        let (exists, value) = map.slots_for_key(key);
362        self.manifest.add_write_slot(exists).add_write_slot(value);
363        self
364    }
365
366    /// Declares removing a map entry (both existence and value slots).
367    pub fn write_map_remove<V: Codec32>(mut self, map: &StorageMap<V>, key: &[u8]) -> Self {
368        let (exists, value) = map.slots_for_key(key);
369        self.manifest.add_write_slot(exists).add_write_slot(value);
370        self
371    }
372
373    /// Declares reading a vector element (length + element slots).
374    pub fn read_vec_index<V: Codec32>(mut self, list: &StorageVec<V>, index: u64) -> Self {
375        self.manifest
376            .add_read_slot(list.len_slot())
377            .add_read_slot(list.slot_for_index(index));
378        self
379    }
380
381    /// Declares writing a vector element (reads length, writes element).
382    pub fn write_vec_index<V: Codec32>(mut self, list: &StorageVec<V>, index: u64) -> Self {
383        self.manifest
384            .add_read_slot(list.len_slot())
385            .add_write_slot(list.slot_for_index(index));
386        self
387    }
388
389    /// Declares writing the vector length.
390    pub fn write_vec_len<V: Codec32>(mut self, list: &StorageVec<V>) -> Self {
391        self.manifest.add_write_slot(list.len_slot());
392        self
393    }
394
395    /// Declares reading a blob chunk (length + chunk slots).
396    pub fn read_blob_chunk(mut self, blob: &StorageBlob, chunk_index: u64) -> Self {
397        self.manifest
398            .add_read_slot(blob.len_slot())
399            .add_read_slot(blob.slot_for_chunk(chunk_index));
400        self
401    }
402
403    /// Declares writing a blob chunk (length + chunk slots).
404    pub fn write_blob_chunk(mut self, blob: &StorageBlob, chunk_index: u64) -> Self {
405        self.manifest
406            .add_write_slot(blob.len_slot())
407            .add_write_slot(blob.slot_for_chunk(chunk_index));
408        self
409    }
410
411    /// Builds the final manifest, normalizing all entries.
412    ///
413    /// This consumes the builder and returns a normalized `ContractManifest`.
414    pub fn build(mut self) -> ContractManifest {
415        self.manifest.normalize();
416        self.manifest
417    }
418}
419
420/// Helper function to add a slot to a vector if not already present.
421fn push_unique_slot(slots: &mut Vec<[u8; 32]>, slot: [u8; 32]) {
422    if !slots.contains(&slot) {
423        slots.push(slot);
424    }
425}
426
427fn push_slot_list(out: &mut String, name: &str, slots: &[[u8; 32]], trailing_comma: bool) {
428    out.push_str("  \"");
429    out.push_str(name);
430    out.push_str("\": [\n");
431    for (idx, slot) in slots.iter().enumerate() {
432        out.push_str("    \"");
433        out.push_str(&hex32(slot));
434        out.push('"');
435        if idx + 1 != slots.len() {
436            out.push(',');
437        }
438        out.push('\n');
439    }
440    out.push_str("  ]");
441    if trailing_comma {
442        out.push(',');
443    }
444    out.push('\n');
445}
446
447fn push_specs_list(out: &mut String, specs: &[StorageKeySpec]) {
448    out.push_str("  \"storage_key_specs\": [\n");
449    for (idx, spec) in specs.iter().enumerate() {
450        out.push_str("    { \"offset\": ");
451        push_usize(out, spec.offset);
452        out.push_str(", \"len\": ");
453        push_usize(out, spec.len);
454        out.push_str(" }");
455        if idx + 1 != specs.len() {
456            out.push(',');
457        }
458        out.push('\n');
459    }
460    out.push_str("  ]\n");
461}
462
463fn push_usize(out: &mut String, value: usize) {
464    // no_std-safe decimal conversion
465    let mut buf = [0u8; 20];
466    let mut n = value;
467    let mut cursor = buf.len();
468    if n == 0 {
469        out.push('0');
470        return;
471    }
472    while n > 0 {
473        cursor -= 1;
474        buf[cursor] = b'0' + (n % 10) as u8;
475        n /= 10;
476    }
477    for b in &buf[cursor..] {
478        out.push(*b as char);
479    }
480}
481
482fn hex32(bytes: &[u8; 32]) -> String {
483    const HEX: &[u8; 16] = b"0123456789abcdef";
484    let mut out = String::with_capacity(64);
485    for b in bytes {
486        out.push(HEX[(b >> 4) as usize] as char);
487        out.push(HEX[(b & 0x0f) as usize] as char);
488    }
489    out
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495    use crate::collections::Namespace;
496
497    #[test]
498    fn manifest_builder_dedups_and_hashes() {
499        let map = StorageMap::<u64>::new(Namespace([1u8; 32]));
500        let manifest = ManifestBuilder::new()
501            .read_map_get(&map, b"alice")
502            .read_map_get(&map, b"alice")
503            .write_map_set(&map, b"alice")
504            .storage_key_spec(4, 32)
505            .storage_key_spec(4, 32)
506            .build();
507
508        assert_eq!(manifest.storage_key_specs.len(), 1);
509        assert_eq!(manifest.declared_reads.len(), 2);
510        assert_eq!(manifest.declared_writes.len(), 2);
511
512        let hash_a = manifest.manifest_hash(&[1, 2, 3]);
513        let hash_b = manifest.clone().normalized().manifest_hash(&[1, 2, 3]);
514        assert_eq!(hash_a, hash_b);
515    }
516
517    #[test]
518    fn manifest_json_contains_required_keys() {
519        let manifest = ContractManifest::new();
520        let json = manifest.to_json_pretty();
521        assert!(json.contains("\"declared_reads\""));
522        assert!(json.contains("\"declared_writes\""));
523        assert!(json.contains("\"commutative_keys\""));
524        assert!(json.contains("\"storage_key_specs\""));
525    }
526
527    #[derive(crate::Manifest)]
528    #[manifest(
529        read_derived(namespace = "tests.counter", key = "value"),
530        write_derived(namespace = "tests.counter", key = "value"),
531        key_spec(offset = 4, len = 32)
532    )]
533    struct CounterManifestSpec;
534
535    #[test]
536    fn derive_manifest_builds_contract_manifest() {
537        let manifest = <CounterManifestSpec as Manifest>::manifest();
538        assert_eq!(manifest.declared_reads.len(), 1);
539        assert_eq!(manifest.declared_writes.len(), 1);
540        assert_eq!(manifest.storage_key_specs.len(), 1);
541    }
542
543    const MANUAL_SLOT: [u8; 32] = [0xAA; 32];
544
545    #[derive(crate::Manifest)]
546    #[manifest(
547        read_map(namespace = "tests.map", key = "alice"),
548        write_map(namespace = "tests.map", key = "alice"),
549        read_vec_index(namespace = "tests.vec", index = 3),
550        write_vec_index(namespace = "tests.vec", index = 5),
551        read_blob_chunk(namespace = "tests.blob", chunk = 0),
552        write_blob_chunk(namespace = "tests.blob", chunk = 2),
553        read_slot_expr = "crate::manifest::tests::MANUAL_SLOT",
554        commutative_label = "tests.delta",
555        key_spec(offset = 8, len = 24)
556    )]
557    struct RichManifestSpec;
558
559    #[test]
560    fn derive_manifest_supports_map_vec_blob_helpers() {
561        let manifest = <RichManifestSpec as Manifest>::manifest();
562        assert_eq!(manifest.declared_reads.len(), 7);
563        assert_eq!(manifest.declared_writes.len(), 5);
564        assert_eq!(manifest.commutative_keys.len(), 1);
565        assert_eq!(manifest.storage_key_specs.len(), 1);
566    }
567}