breez_sdk_spark/signer/
single_key_signer.rs1use std::sync::Arc;
2
3use bitcoin::{
4 Witness,
5 ecdsa::Signature,
6 hashes::Hash as _,
7 key::{Secp256k1, TapTweak as _},
8 secp256k1::SecretKey,
9 sighash::{self, SighashCache},
10};
11
12use crate::error::SignerError;
13
14use super::cpfp::CpfpSigner;
15
16#[cfg_attr(feature = "uniffi", uniffi::export)]
19#[allow(clippy::needless_pass_by_value)]
20pub fn single_key_cpfp_signer(
21 secret_key_bytes: Vec<u8>,
22) -> Result<Arc<dyn CpfpSigner>, SignerError> {
23 Ok(Arc::new(SingleKeySigner::new(secret_key_bytes)?))
24}
25
26pub struct SingleKeySigner {
27 secret_key: SecretKey,
28}
29
30impl SingleKeySigner {
31 #[allow(clippy::needless_pass_by_value)]
32 pub fn new(secret_key_bytes: Vec<u8>) -> Result<Self, SignerError> {
33 let secret_key = SecretKey::from_slice(&secret_key_bytes)
34 .map_err(|e| SignerError::InvalidInput(format!("Invalid secret key: {e}")))?;
35 Ok(Self { secret_key })
36 }
37}
38
39#[macros::async_trait]
40impl CpfpSigner for SingleKeySigner {
41 async fn sign_psbt(&self, psbt_bytes: Vec<u8>) -> Result<Vec<u8>, SignerError> {
42 let mut psbt = bitcoin::Psbt::deserialize(&psbt_bytes)
43 .map_err(|e| SignerError::InvalidInput(format!("Invalid PSBT: {e}")))?;
44
45 let secp = Secp256k1::new();
46 let pubkey = self.secret_key.public_key(&secp);
47 let bitcoin_pubkey = bitcoin::PublicKey::new(pubkey);
48
49 let mut prevouts: Vec<bitcoin::TxOut> = Vec::with_capacity(psbt.inputs.len());
50 let mut has_placeholder_prevout = false;
51 for input in &psbt.inputs {
52 match (&input.witness_utxo, &input.final_script_witness) {
53 (Some(tx_out), _) => prevouts.push(tx_out.clone()),
54 (None, Some(_)) => {
57 has_placeholder_prevout = true;
58 prevouts.push(bitcoin::TxOut::NULL);
59 }
60 (None, None) => {
61 return Err(SignerError::InvalidInput(
62 "PSBT input is missing witness_utxo".to_string(),
63 ));
64 }
65 }
66 }
67
68 let mut cache = SighashCache::new(&psbt.unsigned_tx);
69 let mut ecdsa_signatures = vec![];
70 let mut taproot_indices = vec![];
71
72 for (i, input) in psbt.inputs.iter().enumerate() {
73 if input.final_script_witness.is_some() {
74 continue;
75 }
76 let Some(tx_out) = &input.witness_utxo else {
77 return Err(SignerError::InvalidInput(
78 "PSBT input is missing witness_utxo".to_string(),
79 ));
80 };
81
82 if tx_out.script_pubkey.is_p2tr() {
83 taproot_indices.push(i);
84 } else if tx_out.script_pubkey.is_p2wpkh() {
85 let (msg, ecdsa_type) = psbt
86 .sighash_ecdsa(i, &mut cache)
87 .map_err(|e| SignerError::Signing(format!("ECDSA sighash error: {e}")))?;
88 let sig = secp.sign_ecdsa(&msg, &self.secret_key);
89 ecdsa_signatures.push((
90 i,
91 bitcoin_pubkey,
92 Signature {
93 signature: sig,
94 sighash_type: ecdsa_type,
95 },
96 ));
97 } else {
98 return Err(SignerError::InvalidInput(
99 "unsupported script type for the built-in single-key signer \
100 (only P2WPKH and P2TR key-path)"
101 .to_string(),
102 ));
103 }
104 }
105
106 for (i, pk, signature) in ecdsa_signatures {
107 let mut witness = Witness::new();
108 witness.push(signature.to_vec());
109 witness.push(pk.to_bytes());
110 psbt.inputs[i].final_script_witness = Some(witness);
111 psbt.inputs[i].partial_sigs.clear();
112 }
113
114 if !taproot_indices.is_empty() {
115 if has_placeholder_prevout {
116 return Err(SignerError::InvalidInput(
117 "cannot sign taproot input: another PSBT input is missing witness_utxo"
118 .to_string(),
119 ));
120 }
121 let keypair = bitcoin::key::Keypair::from_secret_key(&secp, &self.secret_key)
122 .tap_tweak(&secp, None)
123 .to_keypair();
124 let prevouts_ref = sighash::Prevouts::All(&prevouts);
125 for i in taproot_indices {
126 let sighash = cache
127 .taproot_key_spend_signature_hash(
128 i,
129 &prevouts_ref,
130 sighash::TapSighashType::Default,
131 )
132 .map_err(|e| SignerError::Signing(format!("Taproot sighash error: {e}")))?;
133 let msg = bitcoin::secp256k1::Message::from_digest(sighash.to_byte_array());
134 let schnorr_sig = secp.sign_schnorr_no_aux_rand(&msg, &keypair);
135 let tap_sig = bitcoin::taproot::Signature {
136 signature: schnorr_sig,
137 sighash_type: sighash::TapSighashType::Default,
138 };
139 let mut witness = Witness::new();
140 witness.push(tap_sig.to_vec());
141 psbt.inputs[i].final_script_witness = Some(witness);
142 psbt.inputs[i].tap_key_sig = None;
143 }
144 }
145
146 Ok(psbt.serialize())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use bitcoin::{
154 Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness,
155 absolute::LockTime, transaction::Version,
156 };
157
158 fn test_secret_key_bytes() -> Vec<u8> {
159 vec![1u8; 32]
160 }
161
162 fn signer_p2wpkh_script() -> ScriptBuf {
163 let secp = Secp256k1::new();
164 let sk = SecretKey::from_slice(&test_secret_key_bytes()).unwrap();
165 let cpk = bitcoin::CompressedPublicKey(sk.public_key(&secp));
166 ScriptBuf::new_p2wpkh(&cpk.wpubkey_hash())
167 }
168
169 fn dummy_txin(vout: u32) -> TxIn {
170 TxIn {
171 previous_output: OutPoint {
172 txid: Txid::all_zeros(),
173 vout,
174 },
175 script_sig: ScriptBuf::new(),
176 sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
177 witness: Witness::new(),
178 }
179 }
180
181 fn empty_psbt(n: u32) -> bitcoin::Psbt {
182 let tx = Transaction {
183 version: Version::TWO,
184 lock_time: LockTime::ZERO,
185 input: (0..n).map(dummy_txin).collect(),
186 output: vec![TxOut {
187 value: Amount::from_sat(1_000),
188 script_pubkey: signer_p2wpkh_script(),
189 }],
190 };
191 bitcoin::Psbt::from_unsigned_tx(tx).unwrap()
192 }
193
194 fn set_finalized_anchor(input: &mut bitcoin::psbt::Input) {
195 input.witness_utxo = Some(TxOut {
196 value: Amount::ZERO,
197 script_pubkey: ScriptBuf::from(vec![0x51, 0x02, 0x4e, 0x73]),
198 });
199 input.final_script_witness = Some(Witness::new());
200 }
201
202 #[macros::async_test_all]
203 async fn single_key_missing_witness_utxo_errors() {
204 let psbt = empty_psbt(1);
205 let signer = SingleKeySigner::new(test_secret_key_bytes()).unwrap();
206 let res = signer.sign_psbt(psbt.serialize()).await;
207 assert!(res.is_err(), "expected error for missing witness_utxo");
208 }
209
210 #[macros::async_test_all]
211 async fn single_key_unsupported_script_type_errors() {
212 let mut psbt = empty_psbt(1);
213 let p2wsh = ScriptBuf::new_p2wsh(&bitcoin::WScriptHash::from_byte_array([2u8; 32]));
214 psbt.inputs[0].witness_utxo = Some(TxOut {
215 value: Amount::from_sat(5_000),
216 script_pubkey: p2wsh,
217 });
218 let signer = SingleKeySigner::new(test_secret_key_bytes()).unwrap();
219 let res = signer.sign_psbt(psbt.serialize()).await;
220 assert!(res.is_err(), "expected error for unsupported script type");
221 }
222
223 #[macros::async_test_all]
224 async fn single_key_signs_p2wpkh_and_skips_anchor() {
225 let mut psbt = empty_psbt(2);
226 psbt.inputs[0].witness_utxo = Some(TxOut {
227 value: Amount::from_sat(10_000),
228 script_pubkey: signer_p2wpkh_script(),
229 });
230 set_finalized_anchor(&mut psbt.inputs[1]);
231
232 let signer = SingleKeySigner::new(test_secret_key_bytes()).unwrap();
233 let signed_bytes = signer.sign_psbt(psbt.serialize()).await.unwrap();
234 let out_psbt = bitcoin::Psbt::deserialize(&signed_bytes).unwrap();
235
236 let funding_witness = out_psbt.inputs[0]
237 .final_script_witness
238 .as_ref()
239 .expect("funding input should be finalized");
240 assert_eq!(funding_witness.len(), 2, "P2WPKH witness is [sig, pubkey]");
241 assert!(out_psbt.inputs[1].final_script_witness.is_some());
242 }
243}