Skip to content

Commit 895cddd

Browse files
committed
add 'simplicity pset run' command which outputs jet calls and inputs and outputs
This is a bit of a hacky and incomplete implementation but it's already very useful so I think we should ship it. It runs through a program and outputs information about each jet that it hits. It special-cases the equality jets and also splits up the input so you can see which values it's comparing. It is not encoding input/output data correctly if the data length isn't a multiple of 8 (or 64 actually); it just takes the raw words from the bit machine and outputs them. It also doesn't attempt to parse values or do any other interpretation, except for the equality checks (and it doesn't do eq_1 or eq_2 since those would require parsing out the nybbles rather than just splitting the hex string). It also doesn't understand the `dbg!` construction although this looks like it will not be too hard to add. Should definitely extend and better-document the ExecTracer trait upstream. But for now this is already pretty useful.
1 parent 4b0e5cf commit 895cddd

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
mod create;
55
mod extract;
66
mod finalize;
7+
mod run;
78
mod update_input;
89

910
use std::sync::Arc;
@@ -31,6 +32,7 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> {
3132
.subcommand(self::create::cmd())
3233
.subcommand(self::extract::cmd())
3334
.subcommand(self::finalize::cmd())
35+
.subcommand(self::run::cmd())
3436
.subcommand(self::update_input::cmd())
3537
}
3638

@@ -39,6 +41,7 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
3941
("create", Some(m)) => self::create::exec(m),
4042
("extract", Some(m)) => self::extract::exec(m),
4143
("finalize", Some(m)) => self::finalize::exec(m),
44+
("run", Some(m)) => self::run::exec(m),
4245
("update-input", Some(m)) => self::update_input::exec(m),
4346
(_, _) => unreachable!("clap prints help"),
4447
};
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2025 Andrew Poelstra
2+
// SPDX-License-Identifier: CC0-1.0
3+
4+
use crate::cmd;
5+
6+
use hal_simplicity::hal_simplicity::Program;
7+
use hal_simplicity::simplicity::bit_machine::{BitMachine, ExecTracker};
8+
use hal_simplicity::simplicity::jet;
9+
use hal_simplicity::simplicity::{Cmr, Ihr};
10+
11+
use super::super::{Error, ErrorExt as _};
12+
13+
pub fn cmd<'a>() -> clap::App<'a, 'a> {
14+
cmd::subcommand("run", "Run a Simplicity program in the context of a PSET input.")
15+
.args(&cmd::opts_networks())
16+
.args(&[
17+
cmd::arg("pset", "PSET to update (base64)").takes_value(true).required(true),
18+
cmd::arg("input-index", "the index of the input to sign (decimal)")
19+
.takes_value(true)
20+
.required(true),
21+
cmd::arg("program", "Simplicity program (base64)").takes_value(true).required(true),
22+
cmd::arg("witness", "Simplicity program witness (hex)")
23+
.takes_value(true)
24+
.required(true),
25+
cmd::opt(
26+
"genesis-hash",
27+
"genesis hash of the blockchain the transaction belongs to (hex)",
28+
)
29+
.short("g")
30+
.required(false),
31+
])
32+
}
33+
34+
pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
35+
let pset_b64 = matches.value_of("pset").expect("tx mandatory");
36+
let input_idx = matches.value_of("input-index").expect("input-idx is mandatory");
37+
let program = matches.value_of("program").expect("program is mandatory");
38+
let witness = matches.value_of("witness").expect("witness is mandatory");
39+
let genesis_hash = matches.value_of("genesis-hash");
40+
41+
match exec_inner(pset_b64, input_idx, program, witness, genesis_hash) {
42+
Ok(info) => cmd::print_output(matches, &info),
43+
Err(e) => cmd::print_output(matches, &e),
44+
}
45+
}
46+
47+
#[derive(serde::Serialize)]
48+
struct JetCall {
49+
jet: String,
50+
source_ty: String,
51+
target_ty: String,
52+
success: bool,
53+
input_hex: String,
54+
output_hex: String,
55+
#[serde(skip_serializing_if = "Option::is_none")]
56+
equality_check: Option<(String, String)>,
57+
}
58+
59+
#[derive(serde::Serialize)]
60+
struct Response {
61+
success: bool,
62+
jets: Vec<JetCall>,
63+
}
64+
65+
#[allow(clippy::too_many_arguments)]
66+
fn exec_inner(
67+
pset_b64: &str,
68+
input_idx: &str,
69+
program: &str,
70+
witness: &str,
71+
genesis_hash: Option<&str>,
72+
) -> Result<Response, Error> {
73+
struct JetTracker(Vec<JetCall>);
74+
impl<J: jet::Jet> ExecTracker<J> for JetTracker {
75+
fn track_left(&mut self, _: Ihr) {}
76+
fn track_right(&mut self, _: Ihr) {}
77+
fn track_jet_call(
78+
&mut self,
79+
jet: &J,
80+
input_buffer: &[simplicity::ffi::ffi::UWORD],
81+
output_buffer: &[simplicity::ffi::ffi::UWORD],
82+
success: bool,
83+
) {
84+
// The word slices are in reverse order for some reason.
85+
// FIXME maybe we should attempt to parse out Simplicity values here which
86+
// can often be displayed in a better way, esp for e.g. option types.
87+
let mut input_hex = String::new();
88+
for word in input_buffer.iter().rev() {
89+
for byte in word.to_be_bytes() {
90+
input_hex.push_str(&format!("{:02x}", byte));
91+
}
92+
}
93+
94+
let mut output_hex = String::new();
95+
for word in output_buffer.iter().rev() {
96+
for byte in word.to_be_bytes() {
97+
output_hex.push_str(&format!("{:02x}", byte));
98+
}
99+
}
100+
101+
let jet_name = jet.to_string();
102+
let equality_check = match jet_name.as_str() {
103+
"eq_1" => None, // FIXME parse bits out of input
104+
"eq_2" => None, // FIXME parse bits out of input
105+
x if x.strip_prefix("eq_").is_some() => {
106+
let split = input_hex.split_at(input_hex.len() / 2);
107+
Some((split.0.to_owned(), split.1.to_owned()))
108+
}
109+
_ => None,
110+
};
111+
self.0.push(JetCall {
112+
jet: jet_name,
113+
source_ty: jet.source_ty().to_final().to_string(),
114+
target_ty: jet.target_ty().to_final().to_string(),
115+
success,
116+
input_hex,
117+
output_hex,
118+
equality_check,
119+
});
120+
}
121+
122+
fn track_dbg_call(&mut self, _: &Cmr, _: simplicity::Value) {}
123+
fn is_track_debug_enabled(&self) -> bool {
124+
false
125+
}
126+
}
127+
128+
// 1. Parse everything.
129+
let pset: elements::pset::PartiallySignedTransaction =
130+
pset_b64.parse().result_context("decoding PSET")?;
131+
let input_idx: u32 = input_idx.parse().result_context("parsing input-idx")?;
132+
let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems
133+
134+
let program = Program::<jet::Elements>::from_str(program, Some(witness))
135+
.result_context("parsing program")?;
136+
137+
// 2. Extract transaction environment.
138+
let (tx_env, _control_block, _tap_leaf) =
139+
super::execution_environment(&pset, input_idx_usize, program.cmr(), genesis_hash)?;
140+
141+
// 3. Prune program.
142+
let redeem_node = program.redeem_node().expect("populated");
143+
144+
let mut mac =
145+
BitMachine::for_program(redeem_node).result_context("constructing bit machine")?;
146+
let mut tracker = JetTracker(vec![]);
147+
// Eat success/failure. FIXME should probably report this to the user.
148+
let success = mac.exec_with_tracker(redeem_node, &tx_env, &mut tracker).is_ok();
149+
Ok(Response {
150+
success,
151+
jets: tracker.0,
152+
})
153+
}

0 commit comments

Comments
 (0)