truthlinked_core/
cells.rs1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct StorageKeySpec {
11 pub offset: usize,
12 pub len: usize,
13}
14
15pub 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 pub fully_resolved: bool,
23}
24
25const 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 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 if bytecode.len() < 6 || &bytecode[0..4] != MAGIC {
82 return Err("Not Axiom bytecode".to_string());
83 }
84
85 let mut pos = 6usize; 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 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 0x01..=0x07 => {
219 pc += 3;
220 } 0x10..=0x12 => {
222 pc += 3;
223 } 0x13 => {
225 pc += 2;
226 } 0x14..=0x15 => {
228 pc += 3;
229 } 0x20..=0x25 => {
231 pc += 3;
232 } 0x26 => {
234 pc += 2;
235 } 0x30 => {
237 pc += 4;
238 } 0x31..=0x32 => {
240 pc += 5;
241 } 0x33 => {
243 pc += 4;
244 } 0x34 => {} 0x35 => {} 0x36 => {
248 pc += 2;
249 } 0x43 => {
251 pc += 2;
252 } 0x44 => {
254 pc += 2;
255 } 0x60..=0x65 => {
257 pc += 1;
258 } 0x66 => {
260 pc += 1;
261 } 0x67 => {
263 pc += 2;
264 } 0x70 => {
266 pc += 2;
267 } 0x71 => {
269 pc += 2;
270 } 0x72 => {
272 pc += 2;
273 } 0x73 => {
275 pc += 3;
276 } 0x80 => {
278 pc += 4;
279 } 0x90 => {
281 pc += 2;
282 } 0x91 => {
284 pc += 3;
285 } 0xA0 => {} 0xA1 => {
288 pc += 1;
289 } 0xA2..=0xA4 => {
291 pc += 2;
292 } 0xA5 => {
294 pc += 1;
295 } 0xA6 => {
297 pc += 8;
298 } 0xB0 => {
300 pc += 3;
301 } 0xB1 => {
303 pc += 4;
304 } 0xB2..=0xB3 => {
306 pc += 3;
307 } 0xB4..=0xB5 => {
309 pc += 2;
310 } 0xC0 => {
312 pc += 4;
313 } 0xC1 => {
315 pc += 2;
316 } _ => {
318 break;
319 } }
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 Ok(())
392 }
393}