|
| 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 | +} |
0 commit comments