use std::fmt;
use std::sync::OnceLock;
use serde_json::{json, Value};
use crate::{argent, disasm::disassemble, p2sh_hash, p2sh_reveal, Registry};
fn registry() -> &'static Registry {
static REGISTRY: OnceLock<Registry> = OnceLock::new();
REGISTRY.get_or_init(Registry::default)
}
fn blake2b_256(bytes: &[u8]) -> String {
hex::encode(
blake2b_simd::Params::new()
.hash_length(32)
.hash(bytes)
.as_bytes(),
)
}
#[derive(Debug)]
pub enum HexError {
Empty,
NotHex(hex::FromHexError),
}
impl fmt::Display for HexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HexError::Empty => f.write_str("empty"),
HexError::NotHex(e) => write!(f, "not hex: {e}"),
}
}
}
pub fn parse_hex(text: &str) -> Result<Vec<u8>, HexError> {
let compact: String = text.split_whitespace().collect();
let digits = compact
.strip_prefix("0x")
.or_else(|| compact.strip_prefix("0X"))
.unwrap_or(&compact);
if digits.is_empty() {
return Err(HexError::Empty);
}
hex::decode(digits).map_err(HexError::NotHex)
}
pub fn name(program: &[u8]) -> Value {
let decoded = registry().decode(0, program);
let digest = blake2b_256(program);
let mut out = json!({
"bytes": program.len(),
"p2sh_script": format!("aa20{digest}87"),
"blake2b_256": digest,
"decoder": decoded.decoder,
"template": decoded.template,
"generation": decoded.generation,
"fields": decoded.fields,
"uses_covenant_ops": decoded.uses_covenant_ops,
});
if let Some(a) = argent::recognize(program) {
out["argent"] = json!({
"generation": a.generation.as_str(),
"entrypoints": a.entrypoint_count(),
"template_hash": hex::encode(a.template_hash),
"template_hash_rule": a.generation.hash_name(),
});
}
out
}
pub fn hash(program: &[u8]) -> Value {
let digest = blake2b_256(program);
json!({
"bytes": program.len(),
"p2sh_script": format!("aa20{digest}87"),
"blake2b_256": digest,
})
}
pub fn disasm(program: &[u8]) -> String {
let (instructions, truncated) = disassemble(program);
let mut out = format!(
"{} bytes, {} instructions{}",
program.len(),
instructions.len(),
if truncated {
", truncated: the last push runs past the end"
} else {
""
}
);
for i in &instructions {
out.push_str(&format!("\n{:04x} {i}", i.offset));
}
out
}
#[derive(Debug)]
pub enum CheckError {
NotP2sh,
EndsInsidePush,
NoFinalPush,
}
impl fmt::Display for CheckError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
CheckError::NotP2sh => "the script public key is not P2SH (aa20 <32-byte hash> 87)",
CheckError::EndsInsidePush => "the signature script ends inside a push",
CheckError::NoFinalPush => {
"the signature script does not end with a push, so it reveals no program"
}
})
}
}
pub fn check(signature_script: &[u8], spk: Option<&[u8]>) -> Result<Value, CheckError> {
if spk.is_some_and(|spk| p2sh_hash(spk).is_none()) {
return Err(CheckError::NotP2sh);
}
let (instructions, truncated) = disassemble(signature_script);
if truncated {
return Err(CheckError::EndsInsidePush);
}
let program = instructions
.last()
.and_then(|i| i.data.clone())
.ok_or(CheckError::NoFinalPush)?;
let push_only = instructions
.iter()
.all(|i| i.opcode <= 0x60 && i.opcode != 0x50);
let matches = spk.map(|spk| p2sh_reveal(spk, signature_script).is_some());
Ok(json!({
"signature_script_bytes": signature_script.len(),
"pushes": instructions.len(),
"push_only": push_only,
"spk_matches": matches,
"program": name(&program),
}))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hex_is_read_the_way_people_paste_it() {
assert_eq!(parse_hex("0xAB cd\n 01").unwrap(), [0xab, 0xcd, 0x01]);
assert_eq!(parse_hex("0X00").unwrap(), [0x00]);
assert!(matches!(parse_hex(" \n"), Err(HexError::Empty)));
assert!(matches!(parse_hex("0x"), Err(HexError::Empty)));
assert!(matches!(parse_hex("abc"), Err(HexError::NotHex(_))));
assert!(matches!(parse_hex("zz"), Err(HexError::NotHex(_))));
}
#[test]
fn check_needs_a_final_push_and_a_p2sh_output() {
assert!(matches!(
check(&[0x51, 0x93], None),
Err(CheckError::NoFinalPush)
));
assert!(matches!(
check(&[0x03, 0xaa], None),
Err(CheckError::EndsInsidePush)
));
assert!(matches!(
check(&[0x01, 0x51], Some(&[0x51][..])),
Err(CheckError::NotP2sh)
));
}
#[test]
fn check_matches_only_the_committed_program() {
let program = [0x51u8];
let sig = [0x01, 0x51];
let committed = hex::decode(hash(&program)["p2sh_script"].as_str().unwrap()).unwrap();
let v = check(&sig, Some(committed.as_slice())).unwrap();
assert_eq!(v["spk_matches"], true);
assert_eq!(v["push_only"], true);
assert_eq!(v["program"]["bytes"], 1);
let other = hex::decode(format!("aa20{}87", "00".repeat(32))).unwrap();
assert_eq!(
check(&sig, Some(other.as_slice())).unwrap()["spk_matches"],
false
);
assert_eq!(check(&sig, None).unwrap()["spk_matches"], Value::Null);
}
#[test]
fn disasm_lists_offsets() {
let text = disasm(&[0x51, 0x93]);
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines[0], "2 bytes, 2 instructions");
assert!(lines[1].starts_with("0000 "));
assert!(lines[2].starts_with("0001 "));
assert!(disasm(&[0x03, 0xaa])
.lines()
.next()
.unwrap()
.ends_with("past the end"));
}
}