hal_simplicity/actions/simplicity/pset/
update_input.rs

1// Copyright 2025 Andrew Poelstra
2// SPDX-License-Identifier: CC0-1.0
3
4use core::str::FromStr;
5use std::collections::BTreeMap;
6
7use elements::bitcoin::secp256k1;
8use elements::schnorr::XOnlyPublicKey;
9use simplicity::hex::parse::FromHex as _;
10
11use crate::hal_simplicity::taproot_spend_info;
12
13use super::{PsetError, UpdatedPset};
14
15use crate::actions::simplicity::ParseElementsUtxoError;
16
17#[derive(Debug, thiserror::Error)]
18pub enum PsetUpdateInputError {
19	#[error(transparent)]
20	SharedError(#[from] PsetError),
21
22	#[error("invalid PSET: {0}")]
23	PsetDecode(elements::pset::ParseError),
24
25	#[error("invalid input index: {0}")]
26	InputIndexParse(std::num::ParseIntError),
27
28	#[error("input index {index} out-of-range for PSET with {total} inputs")]
29	InputIndexOutOfRange {
30		index: usize,
31		total: usize,
32	},
33
34	#[error("invalid CMR: {0}")]
35	CmrParse(elements::hashes::hex::HexToArrayError),
36
37	#[error("invalid internal key: {0}")]
38	InternalKeyParse(secp256k1::Error),
39
40	#[error("internal key must be present if CMR is; PSET requires a control block for each CMR, which in turn requires the internal key. If you don't know the internal key, good chance it is the BIP-0341 'unspendable key' 50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 or the web IDE's 'unspendable key' (highly discouraged for use in production) of f5919fa64ce45f8306849072b26c1bfdd2937e6b81774796ff372bd1eb5362d2")]
41	MissingInternalKey,
42
43	#[error("input UTXO does not appear to be a Taproot output")]
44	NotTaprootOutput,
45
46	#[error("invalid state commitment: {0}")]
47	StateParse(elements::hashes::hex::HexToArrayError),
48
49	#[error("CMR and internal key imply output key {output_key}, which does not match input scriptPubKey {script_pubkey}")]
50	OutputKeyMismatch {
51		output_key: String,
52		script_pubkey: String,
53	},
54
55	#[error("invalid elements UTXO: {0}")]
56	ElementsUtxoParse(ParseElementsUtxoError),
57}
58
59/// Attach UTXO data to a PSET input
60pub fn pset_update_input(
61	pset_b64: &str,
62	input_idx: &str,
63	input_utxo: &str,
64	internal_key: Option<&str>,
65	cmr: Option<&str>,
66	state: Option<&str>,
67) -> Result<UpdatedPset, PsetUpdateInputError> {
68	let mut pset: elements::pset::PartiallySignedTransaction =
69		pset_b64.parse().map_err(PsetUpdateInputError::PsetDecode)?;
70	let input_idx: usize = input_idx.parse().map_err(PsetUpdateInputError::InputIndexParse)?;
71	let input_utxo = super::super::parse_elements_utxo(input_utxo)
72		.map_err(PsetUpdateInputError::ElementsUtxoParse)?;
73
74	let n_inputs = pset.n_inputs();
75	let input = pset.inputs_mut().get_mut(input_idx).ok_or_else(|| {
76		PsetUpdateInputError::InputIndexOutOfRange {
77			index: input_idx,
78			total: n_inputs,
79		}
80	})?;
81
82	let cmr =
83		cmr.map(simplicity::Cmr::from_str).transpose().map_err(PsetUpdateInputError::CmrParse)?;
84	let internal_key = internal_key
85		.map(XOnlyPublicKey::from_str)
86		.transpose()
87		.map_err(PsetUpdateInputError::InternalKeyParse)?;
88	if cmr.is_some() && internal_key.is_none() {
89		return Err(PsetUpdateInputError::MissingInternalKey);
90	}
91
92	if !input_utxo.script_pubkey.is_v1_p2tr() {
93		return Err(PsetUpdateInputError::NotTaprootOutput);
94	}
95
96	// FIXME state is meaningless without CMR; should we warn here
97	// FIXME also should we warn if you don't provide a CMR? seems like if you're calling `simplicity pset update-input`
98	//   you probably have a simplicity program right? maybe we should even provide a --no-cmr flag
99	let state =
100		state.map(<[u8; 32]>::from_hex).transpose().map_err(PsetUpdateInputError::StateParse)?;
101
102	let mut updated_values = vec![];
103	if let Some(internal_key) = internal_key {
104		updated_values.push("tap_internal_key");
105		input.tap_internal_key = Some(internal_key);
106		// FIXME should we check whether we're using the "bad" internal key
107		//  from the web IDE, and warn or something?
108		if let Some(cmr) = cmr {
109			// Guess that the given program is the only Tapleaf. This is the case for addresses
110			// generated from the web IDE, and from `hal-simplicity simplicity info`, and for
111			// most "test" scenarios. We need to design an API to handle more general cases.
112			let spend_info = taproot_spend_info(internal_key, state, cmr);
113			if spend_info.output_key().as_inner().serialize() != input_utxo.script_pubkey[2..] {
114				// If our guess was wrong, at least error out..
115				return Err(PsetUpdateInputError::OutputKeyMismatch {
116					output_key: format!("{}", spend_info.output_key().as_inner()),
117					script_pubkey: format!("{}", input_utxo.script_pubkey),
118				});
119			}
120
121			// FIXME these unwraps and clones should be fixed by a new rust-bitcoin taproot API
122			let script_ver = spend_info.as_script_map().keys().next().unwrap();
123			let cb = spend_info.control_block(script_ver).unwrap();
124			input.tap_merkle_root = spend_info.merkle_root();
125			input.tap_scripts = BTreeMap::new();
126			input.tap_scripts.insert(cb, script_ver.clone());
127			updated_values.push("tap_merkle_root");
128			updated_values.push("tap_scripts");
129		}
130	}
131
132	// FIXME should we bother erroring or warning if we clobber this or other fields?
133	input.witness_utxo = Some(elements::TxOut {
134		asset: input_utxo.asset,
135		value: input_utxo.value,
136		nonce: elements::confidential::Nonce::Null, // not in UTXO set, irrelevant to PSET
137		script_pubkey: input_utxo.script_pubkey,
138		witness: elements::TxOutWitness::empty(), // not in UTXO set, irrelevant to PSET
139	});
140	updated_values.push("witness_utxo");
141
142	Ok(UpdatedPset {
143		pset: pset.to_string(),
144		updated_values,
145	})
146}