use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use hanzo_engine::{self as engine, EngineError};
use hanzo_pqc::signature::MlDsa;
pub const ADDR_PQ_VERIFY: [u8; 20] = addr(0x01, 0x01);
pub const ADDR_QUASAR_QUERY: [u8; 20] = addr(0x01, 0x02);
pub const ADDR_AI_INFERENCE: [u8; 20] = addr(0x02, 0x01);
pub const ADDR_AI_EMBEDDING: [u8; 20] = addr(0x02, 0x02);
pub const LUX_QUASAR_ADDR: &str = "0x0300000000000000000000000000000000000020";
const fn addr(category: u8, index: u8) -> [u8; 20] {
let mut a = [0u8; 20];
a[17] = category;
a[19] = index;
a
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PrecompileResult {
Success {
output: Vec<u8>,
gas_used: u64,
},
Revert {
reason: String,
},
Error {
message: String,
},
}
#[derive(Clone)]
pub struct PrecompileEntry {
pub address: [u8; 20],
pub name: String,
pub base_gas: u64,
pub execute: fn(input: &[u8]) -> PrecompileResult,
}
impl std::fmt::Debug for PrecompileEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PrecompileEntry")
.field("address", &hex::encode(self.address))
.field("name", &self.name)
.field("base_gas", &self.base_gas)
.finish()
}
}
#[derive(Debug)]
pub struct PrecompileRegistry {
entries: HashMap<[u8; 20], PrecompileEntry>,
}
impl PrecompileRegistry {
pub fn new() -> Self {
Self {
entries: HashMap::new(),
}
}
pub fn register(&mut self, entry: PrecompileEntry) {
self.entries.insert(entry.address, entry);
}
pub fn get(&self, address: &[u8; 20]) -> Option<&PrecompileEntry> {
self.entries.get(address)
}
pub fn call(&self, address: &[u8; 20], input: &[u8]) -> Option<PrecompileResult> {
self.entries
.get(address)
.map(|entry| (entry.execute)(input))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for PrecompileRegistry {
fn default() -> Self {
let mut r = Self::new();
r.register(PrecompileEntry {
address: ADDR_PQ_VERIFY,
name: "pq_verify".into(),
base_gas: 3_000,
execute: exec_pq_verify,
});
r.register(PrecompileEntry {
address: ADDR_QUASAR_QUERY,
name: "quasar_query".into(),
base_gas: 3_000,
execute: exec_quasar_query,
});
r.register(PrecompileEntry {
address: ADDR_AI_INFERENCE,
name: "ai_inference".into(),
base_gas: 100_000,
execute: exec_ai_inference,
});
r.register(PrecompileEntry {
address: ADDR_AI_EMBEDDING,
name: "ai_embedding".into(),
base_gas: 50_000,
execute: exec_ai_embedding,
});
r
}
}
fn exec_pq_verify(input: &[u8]) -> PrecompileResult {
if input.len() < 10 {
return PrecompileResult::Revert {
reason: "input too short for pq_verify".into(),
};
}
let pk_len = u32::from_be_bytes([input[0], input[1], input[2], input[3]]) as usize;
if input.len() < 4 + pk_len + 4 {
return PrecompileResult::Revert {
reason: "input truncated at public key".into(),
};
}
let pk_bytes = &input[4..4 + pk_len];
let sig_offset = 4 + pk_len;
let sig_len = u32::from_be_bytes([
input[sig_offset],
input[sig_offset + 1],
input[sig_offset + 2],
input[sig_offset + 3],
]) as usize;
let msg_offset = sig_offset + 4 + sig_len;
if input.len() < msg_offset {
return PrecompileResult::Revert {
reason: "input truncated at signature".into(),
};
}
let sig_bytes = &input[sig_offset + 4..msg_offset];
let msg_bytes = &input[msg_offset..];
let gas_used = 3_000u64.saturating_add((pk_len as u64 + sig_len as u64) / 16);
let valid = match MlDsa::verify_raw(pk_bytes, msg_bytes, sig_bytes) {
Ok(b) => b,
Err(err) => {
return PrecompileResult::Revert {
reason: format!("ml-dsa verify error: {err}"),
};
}
};
let mut output = vec![0u8; 32];
if valid {
output[31] = 1;
}
PrecompileResult::Success { output, gas_used }
}
fn exec_quasar_query(input: &[u8]) -> PrecompileResult {
const REQUIRED_LEN: usize = 20 + 32 + 32 + 1;
if input.len() < REQUIRED_LEN {
return PrecompileResult::Revert {
reason: format!(
"quasar_query requires {} bytes (addr ++ commitment ++ proof ++ flag)",
REQUIRED_LEN
),
};
}
let validator = &input[0..20];
let commitment = &input[20..52];
let proof = &input[52..84];
let threshold_met_byte = input[84];
let mut witness = Vec::with_capacity(65);
witness.extend_from_slice(commitment);
witness.extend_from_slice(proof);
witness.push(threshold_met_byte);
let res = match lux_precompile::run(LUX_QUASAR_ADDR, &witness, 1_000_000) {
Ok(r) => r,
Err(err) => {
return PrecompileResult::Revert {
reason: format!("luxprecompile dispatch failed: {err}"),
};
}
};
let is_member = match res.output.as_slice() {
[b] => *b != 0,
_ => {
return PrecompileResult::Revert {
reason: format!(
"unexpected quasar output length: {} bytes",
res.output.len()
),
};
}
};
let mut output = Vec::with_capacity(20 + 32 + 1);
output.extend_from_slice(validator);
output.extend_from_slice(commitment); output.push(if is_member { 0x01 } else { 0x00 });
let gas_used = 1_000_000u64
.saturating_sub(res.remaining_gas)
.saturating_add(1_000);
PrecompileResult::Success { output, gas_used }
}
fn exec_ai_inference(input: &[u8]) -> PrecompileResult {
const HEADER_LEN: usize = 4 + 32;
if input.len() < HEADER_LEN {
return PrecompileResult::Revert {
reason: format!(
"ai_inference requires at least {} bytes (selector + model id)",
HEADER_LEN
),
};
}
let mut model_id = [0u8; 32];
model_id.copy_from_slice(&input[4..36]);
let prompt = &input[HEADER_LEN..];
if prompt.is_empty() {
return PrecompileResult::Revert {
reason: "ai_inference requires a non-empty prompt".into(),
};
}
match engine::infer(&model_id, prompt) {
Ok(output) => {
let gas_used = 100_000u64.saturating_add(output.len() as u64 * 8);
PrecompileResult::Success { output, gas_used }
}
Err(EngineError::NoInferenceEngine) => PrecompileResult::Revert {
reason: "no inference engine registered on this node".into(),
},
Err(EngineError::NoEmbeddingEngine) => PrecompileResult::Revert {
reason: "no embedding engine registered on this node".into(),
},
Err(EngineError::ModelNotFound(id)) => PrecompileResult::Revert {
reason: format!("ai_inference model not found: {id}"),
},
Err(EngineError::Other(msg)) => PrecompileResult::Revert {
reason: format!("ai_inference engine failure: {msg}"),
},
}
}
fn exec_ai_embedding(input: &[u8]) -> PrecompileResult {
const HEADER_LEN: usize = 4 + 4;
if input.len() < HEADER_LEN {
return PrecompileResult::Revert {
reason: format!(
"ai_embedding requires at least {} bytes (selector + dim)",
HEADER_LEN
),
};
}
let dim = u32::from_be_bytes([input[4], input[5], input[6], input[7]]) as usize;
if dim == 0 || dim > 4096 {
return PrecompileResult::Revert {
reason: format!("invalid embedding dimension: {dim}"),
};
}
let text = &input[HEADER_LEN..];
if text.is_empty() {
return PrecompileResult::Revert {
reason: "ai_embedding requires non-empty text".into(),
};
}
match engine::embed(dim, text) {
Ok(vec) => {
let mut output = Vec::with_capacity(dim * 4);
for v in vec {
output.extend_from_slice(&v.to_le_bytes());
}
let gas_used = 50_000u64.saturating_add(dim as u64 * 16);
PrecompileResult::Success { output, gas_used }
}
Err(EngineError::NoEmbeddingEngine) => PrecompileResult::Revert {
reason: "no embedding engine registered on this node".into(),
},
Err(EngineError::NoInferenceEngine) => PrecompileResult::Revert {
reason: "no inference engine registered on this node".into(),
},
Err(EngineError::ModelNotFound(id)) => PrecompileResult::Revert {
reason: format!("ai_embedding model not found: {id}"),
},
Err(EngineError::Other(msg)) => PrecompileResult::Revert {
reason: format!("ai_embedding engine failure: {msg}"),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_registry_has_four_precompiles() {
let reg = PrecompileRegistry::default();
assert_eq!(reg.len(), 4);
assert!(reg.get(&ADDR_PQ_VERIFY).is_some());
assert!(reg.get(&ADDR_QUASAR_QUERY).is_some());
assert!(reg.get(&ADDR_AI_INFERENCE).is_some());
assert!(reg.get(&ADDR_AI_EMBEDDING).is_some());
}
#[test]
fn registry_call_unknown_address_returns_none() {
let reg = PrecompileRegistry::default();
let unknown = [0xff; 20];
assert!(reg.call(&unknown, &[]).is_none());
}
#[test]
fn addr_helper_layout() {
assert_eq!(ADDR_PQ_VERIFY[17], 0x01);
assert_eq!(ADDR_PQ_VERIFY[19], 0x01);
assert_eq!(ADDR_PQ_VERIFY[0..17], [0u8; 17]);
assert_eq!(ADDR_AI_INFERENCE[17], 0x02);
assert_eq!(ADDR_AI_INFERENCE[19], 0x01);
}
#[test]
fn pq_verify_rejects_short_input() {
let result = exec_pq_verify(&[0; 5]);
assert!(matches!(result, PrecompileResult::Revert { .. }));
}
#[test]
fn pq_verify_rejects_unsupported_pubkey_length() {
let mut input = Vec::new();
input.extend_from_slice(&1u32.to_be_bytes());
input.push(0x00);
input.extend_from_slice(&1u32.to_be_bytes());
input.push(0x00);
input.push(0x42);
let result = exec_pq_verify(&input);
match result {
PrecompileResult::Success { output, .. } => {
assert_eq!(output.len(), 32);
assert!(output.iter().all(|&b| b == 0));
}
other => panic!("expected Success(zero word), got {other:?}"),
}
}
#[test]
fn exec_pq_verify_real_mldsa65_roundtrip() {
use hanzo_pqc::signature::SignatureAlgorithm;
let (vk, sk) = MlDsa::generate_keypair_sync(SignatureAlgorithm::MlDsa65)
.expect("keypair generation");
let message = b"hanzo-vm pq_verify integration message";
let sig = MlDsa::sign_sync(&sk, message).expect("signing");
let mut input = Vec::new();
input.extend_from_slice(&(vk.key_bytes.len() as u32).to_be_bytes());
input.extend_from_slice(&vk.key_bytes);
input.extend_from_slice(&(sig.signature_bytes.len() as u32).to_be_bytes());
input.extend_from_slice(&sig.signature_bytes);
input.extend_from_slice(message);
let result = exec_pq_verify(&input);
match result {
PrecompileResult::Success { output, gas_used } => {
assert_eq!(output.len(), 32, "output should be a 32-byte word");
assert_eq!(output[31], 1, "signature should verify");
assert!(gas_used >= 3_000, "gas should include base cost");
}
other => panic!("expected Success, got {other:?}"),
}
let mut bad_input = input.clone();
let msg_offset = 4 + vk.key_bytes.len() + 4 + sig.signature_bytes.len();
bad_input[msg_offset] ^= 0x01;
match exec_pq_verify(&bad_input) {
PrecompileResult::Success { output, .. } => {
assert_eq!(output[31], 0, "tampered message should not verify");
}
other => panic!("expected Success(0), got {other:?}"),
}
}
#[test]
fn quasar_query_rejects_short_input() {
let result = exec_quasar_query(&[0; 10]);
assert!(matches!(result, PrecompileResult::Revert { .. }));
}
#[test]
fn exec_quasar_query_routes_through_libluxprecompile() {
let registry = lux_precompile::list().expect("list precompiles");
let found = registry
.iter()
.any(|p| p.address.eq_ignore_ascii_case(LUX_QUASAR_ADDR));
assert!(
found,
"expected {} in libluxprecompile registry; got {:?}",
LUX_QUASAR_ADDR, registry
);
let validator = [0x42u8; 20];
let commitment = [0xAAu8; 32];
let proof = commitment;
let mut input = Vec::with_capacity(85);
input.extend_from_slice(&validator);
input.extend_from_slice(&commitment);
input.extend_from_slice(&proof);
input.push(0x01);
match exec_quasar_query(&input) {
PrecompileResult::Success { output, gas_used } => {
assert_eq!(output.len(), 53);
assert_eq!(&output[0..20], &validator[..]);
assert_eq!(&output[20..52], &commitment[..]);
assert_eq!(output[52], 0x01, "should report member");
assert!(gas_used > 0);
}
other => panic!("expected Success(member), got {other:?}"),
}
let mut input = Vec::with_capacity(85);
input.extend_from_slice(&validator);
input.extend_from_slice(&commitment);
input.extend_from_slice(&proof);
input.push(0x00);
match exec_quasar_query(&input) {
PrecompileResult::Success { output, .. } => {
assert_eq!(output[52], 0x00, "should report non-member");
}
other => panic!("expected Success(non-member), got {other:?}"),
}
}
#[test]
fn ai_inference_rejects_empty() {
let result = exec_ai_inference(&[]);
assert!(matches!(result, PrecompileResult::Revert { .. }));
}
#[test]
fn ai_inference_rejects_missing_prompt() {
let mut input = vec![0u8; 4 + 32];
input.extend_from_slice(&[]);
let result = exec_ai_inference(&input);
match result {
PrecompileResult::Revert { reason } => {
assert!(reason.contains("non-empty"), "got reason: {reason}");
}
other => panic!("expected Revert, got {other:?}"),
}
}
#[test]
fn exec_ai_inference_real_model() {
let mut input = Vec::new();
input.extend_from_slice(&[0u8; 4]); input.extend_from_slice(&[0xABu8; 32]); input.extend_from_slice(b"summarize: this is a tiny prompt");
let res = exec_ai_inference(&input);
match res {
PrecompileResult::Revert { reason } => {
assert!(
reason.contains("inference") && reason.contains("engine"),
"expected 'no inference engine registered'-like reason, got: {reason}"
);
}
PrecompileResult::Success { output, .. } => {
assert!(!output.is_empty(), "engine must return real output");
}
other => panic!("unexpected result: {other:?}"),
}
if !hanzo_engine::inference_engine_registered() {
}
}
#[test]
fn ai_embedding_validates_dimension() {
let result = exec_ai_embedding(&[0; 2]);
assert!(matches!(result, PrecompileResult::Revert { .. }));
let mut input = vec![0u8; 4];
input.extend_from_slice(&0u32.to_be_bytes());
input.extend_from_slice(b"text");
let result = exec_ai_embedding(&input);
assert!(matches!(result, PrecompileResult::Revert { .. }));
let mut input = vec![0u8; 4];
input.extend_from_slice(&5000u32.to_be_bytes());
input.extend_from_slice(b"text");
let result = exec_ai_embedding(&input);
assert!(matches!(result, PrecompileResult::Revert { .. }));
}
#[test]
fn exec_ai_embedding_real_model() {
let dim: u32 = 128;
let mut input = Vec::new();
input.extend_from_slice(&[0u8; 4]); input.extend_from_slice(&dim.to_be_bytes());
input.extend_from_slice(b"hello world");
let res = exec_ai_embedding(&input);
match res {
PrecompileResult::Revert { reason } => {
assert!(
reason.contains("embedding") && reason.contains("engine"),
"expected 'no embedding engine registered'-like reason, got: {reason}"
);
}
PrecompileResult::Success { output, .. } => {
assert_eq!(output.len(), (dim as usize) * 4);
}
other => panic!("unexpected result: {other:?}"),
}
if !hanzo_engine::embedding_engine_registered() {
}
}
}