Skip to content

Commit 4e73c48

Browse files
committed
simplicity: add 'simplicity pset finalize' command
This command takes a PSET where we've updated all the inputs (with 'simplicity pset update-input' for example), as well as a program and a valid witness. It executes the program, prunes it, then adds it to the PSET. Then you can extract and send the program with elements-cli.
1 parent def1c83 commit 4e73c48

File tree

3 files changed

+161
-7
lines changed

3 files changed

+161
-7
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// Copyright 2025 Andrew Poelstra
2+
// SPDX-License-Identifier: CC0-1.0
3+
4+
use crate::cmd;
5+
6+
use std::sync::Arc;
7+
8+
use elements::hashes::Hash as _;
9+
use hal_simplicity::hal_simplicity::Program;
10+
use hal_simplicity::simplicity::jet;
11+
use hal_simplicity::simplicity::jet::elements::{ElementsEnv, ElementsUtxo};
12+
13+
use super::super::{Error, ErrorExt as _};
14+
use super::UpdatedPset;
15+
16+
pub fn cmd<'a>() -> clap::App<'a, 'a> {
17+
cmd::subcommand("finalize", "Attach a Simplicity program and witness to a PSET input")
18+
.args(&cmd::opts_networks())
19+
.args(&[
20+
cmd::arg("pset", "PSET to update (base64)").takes_value(true).required(true),
21+
cmd::arg("input-index", "the index of the input to sign (decimal)")
22+
.takes_value(true)
23+
.required(true),
24+
cmd::arg("program", "Simplicity program (base64)").takes_value(true).required(true),
25+
cmd::arg("witness", "Simplicity program witness (hex)")
26+
.takes_value(true)
27+
.required(true),
28+
cmd::opt(
29+
"genesis-hash",
30+
"genesis hash of the blockchain the transaction belongs to (hex)",
31+
)
32+
.short("g")
33+
.required(false),
34+
])
35+
}
36+
37+
pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
38+
let pset_b64 = matches.value_of("pset").expect("tx mandatory");
39+
let input_idx = matches.value_of("input-index").expect("input-idx is mandatory");
40+
let program = matches.value_of("program").expect("program is mandatory");
41+
let witness = matches.value_of("witness").expect("witness is mandatory");
42+
let genesis_hash = matches.value_of("genesis-hash");
43+
44+
match exec_inner(pset_b64, input_idx, program, witness, genesis_hash) {
45+
Ok(info) => cmd::print_output(matches, &info),
46+
Err(e) => cmd::print_output(matches, &e),
47+
}
48+
}
49+
50+
#[allow(clippy::too_many_arguments)]
51+
fn exec_inner(
52+
pset_b64: &str,
53+
input_idx: &str,
54+
program: &str,
55+
witness: &str,
56+
genesis_hash: Option<&str>,
57+
) -> Result<UpdatedPset, Error> {
58+
// 1. Parse everything.
59+
let mut pset: elements::pset::PartiallySignedTransaction =
60+
pset_b64.parse().result_context("decoding PSET")?;
61+
let input_idx: u32 = input_idx.parse().result_context("parsing input-idx")?;
62+
let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems
63+
64+
let n_inputs = pset.n_inputs();
65+
let input = pset
66+
.inputs_mut()
67+
.get_mut(input_idx_usize)
68+
.ok_or_else(|| {
69+
format!("index {} out-of-range for PSET with {} inputs", input_idx, n_inputs)
70+
})
71+
.result_context("parsing input index")?;
72+
73+
let program = Program::<jet::Elements>::from_str(program, Some(witness))
74+
.result_context("parsing program")?;
75+
76+
// 2. Build transaction environment.
77+
// Default to Liquid Testnet genesis block
78+
let genesis_hash = match genesis_hash {
79+
Some(s) => s.parse().result_context("parsing genesis hash")?,
80+
None => elements::BlockHash::from_byte_array([
81+
// copied out of simplicity-webide source
82+
0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92,
83+
0x79, 0x3b, 0x5b, 0x5e, 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52,
84+
0x8e, 0xda, 0x71, 0xa7,
85+
]),
86+
};
87+
88+
let cmr = program.cmr();
89+
// Unlike in the 'update-input' case we don't insist on any particular form of
90+
// the Taptree. We just look for the CMR in the list.
91+
let mut control_block_leaf = None;
92+
for (cb, script_ver) in &input.tap_scripts {
93+
if script_ver.1 == simplicity::leaf_version() && &script_ver.0[..] == cmr.as_ref() {
94+
control_block_leaf = Some((cb.clone(), script_ver.0.clone()));
95+
}
96+
}
97+
let (control_block, tap_leaf) = match control_block_leaf {
98+
Some((cb, leaf)) => (cb, leaf),
99+
None => {
100+
return Err(format!("could not find Simplicity leaf in PSET taptree with CMR {}; did you forget to run 'simplicity pset update-input'?", cmr))
101+
.result_context("PSET tap_scripts field")
102+
}
103+
};
104+
105+
let tx = pset.extract_tx().result_context("extracting transaction from PSET")?;
106+
let tx = Arc::new(tx);
107+
108+
let input_utxos = pset
109+
.inputs()
110+
.iter()
111+
.enumerate()
112+
.map(|(n, input)| match input.witness_utxo {
113+
Some(ref utxo) => Ok(ElementsUtxo {
114+
script_pubkey: utxo.script_pubkey.clone(),
115+
asset: utxo.asset,
116+
value: utxo.value,
117+
}),
118+
None => Err(format!("witness_utxo field not populated for input {n}")),
119+
})
120+
.collect::<Result<Vec<_>, _>>()
121+
.result_context("extracting input UTXO information")?;
122+
123+
let cb_serialized = control_block.serialize();
124+
let tx_env = ElementsEnv::new(
125+
tx,
126+
input_utxos,
127+
input_idx,
128+
cmr,
129+
control_block,
130+
None, // FIXME populate this; needs https://github.com/BlockstreamResearch/rust-simplicity/issues/315 first
131+
genesis_hash,
132+
);
133+
134+
// 3. Prune program.
135+
let redeem_node = program.redeem_node().expect("populated");
136+
let pruned = redeem_node.prune(&tx_env).result_context("pruning program")?;
137+
138+
let (prog, witness) = pruned.to_vec_with_witness();
139+
// Rust makes us re-borrow 'input' mutably since we used 'pset' immutably since we
140+
// last borrowed it. We can unwrap() this time since we know it'll succeed.
141+
let input = &mut pset.inputs_mut()[input_idx_usize];
142+
input.final_script_witness = Some(vec![witness, prog, tap_leaf.into_bytes(), cb_serialized]);
143+
144+
let updated_values = vec!["final_script_witness"];
145+
146+
Ok(UpdatedPset {
147+
pset: pset.to_string(),
148+
updated_values,
149+
})
150+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
// Copyright 2025 Andrew Poelstra
22
// SPDX-License-Identifier: CC0-1.0
33

4+
mod finalize;
45
mod update_input;
56

67
use crate::cmd;
8+
use serde::Serialize;
9+
10+
#[derive(Serialize)]
11+
struct UpdatedPset {
12+
pset: String,
13+
updated_values: Vec<&'static str>,
14+
}
715

816
pub fn cmd<'a>() -> clap::App<'a, 'a> {
917
cmd::subcommand_group("pset", "manipulate PSETs for spending from Simplicity programs")
18+
.subcommand(self::finalize::cmd())
1019
.subcommand(self::update_input::cmd())
1120
}
1221

1322
pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
1423
match matches.subcommand() {
24+
("finalize", Some(m)) => self::finalize::exec(m),
1525
("update-input", Some(m)) => self::update_input::exec(m),
1626
(_, _) => unreachable!("clap prints help"),
1727
};

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,10 @@ use core::str::FromStr;
77
use std::collections::BTreeMap;
88

99
use super::super::{Error, ErrorExt as _};
10+
use super::UpdatedPset;
1011

1112
use elements::schnorr::XOnlyPublicKey;
1213
use hal_simplicity::hal_simplicity::taproot_spend_info;
13-
use serde::Serialize;
14-
15-
#[derive(Serialize)]
16-
struct UpdatedPset {
17-
pset: String,
18-
updated_values: Vec<&'static str>,
19-
}
2014

2115
pub fn cmd<'a>() -> clap::App<'a, 'a> {
2216
cmd::subcommand("update-input", "Attach UTXO data to a PSET input")

0 commit comments

Comments
 (0)