diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs index e528d42..53033c2 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs @@ -3,12 +3,8 @@ use crate::cmd; -use std::sync::Arc; - -use elements::hashes::Hash as _; use hal_simplicity::hal_simplicity::Program; use hal_simplicity::simplicity::jet; -use hal_simplicity::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; use super::super::{Error, ErrorExt as _}; use super::UpdatedPset; @@ -61,83 +57,20 @@ fn exec_inner( let input_idx: u32 = input_idx.parse().result_context("parsing input-idx")?; let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems - let n_inputs = pset.n_inputs(); - let input = pset - .inputs_mut() - .get_mut(input_idx_usize) - .ok_or_else(|| { - format!("index {} out-of-range for PSET with {} inputs", input_idx, n_inputs) - }) - .result_context("parsing input index")?; - let program = Program::::from_str(program, Some(witness)) .result_context("parsing program")?; - // 2. Build transaction environment. - // Default to Liquid Testnet genesis block - let genesis_hash = match genesis_hash { - Some(s) => s.parse().result_context("parsing genesis hash")?, - None => elements::BlockHash::from_byte_array([ - // copied out of simplicity-webide source - 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, - 0x79, 0x3b, 0x5b, 0x5e, 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, - 0x8e, 0xda, 0x71, 0xa7, - ]), - }; - - let cmr = program.cmr(); - // Unlike in the 'update-input' case we don't insist on any particular form of - // the Taptree. We just look for the CMR in the list. - let mut control_block_leaf = None; - for (cb, script_ver) in &input.tap_scripts { - if script_ver.1 == simplicity::leaf_version() && &script_ver.0[..] == cmr.as_ref() { - control_block_leaf = Some((cb.clone(), script_ver.0.clone())); - } - } - let (control_block, tap_leaf) = match control_block_leaf { - Some((cb, leaf)) => (cb, leaf), - None => { - return Err(format!("could not find Simplicity leaf in PSET taptree with CMR {}; did you forget to run 'simplicity pset update-input'?", cmr)) - .result_context("PSET tap_scripts field") - } - }; - - let tx = pset.extract_tx().result_context("extracting transaction from PSET")?; - let tx = Arc::new(tx); - - let input_utxos = pset - .inputs() - .iter() - .enumerate() - .map(|(n, input)| match input.witness_utxo { - Some(ref utxo) => Ok(ElementsUtxo { - script_pubkey: utxo.script_pubkey.clone(), - asset: utxo.asset, - value: utxo.value, - }), - None => Err(format!("witness_utxo field not populated for input {n}")), - }) - .collect::, _>>() - .result_context("extracting input UTXO information")?; - + // 2. Extract transaction environment. + let (tx_env, control_block, tap_leaf) = + super::execution_environment(&pset, input_idx_usize, program.cmr(), genesis_hash)?; let cb_serialized = control_block.serialize(); - let tx_env = ElementsEnv::new( - tx, - input_utxos, - input_idx, - cmr, - control_block, - None, // FIXME populate this; needs https://github.com/BlockstreamResearch/rust-simplicity/issues/315 first - genesis_hash, - ); // 3. Prune program. let redeem_node = program.redeem_node().expect("populated"); let pruned = redeem_node.prune(&tx_env).result_context("pruning program")?; let (prog, witness) = pruned.to_vec_with_witness(); - // Rust makes us re-borrow 'input' mutably since we used 'pset' immutably since we - // last borrowed it. We can unwrap() this time since we know it'll succeed. + // If `execution_environment` above succeeded we are guaranteed that this index is in bounds. let input = &mut pset.inputs_mut()[input_idx_usize]; input.final_script_witness = Some(vec![witness, prog, tap_leaf.into_bytes(), cb_serialized]); diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs index aa97ee4..16d8a21 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs @@ -4,9 +4,21 @@ mod create; mod extract; mod finalize; +mod run; mod update_input; +use std::sync::Arc; + +use super::{Error, ErrorExt as _}; use crate::cmd; + +use elements::hashes::Hash as _; +use elements::pset::PartiallySignedTransaction; +use elements::taproot::ControlBlock; +use elements::Script; +use hal_simplicity::simplicity::elements::Transaction; +use hal_simplicity::simplicity::jet::elements::{ElementsEnv, ElementsUtxo}; +use hal_simplicity::simplicity::Cmr; use serde::Serialize; #[derive(Serialize)] @@ -20,6 +32,7 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> { .subcommand(self::create::cmd()) .subcommand(self::extract::cmd()) .subcommand(self::finalize::cmd()) + .subcommand(self::run::cmd()) .subcommand(self::update_input::cmd()) } @@ -28,7 +41,82 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { ("create", Some(m)) => self::create::exec(m), ("extract", Some(m)) => self::extract::exec(m), ("finalize", Some(m)) => self::finalize::exec(m), + ("run", Some(m)) => self::run::exec(m), ("update-input", Some(m)) => self::update_input::exec(m), (_, _) => unreachable!("clap prints help"), }; } + +fn execution_environment( + pset: &PartiallySignedTransaction, + input_idx: usize, + cmr: Cmr, + genesis_hash: Option<&str>, +) -> Result<(ElementsEnv>, ControlBlock, Script), Error> { + let n_inputs = pset.n_inputs(); + let input = pset + .inputs() + .get(input_idx) + .ok_or_else(|| { + format!("index {} out-of-range for PSET with {} inputs", input_idx, n_inputs) + }) + .result_context("parsing input index")?; + + // Default to Liquid Testnet genesis block + let genesis_hash = match genesis_hash { + Some(s) => s.parse().result_context("parsing genesis hash")?, + None => elements::BlockHash::from_byte_array([ + // copied out of simplicity-webide source + 0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92, + 0x79, 0x3b, 0x5b, 0x5e, 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52, + 0x8e, 0xda, 0x71, 0xa7, + ]), + }; + + // Unlike in the 'update-input' case we don't insist on any particular form of + // the Taptree. We just look for the CMR in the list. + let mut control_block_leaf = None; + for (cb, script_ver) in &input.tap_scripts { + if script_ver.1 == simplicity::leaf_version() && &script_ver.0[..] == cmr.as_ref() { + control_block_leaf = Some((cb.clone(), script_ver.0.clone())); + } + } + let (control_block, tap_leaf) = match control_block_leaf { + Some((cb, leaf)) => (cb, leaf), + None => { + return Err(format!("could not find Simplicity leaf in PSET taptree with CMR {}; did you forget to run 'simplicity pset update-input'?", cmr)) + .result_context("PSET tap_scripts field") + } + }; + + let tx = pset.extract_tx().result_context("extracting transaction from PSET")?; + let tx = Arc::new(tx); + + let input_utxos = pset + .inputs() + .iter() + .enumerate() + .map(|(n, input)| match input.witness_utxo { + Some(ref utxo) => Ok(ElementsUtxo { + script_pubkey: utxo.script_pubkey.clone(), + asset: utxo.asset, + value: utxo.value, + }), + None => Err(format!("witness_utxo field not populated for input {n}")), + }) + .collect::, _>>() + .result_context("extracting input UTXO information")?; + + let tx_env = ElementsEnv::new( + tx, + input_utxos, + input_idx as u32, // cast fine, input indices are always small + cmr, + control_block.clone(), + None, // FIXME populate this; needs https://github.com/BlockstreamResearch/rust-simplicity/issues/315 first + genesis_hash, + ); + + // 3. Prune program. + Ok((tx_env, control_block, tap_leaf)) +} diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/run.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/run.rs new file mode 100644 index 0000000..4886ef6 --- /dev/null +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/run.rs @@ -0,0 +1,153 @@ +// Copyright 2025 Andrew Poelstra +// SPDX-License-Identifier: CC0-1.0 + +use crate::cmd; + +use hal_simplicity::hal_simplicity::Program; +use hal_simplicity::simplicity::bit_machine::{BitMachine, ExecTracker}; +use hal_simplicity::simplicity::jet; +use hal_simplicity::simplicity::{Cmr, Ihr}; + +use super::super::{Error, ErrorExt as _}; + +pub fn cmd<'a>() -> clap::App<'a, 'a> { + cmd::subcommand("run", "Run a Simplicity program in the context of a PSET input.") + .args(&cmd::opts_networks()) + .args(&[ + cmd::arg("pset", "PSET to update (base64)").takes_value(true).required(true), + cmd::arg("input-index", "the index of the input to sign (decimal)") + .takes_value(true) + .required(true), + cmd::arg("program", "Simplicity program (base64)").takes_value(true).required(true), + cmd::arg("witness", "Simplicity program witness (hex)") + .takes_value(true) + .required(true), + cmd::opt( + "genesis-hash", + "genesis hash of the blockchain the transaction belongs to (hex)", + ) + .short("g") + .required(false), + ]) +} + +pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { + let pset_b64 = matches.value_of("pset").expect("tx mandatory"); + let input_idx = matches.value_of("input-index").expect("input-idx is mandatory"); + let program = matches.value_of("program").expect("program is mandatory"); + let witness = matches.value_of("witness").expect("witness is mandatory"); + let genesis_hash = matches.value_of("genesis-hash"); + + match exec_inner(pset_b64, input_idx, program, witness, genesis_hash) { + Ok(info) => cmd::print_output(matches, &info), + Err(e) => cmd::print_output(matches, &e), + } +} + +#[derive(serde::Serialize)] +struct JetCall { + jet: String, + source_ty: String, + target_ty: String, + success: bool, + input_hex: String, + output_hex: String, + #[serde(skip_serializing_if = "Option::is_none")] + equality_check: Option<(String, String)>, +} + +#[derive(serde::Serialize)] +struct Response { + success: bool, + jets: Vec, +} + +#[allow(clippy::too_many_arguments)] +fn exec_inner( + pset_b64: &str, + input_idx: &str, + program: &str, + witness: &str, + genesis_hash: Option<&str>, +) -> Result { + struct JetTracker(Vec); + impl ExecTracker for JetTracker { + fn track_left(&mut self, _: Ihr) {} + fn track_right(&mut self, _: Ihr) {} + fn track_jet_call( + &mut self, + jet: &J, + input_buffer: &[simplicity::ffi::ffi::UWORD], + output_buffer: &[simplicity::ffi::ffi::UWORD], + success: bool, + ) { + // The word slices are in reverse order for some reason. + // FIXME maybe we should attempt to parse out Simplicity values here which + // can often be displayed in a better way, esp for e.g. option types. + let mut input_hex = String::new(); + for word in input_buffer.iter().rev() { + for byte in word.to_be_bytes() { + input_hex.push_str(&format!("{:02x}", byte)); + } + } + + let mut output_hex = String::new(); + for word in output_buffer.iter().rev() { + for byte in word.to_be_bytes() { + output_hex.push_str(&format!("{:02x}", byte)); + } + } + + let jet_name = jet.to_string(); + let equality_check = match jet_name.as_str() { + "eq_1" => None, // FIXME parse bits out of input + "eq_2" => None, // FIXME parse bits out of input + x if x.strip_prefix("eq_").is_some() => { + let split = input_hex.split_at(input_hex.len() / 2); + Some((split.0.to_owned(), split.1.to_owned())) + } + _ => None, + }; + self.0.push(JetCall { + jet: jet_name, + source_ty: jet.source_ty().to_final().to_string(), + target_ty: jet.target_ty().to_final().to_string(), + success, + input_hex, + output_hex, + equality_check, + }); + } + + fn track_dbg_call(&mut self, _: &Cmr, _: simplicity::Value) {} + fn is_track_debug_enabled(&self) -> bool { + false + } + } + + // 1. Parse everything. + let pset: elements::pset::PartiallySignedTransaction = + pset_b64.parse().result_context("decoding PSET")?; + let input_idx: u32 = input_idx.parse().result_context("parsing input-idx")?; + let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems + + let program = Program::::from_str(program, Some(witness)) + .result_context("parsing program")?; + + // 2. Extract transaction environment. + let (tx_env, _control_block, _tap_leaf) = + super::execution_environment(&pset, input_idx_usize, program.cmr(), genesis_hash)?; + + // 3. Prune program. + let redeem_node = program.redeem_node().expect("populated"); + + let mut mac = + BitMachine::for_program(redeem_node).result_context("constructing bit machine")?; + let mut tracker = JetTracker(vec![]); + // Eat success/failure. FIXME should probably report this to the user. + let success = mac.exec_with_tracker(redeem_node, &tx_env, &mut tracker).is_ok(); + Ok(Response { + success, + jets: tracker.0, + }) +} diff --git a/src/hal_simplicity.rs b/src/hal_simplicity.rs index e3732d5..396f231 100644 --- a/src/hal_simplicity.rs +++ b/src/hal_simplicity.rs @@ -85,8 +85,8 @@ impl Program { } /// Accessor for the commitment-time program. - pub fn redeem_node(&self) -> Option<&RedeemNode> { - self.redeem_prog.as_ref().map(Arc::as_ref) + pub fn redeem_node(&self) -> Option<&Arc>> { + self.redeem_prog.as_ref() } }