use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageKeySpec {
pub offset: usize,
pub len: usize,
}
pub struct ManifestAnalysis {
pub static_read_slots: Vec<[u8; 32]>,
pub static_write_slots: Vec<[u8; 32]>,
pub has_storage_reads: bool,
pub has_storage_writes: bool,
pub fully_resolved: bool,
}
const MAGIC: &[u8; 4] = b"AXIO";
const LOAD_CONST: u8 = 0x40;
const LOAD_IMM8: u8 = 0x41;
const LOAD_IMM64: u8 = 0x42;
const SLOAD: u8 = 0x50;
const SSTORE: u8 = 0x51;
const SDELETE: u8 = 0x52;
pub struct CellAccount;
impl CellAccount {
pub fn compute_manifest_hash(
bytecode: &[u8],
declared_reads: &[[u8; 32]],
declared_writes: &[[u8; 32]],
commutative_keys: &[[u8; 32]],
oracle_schema_ids: &[[u8; 32]],
) -> [u8; 32] {
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(bytecode);
for slot in declared_reads {
hasher.update(slot);
}
for slot in declared_writes {
hasher.update(slot);
}
for slot in commutative_keys {
hasher.update(slot);
}
for schema_id in oracle_schema_ids {
hasher.update(schema_id);
}
*hasher.finalize().as_bytes()
}
pub fn analyze_bytecode(bytecode: &[u8]) -> Result<ManifestAnalysis, String> {
if bytecode.is_empty() {
return Ok(ManifestAnalysis {
static_read_slots: vec![],
static_write_slots: vec![],
has_storage_reads: false,
has_storage_writes: false,
fully_resolved: true,
});
}
if bytecode.len() < 6 || &bytecode[0..4] != MAGIC {
return Err("Not Axiom bytecode".to_string());
}
let mut pos = 6usize; if pos + 2 > bytecode.len() {
return Err("Truncated bytecode: missing const pool count".to_string());
}
let pool_count = u16::from_le_bytes([bytecode[pos], bytecode[pos + 1]]) as usize;
pos += 2;
let mut const_pool: Vec<Vec<u8>> = Vec::with_capacity(pool_count);
for _ in 0..pool_count {
if pos + 4 > bytecode.len() {
return Err("Truncated const pool".to_string());
}
let entry_len = u32::from_le_bytes([
bytecode[pos],
bytecode[pos + 1],
bytecode[pos + 2],
bytecode[pos + 3],
]) as usize;
pos += 4;
if pos + entry_len > bytecode.len() {
return Err("Truncated const pool entry".to_string());
}
const_pool.push(bytecode[pos..pos + entry_len].to_vec());
pos += entry_len;
}
if pos + 4 > bytecode.len() {
return Err("Truncated bytecode: missing code length".to_string());
}
let code_len = u32::from_le_bytes([
bytecode[pos],
bytecode[pos + 1],
bytecode[pos + 2],
bytecode[pos + 3],
]) as usize;
pos += 4;
if pos + code_len > bytecode.len() {
return Err("Truncated code section".to_string());
}
let code = &bytecode[pos..pos + code_len];
let mut reg_consts: [Option<[u8; 32]>; 256] = [None; 256];
let mut static_read_slots: Vec<[u8; 32]> = Vec::new();
let mut static_write_slots: Vec<[u8; 32]> = Vec::new();
let mut has_reads = false;
let mut has_writes = false;
let mut fully_resolved = true;
let mut pc = 0usize;
while pc < code.len() {
let op = code[pc];
pc += 1;
match op {
LOAD_CONST => {
if pc + 3 > code.len() {
break;
}
let dst = code[pc] as usize;
pc += 1;
let idx = u16::from_le_bytes([code[pc], code[pc + 1]]) as usize;
pc += 2;
if let Some(entry) = const_pool.get(idx) {
if entry.len() == 32 {
let mut key = [0u8; 32];
key.copy_from_slice(entry);
reg_consts[dst] = Some(key);
} else {
reg_consts[dst] = None;
}
} else {
reg_consts[dst] = None;
}
}
LOAD_IMM8 => {
if pc + 2 > code.len() {
break;
}
let dst = code[pc] as usize;
pc += 2;
reg_consts[dst] = None;
}
LOAD_IMM64 => {
if pc + 9 > code.len() {
break;
}
let dst = code[pc] as usize;
pc += 9;
reg_consts[dst] = None;
}
SLOAD => {
if pc + 2 > code.len() {
break;
}
let _dst = code[pc] as usize;
pc += 1;
let key_reg = code[pc] as usize;
pc += 1;
has_reads = true;
match reg_consts[key_reg] {
Some(key) => static_read_slots.push(key),
None => fully_resolved = false,
}
}
SSTORE => {
if pc + 2 > code.len() {
break;
}
let key_reg = code[pc] as usize;
pc += 1;
let _val_reg = code[pc];
pc += 1;
has_writes = true;
match reg_consts[key_reg] {
Some(key) => static_write_slots.push(key),
None => fully_resolved = false,
}
}
SDELETE => {
if pc + 1 > code.len() {
break;
}
let key_reg = code[pc] as usize;
pc += 1;
has_writes = true;
match reg_consts[key_reg] {
Some(key) => static_write_slots.push(key),
None => fully_resolved = false,
}
}
0x01..=0x07 => {
pc += 3;
} 0x10..=0x12 => {
pc += 3;
} 0x13 => {
pc += 2;
} 0x14..=0x15 => {
pc += 3;
} 0x20..=0x25 => {
pc += 3;
} 0x26 => {
pc += 2;
} 0x30 => {
pc += 4;
} 0x31..=0x32 => {
pc += 5;
} 0x33 => {
pc += 4;
} 0x34 => {} 0x35 => {} 0x36 => {
pc += 2;
} 0x43 => {
pc += 2;
} 0x44 => {
pc += 2;
} 0x60..=0x65 => {
pc += 1;
} 0x66 => {
pc += 1;
} 0x67 => {
pc += 2;
} 0x70 => {
pc += 2;
} 0x71 => {
pc += 2;
} 0x72 => {
pc += 2;
} 0x73 => {
pc += 3;
} 0x80 => {
pc += 4;
} 0x90 => {
pc += 2;
} 0x91 => {
pc += 3;
} 0xA0 => {} 0xA1 => {
pc += 1;
} 0xA2..=0xA4 => {
pc += 2;
} 0xA5 => {
pc += 1;
} 0xA6 => {
pc += 8;
} 0xB0 => {
pc += 3;
} 0xB1 => {
pc += 4;
} 0xB2..=0xB3 => {
pc += 3;
} 0xB4..=0xB5 => {
pc += 2;
} 0xC0 => {
pc += 4;
} 0xC1 => {
pc += 2;
} _ => {
break;
} }
}
static_read_slots.sort_unstable();
static_read_slots.dedup();
static_write_slots.sort_unstable();
static_write_slots.dedup();
Ok(ManifestAnalysis {
static_read_slots,
static_write_slots,
has_storage_reads: has_reads,
has_storage_writes: has_writes,
fully_resolved,
})
}
pub fn verify_manifest_against_bytecode(
bytecode: &[u8],
declared_reads: &[[u8; 32]],
declared_writes: &[[u8; 32]],
storage_key_specs: &[StorageKeySpec],
) -> Result<(), String> {
if bytecode.is_empty() {
return Ok(());
}
let analysis = Self::analyze_bytecode(bytecode)?;
if analysis.has_storage_reads && declared_reads.is_empty() && storage_key_specs.is_empty() {
return Err("Bytecode reads storage but declared_reads is empty.".to_string());
}
if analysis.has_storage_writes && declared_writes.is_empty() && storage_key_specs.is_empty()
{
return Err("Bytecode writes storage but declared_writes is empty.".to_string());
}
if analysis.fully_resolved {
let declared_r: std::collections::HashSet<[u8; 32]> =
declared_reads.iter().copied().collect();
for slot in &analysis.static_read_slots {
if !declared_r.contains(slot) {
return Err(format!(
"Bytecode reads slot {} not in declared_reads.",
hex::encode(slot)
));
}
}
let declared_w: std::collections::HashSet<[u8; 32]> =
declared_writes.iter().copied().collect();
for slot in &analysis.static_write_slots {
if !declared_w.contains(slot) {
return Err(format!(
"Bytecode writes slot {} not in declared_writes.",
hex::encode(slot)
));
}
}
}
Ok(())
}
pub fn require_inferable(
_bytecode: &[u8],
storage_key_specs: &[StorageKeySpec],
) -> Result<(), String> {
if !storage_key_specs.is_empty() {
return Ok(());
}
Ok(())
}
}