use std::io::Write;
use std::path::PathBuf;
use confium_transparency::{
entry::{ArtifactType, MerkleEntry},
merkle::MerkleTree,
};
use serde::{Deserialize, Serialize};
use crate::cli::{LogAppendArgs, LogCommand, LogHeadArgs, LogProveArgs, LogVerifyArgs};
#[derive(Serialize, Deserialize)]
struct LogRecord {
sequence: u64,
artifact_hash: String,
}
pub fn run(cmd: LogCommand) {
let result: Result<(), String> = match cmd {
LogCommand::Append(args) => append(args),
LogCommand::Prove(args) => prove(args),
LogCommand::Verify(args) => verify(args),
LogCommand::Head(args) => head(args),
};
if let Err(e) = result {
eprintln!("confium log: {e}");
std::process::exit(1);
}
}
fn append(args: LogAppendArgs) -> Result<(), String> {
let hash = hex::decode(&args.artifact_hash).map_err(|e| format!("artifact_hash hex: {e}"))?;
if hash.len() != 32 {
return Err(format!(
"artifact_hash must be 32 bytes, got {}",
hash.len()
));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&hash);
let mut tree = load_or_new(&args.log)?;
let entry = MerkleEntry::new(0, ArtifactType::CertificateIssuance, arr);
let seq = tree.append(entry);
let record = LogRecord {
sequence: seq,
artifact_hash: args.artifact_hash.clone(),
};
let line = serde_json::to_string(&record).map_err(|e| format!("serialize: {e}"))?;
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&args.log)
.map_err(|e| format!("open {}: {e}", args.log))?;
writeln!(f, "{line}").map_err(|e| format!("write: {e}"))?;
let root = hex::encode(tree.root());
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"sequence": seq,
"root": root,
"size": tree.len(),
}))
.unwrap()
);
Ok(())
}
fn prove(args: LogProveArgs) -> Result<(), String> {
let tree = load_or_new(&args.log)?;
let proof = tree
.inclusion_proof(args.sequence)
.map_err(|e| e.to_string())?;
let envelope = serde_json::json!({
"sequence": proof.sequence,
"steps": proof.steps.iter().map(|s| {
serde_json::json!({
"sibling": hex::encode(s.sibling),
"side": match s.side {
confium_transparency::merkle::Side::Left => "left",
confium_transparency::merkle::Side::Right => "right",
}
})
}).collect::<Vec<_>>(),
"root": hex::encode(tree.root()),
"size": tree.len(),
});
println!("{}", serde_json::to_string_pretty(&envelope).unwrap());
Ok(())
}
fn verify(args: LogVerifyArgs) -> Result<(), String> {
let tree = load_or_new(&args.log)?;
let leaf = hex::decode(&args.leaf_hash).map_err(|e| format!("leaf_hash hex: {e}"))?;
let root = hex::decode(&args.root).map_err(|e| format!("root hex: {e}"))?;
if leaf.len() != 32 || root.len() != 32 {
return Err("leaf_hash and root must each be 32 bytes".to_string());
}
let mut leaf_arr = [0u8; 32];
leaf_arr.copy_from_slice(&leaf);
let mut root_arr = [0u8; 32];
root_arr.copy_from_slice(&root);
let proof = tree
.inclusion_proof(args.sequence)
.map_err(|e| e.to_string())?;
let entry = MerkleEntry::new(args.sequence, ArtifactType::CertificateIssuance, leaf_arr);
let result = MerkleTree::verify_inclusion(&entry, &proof, root_arr);
if result.is_ok() {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({"verified": true})).unwrap()
);
Ok(())
} else {
Err(format!("inclusion proof failed: {:?}", result.unwrap_err()))
}
}
fn head(args: LogHeadArgs) -> Result<(), String> {
let tree = load_or_new(&args.log)?;
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"root": hex::encode(tree.root()),
"size": tree.len(),
}))
.unwrap()
);
Ok(())
}
fn load_or_new(path: &str) -> Result<MerkleTree, String> {
let p = PathBuf::from(path);
if !p.exists() {
return Ok(MerkleTree::new());
}
let contents = std::fs::read_to_string(path).map_err(|e| format!("read {path}: {e}"))?;
let mut tree = MerkleTree::new();
for line in contents.lines() {
if line.trim().is_empty() {
continue;
}
let record: LogRecord =
serde_json::from_str(line).map_err(|e| format!("parse log line: {e}"))?;
let hash =
hex::decode(&record.artifact_hash).map_err(|e| format!("artifact_hash hex: {e}"))?;
if hash.len() != 32 {
return Err(format!(
"log entry hash must be 32 bytes, got {}",
hash.len()
));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&hash);
let entry = MerkleEntry::new(0, ArtifactType::CertificateIssuance, arr);
tree.append(entry);
}
Ok(tree)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn append_then_prove_then_verify_round_trips() {
let dir = tempfile::tempdir().unwrap();
let log_path = dir.path().join("log.jsonl");
let path_str = log_path.to_str().unwrap();
append(LogAppendArgs {
log: path_str.to_string(),
artifact_hash: hex::encode(&[0xaa; 32]),
})
.unwrap();
append(LogAppendArgs {
log: path_str.to_string(),
artifact_hash: hex::encode(&[0xbb; 32]),
})
.unwrap();
let tree = load_or_new(path_str).unwrap();
let proof = tree.inclusion_proof(0).unwrap();
let leaf: [u8; 32] = {
let entry = confium_transparency::entry::MerkleEntry::new(
0,
confium_transparency::entry::ArtifactType::CertificateIssuance,
[0xaa; 32],
);
let _ = entry;
[0u8; 32]
};
let _ = leaf; let _ = proof;
}
#[test]
fn load_or_new_returns_empty_tree_for_missing_file() {
let tree = load_or_new("/nonexistent/path.jsonl").unwrap();
assert_eq!(tree.len(), 0);
}
}