Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 4 additions & 71 deletions src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<jet::Elements>::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<Vec<_>, _>>()
.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]);

Expand Down
88 changes: 88 additions & 0 deletions src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -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())
}

Expand All @@ -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<Arc<Transaction>>, 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<Vec<_>, _>>()
.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))
}
153 changes: 153 additions & 0 deletions src/bin/hal-simplicity/cmd/simplicity/pset/run.rs
Original file line number Diff line number Diff line change
@@ -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<JetCall>,
}

#[allow(clippy::too_many_arguments)]
fn exec_inner(
pset_b64: &str,
input_idx: &str,
program: &str,
witness: &str,
genesis_hash: Option<&str>,
) -> Result<Response, Error> {
struct JetTracker(Vec<JetCall>);
impl<J: jet::Jet> ExecTracker<J> 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::<jet::Elements>::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,
})
}
4 changes: 2 additions & 2 deletions src/hal_simplicity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ impl<J: Jet> Program<J> {
}

/// Accessor for the commitment-time program.
pub fn redeem_node(&self) -> Option<&RedeemNode<J>> {
self.redeem_prog.as_ref().map(Arc::as_ref)
pub fn redeem_node(&self) -> Option<&Arc<RedeemNode<J>>> {
self.redeem_prog.as_ref()
}
}

Expand Down