use confium_transparency::merkle::{Hash, InclusionProof as RustInclusionProof};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct MerkleTree {
inner: std::cell::RefCell<confium_transparency::merkle::MerkleTree>,
leaf_hashes: std::cell::RefCell<std::collections::HashMap<u64, Hash>>,
}
#[wasm_bindgen]
impl MerkleTree {
#[wasm_bindgen(constructor)]
pub fn new() -> MerkleTree {
Self {
inner: std::cell::RefCell::new(confium_transparency::merkle::MerkleTree::new()),
leaf_hashes: std::cell::RefCell::new(Default::default()),
}
}
pub fn append(&self, artifact_hash: &[u8]) -> Result<u64, JsValue> {
if artifact_hash.len() != 32 {
return Err(JsValue::from_str(&format!(
"artifact_hash must be exactly 32 bytes, got {}",
artifact_hash.len()
)));
}
let mut hash_arr = [0u8; 32];
hash_arr.copy_from_slice(artifact_hash);
let seq = self.inner.borrow().len() as u64;
let entry = confium_transparency::entry::MerkleEntry::new(
seq,
confium_transparency::entry::ArtifactType::CertificateIssuance,
hash_arr,
);
let leaf_hash = hash_leaf(entry.entry_hash());
let assigned = self.inner.borrow_mut().append(entry);
debug_assert_eq!(assigned, seq, "predicted sequence must match assigned");
self.leaf_hashes.borrow_mut().insert(assigned, leaf_hash);
Ok(assigned)
}
#[wasm_bindgen(getter)]
pub fn length(&self) -> usize {
self.inner.borrow().len()
}
#[wasm_bindgen]
pub fn root(&self) -> Vec<u8> {
self.inner.borrow().root().to_vec()
}
pub fn consistency_proof(&self, old_size: usize) -> Result<Vec<u8>, JsValue> {
let proof = self
.inner
.borrow()
.consistency_proof(old_size)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let mut flat = Vec::with_capacity(proof.len() * 32);
for h in &proof {
flat.extend_from_slice(h);
}
Ok(flat)
}
pub fn inclusion_proof(&self, sequence: u64) -> Result<InclusionProof, JsValue> {
let proof = self
.inner
.borrow()
.inclusion_proof(sequence)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let leaf = self
.leaf_hashes
.borrow()
.get(&sequence)
.copied()
.ok_or_else(|| JsValue::from_str("missing leaf hash for sequence"))?;
Ok(InclusionProof {
inner: proof,
leaf_hash: leaf,
})
}
}
impl Default for MerkleTree {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
pub struct InclusionProof {
inner: RustInclusionProof,
leaf_hash: Hash,
}
#[wasm_bindgen]
impl InclusionProof {
#[wasm_bindgen(getter)]
pub fn sequence(&self) -> u64 {
self.inner.sequence
}
#[wasm_bindgen]
pub fn verify(&self, root: &[u8]) -> Result<bool, JsValue> {
if root.len() != 32 {
return Err(JsValue::from_str(&format!(
"root must be exactly 32 bytes, got {}",
root.len()
)));
}
let mut root_arr = [0u8; 32];
root_arr.copy_from_slice(root);
let mut current = self.leaf_hash;
for step in &self.inner.steps {
current = match step.side {
confium_transparency::merkle::Side::Left => hash_internal(step.sibling, current),
confium_transparency::merkle::Side::Right => hash_internal(current, step.sibling),
};
}
use subtle::ConstantTimeEq;
Ok(current.ct_eq(&root_arr).into())
}
}
fn hash_leaf(entry_hash: Hash) -> Hash {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update([0x01]);
h.update(entry_hash);
let r = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&r);
out
}
#[wasm_bindgen]
pub fn compute_artifact_hash(artifact_bytes: &[u8]) -> Vec<u8> {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(artifact_bytes);
let r = h.finalize();
r.to_vec()
}
#[wasm_bindgen]
pub fn compute_leaf_hash(sequence: u64, timestamp_ms: f64, artifact_bytes: &[u8]) -> Vec<u8> {
use sha2::{Digest, Sha256};
let artifact_hash = {
let mut h = Sha256::new();
h.update(artifact_bytes);
let r = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&r);
out
};
let mut entry_hasher = Sha256::new();
entry_hasher.update(sequence.to_le_bytes());
entry_hasher.update((timestamp_ms as i64).to_le_bytes());
entry_hasher.update(artifact_hash);
let entry_hash: [u8; 32] = {
let r = entry_hasher.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&r);
out
};
hash_leaf(entry_hash).to_vec()
}
fn hash_internal(left: Hash, right: Hash) -> Hash {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update([0x02]);
h.update(left);
h.update(right);
let r = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&r);
out
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TreeHead {
pub size: usize,
pub root: Vec<u8>,
}
#[wasm_bindgen]
pub fn tree_head_from_json(json: &str) -> Result<String, JsValue> {
let head: TreeHead = serde_json::from_str(json)
.map_err(|e| JsValue::from_str(&format!("TreeHead JSON parse error: {e}")))?;
serde_json::to_string(&serde_json::json!({
"size": head.size,
"root_hex": head.root.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
}))
.map_err(|e| JsValue::from_str(&format!("serialize: {e}")))
}
#[wasm_bindgen]
pub fn verify_inclusion_with_head(
leaf_entry_hash: &[u8],
proof_json: &str,
head_json: &str,
) -> Result<bool, JsValue> {
if leaf_entry_hash.len() != 32 {
return Err(JsValue::from_str(&format!(
"leaf_entry_hash must be 32 bytes, got {}",
leaf_entry_hash.len()
)));
}
let mut leaf_arr = [0u8; 32];
leaf_arr.copy_from_slice(leaf_entry_hash);
let proof: RustInclusionProof = serde_json::from_str(proof_json)
.map_err(|e| JsValue::from_str(&format!("proof JSON parse: {e}")))?;
let head: TreeHead = serde_json::from_str(head_json)
.map_err(|e| JsValue::from_str(&format!("head JSON parse: {e}")))?;
if head.root.len() != 32 {
return Err(JsValue::from_str("head.root must be 32 bytes"));
}
let mut root_arr = [0u8; 32];
root_arr.copy_from_slice(&head.root);
let mut current = hash_leaf(leaf_arr);
for step in &proof.steps {
current = match step.side {
confium_transparency::merkle::Side::Left => hash_internal(step.sibling, current),
confium_transparency::merkle::Side::Right => hash_internal(current, step.sibling),
};
}
use subtle::ConstantTimeEq;
Ok(current.ct_eq(&root_arr).into())
}