|
| 1 | +//! Single-key transparent PSBT signing helpers. |
| 2 | +//! |
| 3 | +//! These operate on plain `miniscript::bitcoin::psbt::Psbt` and have no `wasm_bindgen` surface — |
| 4 | +//! they're shared by `wasm::psbt::WrapPsbt`'s wasm-bindgen methods and directly by |
| 5 | +//! `wasm-utxo-cli`'s `psbt` build commands (which never go through the wasm layer). No new |
| 6 | +//! sighash code: these compose the existing `Psbt::sign`/`sign_forkid`/`sign_zcash` and |
| 7 | +//! `PsbtExt` primitives that already back `fixed_script_wallet`. |
| 8 | +
|
| 9 | +use crate::zcash::transaction::ZcashTransactionParts; |
| 10 | +use miniscript::bitcoin::bip32::Fingerprint; |
| 11 | +use miniscript::bitcoin::secp256k1::{Secp256k1, Signing, Verification}; |
| 12 | +use miniscript::bitcoin::{ |
| 13 | + psbt, Amount, OutPoint, PrivateKey, PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, |
| 14 | + Txid, XOnlyPublicKey, |
| 15 | +}; |
| 16 | +use miniscript::descriptor::{SinglePub, SinglePubKey}; |
| 17 | +use miniscript::psbt::{PsbtExt, PsbtInputExt}; |
| 18 | +use miniscript::{DefiniteDescriptorKey, Descriptor, DescriptorPublicKey, ToPublicKey}; |
| 19 | +use std::str::FromStr; |
| 20 | + |
| 21 | +#[derive(Debug)] |
| 22 | +pub(crate) struct SingleKeySigner { |
| 23 | + privkey: PrivateKey, |
| 24 | + pubkey: PublicKey, |
| 25 | + _pubkey_xonly: XOnlyPublicKey, |
| 26 | + pub(crate) fingerprint: Fingerprint, |
| 27 | + fingerprint_xonly: Fingerprint, |
| 28 | +} |
| 29 | + |
| 30 | +impl SingleKeySigner { |
| 31 | + fn fingerprint(key: SinglePubKey) -> Fingerprint { |
| 32 | + DescriptorPublicKey::Single(SinglePub { origin: None, key }).master_fingerprint() |
| 33 | + } |
| 34 | + |
| 35 | + pub(crate) fn from_privkey<C: Signing>( |
| 36 | + privkey: PrivateKey, |
| 37 | + secp: &Secp256k1<C>, |
| 38 | + ) -> SingleKeySigner { |
| 39 | + let pubkey = privkey.public_key(secp); |
| 40 | + let pubkey_xonly = pubkey.to_x_only_pubkey(); |
| 41 | + SingleKeySigner { |
| 42 | + privkey, |
| 43 | + pubkey, |
| 44 | + _pubkey_xonly: pubkey_xonly, |
| 45 | + fingerprint: SingleKeySigner::fingerprint(SinglePubKey::FullKey(pubkey)), |
| 46 | + fingerprint_xonly: SingleKeySigner::fingerprint(SinglePubKey::XOnly(pubkey_xonly)), |
| 47 | + } |
| 48 | + } |
| 49 | +} |
| 50 | + |
| 51 | +impl psbt::GetKey for SingleKeySigner { |
| 52 | + type Error = String; |
| 53 | + |
| 54 | + fn get_key<C: Signing>( |
| 55 | + &self, |
| 56 | + key_request: psbt::KeyRequest, |
| 57 | + _secp: &Secp256k1<C>, |
| 58 | + ) -> Result<Option<PrivateKey>, Self::Error> { |
| 59 | + match key_request { |
| 60 | + // NOTE: this KeyRequest does not occur for taproot signatures |
| 61 | + // even if the descriptor keys are definite, we will receive a bip32 request |
| 62 | + // instead based on `DescriptorPublicKey::Single(SinglePub { origin: None, key, })` |
| 63 | + psbt::KeyRequest::Pubkey(req_pubkey) => { |
| 64 | + if req_pubkey == self.pubkey { |
| 65 | + Ok(Some(self.privkey)) |
| 66 | + } else { |
| 67 | + Ok(None) |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + psbt::KeyRequest::Bip32((fingerprint, _path)) => { |
| 72 | + if fingerprint.eq(&self.fingerprint) || fingerprint.eq(&self.fingerprint_xonly) { |
| 73 | + Ok(Some(self.privkey)) |
| 74 | + } else { |
| 75 | + Ok(None) |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + _ => Ok(None), |
| 80 | + } |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +/// Compute the output script for a descriptor string, which may embed private keys (e.g. |
| 85 | +/// `pkh(<privkey>)`, `wpkh(<privkey>)`) — used to derive an address directly from a signing |
| 86 | +/// descriptor without a separate private-key argument. |
| 87 | +/// |
| 88 | +/// Uses `Descriptor::parse_descriptor`, which turns any embedded secret keys into their |
| 89 | +/// corresponding public keys (discarding the resulting key map — this is a read-only address |
| 90 | +/// derivation, not a signing operation). Resolves at derivation index 0; index is a no-op for |
| 91 | +/// non-wildcard descriptors, which is the expected case for a descriptor naming concrete keys. |
| 92 | +pub fn script_pubkey_from_descriptor(descriptor: &str) -> Result<ScriptBuf, String> { |
| 93 | + let secp = Secp256k1::new(); |
| 94 | + let (desc, _keymap) = |
| 95 | + Descriptor::parse_descriptor(&secp, descriptor).map_err(|e| e.to_string())?; |
| 96 | + let definite = desc.at_derivation_index(0).map_err(|e| e.to_string())?; |
| 97 | + Ok(definite.script_pubkey()) |
| 98 | +} |
| 99 | + |
| 100 | +/// Add a witness_utxo input spending the given definite descriptor's output, populating |
| 101 | +/// `bip32_derivation`/`tap_key_origins` from the descriptor so the input is signable via |
| 102 | +/// [`sign_with_privkey`] / [`sign_zcash_with_privkey`]. |
| 103 | +/// |
| 104 | +/// `descriptor` is a definite descriptor string with concrete keys, e.g. `pkh(<pubkey>)` or |
| 105 | +/// `wpkh(<pubkey>)`; not limited to P2PKH. |
| 106 | +/// |
| 107 | +/// `non_witness_utxo`, when given, is validated against `witness_utxo` and the descriptor via |
| 108 | +/// the checked `PsbtExt::update_input_with_descriptor` (BIP174-safe). When omitted, falls back |
| 109 | +/// to `PsbtInputExt::update_with_descriptor_unchecked` — safe only for value-committing sighash |
| 110 | +/// networks where `non_witness_utxo` is cryptographically pointless (see |
| 111 | +/// [`Network::requires_prev_tx_for_legacy_input`](crate::Network::requires_prev_tx_for_legacy_input)). |
| 112 | +/// Callers must gate on that predicate themselves before omitting `non_witness_utxo`. |
| 113 | +#[allow(clippy::too_many_arguments)] |
| 114 | +pub fn add_input_with_descriptor( |
| 115 | + psbt: &mut miniscript::bitcoin::Psbt, |
| 116 | + index: usize, |
| 117 | + txid: Txid, |
| 118 | + vout: u32, |
| 119 | + value: u64, |
| 120 | + script_pubkey: ScriptBuf, |
| 121 | + descriptor: &str, |
| 122 | + sequence: u32, |
| 123 | + non_witness_utxo: Option<Transaction>, |
| 124 | +) -> Result<(), String> { |
| 125 | + let desc = |
| 126 | + Descriptor::<DefiniteDescriptorKey>::from_str(descriptor).map_err(|e| e.to_string())?; |
| 127 | + |
| 128 | + let tx_in = TxIn { |
| 129 | + previous_output: OutPoint { txid, vout }, |
| 130 | + script_sig: ScriptBuf::new(), |
| 131 | + sequence: Sequence(sequence), |
| 132 | + witness: miniscript::bitcoin::Witness::default(), |
| 133 | + }; |
| 134 | + let has_non_witness_utxo = non_witness_utxo.is_some(); |
| 135 | + let psbt_input = psbt::Input { |
| 136 | + witness_utxo: Some(TxOut { |
| 137 | + value: Amount::from_sat(value), |
| 138 | + script_pubkey, |
| 139 | + }), |
| 140 | + non_witness_utxo, |
| 141 | + ..Default::default() |
| 142 | + }; |
| 143 | + crate::psbt_ops::insert_input(psbt, index, tx_in, psbt_input)?; |
| 144 | + |
| 145 | + if has_non_witness_utxo { |
| 146 | + psbt.update_input_with_descriptor(index, &desc) |
| 147 | + .map_err(|e| e.to_string()) |
| 148 | + } else { |
| 149 | + psbt.inputs[index] |
| 150 | + .update_with_descriptor_unchecked(&desc) |
| 151 | + .map(|_| ()) |
| 152 | + .map_err(|e| e.to_string()) |
| 153 | + } |
| 154 | +} |
| 155 | + |
| 156 | +/// Sign all inputs of a PSBT with a single private key. |
| 157 | +/// |
| 158 | +/// `fork_id` selects the sighash algorithm: `Some(id)` uses `Psbt::sign_forkid` (BCH-family / |
| 159 | +/// BTG), `None` uses plain `Psbt::sign`. See [`Network::sighash_fork_id`](crate::Network::sighash_fork_id). |
| 160 | +pub fn sign_with_privkey<C: Signing + Verification>( |
| 161 | + psbt: &mut miniscript::bitcoin::Psbt, |
| 162 | + privkey: PrivateKey, |
| 163 | + secp: &Secp256k1<C>, |
| 164 | + fork_id: Option<u32>, |
| 165 | +) -> Result<psbt::SigningKeysMap, psbt::SigningErrors> { |
| 166 | + let signer = SingleKeySigner::from_privkey(privkey, secp); |
| 167 | + match fork_id { |
| 168 | + Some(id) => psbt.sign_forkid(&signer, secp, id), |
| 169 | + None => psbt.sign(&signer, secp), |
| 170 | + } |
| 171 | + .map_err(|(_, errors)| errors) |
| 172 | +} |
| 173 | + |
| 174 | +/// Finalize a fully-signed PSBT (using `finalize_mut_with_fork_id`, matching the `fork_id` used |
| 175 | +/// to sign) and encode the extracted transaction. |
| 176 | +pub fn finalize_and_encode_transaction( |
| 177 | + mut psbt: miniscript::bitcoin::Psbt, |
| 178 | + fork_id: Option<u32>, |
| 179 | +) -> Result<Vec<u8>, String> { |
| 180 | + use miniscript::bitcoin::consensus::Encodable; |
| 181 | + |
| 182 | + psbt.finalize_mut_with_fork_id(&Secp256k1::verification_only(), fork_id) |
| 183 | + .map_err(|errors| { |
| 184 | + format!( |
| 185 | + "Failed to finalize PSBT: {}", |
| 186 | + errors |
| 187 | + .into_iter() |
| 188 | + .map(|e| e.to_string()) |
| 189 | + .collect::<Vec<_>>() |
| 190 | + .join(", ") |
| 191 | + ) |
| 192 | + })?; |
| 193 | + let tx = psbt |
| 194 | + .extract_tx() |
| 195 | + .map_err(|e| format!("Failed to extract transaction: {}", e))?; |
| 196 | + let mut bytes = Vec::new(); |
| 197 | + tx.consensus_encode(&mut bytes) |
| 198 | + .map_err(|e| format!("Failed to encode transaction: {}", e))?; |
| 199 | + Ok(bytes) |
| 200 | +} |
| 201 | + |
| 202 | +/// Sign all Zcash transparent inputs of a PSBT using the ZIP-243 sighash algorithm |
| 203 | +/// (`Psbt::sign_zcash` in the rust-bitcoin fork). |
| 204 | +pub fn sign_zcash_with_privkey<C: Signing + Verification>( |
| 205 | + psbt: &mut miniscript::bitcoin::Psbt, |
| 206 | + privkey: PrivateKey, |
| 207 | + secp: &Secp256k1<C>, |
| 208 | + consensus_branch_id: u32, |
| 209 | + version_group_id: u32, |
| 210 | + expiry_height: u32, |
| 211 | +) -> Result<psbt::SigningKeysMap, psbt::SigningErrors> { |
| 212 | + let signer = SingleKeySigner::from_privkey(privkey, secp); |
| 213 | + psbt.sign_zcash( |
| 214 | + &signer, |
| 215 | + secp, |
| 216 | + consensus_branch_id, |
| 217 | + version_group_id, |
| 218 | + expiry_height, |
| 219 | + ) |
| 220 | + .map_err(|(_, errors)| errors) |
| 221 | +} |
| 222 | + |
| 223 | +/// Finalize a fully-signed PSBT using ZIP-243 sighash verification (`finalize_mut_with_zcash`) |
| 224 | +/// and encode it as Zcash overwintered transaction bytes. |
| 225 | +pub fn finalize_and_encode_zcash_transaction( |
| 226 | + mut psbt: miniscript::bitcoin::Psbt, |
| 227 | + consensus_branch_id: u32, |
| 228 | + version_group_id: u32, |
| 229 | + expiry_height: u32, |
| 230 | +) -> Result<Vec<u8>, String> { |
| 231 | + psbt.finalize_mut_with_zcash( |
| 232 | + &Secp256k1::verification_only(), |
| 233 | + consensus_branch_id, |
| 234 | + version_group_id, |
| 235 | + expiry_height, |
| 236 | + ) |
| 237 | + .map_err(|errors| { |
| 238 | + format!( |
| 239 | + "Failed to finalize PSBT: {}", |
| 240 | + errors |
| 241 | + .into_iter() |
| 242 | + .map(|e| e.to_string()) |
| 243 | + .collect::<Vec<_>>() |
| 244 | + .join(", ") |
| 245 | + ) |
| 246 | + })?; |
| 247 | + let parts = ZcashTransactionParts::extract_from_psbt(psbt, version_group_id, expiry_height)?; |
| 248 | + crate::zcash::transaction::encode_zcash_transaction_parts(&parts) |
| 249 | +} |
0 commit comments