hal_simplicity/actions/simplicity/
sighash.rs

1use crate::simplicity::bitcoin::secp256k1::{
2	schnorr, Keypair, Message, Secp256k1, SecretKey, XOnlyPublicKey,
3};
4use crate::simplicity::elements;
5use crate::simplicity::elements::hashes::sha256;
6use crate::simplicity::elements::hex::FromHex;
7
8use crate::simplicity::jet::elements::ElementsUtxo;
9use crate::simplicity::Cmr;
10
11use elements::bitcoin::secp256k1;
12use elements::hashes::Hash as _;
13use elements::pset::PartiallySignedTransaction;
14use serde::Serialize;
15
16use crate::simplicity::elements::taproot::ControlBlock;
17use crate::simplicity::jet::elements::ElementsEnv;
18
19use crate::actions::simplicity::ParseElementsUtxoError;
20
21#[derive(Debug, thiserror::Error)]
22pub enum SimplicitySighashError {
23	#[error("failed extracting transaction from PSET: {0}")]
24	PsetExtraction(elements::pset::Error),
25
26	#[error("invalid transaction hex: {0}")]
27	TransactionHexParsing(elements::hex::Error),
28
29	#[error("invalid transaction decoding: {0}")]
30	TransactionDecoding(elements::encode::Error),
31
32	#[error("invalid input index: {0}")]
33	InputIndexParsing(std::num::ParseIntError),
34
35	#[error("invalid CMR: {0}")]
36	CmrParsing(elements::hashes::hex::HexToArrayError),
37
38	#[error("invalid control block hex: {0}")]
39	ControlBlockHexParsing(elements::hex::Error),
40
41	#[error("invalid control block decoding: {0}")]
42	ControlBlockDecoding(elements::taproot::TaprootError),
43
44	#[error("input index {index} out-of-range for PSET with {n_inputs} inputs")]
45	InputIndexOutOfRange {
46		index: u32,
47		n_inputs: usize,
48	},
49
50	#[error("could not find control block in PSET for CMR {cmr}")]
51	ControlBlockNotFound {
52		cmr: String,
53	},
54
55	#[error("with a raw transaction, control-block must be provided")]
56	ControlBlockRequired,
57
58	#[error("witness UTXO field not populated for input {input}")]
59	WitnessUtxoMissing {
60		input: usize,
61	},
62
63	#[error("with a raw transaction, input-utxos must be provided")]
64	InputUtxosRequired,
65
66	#[error("expected {expected} input UTXOs but got {actual}")]
67	InputUtxoCountMismatch {
68		expected: usize,
69		actual: usize,
70	},
71
72	#[error("invalid genesis hash: {0}")]
73	GenesisHashParsing(elements::hashes::hex::HexToArrayError),
74
75	#[error("invalid secret key: {0}")]
76	SecretKeyParsing(secp256k1::Error),
77
78	#[error("secret key had public key {derived}, but was passed explicit public key {provided}")]
79	PublicKeyMismatch {
80		derived: String,
81		provided: String,
82	},
83
84	#[error("invalid public key: {0}")]
85	PublicKeyParsing(secp256k1::Error),
86
87	#[error("invalid signature: {0}")]
88	SignatureParsing(secp256k1::Error),
89
90	#[error("if signature is provided, public-key must be provided as well")]
91	SignatureWithoutPublicKey,
92
93	#[error("invalid input UTXO: {0}")]
94	InputUtxoParsing(ParseElementsUtxoError),
95}
96
97#[derive(Serialize)]
98pub struct SighashInfo {
99	pub sighash: sha256::Hash,
100	pub signature: Option<schnorr::Signature>,
101	pub valid_signature: Option<bool>,
102}
103
104/// Compute signature hash for a Simplicity program.
105#[allow(clippy::too_many_arguments)]
106pub fn simplicity_sighash(
107	tx_hex: &str,
108	input_idx: &str,
109	cmr: &str,
110	control_block: Option<&str>,
111	genesis_hash: Option<&str>,
112	secret_key: Option<&str>,
113	public_key: Option<&str>,
114	signature: Option<&str>,
115	input_utxos: Option<&[&str]>,
116) -> Result<SighashInfo, SimplicitySighashError> {
117	let secp = Secp256k1::new();
118
119	// Attempt to decode transaction as PSET first. If it succeeds, we can extract
120	// a lot of information from it. If not, we assume the transaction is hex and
121	// will give the user an error corresponding to this.
122	let pset = tx_hex.parse::<PartiallySignedTransaction>().ok();
123
124	// In the future we should attempt to parse as a Bitcoin program if parsing as
125	// Elements fails. May be tricky/annoying in Rust since Program<Elements> is a
126	// different type from Program<Bitcoin>.
127	let tx = match pset {
128		Some(ref pset) => pset.extract_tx().map_err(SimplicitySighashError::PsetExtraction)?,
129		None => {
130			let tx_bytes =
131				Vec::from_hex(tx_hex).map_err(SimplicitySighashError::TransactionHexParsing)?;
132			elements::encode::deserialize(&tx_bytes)
133				.map_err(SimplicitySighashError::TransactionDecoding)?
134		}
135	};
136	let input_idx: u32 = input_idx.parse().map_err(SimplicitySighashError::InputIndexParsing)?;
137	let cmr: Cmr = cmr.parse().map_err(SimplicitySighashError::CmrParsing)?;
138
139	// If the user specifies a control block, use it. Otherwise query the PSET.
140	let control_block = if let Some(cb) = control_block {
141		let cb_bytes = Vec::from_hex(cb).map_err(SimplicitySighashError::ControlBlockHexParsing)?;
142		// For txes from webide, the internal key in this control block will be the hardcoded
143		// value f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2
144		ControlBlock::from_slice(&cb_bytes).map_err(SimplicitySighashError::ControlBlockDecoding)?
145	} else if let Some(ref pset) = pset {
146		let n_inputs = pset.n_inputs();
147		let input = pset
148			.inputs()
149			.get(input_idx as usize) // cast u32->usize probably fine
150			.ok_or(SimplicitySighashError::InputIndexOutOfRange {
151				index: input_idx,
152				n_inputs,
153			})?;
154
155		let mut control_block = None;
156		for (cb, script_ver) in &input.tap_scripts {
157			if script_ver.1 == simplicity::leaf_version() && &script_ver.0[..] == cmr.as_ref() {
158				control_block = Some(cb.clone());
159			}
160		}
161		match control_block {
162			Some(cb) => cb,
163			None => {
164				return Err(SimplicitySighashError::ControlBlockNotFound {
165					cmr: cmr.to_string(),
166				})
167			}
168		}
169	} else {
170		return Err(SimplicitySighashError::ControlBlockRequired);
171	};
172
173	let input_utxos = if let Some(input_utxos) = input_utxos {
174		input_utxos
175			.iter()
176			.map(|utxo_str| {
177				crate::actions::simplicity::parse_elements_utxo(utxo_str)
178					.map_err(SimplicitySighashError::InputUtxoParsing)
179			})
180			.collect::<Result<Vec<_>, SimplicitySighashError>>()?
181	} else if let Some(ref pset) = pset {
182		pset.inputs()
183			.iter()
184			.enumerate()
185			.map(|(n, input)| match input.witness_utxo {
186				Some(ref utxo) => Ok(ElementsUtxo {
187					script_pubkey: utxo.script_pubkey.clone(),
188					asset: utxo.asset,
189					value: utxo.value,
190				}),
191				None => Err(SimplicitySighashError::WitnessUtxoMissing {
192					input: n,
193				}),
194			})
195			.collect::<Result<Vec<_>, SimplicitySighashError>>()?
196	} else {
197		return Err(SimplicitySighashError::InputUtxosRequired);
198	};
199	if input_utxos.len() != tx.input.len() {
200		return Err(SimplicitySighashError::InputUtxoCountMismatch {
201			expected: tx.input.len(),
202			actual: input_utxos.len(),
203		});
204	}
205
206	// Default to Bitcoin blockhash.
207	let genesis_hash = match genesis_hash {
208		Some(s) => s.parse().map_err(SimplicitySighashError::GenesisHashParsing)?,
209		None => elements::BlockHash::from_byte_array([
210			// copied out of simplicity-webide source
211			0xc1, 0xb1, 0x6a, 0xe2, 0x4f, 0x24, 0x23, 0xae, 0xa2, 0xea, 0x34, 0x55, 0x22, 0x92,
212			0x79, 0x3b, 0x5b, 0x5e, 0x82, 0x99, 0x9a, 0x1e, 0xed, 0x81, 0xd5, 0x6a, 0xee, 0x52,
213			0x8e, 0xda, 0x71, 0xa7,
214		]),
215	};
216
217	let tx_env = ElementsEnv::new(
218		&tx,
219		input_utxos,
220		input_idx,
221		cmr,
222		control_block,
223		None, // FIXME populate this; needs https://github.com/BlockstreamResearch/rust-simplicity/issues/315 first
224		genesis_hash,
225	);
226
227	let (pk, sig) = match (public_key, signature) {
228		(Some(pk), None) => (
229			Some(pk.parse::<XOnlyPublicKey>().map_err(SimplicitySighashError::PublicKeyParsing)?),
230			None,
231		),
232		(Some(pk), Some(sig)) => (
233			Some(pk.parse::<XOnlyPublicKey>().map_err(SimplicitySighashError::PublicKeyParsing)?),
234			Some(
235				sig.parse::<schnorr::Signature>()
236					.map_err(SimplicitySighashError::SignatureParsing)?,
237			),
238		),
239		(None, Some(_)) => return Err(SimplicitySighashError::SignatureWithoutPublicKey),
240		(None, None) => (None, None),
241	};
242
243	let sighash = tx_env.c_tx_env().sighash_all();
244	let sighash_msg = Message::from_digest(sighash.to_byte_array()); // FIXME can remove in next version ofrust-secp
245	Ok(SighashInfo {
246		sighash,
247		signature: match secret_key {
248			Some(sk) => {
249				let sk: SecretKey = sk.parse().map_err(SimplicitySighashError::SecretKeyParsing)?;
250				let keypair = Keypair::from_secret_key(&secp, &sk);
251
252				if let Some(ref pk) = pk {
253					if pk != &keypair.x_only_public_key().0 {
254						return Err(SimplicitySighashError::PublicKeyMismatch {
255							derived: keypair.x_only_public_key().0.to_string(),
256							provided: pk.to_string(),
257						});
258					}
259				}
260
261				Some(secp.sign_schnorr(&sighash_msg, &keypair))
262			}
263			None => None,
264		},
265		valid_signature: match (pk, sig) {
266			(Some(pk), Some(sig)) => Some(secp.verify_schnorr(&sig, &sighash_msg, &pk).is_ok()),
267			_ => None,
268		},
269	})
270}