Skip to content
Open
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
64 changes: 5 additions & 59 deletions Cargo.lock

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

5 changes: 3 additions & 2 deletions 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.2", 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 All @@ -55,8 +55,9 @@ hyper-util = { version = "0.1", features = ["tokio"], optional = true }
http-body-util = { version = "0.1", optional = true }
tokio = { version = "1.48.0", features = ["full"], optional = true }


[lints.clippy]
# Exclude lints we don't think are valuable.
needless_question_mark = "allow" # https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
needless_question_mark = "allow" # https://github.com/rust-lang/rust-clippy/pull/2134
manual_range_contains = "allow" # More readable than clippy's format.
uninlined_format_args = "allow" # Stylistic and dumb and inconsistent
5 changes: 2 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};
use serde::Serialize;

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -44,8 +44,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 Down
20 changes: 20 additions & 0 deletions src/actions/simplicity/pset/decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2025 Andrew Poelstra
// SPDX-License-Identifier: CC0-1.0

use serde_json::Value;

#[derive(Debug, thiserror::Error)]
pub enum PsetDecodeError {
#[error("invalid PSET: {0}")]
PsetParse(elements::pset::ParseError),

#[error("failed to serialize PSET to JSON: {0}")]
JsonSerialize(serde_json::Error),
}

/// Decode a PSET into a JSON representation
pub fn pset_decode(pset_b64: &str) -> Result<Value, PsetDecodeError> {
let pset: elements::pset::PartiallySignedTransaction =
pset_b64.parse().map_err(PsetDecodeError::PsetParse)?;
serde_json::to_value(&pset).map_err(PsetDecodeError::JsonSerialize)
}
5 changes: 2 additions & 3 deletions src/actions/simplicity/pset/finalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: CC0-1.0

use crate::hal_simplicity::Program;
use crate::simplicity::jet;

use super::{execution_environment, PsetError, UpdatedPset};

Expand Down Expand Up @@ -41,8 +40,8 @@ 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)?;

// 2. Extract transaction environment.
let (tx_env, control_block, tap_leaf) =
Expand Down
2 changes: 2 additions & 0 deletions src/actions/simplicity/pset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
// SPDX-License-Identifier: CC0-1.0

mod create;
mod decode;
mod extract;
mod finalize;
mod run;
mod update_input;

pub use create::*;
pub use decode::*;
pub use extract::*;
pub use finalize::*;
pub use run::*;
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
24 changes: 24 additions & 0 deletions src/bin/hal-simplicity/cmd/simplicity/pset/decode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2025 Andrew Poelstra
// SPDX-License-Identifier: CC0-1.0

use super::super::Error;
use crate::cmd;

pub fn cmd<'a>() -> clap::App<'a, 'a> {
cmd::subcommand("decode", "decode a PSET into JSON")
.args(&cmd::opts_networks())
.args(&[cmd::arg("pset", "PSET to decode (base64)").takes_value(true).required(true)])
}

pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
let pset_b64 = matches.value_of("pset").expect("pset mandatory");
match hal_simplicity::actions::simplicity::pset::pset_decode(pset_b64) {
Ok(info) => cmd::print_output(matches, &info),
Err(e) => cmd::print_output(
matches,
&Error {
error: format!("{}", e),
},
),
}
}
3 changes: 3 additions & 0 deletions src/bin/hal-simplicity/cmd/simplicity/pset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: CC0-1.0

mod create;
mod decode;
mod extract;
mod finalize;
mod run;
Expand All @@ -12,6 +13,7 @@ use crate::cmd;
pub fn cmd<'a>() -> clap::App<'a, 'a> {
cmd::subcommand_group("pset", "manipulate PSETs for spending from Simplicity programs")
.subcommand(self::create::cmd())
.subcommand(self::decode::cmd())
.subcommand(self::extract::cmd())
.subcommand(self::finalize::cmd())
.subcommand(self::run::cmd())
Expand All @@ -21,6 +23,7 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> {
pub fn exec<'a>(matches: &clap::ArgMatches<'a>) {
match matches.subcommand() {
("create", Some(m)) => self::create::exec(m),
("decode", Some(m)) => self::decode::exec(m),
("extract", Some(m)) => self::extract::exec(m),
("finalize", Some(m)) => self::finalize::exec(m),
("run", Some(m)) => self::run::exec(m),
Expand Down
1 change: 1 addition & 0 deletions src/bin/hal-simplicity/cmd/simplicity/pset/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub fn cmd<'a>() -> clap::App<'a, 'a> {
"genesis hash of the blockchain the transaction belongs to (hex)",
)
.short("g")
.takes_value(true)
.required(false),
])
}
Expand Down
29 changes: 16 additions & 13 deletions src/hal_simplicity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<J: Jet> {
pub struct Program {
/// A commitment-time program. This should have no hidden branches (though the
/// rust-simplicity encoding allows this) and no witness data.
///
Expand All @@ -19,13 +19,13 @@ pub struct Program<J: Jet> {
/// 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<CommitNode<J>>,
commit_prog: Arc<CommitNode>,
/// 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<Arc<RedeemNode<J>>>,
redeem_prog: Option<Arc<RedeemNode>>,
}

impl<J: Jet> Program<J> {
impl Program {
/// Constructs a program from a hex representation.
///
/// The canonical representation of Simplicity programs is base64, but hex is a
Expand All @@ -37,14 +37,15 @@ impl<J: Jet> Program<J> {
pub fn from_str(prog_b64: &str, wit_hex: Option<&str>) -> Result<Self, ParseError> {
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()?;

Expand All @@ -59,8 +60,10 @@ impl<J: Jet> Program<J> {
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()?,
})
}

Expand All @@ -80,12 +83,12 @@ impl<J: Jet> Program<J> {
}

/// Accessor for the commitment-time program.
pub fn commit_prog(&self) -> &CommitNode<J> {
pub fn commit_prog(&self) -> &CommitNode {
&self.commit_prog
}

/// Accessor for the commitment-time program.
pub fn redeem_node(&self) -> Option<&Arc<RedeemNode<J>>> {
pub fn redeem_node(&self) -> Option<&Arc<RedeemNode>> {
self.redeem_prog.as_ref()
}
}
Expand Down Expand Up @@ -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::<simplicity::jet::Core>::from_str(b64, Some("")).unwrap();
let prog = Program::from_str(b64, Some("")).unwrap();

assert_eq!(
prog.cmr(),
Expand All @@ -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::<simplicity::jet::Core>::from_str(b64, None).unwrap();
let prog = Program::from_str(b64, None).unwrap();

assert_eq!(
prog.cmr(),
Expand Down
Loading