Skip to main content

truthlinked_core/
cells.rs

1//! Axiom Cell metadata and bytecode manifest helpers.
2//!
3//! The types in this module describe declared storage access and provide a small
4//! static analyzer for Axiom bytecode. Consensus and tooling use this information
5//! to reason about storage keys before execution.
6
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct StorageKeySpec {
11    pub offset: usize,
12    pub len: usize,
13}
14
15/// Static analysis result from scanning Axiom bytecode.
16pub struct ManifestAnalysis {
17    pub static_read_slots: Vec<[u8; 32]>,
18    pub static_write_slots: Vec<[u8; 32]>,
19    pub has_storage_reads: bool,
20    pub has_storage_writes: bool,
21    /// True if every storage access used a statically-known key.
22    pub fully_resolved: bool,
23}
24
25// Axiom opcode tags mirrored from the bytecode encoder.
26const MAGIC: &[u8; 4] = b"AXIO";
27const LOAD_CONST: u8 = 0x40;
28const LOAD_IMM8: u8 = 0x41;
29const LOAD_IMM64: u8 = 0x42;
30const SLOAD: u8 = 0x50;
31const SSTORE: u8 = 0x51;
32const SDELETE: u8 = 0x52;
33
34pub struct CellAccount;
35
36impl CellAccount {
37    pub fn compute_manifest_hash(
38        bytecode: &[u8],
39        declared_reads: &[[u8; 32]],
40        declared_writes: &[[u8; 32]],
41        commutative_keys: &[[u8; 32]],
42        oracle_schema_ids: &[[u8; 32]],
43    ) -> [u8; 32] {
44        use blake3::Hasher;
45        let mut hasher = Hasher::new();
46        hasher.update(bytecode);
47        for slot in declared_reads {
48            hasher.update(slot);
49        }
50        for slot in declared_writes {
51            hasher.update(slot);
52        }
53        for slot in commutative_keys {
54            hasher.update(slot);
55        }
56        for schema_id in oracle_schema_ids {
57            hasher.update(schema_id);
58        }
59        *hasher.finalize().as_bytes()
60    }
61
62    /// Statically analyze Axiom bytecode to extract storage key access patterns.
63    ///
64    /// Strategy: scan the instruction stream. Track the last value loaded into
65    /// each register via LOAD_CONST (32-byte key). When SLOAD/SSTORE/SDELETE
66    /// is encountered, check if the key register has a known static value.
67    /// If the register is known, record the static slot. Otherwise, mark
68    /// the manifest as partially unresolved.
69    pub fn analyze_bytecode(bytecode: &[u8]) -> Result<ManifestAnalysis, String> {
70        if bytecode.is_empty() {
71            return Ok(ManifestAnalysis {
72                static_read_slots: vec![],
73                static_write_slots: vec![],
74                has_storage_reads: false,
75                has_storage_writes: false,
76                fully_resolved: true,
77            });
78        }
79
80        // Validate the bytecode header before decoding the instruction stream.
81        if bytecode.len() < 6 || &bytecode[0..4] != MAGIC {
82            return Err("Not Axiom bytecode".to_string());
83        }
84
85        // Decode the constant pool using the same layout as the Axiom bytecode encoder.
86        let mut pos = 6usize; // skip magic(4) + version(1) + reserved(1)
87        if pos + 2 > bytecode.len() {
88            return Err("Truncated bytecode: missing const pool count".to_string());
89        }
90        let pool_count = u16::from_le_bytes([bytecode[pos], bytecode[pos + 1]]) as usize;
91        pos += 2;
92
93        let mut const_pool: Vec<Vec<u8>> = Vec::with_capacity(pool_count);
94        for _ in 0..pool_count {
95            if pos + 4 > bytecode.len() {
96                return Err("Truncated const pool".to_string());
97            }
98            let entry_len = u32::from_le_bytes([
99                bytecode[pos],
100                bytecode[pos + 1],
101                bytecode[pos + 2],
102                bytecode[pos + 3],
103            ]) as usize;
104            pos += 4;
105            if pos + entry_len > bytecode.len() {
106                return Err("Truncated const pool entry".to_string());
107            }
108            const_pool.push(bytecode[pos..pos + entry_len].to_vec());
109            pos += entry_len;
110        }
111
112        if pos + 4 > bytecode.len() {
113            return Err("Truncated bytecode: missing code length".to_string());
114        }
115        let code_len = u32::from_le_bytes([
116            bytecode[pos],
117            bytecode[pos + 1],
118            bytecode[pos + 2],
119            bytecode[pos + 3],
120        ]) as usize;
121        pos += 4;
122        if pos + code_len > bytecode.len() {
123            return Err("Truncated code section".to_string());
124        }
125        let code = &bytecode[pos..pos + code_len];
126
127        // Scan the instruction stream.
128        // reg_consts[r] = Some(key) if register r was last loaded with a known 32-byte const.
129        let mut reg_consts: [Option<[u8; 32]>; 256] = [None; 256];
130        let mut static_read_slots: Vec<[u8; 32]> = Vec::new();
131        let mut static_write_slots: Vec<[u8; 32]> = Vec::new();
132        let mut has_reads = false;
133        let mut has_writes = false;
134        let mut fully_resolved = true;
135
136        let mut pc = 0usize;
137        while pc < code.len() {
138            let op = code[pc];
139            pc += 1;
140            match op {
141                LOAD_CONST => {
142                    if pc + 3 > code.len() {
143                        break;
144                    }
145                    let dst = code[pc] as usize;
146                    pc += 1;
147                    let idx = u16::from_le_bytes([code[pc], code[pc + 1]]) as usize;
148                    pc += 2;
149                    if let Some(entry) = const_pool.get(idx) {
150                        if entry.len() == 32 {
151                            let mut key = [0u8; 32];
152                            key.copy_from_slice(entry);
153                            reg_consts[dst] = Some(key);
154                        } else {
155                            reg_consts[dst] = None;
156                        }
157                    } else {
158                        reg_consts[dst] = None;
159                    }
160                }
161                LOAD_IMM8 => {
162                    if pc + 2 > code.len() {
163                        break;
164                    }
165                    let dst = code[pc] as usize;
166                    pc += 2;
167                    reg_consts[dst] = None;
168                }
169                LOAD_IMM64 => {
170                    if pc + 9 > code.len() {
171                        break;
172                    }
173                    let dst = code[pc] as usize;
174                    pc += 9;
175                    reg_consts[dst] = None;
176                }
177                SLOAD => {
178                    if pc + 2 > code.len() {
179                        break;
180                    }
181                    let _dst = code[pc] as usize;
182                    pc += 1;
183                    let key_reg = code[pc] as usize;
184                    pc += 1;
185                    has_reads = true;
186                    match reg_consts[key_reg] {
187                        Some(key) => static_read_slots.push(key),
188                        None => fully_resolved = false,
189                    }
190                }
191                SSTORE => {
192                    if pc + 2 > code.len() {
193                        break;
194                    }
195                    let key_reg = code[pc] as usize;
196                    pc += 1;
197                    let _val_reg = code[pc];
198                    pc += 1;
199                    has_writes = true;
200                    match reg_consts[key_reg] {
201                        Some(key) => static_write_slots.push(key),
202                        None => fully_resolved = false,
203                    }
204                }
205                SDELETE => {
206                    if pc + 1 > code.len() {
207                        break;
208                    }
209                    let key_reg = code[pc] as usize;
210                    pc += 1;
211                    has_writes = true;
212                    match reg_consts[key_reg] {
213                        Some(key) => static_write_slots.push(key),
214                        None => fully_resolved = false,
215                    }
216                }
217                // Skip all other opcodes by their known sizes
218                0x01..=0x07 => {
219                    pc += 3;
220                } // ADD/SUB/MUL/DIV/MOD/ADD_SAT/SUB_SAT: op+dst+a+b = 4 bytes, already consumed op
221                0x10..=0x12 => {
222                    pc += 3;
223                } // AND/OR/XOR
224                0x13 => {
225                    pc += 2;
226                } // NOT: op+dst+a
227                0x14..=0x15 => {
228                    pc += 3;
229                } // SHL/SHR: op+dst+a+s
230                0x20..=0x25 => {
231                    pc += 3;
232                } // EQ/NE/LT/LTE/GT/GTE
233                0x26 => {
234                    pc += 2;
235                } // IS_ZERO
236                0x30 => {
237                    pc += 4;
238                } // JUMP: op+u32
239                0x31..=0x32 => {
240                    pc += 5;
241                } // JUMP_IF/JUMP_IF_NOT: op+reg+u32
242                0x33 => {
243                    pc += 4;
244                } // CALL: op+u32
245                0x34 => {} // RETURN: op only
246                0x35 => {} // HALT
247                0x36 => {
248                    pc += 2;
249                } // TRAP: op+u16
250                0x43 => {
251                    pc += 2;
252                } // MOVE: op+dst+src
253                0x44 => {
254                    pc += 2;
255                } // SWAP: op+a+b
256                0x60..=0x65 => {
257                    pc += 1;
258                } // GET_CALLER..GET_VALUE: op+dst
259                0x66 => {
260                    pc += 1;
261                } // GET_CALLDATA_LEN: op+dst
262                0x67 => {
263                    pc += 2;
264                } // GET_CALLDATA: op+dst+off
265                0x70 => {
266                    pc += 2;
267                } // SET_RETURN: op+i+l
268                0x71 => {
269                    pc += 2;
270                } // EMIT_LOG: op+t+d
271                0x72 => {
272                    pc += 2;
273                } // SET_RETURN_REG: op+d+l
274                0x73 => {
275                    pc += 3;
276                } // EMIT_LOG_REG: op+t+d+l
277                0x80 => {
278                    pc += 4;
279                } // CALL_CELL: op+cell+cd+len+val
280                0x90 => {
281                    pc += 2;
282                } // HASH32: op+dst+src
283                0x91 => {
284                    pc += 3;
285                } // HASH32_CONST: op+dst+u16
286                0xA0 => {} // REQUIRE_OWNER
287                0xA1 => {
288                    pc += 1;
289                } // REQUIRE_CALLER: op+reg
290                0xA2..=0xA4 => {
291                    pc += 2;
292                } // REQUIRE_EQ/NE/LT: op+a+b
293                0xA5 => {
294                    pc += 1;
295                } // REQUIRE_NON_ZERO: op+reg
296                0xA6 => {
297                    pc += 8;
298                } // REQUIRE_GAS: op+u64
299                0xB0 => {
300                    pc += 3;
301                } // TOKEN_BALANCE: op+dst+tok+acc
302                0xB1 => {
303                    pc += 4;
304                } // TOKEN_TRANSFER: op+tok+from+to+amt
305                0xB2..=0xB3 => {
306                    pc += 3;
307                } // TOKEN_MINT/BURN: op+tok+rec+amt
308                0xB4..=0xB5 => {
309                    pc += 2;
310                } // TOKEN_FREEZE/THAW: op+tok+acc
311                0xC0 => {
312                    pc += 4;
313                } // ORACLE_REQUEST: op+dst+url+method+body
314                0xC1 => {
315                    pc += 2;
316                } // ORACLE_READ: op+dst+req_id
317                _ => {
318                    break;
319                } // unknown opcode - stop scanning
320            }
321        }
322
323        static_read_slots.sort_unstable();
324        static_read_slots.dedup();
325        static_write_slots.sort_unstable();
326        static_write_slots.dedup();
327
328        Ok(ManifestAnalysis {
329            static_read_slots,
330            static_write_slots,
331            has_storage_reads: has_reads,
332            has_storage_writes: has_writes,
333            fully_resolved,
334        })
335    }
336
337    pub fn verify_manifest_against_bytecode(
338        bytecode: &[u8],
339        declared_reads: &[[u8; 32]],
340        declared_writes: &[[u8; 32]],
341        storage_key_specs: &[StorageKeySpec],
342    ) -> Result<(), String> {
343        if bytecode.is_empty() {
344            return Ok(());
345        }
346
347        let analysis = Self::analyze_bytecode(bytecode)?;
348
349        if analysis.has_storage_reads && declared_reads.is_empty() && storage_key_specs.is_empty() {
350            return Err("Bytecode reads storage but declared_reads is empty.".to_string());
351        }
352        if analysis.has_storage_writes && declared_writes.is_empty() && storage_key_specs.is_empty()
353        {
354            return Err("Bytecode writes storage but declared_writes is empty.".to_string());
355        }
356
357        if analysis.fully_resolved {
358            let declared_r: std::collections::HashSet<[u8; 32]> =
359                declared_reads.iter().copied().collect();
360            for slot in &analysis.static_read_slots {
361                if !declared_r.contains(slot) {
362                    return Err(format!(
363                        "Bytecode reads slot {} not in declared_reads.",
364                        hex::encode(slot)
365                    ));
366                }
367            }
368            let declared_w: std::collections::HashSet<[u8; 32]> =
369                declared_writes.iter().copied().collect();
370            for slot in &analysis.static_write_slots {
371                if !declared_w.contains(slot) {
372                    return Err(format!(
373                        "Bytecode writes slot {} not in declared_writes.",
374                        hex::encode(slot)
375                    ));
376                }
377            }
378        }
379
380        Ok(())
381    }
382
383    pub fn require_inferable(
384        _bytecode: &[u8],
385        storage_key_specs: &[StorageKeySpec],
386    ) -> Result<(), String> {
387        if !storage_key_specs.is_empty() {
388            return Ok(());
389        }
390        // For Axiom cells, static analysis handles key extraction - no specs needed
391        Ok(())
392    }
393}