1use bdk_wallet::bitcoin::blockdata::opcodes;
22use bdk_wallet::bitcoin::blockdata::script::{Builder, Script, ScriptBuf};
23use bdk_wallet::bitcoin::blockdata::transaction::{OutPoint, TxIn, TxOut};
24use bdk_wallet::bitcoin::consensus::encode::serialize;
25use bdk_wallet::bitcoin::hash_types::Txid;
26use bdk_wallet::bitcoin::hashes::{Hash, hash160, sha256d};
27use bdk_wallet::bitcoin::psbt::ExtractTxError;
28use bdk_wallet::bitcoin::psbt::{Input, Psbt};
29use bdk_wallet::bitcoin::sighash::EcdsaSighashType;
30use bdk_wallet::bitcoin::{Amount, PubkeyHash, Sequence};
31use bdk_wallet::chain::ChainPosition;
32use bdk_wallet::{AddForeignUtxoError, TxOrdering, Wallet};
33use bdk_wallet::{error::CreateTxError, signer::SignerError};
34use units::weight::Weight;
35
36pub trait ProofOfReserves {
38 fn create_proof(&mut self, message: &str) -> Result<Psbt, ProofError>;
40
41 fn verify_proof(
50 &self,
51 psbt: &Psbt,
52 message: &str,
53 max_block_height: Option<usize>,
54 ) -> Result<Amount, ProofError>;
55}
56
57#[derive(Debug)]
59pub enum ProofError {
60 WrongNumberOfInputs,
62 WrongNumberOfOutputs,
64 ChallengeInputMismatch,
66 NonSpendableInput(usize),
68 NotSignedInput(usize),
70 UnsupportedSighashType(usize),
72 NeitherWitnessNorLegacy(usize),
74 SignatureValidation(usize, String),
76 InvalidOutput,
78 InAndOutValueNotEqual,
80 OutpointNotFound(usize),
82 MissingConfirmationInfo,
84 ForeignUtxo(AddForeignUtxoError),
86 TxError(CreateTxError),
88 TxExtraction(Box<ExtractTxError>),
90 Wallet(bdk_wallet::descriptor::error::Error),
92 Sign(SignerError),
94}
95
96impl From<AddForeignUtxoError> for ProofError {
97 fn from(error: AddForeignUtxoError) -> Self {
98 ProofError::ForeignUtxo(error)
99 }
100}
101
102impl From<CreateTxError> for ProofError {
103 fn from(error: CreateTxError) -> Self {
104 ProofError::TxError(error)
105 }
106}
107
108impl From<ExtractTxError> for ProofError {
109 fn from(error: ExtractTxError) -> Self {
110 ProofError::TxExtraction(Box::new(error))
111 }
112}
113
114impl From<bdk_wallet::descriptor::error::Error> for ProofError {
115 fn from(error: bdk_wallet::descriptor::error::Error) -> Self {
116 ProofError::Wallet(error)
117 }
118}
119
120impl From<SignerError> for ProofError {
121 fn from(error: SignerError) -> Self {
122 ProofError::Sign(error)
123 }
124}
125
126impl ProofOfReserves for Wallet {
127 fn create_proof(&mut self, message: &str) -> Result<Psbt, ProofError> {
128 if message.is_empty() {
129 return Err(ProofError::ChallengeInputMismatch);
130 }
131 let challenge_txin = challenge_txin(message);
132 let challenge_psbt_inp = Input {
133 witness_utxo: Some(TxOut {
134 value: Amount::from_sat(0),
135 script_pubkey: Builder::new().push_opcode(opcodes::OP_TRUE).into_script(),
136 }),
137 final_script_sig: Some(Script::new().into()), ..Default::default()
139 };
140
141 let pkh = PubkeyHash::from_raw_hash(hash160::Hash::hash(&[0]));
142 let out_script_unspendable = ScriptBuf::new_p2pkh(&pkh);
143
144 let mut builder = self.build_tx();
145 builder
146 .drain_wallet()
147 .add_foreign_utxo(
148 challenge_txin.previous_output,
149 challenge_psbt_inp,
150 Weight::from_wu(42),
151 )?
152 .fee_absolute(Amount::from_sat(0))
153 .only_witness_utxo()
154 .current_height(0)
155 .drain_to(out_script_unspendable)
156 .ordering(TxOrdering::Untouched);
157 let psbt = builder.finish()?;
158
159 Ok(psbt)
160 }
161
162 fn verify_proof(
163 &self,
164 psbt: &Psbt,
165 message: &str,
166 max_block_height: Option<usize>,
167 ) -> Result<Amount, ProofError> {
168 let unspents = self
170 .list_unspent()
171 .map(|utxo| {
172 if max_block_height.is_none() {
173 Ok((utxo, None))
174 } else {
175 let tx_details = self.get_tx(utxo.outpoint.txid);
176 if let Some(tx_details) = tx_details {
177 if let ChainPosition::<_>::Confirmed {
178 anchor,
179 transitively: _,
180 } = tx_details.chain_position
181 {
182 Ok((utxo, Some(anchor.block_id.height as usize)))
183 } else {
184 Ok((utxo, None))
185 }
186 } else {
187 Err(ProofError::MissingConfirmationInfo)
188 }
189 }
190 })
191 .collect::<Result<Vec<_>, ProofError>>()?;
192 let outpoints = unspents
193 .iter()
194 .filter(|(_utxo, block_height)| {
195 block_height.unwrap_or(usize::MAX) <= max_block_height.unwrap_or(usize::MAX)
196 })
197 .map(|(utxo, _)| (utxo.outpoint, utxo.txout.clone()))
198 .collect();
199
200 verify_proof(psbt, message, outpoints)
201 }
202}
203
204pub fn verify_proof(
212 psbt: &Psbt,
213 message: &str,
214 outpoints: Vec<(OutPoint, TxOut)>,
215) -> Result<Amount, ProofError> {
216 if psbt.outputs.len() != 1 || psbt.unsigned_tx.output.len() != 1 {
217 return Err(ProofError::WrongNumberOfOutputs);
218 }
219 if psbt.inputs.len() <= 1 || psbt.unsigned_tx.input.len() <= 1 {
220 return Err(ProofError::WrongNumberOfInputs);
221 }
222
223 let tx = psbt.clone().extract_tx()?;
224
225 if tx.output.len() != 1 {
226 return Err(ProofError::WrongNumberOfOutputs);
227 }
228 if tx.input.len() <= 1 {
229 return Err(ProofError::WrongNumberOfInputs);
230 }
231
232 let challenge_txin = challenge_txin(message);
234 if tx.input[0].previous_output != challenge_txin.previous_output {
235 return Err(ProofError::ChallengeInputMismatch);
236 }
237
238 if let Some((i, _inp)) = tx
240 .input
241 .iter()
242 .enumerate()
243 .skip(1)
244 .find(|(_i, inp)| !outpoints.iter().any(|op| op.0 == inp.previous_output))
245 {
246 return Err(ProofError::NonSpendableInput(i));
247 }
248
249 if let Some((i, _inp)) = psbt
251 .inputs
252 .iter()
253 .enumerate()
254 .skip(1)
255 .find(|(_i, inp)| inp.final_script_sig.is_none() && inp.final_script_witness.is_none())
256 {
257 return Err(ProofError::NotSignedInput(i));
258 }
259
260 if let Some((i, _psbt_in)) = psbt.inputs.iter().enumerate().find(|(_i, psbt_in)| {
262 psbt_in.sighash_type.is_some() && psbt_in.sighash_type != Some(EcdsaSighashType::All.into())
263 }) {
264 return Err(ProofError::UnsupportedSighashType(i));
265 }
266
267 let sum = tx
269 .input
270 .iter()
271 .map(|tx_in| {
272 if let Some(op) = outpoints.iter().find(|op| op.0 == tx_in.previous_output) {
273 op.1.value
274 } else {
275 Amount::from_sat(0)
276 }
277 })
278 .sum();
279
280 if tx.output[0].value != sum {
282 return Err(ProofError::InAndOutValueNotEqual);
283 }
284
285 let pkh = PubkeyHash::from_raw_hash(hash160::Hash::hash(&[0]));
287 let out_script_unspendable = ScriptBuf::new_p2pkh(&pkh);
288
289 if tx.output[0].script_pubkey != out_script_unspendable {
290 return Err(ProofError::InvalidOutput);
291 }
292
293 let serialized_tx = serialize(&tx);
294
295 if let Some((i, res)) = tx
297 .input
298 .iter()
299 .enumerate()
300 .skip(1)
301 .map(|(i, tx_in)| {
302 if let Some(op) = outpoints.iter().find(|op| op.0 == tx_in.previous_output) {
303 (i, Ok(op.1.clone()))
304 } else {
305 (i, Err(ProofError::OutpointNotFound(i)))
306 }
307 })
308 .map(|(i, res)| match res {
309 Ok(txout) => (
310 i,
311 bitcoinconsensus::verify(
312 txout.script_pubkey.to_bytes().as_slice(),
313 txout.value.to_sat(),
314 &serialized_tx,
315 i,
316 )
317 .map_err(|e| ProofError::SignatureValidation(i, format!("{:?}", e))),
318 ),
319 Err(err) => (i, Err(err)),
320 })
321 .find(|(_i, res)| res.is_err())
322 {
323 return Err(ProofError::SignatureValidation(
324 i,
325 format!("{:?}", res.err().unwrap()),
326 ));
327 }
328
329 Ok(sum)
330}
331
332fn challenge_txin(message: &str) -> TxIn {
334 let message = "Proof-of-Reserves: ".to_string() + message;
335 let message = sha256d::Hash::hash(message.as_bytes());
336 TxIn {
337 previous_output: OutPoint::new(Txid::from_raw_hash(message), 0),
338 sequence: Sequence(0xFFFFFFFF),
339 ..Default::default()
340 }
341}
342
343#[cfg(test)]
344mod test {
345 use super::*;
346 use bdk_wallet::SignOptions;
347 use bdk_wallet::bitcoin::{Address, Network, Witness};
348 use bdk_wallet::test_utils::get_funded_wallet_single;
349 use std::str::FromStr;
350
351 #[test]
352 fn test_proof() {
353 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
354 let (mut wallet, _) = get_funded_wallet_single(descriptor);
355
356 let message = "This belongs to me.";
357 let mut psbt = wallet.create_proof(message).unwrap();
358
359 let psbt_b64 = psbt.to_string();
360
361 let expected = r#"cHNidP8BAH4CAAAAAmw1RvG4UzfnSafpx62EPTyha6VslP0Er7n3TxjEpeBeAAAAAAD/////MQvsP2eDTCk3vWfQJ50IOFWLwuTHPsnYikR1hosdK0sAAAAAAP3///8BUMMAAAAAAAAZdqkUn3/QltN+0sDj9/DPySS+70/862iIrAAAAAAAAQEKAAAAAAAAAAABUQEHAAABAR9QwwAAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qIgYDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+ME7OUmVwAA"#;
362
363 assert_eq!(psbt_b64, expected);
364
365 let signopts = SignOptions {
366 trust_witness_utxo: true,
367 ..Default::default()
368 };
369 wallet.sign(&mut psbt, signopts).unwrap();
370
371 let spendable = wallet.verify_proof(&psbt, message, None).unwrap();
372 assert_eq!(spendable, Amount::from_sat(50_000));
373
374 let psbt_b64 = psbt.to_string();
375
376 let expected = r#"cHNidP8BAH4CAAAAAmw1RvG4UzfnSafpx62EPTyha6VslP0Er7n3TxjEpeBeAAAAAAD/////MQvsP2eDTCk3vWfQJ50IOFWLwuTHPsnYikR1hosdK0sAAAAAAP3///8BUMMAAAAAAAAZdqkUn3/QltN+0sDj9/DPySS+70/862iIrAAAAAAAAQEKAAAAAAAAAAABUQEHAAABAR9QwwAAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qAQhrAkcwRAIgMjk39sEdlh9VEb0GOKNy+C/X4yiZd4/AteVnZjdSuPYCIBQKVpBGTpUSZNoXqT93EucKLLFVQnLJIkPXKjRYO4eJASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAA=="#;
377
378 assert_eq!(psbt_b64, expected);
379 }
380
381 #[test]
382 #[should_panic(expected = "\"Key too short (<66 char), doesn't match any format\"")]
383 fn invalid_descriptor() {
384 let descriptor = "wpkh(cVpPVqXRyPcFW)";
385 let (mut wallet, _) = get_funded_wallet_single(descriptor);
386
387 let message = "This belongs to me.";
388 let _psbt = wallet.create_proof(message).unwrap();
389 }
390
391 #[test]
392 #[should_panic(expected = "ChallengeInputMismatch")]
393 fn empty_message() {
394 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
395 let (mut wallet, _) = get_funded_wallet_single(descriptor);
396
397 let message = "";
398 let _psbt = wallet.create_proof(message).unwrap();
399 }
400
401 fn get_signed_proof() -> Psbt {
402 let psbt = "cHNidP8BAH4BAAAAAmw1RvG4UzfnSafpx62EPTyha6VslP0Er7n3TxjEpeBeAAAAAAD/////MQvsP2eDTCk3vWfQJ50IOFWLwuTHPsnYikR1hosdK0sAAAAAAP3///8BUMMAAAAAAAAZdqkUn3/QltN+0sDj9/DPySS+70/862iIrAAAAAAAAQEKAAAAAAAAAAABUQEHAAABAR9QwwAAAAAAABYAFOzlJlcQU9qGRUyeBmd56vnRUC5qAQhrAkcwRAIgR9XtbnBY0jUe9zXI0kCEzEua5pHm6Bal6mCHNBmTTIECIE2NbjpRjD6/nZv72GQ7qPZ1Sdo3BPps0cBN1DOsB+S+ASEDKwVYB4vsOGlKhJM9ZZMD4lddrn6RaFkRRUEVv9ZEh+MAAA==";
403 Psbt::from_str(psbt).unwrap()
404 }
405
406 #[test]
407 fn verify_internal() {
408 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
409 let (wallet, _) = get_funded_wallet_single(descriptor);
410
411 let message = "This belongs to me.";
412 let psbt = get_signed_proof();
413 let spendable = wallet.verify_proof(&psbt, message, None).unwrap();
414 assert_eq!(spendable, Amount::from_sat(50_000));
415 }
416
417 #[test]
418 #[should_panic(expected = "NonSpendableInput")]
419 fn verify_internal_1990() {
420 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
421 let (wallet, _) = get_funded_wallet_single(descriptor);
422
423 let message = "This belongs to me.";
424 let psbt = get_signed_proof();
425 let spendable = wallet.verify_proof(&psbt, message, Some(1990)).unwrap();
426 assert_eq!(spendable, Amount::from_sat(0));
427 }
428
429 #[test]
430 fn verify_internal_2000() {
431 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
432 let (wallet, _) = get_funded_wallet_single(descriptor);
433
434 let message = "This belongs to me.";
435 let psbt = get_signed_proof();
436 let spendable = wallet.verify_proof(&psbt, message, Some(2000)).unwrap();
437 assert_eq!(spendable, Amount::from_sat(50_000));
438 }
439
440 #[test]
441 fn verify_external() {
442 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
443 let (wallet, _) = get_funded_wallet_single(descriptor);
444
445 let message = "This belongs to me.";
446 let psbt = get_signed_proof();
447 let outpoints = wallet
448 .list_unspent()
449 .map(|utxo| (utxo.outpoint, utxo.txout))
450 .collect();
451 let spendable = verify_proof(&psbt, message, outpoints).unwrap();
452
453 assert_eq!(spendable, Amount::from_sat(50_000));
454 }
455
456 #[test]
457 #[should_panic(expected = "ChallengeInputMismatch")]
458 fn wrong_message() {
459 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
460 let (wallet, _) = get_funded_wallet_single(descriptor);
461
462 let message = "Wrong message!";
463 let psbt = get_signed_proof();
464 wallet.verify_proof(&psbt, message, None).unwrap();
465 }
466
467 #[test]
468 #[should_panic(expected = "WrongNumberOfInputs")]
469 fn too_few_inputs() {
470 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
471 let (wallet, _) = get_funded_wallet_single(descriptor);
472
473 let message = "This belongs to me.";
474 let mut psbt = get_signed_proof();
475 psbt.unsigned_tx.input.truncate(1);
476 psbt.inputs.truncate(1);
477
478 wallet.verify_proof(&psbt, message, None).unwrap();
479 }
480
481 #[test]
482 #[should_panic(expected = "WrongNumberOfOutputs")]
483 fn no_output() {
484 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
485 let (wallet, _) = get_funded_wallet_single(descriptor);
486
487 let message = "This belongs to me.";
488 let mut psbt = get_signed_proof();
489 psbt.inputs.clear();
490 psbt.unsigned_tx.output.clear();
491
492 wallet.verify_proof(&psbt, message, None).unwrap();
493 }
494
495 #[test]
496 #[should_panic(expected = "NotSignedInput")]
497 fn missing_signature() {
498 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
499 let (wallet, _) = get_funded_wallet_single(descriptor);
500
501 let message = "This belongs to me.";
502 let mut psbt = get_signed_proof();
503 psbt.inputs[1].final_script_sig = None;
504 psbt.inputs[1].final_script_witness = None;
505
506 wallet.verify_proof(&psbt, message, None).unwrap();
507 }
508
509 #[test]
510 #[should_panic(expected = "SignatureValidation")]
511 fn invalid_signature() {
512 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
513 let (wallet, _) = get_funded_wallet_single(descriptor);
514
515 let message = "This belongs to me.";
516 let mut psbt = get_signed_proof();
517 psbt.inputs[1].final_script_sig = None;
518
519 let invalid_signature = bdk_wallet::bitcoin::secp256k1::ecdsa::Signature::from_str("3045022100f3b7b0b1400287766edfe8ba66bc0412984cdb97da6bb4092d5dc63a84e1da6f02204da10796361dbeaeead8f68a23157dffa23b356ec14ec2c0c384ad68d582bb14").unwrap();
520 let invalid_signature =
521 bdk_wallet::bitcoin::ecdsa::Signature::sighash_all(invalid_signature);
522
523 let mut invalid_witness = Witness::new();
524 invalid_witness.push_ecdsa_signature(&invalid_signature);
525
526 psbt.inputs[1].final_script_witness = Some(invalid_witness);
527
528 wallet.verify_proof(&psbt, message, None).unwrap();
529 }
530
531 #[test]
532 #[should_panic(expected = "UnsupportedSighashType(1)")]
533 fn wrong_sighash_type() {
534 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
535 let (wallet, _) = get_funded_wallet_single(descriptor);
536
537 let message = "This belongs to me.";
538 let mut psbt = get_signed_proof();
539 psbt.inputs[1].sighash_type = Some(EcdsaSighashType::SinglePlusAnyoneCanPay.into());
540
541 wallet.verify_proof(&psbt, message, None).unwrap();
542 }
543
544 #[test]
545 fn burner_output() {
546 let psbt = get_signed_proof();
547
548 let pkh = PubkeyHash::from_raw_hash(hash160::Hash::hash(&[0]));
549 let out_script_unspendable = ScriptBuf::new_p2pkh(&pkh);
550 assert_eq!(
551 psbt.unsigned_tx.output[0].script_pubkey,
552 out_script_unspendable
553 );
554
555 let addr_unspendable = Address::p2pkh(pkh, Network::Bitcoin);
556 assert_eq!(
557 addr_unspendable.to_string(),
558 "1FYMZEHnszCHKTBdFZ2DLrUuk3dGwYKQxh"
559 );
560 let addr_unspendable_testnet = Address::p2pkh(pkh, Network::Testnet);
564 assert_eq!(
565 addr_unspendable_testnet.to_string(),
566 "mv4JrHNmh1dY6ZfEy7zbAmhEc3Dyr8ULqX"
567 );
568 }
570
571 #[test]
572 #[should_panic(expected = "InvalidOutput")]
573 fn invalid_output() {
574 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
575 let (wallet, _) = get_funded_wallet_single(descriptor);
576
577 let message = "This belongs to me.";
578 let mut psbt = get_signed_proof();
579
580 let pkh = PubkeyHash::from_raw_hash(hash160::Hash::hash(&[0, 1, 2, 3]));
581 let out_script_unspendable = ScriptBuf::new_p2pkh(&pkh);
582 psbt.unsigned_tx.output[0].script_pubkey = out_script_unspendable;
583
584 wallet.verify_proof(&psbt, message, None).unwrap();
585 }
586
587 #[test]
588 #[should_panic(expected = "InAndOutValueNotEqual")]
589 fn sum_mismatch() {
590 let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)";
591 let (wallet, _) = get_funded_wallet_single(descriptor);
592
593 let message = "This belongs to me.";
594 let mut psbt = get_signed_proof();
595 psbt.unsigned_tx.output[0].value = Amount::from_sat(123);
596
597 wallet.verify_proof(&psbt, message, None).unwrap();
598 }
599}