hal_simplicity/
hal_simplicity.rs

1// Copyright 2025 Andrew Poelstra
2// SPDX-License-Identifier: CC0-1.0
3
4use std::sync::Arc;
5
6use elements::taproot::{TaprootBuilder, TaprootSpendInfo};
7use simplicity::bitcoin::secp256k1;
8use simplicity::jet::Jet;
9use simplicity::{BitIter, CommitNode, DecodeError, ParseError, RedeemNode};
10
11/// A representation of a hex or base64-encoded Simplicity program, as seen by
12/// hal-simplicity.
13pub struct Program<J: Jet> {
14	/// A commitment-time program. This should have no hidden branches (though the
15	/// rust-simplicity encoding allows this) and no witness data.
16	///
17	/// When parsing a redeem-time program, we first parse it as a commitment-time
18	/// program (which will always succeed, despite the potentially hidden branches)
19	/// because this lets the tool provide information like CMRs or addresses even
20	/// if there is no witness data available or if the program is improperly
21	/// pruned.
22	commit_prog: Arc<CommitNode<J>>,
23	/// A redemption-time program. This should be pruned (though an unpruned or
24	/// improperly-pruned program can still be parsed) and have witness data.
25	redeem_prog: Option<Arc<RedeemNode<J>>>,
26}
27
28impl<J: Jet> Program<J> {
29	/// Constructs a program from a hex representation.
30	///
31	/// The canonical representation of Simplicity programs is base64, but hex is a
32	/// common output mode from rust-simplicity and what you will probably get when
33	/// decoding data straight off the blockchain.
34	///
35	/// The canonical representation of witnesses is hex, but old versions of simc
36	/// (e.g. every released version, and master, as of 2025-10-25) output base64.
37	pub fn from_str(prog_b64: &str, wit_hex: Option<&str>) -> Result<Self, ParseError> {
38		let prog_bytes = crate::hex_or_base64(prog_b64).map_err(ParseError::Base64)?;
39		let iter = BitIter::new(prog_bytes.iter().copied());
40		let commit_prog = CommitNode::decode(iter).map_err(ParseError::Decode)?;
41
42		let redeem_prog = wit_hex
43			.map(|wit_hex| {
44				let wit_bytes = crate::hex_or_base64(wit_hex).map_err(ParseError::Base64)?;
45				let prog_iter = BitIter::new(prog_bytes.into_iter());
46				let wit_iter = BitIter::new(wit_bytes.into_iter());
47				RedeemNode::decode(prog_iter, wit_iter).map_err(ParseError::Decode)
48			})
49			.transpose()?;
50
51		Ok(Self {
52			commit_prog,
53			redeem_prog,
54		})
55	}
56
57	/// Constructs a program from raw bytes.
58	pub fn from_bytes(prog_bytes: &[u8], wit_bytes: Option<&[u8]>) -> Result<Self, DecodeError> {
59		let prog_iter = BitIter::from(prog_bytes);
60		let wit_iter = wit_bytes.map(BitIter::from);
61		Ok(Self {
62			commit_prog: CommitNode::decode(prog_iter.clone())?,
63			redeem_prog: wit_iter.map(|iter| RedeemNode::decode(prog_iter, iter)).transpose()?,
64		})
65	}
66
67	/// The CMR of the program.
68	pub fn cmr(&self) -> simplicity::Cmr {
69		self.commit_prog.cmr()
70	}
71
72	/// The AMR of the program, if it exists.
73	pub fn amr(&self) -> Option<simplicity::Amr> {
74		self.redeem_prog.as_ref().map(Arc::as_ref).map(RedeemNode::amr)
75	}
76
77	/// The IHR of the program, if it exists.
78	pub fn ihr(&self) -> Option<simplicity::Ihr> {
79		self.redeem_prog.as_ref().map(Arc::as_ref).map(RedeemNode::ihr)
80	}
81
82	/// Accessor for the commitment-time program.
83	pub fn commit_prog(&self) -> &CommitNode<J> {
84		&self.commit_prog
85	}
86
87	/// Accessor for the commitment-time program.
88	pub fn redeem_node(&self) -> Option<&Arc<RedeemNode<J>>> {
89		self.redeem_prog.as_ref()
90	}
91}
92
93/// The unspendable internal key specified in BIP-0341.
94///
95/// This is a "nothing up my sleeve" (NUMS) point. See the text of BIP-0341
96/// for its derivation.
97#[rustfmt::skip] // mangles byte vectors
98pub fn unspendable_internal_key() -> secp256k1::XOnlyPublicKey {
99	secp256k1::XOnlyPublicKey::from_slice(&[
100		0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, 0x5e,
101		0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, 
102	])
103	.expect("key should be valid")
104}
105
106fn script_ver(cmr: simplicity::Cmr) -> (elements::Script, elements::taproot::LeafVersion) {
107	let script = elements::script::Script::from(cmr.as_ref().to_vec());
108	(script, simplicity::leaf_version())
109}
110
111/// Given a Simplicity CMR and an internal key, computes the [`TaprootSpendInfo`]
112/// for a Taptree with this CMR as its single leaf.
113pub fn taproot_spend_info(
114	internal_key: secp256k1::XOnlyPublicKey,
115	state: Option<[u8; 32]>,
116	cmr: simplicity::Cmr,
117) -> TaprootSpendInfo {
118	let builder = TaprootBuilder::new();
119	let (script, version) = script_ver(cmr);
120	let builder = if let Some(state) = state {
121		use elements::hashes::{sha256, Hash as _, HashEngine as _};
122		let tag = sha256::Hash::hash(b"TapData");
123		let mut eng = sha256::Hash::engine();
124		eng.input(tag.as_byte_array());
125		eng.input(tag.as_byte_array());
126		eng.input(&state);
127		let state_hash = sha256::Hash::from_engine(eng);
128
129		builder
130			.add_leaf_with_ver(1, script, version)
131			.expect("tap tree should be valid")
132			.add_hidden(1, state_hash)
133			.expect("tap tree should be valid")
134	} else {
135		builder.add_leaf_with_ver(0, script, version).expect("tap tree should be valid")
136	};
137
138	builder.finalize(secp256k1::SECP256K1, internal_key).expect("tap tree should be valid")
139}
140
141/// Given a Simplicity CMR, computes an unconfidential Elements address
142/// (for the given network) corresponding to a Taptree with an unspendable
143/// internal key and this CMR as its single leaf.
144pub fn elements_address(
145	cmr: simplicity::Cmr,
146	state: Option<[u8; 32]>,
147	params: &'static elements::AddressParams,
148) -> elements::Address {
149	let info = taproot_spend_info(unspendable_internal_key(), state, cmr);
150	let blinder = None;
151	elements::Address::p2tr(
152		secp256k1::SECP256K1,
153		info.internal_key(),
154		info.merkle_root(),
155		blinder,
156		params,
157	)
158}
159
160#[cfg(test)]
161mod tests {
162	use super::*;
163
164	#[test]
165	fn fixed_hex_vector_1() {
166		// Taken from rust-simplicity `assert_lr`. This program works with no witness data.
167		let b64 = "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA";
168		let prog = Program::<simplicity::jet::Core>::from_str(b64, Some("")).unwrap();
169
170		assert_eq!(
171			prog.cmr(),
172			"abdd773fc7a503908739b4a63198416fdd470948830cb5a6516b98fe0a3bfa85".parse().unwrap()
173		);
174		assert_eq!(
175			prog.amr(),
176			Some(
177				"1362ee53ae75218ed51dc4bd46cdbfa585f934ac6c6c3ff787e27dce91ccd80b".parse().unwrap()
178			)
179		);
180		assert_eq!(
181			prog.ihr(),
182			Some(
183				"251c6778129e0f12da3f2388ab30184e815e9d9456b5931e54802a6715d9ca42".parse().unwrap()
184			),
185		);
186
187		// The same program with no provided witness has no AMR or IHR, even though
188		// the provided witness was merely the empty string.
189		//
190		// Maybe in the UI we should detect this case and output some sort of warning?
191		let b64 = "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA";
192		let prog = Program::<simplicity::jet::Core>::from_str(b64, None).unwrap();
193
194		assert_eq!(
195			prog.cmr(),
196			"abdd773fc7a503908739b4a63198416fdd470948830cb5a6516b98fe0a3bfa85".parse().unwrap()
197		);
198		assert_eq!(prog.amr(), None);
199		assert_eq!(prog.ihr(), None);
200	}
201}