hal_simplicity/
hal_simplicity.rs

1// Copyright 2025 Andrew Poelstra
2// SPDX-License-Identifier: CC0-1.0
3
4use std::sync::Arc;
5
6use simplicity::bitcoin::secp256k1;
7use simplicity::jet::Jet;
8use simplicity::{BitIter, CommitNode, DecodeError, ParseError, RedeemNode};
9
10/// A representation of a hex or base64-encoded Simplicity program, as seen by
11/// hal-simplicity.
12pub struct Program<J: Jet> {
13	/// A commitment-time program. This should have no hidden branches (though the
14	/// rust-simplicity encoding allows this) and no witness data.
15	///
16	/// When parsing a redeem-time program, we first parse it as a commitment-time
17	/// program (which will always succeed, despite the potentially hidden branches)
18	/// because this lets the tool provide information like CMRs or addresses even
19	/// if there is no witness data available or if the program is improperly
20	/// pruned.
21	commit_prog: Arc<CommitNode<J>>,
22	/// A redemption-time program. This should be pruned (though an unpruned or
23	/// improperly-pruned program can still be parsed) and have witness data.
24	redeem_prog: Option<Arc<RedeemNode<J>>>,
25}
26
27impl<J: Jet> Program<J> {
28	/// Constructs a program from a hex representation.
29	///
30	/// The canonical representation of Simplicity programs is base64, but hex is a
31	/// common output mode from rust-simplicity and what you will probably get when
32	/// decoding data straight off the blockchain.
33	pub fn from_str(prog_b64: &str, wit_hex: Option<&str>) -> Result<Self, ParseError> {
34		Ok(Self {
35			commit_prog: CommitNode::from_str(prog_b64)?,
36			redeem_prog: wit_hex.map(|hex| RedeemNode::from_str(prog_b64, hex)).transpose()?,
37		})
38	}
39
40	/// Constructs a program from raw bytes.
41	pub fn from_bytes(prog_bytes: &[u8], wit_bytes: Option<&[u8]>) -> Result<Self, DecodeError> {
42		let prog_iter = BitIter::from(prog_bytes);
43		let wit_iter = wit_bytes.map(BitIter::from);
44		Ok(Self {
45			commit_prog: CommitNode::decode(prog_iter.clone())?,
46			redeem_prog: wit_iter.map(|iter| RedeemNode::decode(prog_iter, iter)).transpose()?,
47		})
48	}
49
50	/// The CMR of the program.
51	pub fn cmr(&self) -> simplicity::Cmr {
52		self.commit_prog.cmr()
53	}
54
55	/// The AMR of the program, if it exists.
56	pub fn amr(&self) -> Option<simplicity::Amr> {
57		self.redeem_prog.as_ref().map(Arc::as_ref).map(RedeemNode::amr)
58	}
59
60	/// The IHR of the program, if it exists.
61	pub fn ihr(&self) -> Option<simplicity::Ihr> {
62		self.redeem_prog.as_ref().map(Arc::as_ref).map(RedeemNode::ihr)
63	}
64
65	/// Accessor for the commitment-time program.
66	pub fn commit_prog(&self) -> &CommitNode<J> {
67		&self.commit_prog
68	}
69
70	/// Accessor for the commitment-time program.
71	pub fn redeem_node(&self) -> Option<&RedeemNode<J>> {
72		self.redeem_prog.as_ref().map(Arc::as_ref)
73	}
74}
75
76// Stolen from simplicity-webide
77fn unspendable_internal_key() -> secp256k1::XOnlyPublicKey {
78	secp256k1::XOnlyPublicKey::from_slice(&[
79		0xf5, 0x91, 0x9f, 0xa6, 0x4c, 0xe4, 0x5f, 0x83, 0x06, 0x84, 0x90, 0x72, 0xb2, 0x6c, 0x1b,
80		0xfd, 0xd2, 0x93, 0x7e, 0x6b, 0x81, 0x77, 0x47, 0x96, 0xff, 0x37, 0x2b, 0xd1, 0xeb, 0x53,
81		0x62, 0xd2,
82	])
83	.expect("key should be valid")
84}
85
86fn script_ver(cmr: simplicity::Cmr) -> (elements::Script, elements::taproot::LeafVersion) {
87	let script = elements::script::Script::from(cmr.as_ref().to_vec());
88	(script, simplicity::leaf_version())
89}
90
91fn taproot_spend_info(cmr: simplicity::Cmr) -> elements::taproot::TaprootSpendInfo {
92	let builder = elements::taproot::TaprootBuilder::new();
93	let (script, version) = script_ver(cmr);
94	let builder = builder.add_leaf_with_ver(0, script, version).expect("tap tree should be valid");
95	builder
96		.finalize(secp256k1::SECP256K1, unspendable_internal_key())
97		.expect("tap tree should be valid")
98}
99
100pub fn elements_address(
101	cmr: simplicity::Cmr,
102	params: &'static elements::AddressParams,
103) -> elements::Address {
104	let info = taproot_spend_info(cmr);
105	let blinder = None;
106	elements::Address::p2tr(
107		secp256k1::SECP256K1,
108		info.internal_key(),
109		info.merkle_root(),
110		blinder,
111		params,
112	)
113}
114
115#[cfg(test)]
116mod tests {
117	use super::*;
118
119	#[test]
120	fn fixed_hex_vector_1() {
121		// Taken from rust-simplicity `assert_lr`. This program works with no witness data.
122		let b64 = "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA";
123		let prog = Program::<simplicity::jet::Core>::from_str(b64, Some("")).unwrap();
124
125		assert_eq!(
126			prog.cmr(),
127			"abdd773fc7a503908739b4a63198416fdd470948830cb5a6516b98fe0a3bfa85".parse().unwrap()
128		);
129		assert_eq!(
130			prog.amr(),
131			Some(
132				"1362ee53ae75218ed51dc4bd46cdbfa585f934ac6c6c3ff787e27dce91ccd80b".parse().unwrap()
133			)
134		);
135		assert_eq!(
136			prog.ihr(),
137			Some(
138				"251c6778129e0f12da3f2388ab30184e815e9d9456b5931e54802a6715d9ca42".parse().unwrap()
139			),
140		);
141
142		// The same program with no provided witness has no AMR or IHR, even though
143		// the provided witness was merely the empty string.
144		//
145		// Maybe in the UI we should detect this case and output some sort of warning?
146		let b64 = "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA";
147		let prog = Program::<simplicity::jet::Core>::from_str(b64, None).unwrap();
148
149		assert_eq!(
150			prog.cmr(),
151			"abdd773fc7a503908739b4a63198416fdd470948830cb5a6516b98fe0a3bfa85".parse().unwrap()
152		);
153		assert_eq!(prog.amr(), None);
154		assert_eq!(prog.ihr(), None);
155	}
156}