diff --git a/Cargo.lock b/Cargo.lock index f77b316..e71a00f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - [[package]] name = "ansi_term" version = "0.11.0" @@ -592,12 +583,6 @@ dependencies = [ "cfg-if 0.1.7", ] -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - [[package]] name = "miniscript" version = "12.3.2" @@ -871,50 +856,12 @@ dependencies = [ "redox_syscall 0.1.51", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "ryu" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" -[[package]] -name = "santiago" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" -dependencies = [ - "regex", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -1032,9 +979,9 @@ dependencies = [ [[package]] name = "simplicity-lang" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +checksum = "13ed081e3046d66c146d7201bcbf3b655ca3436cb83f6efc26d7895bd2b79d06" dependencies = [ "bitcoin", "bitcoin_hashes 0.14.0", @@ -1044,16 +991,15 @@ dependencies = [ "ghost-cell", "hex-conservative 0.2.1", "miniscript", - "santiago", "serde", "simplicity-sys", ] [[package]] name = "simplicity-sys" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875630d128f19818161cefe0a3d910b6aae921d8246711db574a689cb2c11747" +checksum = "96d1ec5477c7650b8ef511aa56dccb28f2e8cdb6e87f260ecffdaf0ebfef2d3b" dependencies = [ "bitcoin_hashes 0.14.0", "cc", diff --git a/Cargo.toml b/Cargo.toml index d8154d0..8c0af83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,7 +45,7 @@ serde_yaml = "0.8.8" hex = "0.3.2" elements = { version = "0.25.3", features = [ "serde", "base64" ] } -simplicity = { package = "simplicity-lang", version = "0.7.0", features = [ "base64", "serde" ] } +simplicity = { package = "simplicity-lang", version = "0.8.0", features = [ "base64", "serde" ] } thiserror = "2.0.17" # Daemon-only dependencies diff --git a/src/actions/simplicity/info.rs b/src/actions/simplicity/info.rs index c1754e3..4303f8d 100644 --- a/src/actions/simplicity/info.rs +++ b/src/actions/simplicity/info.rs @@ -1,6 +1,6 @@ use crate::hal_simplicity::{elements_address, Program}; use crate::simplicity::hex::parse::FromHex as _; -use crate::simplicity::{jet, Amr, Cmr, Ihr}; +use crate::simplicity::{Amr, Cmr, Ihr, NodeBounds}; use serde::Serialize; #[derive(Debug, thiserror::Error)] @@ -18,6 +18,7 @@ pub struct RedeemInfo { pub witness_hex: String, pub amr: Amr, pub ihr: Ihr, + pub bounds: NodeBounds, } #[derive(Serialize)] @@ -44,8 +45,7 @@ pub fn simplicity_info( // In the future we should attempt to parse as a Bitcoin program if parsing as // Elements fails. May be tricky/annoying in Rust since Program is a // different type from Program. - let program = Program::::from_str(program, witness) - .map_err(SimplicityInfoError::ProgramParse)?; + let program = Program::from_str(program, witness).map_err(SimplicityInfoError::ProgramParse)?; let redeem_info = program.redeem_node().map(|node| { let disp = node.display(); @@ -56,6 +56,7 @@ pub fn simplicity_info( witness_hex, amr: node.amr(), ihr: node.ihr(), + bounds: node.bounds(), } }); diff --git a/src/actions/simplicity/pset/finalize.rs b/src/actions/simplicity/pset/finalize.rs index 8b7d984..9c55a66 100644 --- a/src/actions/simplicity/pset/finalize.rs +++ b/src/actions/simplicity/pset/finalize.rs @@ -1,8 +1,9 @@ // Copyright 2025 Andrew Poelstra // SPDX-License-Identifier: CC0-1.0 +use elements::bitcoin::taproot::TAPROOT_ANNEX_PREFIX; + use crate::hal_simplicity::Program; -use crate::simplicity::jet; use super::{execution_environment, PsetError, UpdatedPset}; @@ -25,6 +26,12 @@ pub enum PsetFinalizeError { #[error("failed to prune program: {0}")] ProgramPrune(simplicity::bit_machine::ExecutionError), + + #[error("program cost exceeds the witness budget: needs an annex of at least {required} bytes (including the 0x50 prefix), got {provided}")] + CostExceedsBudget { + required: usize, + provided: usize, + }, } /// Attach a Simplicity program and witness to a PSET input @@ -33,6 +40,7 @@ pub fn pset_finalize( input_idx: &str, program: &str, witness: &str, + annex_padding_size: usize, genesis_hash: Option<&str>, ) -> Result { // 1. Parse everything. @@ -41,8 +49,21 @@ pub fn pset_finalize( let input_idx: u32 = input_idx.parse().map_err(PsetFinalizeError::InputIndexParse)?; let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems - let program = Program::::from_str(program, Some(witness)) - .map_err(PsetFinalizeError::ProgramParse)?; + let program = + Program::from_str(program, Some(witness)).map_err(PsetFinalizeError::ProgramParse)?; + + let annex = (annex_padding_size > 0).then(|| { + let mut annex = vec![0u8; annex_padding_size]; + annex[0] = TAPROOT_ANNEX_PREFIX; + annex + }); + + // The sighash commits to the annex, so the program has to run against a transaction + // that has it. rust-simplicity reads the annex from the input's witness, so stage it + // there. This also discards the witness from any earlier finalization. + if let Some(input) = pset.inputs_mut().get_mut(input_idx_usize) { + input.final_script_witness = annex.clone().map(|annex| vec![annex]); + } // 2. Extract transaction environment. let (tx_env, control_block, tap_leaf) = @@ -54,9 +75,22 @@ pub fn pset_finalize( let pruned = redeem_node.prune(&tx_env).map_err(PsetFinalizeError::ProgramPrune)?; let (prog, witness) = pruned.to_vec_with_witness(); + let mut final_script_witness = vec![witness, prog, tap_leaf.into_bytes(), cb_serialized]; + + // 4. Check that the witness pays for the program's cost. The required padding is + // computed before adding the annex so that it is the total annex size needed. + let cost = pruned.bounds().cost; + let required = cost.get_padding(&final_script_witness).map_or(0, |annex| annex.len()); + final_script_witness.extend(annex); + if !cost.is_budget_valid(&final_script_witness) { + return Err(PsetFinalizeError::CostExceedsBudget { + required, + provided: annex_padding_size, + }); + } + // 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]); + pset.inputs_mut()[input_idx_usize].final_script_witness = Some(final_script_witness); let updated_values = vec!["final_script_witness"]; diff --git a/src/actions/simplicity/pset/run.rs b/src/actions/simplicity/pset/run.rs index 5105fd3..7c10882 100644 --- a/src/actions/simplicity/pset/run.rs +++ b/src/actions/simplicity/pset/run.rs @@ -5,8 +5,8 @@ use serde::Serialize; use crate::hal_simplicity::Program; use crate::simplicity::bit_machine::{BitMachine, ExecTracker, FrameIter, NodeOutput}; +use crate::simplicity::node; use crate::simplicity::Value; -use crate::simplicity::{jet, node}; use super::{execution_environment, PsetError}; @@ -51,10 +51,10 @@ pub struct RunResponse { struct JetTracker(Vec); -impl ExecTracker for JetTracker { +impl ExecTracker for JetTracker { fn visit_node( &mut self, - node: &simplicity::RedeemNode, + node: &simplicity::RedeemNode, mut input: FrameIter, output: NodeOutput, ) { @@ -107,8 +107,7 @@ pub fn pset_run( let input_idx: u32 = input_idx.parse().map_err(PsetRunError::InputIndexParse)?; let input_idx_usize = input_idx as usize; // 32->usize cast ok on almost all systems - let program = Program::::from_str(program, Some(witness)) - .map_err(PsetRunError::ProgramParse)?; + let program = Program::from_str(program, Some(witness)).map_err(PsetRunError::ProgramParse)?; // 2. Extract transaction environment. let (tx_env, _control_block, _tap_leaf) = diff --git a/src/actions/simplicity/sighash.rs b/src/actions/simplicity/sighash.rs index 6ac9640..0f924f2 100644 --- a/src/actions/simplicity/sighash.rs +++ b/src/actions/simplicity/sighash.rs @@ -1,3 +1,5 @@ +use crate::hex_or_base64; +use crate::simplicity::base64; use crate::simplicity::bitcoin::secp256k1::{ schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey, }; @@ -9,6 +11,7 @@ use crate::simplicity::jet::elements::ElementsUtxo; use crate::simplicity::Cmr; use elements::bitcoin::secp256k1; +use elements::bitcoin::taproot::TAPROOT_ANNEX_PREFIX; use elements::hashes::Hash as _; use elements::pset::PartiallySignedTransaction; use serde::Serialize; @@ -92,6 +95,12 @@ pub enum SimplicitySighashError { #[error("invalid input UTXO: {0}")] InputUtxoParsing(ParseElementsUtxoError), + + #[error("invalid annex: {0}")] + AnnexParsing(base64::DecodeError), + + #[error("annex must start with the 0x50 prefix byte")] + AnnexMissingPrefix, } #[derive(Serialize)] @@ -113,6 +122,7 @@ pub fn simplicity_sighash( public_key: Option<&str>, signature: Option<&str>, input_utxos: Option<&[&str]>, + annex: Option<&str>, ) -> Result { let secp = Secp256k1::new(); @@ -124,7 +134,7 @@ pub fn simplicity_sighash( // In the future we should attempt to parse as a Bitcoin program if parsing as // Elements fails. May be tricky/annoying in Rust since Program is a // different type from Program. - let tx = match pset { + let mut tx = match pset { Some(ref pset) => pset.extract_tx().map_err(SimplicitySighashError::PsetExtraction)?, None => { let tx_bytes = @@ -214,15 +224,25 @@ pub fn simplicity_sighash( ]), }; - 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, - ); + // The sighash commits to the annex. rust-simplicity reads it from the last element of the + // input's witness, not from the `ElementsEnv` annex parameter, so put it there. + if let Some(annex) = annex { + let annex = hex_or_base64(annex).map_err(SimplicitySighashError::AnnexParsing)?; + if annex.first() != Some(&TAPROOT_ANNEX_PREFIX) { + return Err(SimplicitySighashError::AnnexMissingPrefix); + } + let n_inputs = tx.input.len(); + let input = tx.input.get_mut(input_idx as usize).ok_or( + SimplicitySighashError::InputIndexOutOfRange { + index: input_idx, + n_inputs, + }, + )?; + input.witness.script_witness = vec![annex]; + } + + let tx_env = + ElementsEnv::new(&tx, input_utxos, input_idx, cmr, control_block, None, genesis_hash); let (pk, sig) = match (public_key, signature) { (Some(pk), None) => ( diff --git a/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs b/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs index 8e3a694..3970ddd 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs @@ -22,6 +22,14 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> { ) .short("g") .required(false), + cmd::opt( + "padding-bytes", + "size of the annex to pad the witness with so the program's cost fits its budget, including the 0x50 prefix byte (decimal)", + ) + .short("a") + .alias("padding") + .takes_value(true) + .required(false), ]) } @@ -31,12 +39,24 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { 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"); + let padding_bytes = match matches.value_of("padding-bytes").map(str::parse).transpose() { + Ok(n) => n.unwrap_or(0), + Err(e) => { + return cmd::print_output( + matches, + &Error { + error: format!("invalid padding-bytes: {}", e), + }, + ) + } + }; match hal_simplicity::actions::simplicity::pset::pset_finalize( pset_b64, input_idx, program, witness, + padding_bytes, genesis_hash, ) { Ok(info) => cmd::print_output(matches, &info), diff --git a/src/bin/hal-simplicity/cmd/simplicity/sighash.rs b/src/bin/hal-simplicity/cmd/simplicity/sighash.rs index a685f9e..930b0ae 100644 --- a/src/bin/hal-simplicity/cmd/simplicity/sighash.rs +++ b/src/bin/hal-simplicity/cmd/simplicity/sighash.rs @@ -38,6 +38,9 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> { .multiple(true) .number_of_values(1) .required(false), + cmd::opt("annex", "taproot annex of the input, starting with the 0x50 prefix byte. Used to pad the witness of CPU-intensive programs; must match the annex added by `pset finalize --padding-bytes` (hex or base64)") + .takes_value(true) + .required(false), ]) } @@ -51,6 +54,7 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { let public_key = matches.value_of("public-key"); let signature = matches.value_of("signature"); let input_utxos: Option> = matches.values_of("input-utxo").map(|vals| vals.collect()); + let annex = matches.value_of("annex"); match hal_simplicity::actions::simplicity::simplicity_sighash( tx_hex, @@ -62,6 +66,7 @@ pub fn exec<'a>(matches: &clap::ArgMatches<'a>) { public_key, signature, input_utxos.as_deref(), + annex, ) { Ok(info) => cmd::print_output(matches, &info), Err(e) => cmd::print_output( diff --git a/src/daemon/handler.rs b/src/daemon/handler.rs index 2298b8d..afc23c3 100644 --- a/src/daemon/handler.rs +++ b/src/daemon/handler.rs @@ -160,6 +160,7 @@ impl RpcHandler for DefaultRpcHandler { req.public_key.as_deref(), req.signature.as_deref(), input_utxos.as_deref(), + req.annex.as_deref(), ) .map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?; serialize_result(result) @@ -190,6 +191,7 @@ impl RpcHandler for DefaultRpcHandler { &req.input_index.to_string(), &req.program, &req.witness, + req.padding_bytes.unwrap_or(0), req.genesis_hash.as_deref(), ) .map_err(|e| RpcError::custom(ErrorCode::InternalError.code(), e.to_string()))?; diff --git a/src/daemon/types.rs b/src/daemon/types.rs index cddf6bf..4da90bc 100644 --- a/src/daemon/types.rs +++ b/src/daemon/types.rs @@ -140,6 +140,7 @@ pub struct SimplicitySighashRequest { pub public_key: Option, pub signature: Option, pub input_utxos: Option>, + pub annex: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -180,6 +181,7 @@ pub struct PsetFinalizeRequest { pub program: String, pub witness: String, pub genesis_hash: Option, + pub padding_bytes: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/src/hal_simplicity.rs b/src/hal_simplicity.rs index 13d2e6e..9783a9f 100644 --- a/src/hal_simplicity.rs +++ b/src/hal_simplicity.rs @@ -5,12 +5,12 @@ use std::sync::Arc; use elements::taproot::{TaprootBuilder, TaprootSpendInfo}; use simplicity::bitcoin::secp256k1; -use simplicity::jet::Jet; +use simplicity::jet::Elements; use simplicity::{BitIter, CommitNode, DecodeError, ParseError, RedeemNode}; /// A representation of a hex or base64-encoded Simplicity program, as seen by /// hal-simplicity. -pub struct Program { +pub struct Program { /// A commitment-time program. This should have no hidden branches (though the /// rust-simplicity encoding allows this) and no witness data. /// @@ -19,13 +19,13 @@ pub struct Program { /// because this lets the tool provide information like CMRs or addresses even /// if there is no witness data available or if the program is improperly /// pruned. - commit_prog: Arc>, + commit_prog: Arc, /// A redemption-time program. This should be pruned (though an unpruned or /// improperly-pruned program can still be parsed) and have witness data. - redeem_prog: Option>>, + redeem_prog: Option>, } -impl Program { +impl Program { /// Constructs a program from a hex representation. /// /// The canonical representation of Simplicity programs is base64, but hex is a @@ -37,14 +37,15 @@ impl Program { pub fn from_str(prog_b64: &str, wit_hex: Option<&str>) -> Result { let prog_bytes = crate::hex_or_base64(prog_b64).map_err(ParseError::Base64)?; let iter = BitIter::new(prog_bytes.iter().copied()); - let commit_prog = CommitNode::decode(iter).map_err(ParseError::Decode)?; + let commit_prog = CommitNode::decode::<_, Elements>(iter).map_err(ParseError::Decode)?; let redeem_prog = wit_hex .map(|wit_hex| { let wit_bytes = crate::hex_or_base64(wit_hex).map_err(ParseError::Base64)?; let prog_iter = BitIter::new(prog_bytes.into_iter()); let wit_iter = BitIter::new(wit_bytes.into_iter()); - RedeemNode::decode(prog_iter, wit_iter).map_err(ParseError::Decode) + RedeemNode::decode::<_, _, Elements>(prog_iter, wit_iter) + .map_err(ParseError::Decode) }) .transpose()?; @@ -59,8 +60,10 @@ impl Program { let prog_iter = BitIter::from(prog_bytes); let wit_iter = wit_bytes.map(BitIter::from); Ok(Self { - commit_prog: CommitNode::decode(prog_iter.clone())?, - redeem_prog: wit_iter.map(|iter| RedeemNode::decode(prog_iter, iter)).transpose()?, + commit_prog: CommitNode::decode::<_, Elements>(prog_iter.clone())?, + redeem_prog: wit_iter + .map(|iter| RedeemNode::decode::<_, _, Elements>(prog_iter, iter)) + .transpose()?, }) } @@ -80,12 +83,12 @@ impl Program { } /// Accessor for the commitment-time program. - pub fn commit_prog(&self) -> &CommitNode { + pub fn commit_prog(&self) -> &CommitNode { &self.commit_prog } /// Accessor for the commitment-time program. - pub fn redeem_node(&self) -> Option<&Arc>> { + pub fn redeem_node(&self) -> Option<&Arc> { self.redeem_prog.as_ref() } } @@ -165,7 +168,7 @@ mod tests { fn fixed_hex_vector_1() { // Taken from rust-simplicity `assert_lr`. This program works with no witness data. let b64 = "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA"; - let prog = Program::::from_str(b64, Some("")).unwrap(); + let prog = Program::from_str(b64, Some("")).unwrap(); assert_eq!( prog.cmr(), @@ -189,7 +192,7 @@ mod tests { // // Maybe in the UI we should detect this case and output some sort of warning? let b64 = "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA"; - let prog = Program::::from_str(b64, None).unwrap(); + let prog = Program::from_str(b64, None).unwrap(); assert_eq!( prog.cmr(),