Skip to content
Draft
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
62 changes: 4 additions & 58 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/actions/simplicity/info.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -18,6 +18,7 @@ pub struct RedeemInfo {
pub witness_hex: String,
pub amr: Amr,
pub ihr: Ihr,
pub bounds: NodeBounds,
}

#[derive(Serialize)]
Expand All @@ -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<Elements> is a
// different type from Program<Bitcoin>.
let program = Program::<jet::Elements>::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();
Expand All @@ -56,6 +56,7 @@ pub fn simplicity_info(
witness_hex,
amr: node.amr(),
ihr: node.ihr(),
bounds: node.bounds(),
}
});

Expand Down
44 changes: 39 additions & 5 deletions src/actions/simplicity/pset/finalize.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand All @@ -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
Expand All @@ -33,6 +40,7 @@ pub fn pset_finalize(
input_idx: &str,
program: &str,
witness: &str,
annex_padding_size: usize,
genesis_hash: Option<&str>,
) -> Result<UpdatedPset, PsetFinalizeError> {
// 1. Parse everything.
Expand All @@ -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::<jet::Elements>::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) =
Expand All @@ -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"];

Expand Down
9 changes: 4 additions & 5 deletions src/actions/simplicity/pset/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -51,10 +51,10 @@ pub struct RunResponse {

struct JetTracker(Vec<JetCall>);

impl<J: jet::Jet> ExecTracker<J> for JetTracker {
impl ExecTracker for JetTracker {
fn visit_node(
&mut self,
node: &simplicity::RedeemNode<J>,
node: &simplicity::RedeemNode,
mut input: FrameIter,
output: NodeOutput,
) {
Expand Down Expand Up @@ -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::<jet::Elements>::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) =
Expand Down
40 changes: 30 additions & 10 deletions src/actions/simplicity/sighash.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::hex_or_base64;
use crate::simplicity::base64;
use crate::simplicity::bitcoin::secp256k1::{
schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey,
};
Expand All @@ -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;
Expand Down Expand Up @@ -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)]
Expand All @@ -113,6 +122,7 @@ pub fn simplicity_sighash(
public_key: Option<&str>,
signature: Option<&str>,
input_utxos: Option<&[&str]>,
annex: Option<&str>,
) -> Result<SighashInfo, SimplicitySighashError> {
let secp = Secp256k1::new();

Expand All @@ -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<Elements> is a
// different type from Program<Bitcoin>.
let tx = match pset {
let mut tx = match pset {
Some(ref pset) => pset.extract_tx().map_err(SimplicitySighashError::PsetExtraction)?,
None => {
let tx_bytes =
Expand Down Expand Up @@ -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) => (
Expand Down
20 changes: 20 additions & 0 deletions src/bin/hal-simplicity/cmd/simplicity/pset/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
])
}

Expand All @@ -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),
Expand Down
Loading
Loading