Skip to main content

bdk_reserves/
reserves.rs

1// Bitcoin Dev Kit
2// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
3//
4// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
5//
6// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
7// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
8// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
9// You may not use this file except in accordance with one or both of these
10// licenses.
11
12//! Proof of reserves
13//!
14//! This module provides the ability to create proofs of reserves.
15//! A proof is a valid but unspendable transaction. By signing a transaction
16//! that spends some UTXOs we are proofing that we have control over these funds.
17//! The implementation is inspired by the following BIPs:
18//! https://github.com/bitcoin/bips/blob/master/bip-0127.mediawiki
19//! https://github.com/bitcoin/bips/blob/master/bip-0322.mediawiki
20
21use 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
36/// The API for proof of reserves
37pub trait ProofOfReserves {
38    /// Create a proof for all spendable UTXOs in a wallet
39    fn create_proof(&mut self, message: &str) -> Result<Psbt, ProofError>;
40
41    /// Make sure this is a proof, and not a spendable transaction.
42    /// Make sure the proof is valid.
43    /// Currently proofs can only be validated against the tip of the chain.
44    /// If some of the UTXOs in the proof were spent in the meantime, the proof will fail.
45    /// We can currently not validate whether it was valid at a certain block height.
46    /// With the max_block_height parameter the caller can ensure that only UTXOs with sufficient confirmations are considered.
47    /// If no max_block_height is provided, also UTXOs from transactions in the mempool are considered.
48    /// Returns the spendable amount of the proof.
49    fn verify_proof(
50        &self,
51        psbt: &Psbt,
52        message: &str,
53        max_block_height: Option<usize>,
54    ) -> Result<Amount, ProofError>;
55}
56
57/// Proof error
58#[derive(Debug)]
59pub enum ProofError {
60    /// Less than two inputs
61    WrongNumberOfInputs,
62    /// Must have exactly 1 output
63    WrongNumberOfOutputs,
64    /// Challenge input does not match
65    ChallengeInputMismatch,
66    /// Found an input other than the challenge which is not spendable. Holds the position of the input.
67    NonSpendableInput(usize),
68    /// Found an input that has no signature at position
69    NotSignedInput(usize),
70    /// Found an input with an unsupported SIGHASH type at position
71    UnsupportedSighashType(usize),
72    /// Found an input that is neither witness nor legacy at position
73    NeitherWitnessNorLegacy(usize),
74    /// Signature validation failed
75    SignatureValidation(usize, String),
76    /// The output is not valid
77    InvalidOutput,
78    /// Input and output values are not equal, implying a miner fee
79    InAndOutValueNotEqual,
80    /// No matching outpoint found
81    OutpointNotFound(usize),
82    /// Failed to retrieve the block height of a Tx or UTXO
83    MissingConfirmationInfo,
84    /// Error adding foreign UTXO
85    ForeignUtxo(AddForeignUtxoError),
86    /// Failed to create a transaction
87    TxError(CreateTxError),
88    /// Failed to extract TX from a PSBT
89    TxExtraction(Box<ExtractTxError>),
90    /// Failed to construct a Wallet
91    Wallet(bdk_wallet::descriptor::error::Error),
92    /// Failed to sign a transaction
93    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()), /* "finalize" the input with an empty scriptSig */
138            ..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        // verify the proof UTXOs are still spendable
169        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
204/// Make sure this is a proof, and not a spendable transaction.
205/// Make sure the proof is valid.
206/// Currently proofs can only be validated against the tip of the chain.
207/// If some of the UTXOs in the proof were spent in the meantime, the proof will fail.
208/// We can currently not validate whether it was valid at a certain block height.
209/// Since the caller provides the outpoints, he is also responsible to make sure they have enough confirmations.
210/// Returns the spendable amount of the proof.
211pub 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    // verify the challenge txin
233    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    // verify the proof UTXOs are still spendable
239    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    // verify that the inputs are signed, except the challenge
250    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    // Verify the SIGHASH
261    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    // calculate the spendable amount of the proof
268    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    // inflow and outflow being equal means no miner fee
281    if tx.output[0].value != sum {
282        return Err(ProofError::InAndOutValueNotEqual);
283    }
284
285    // verify the unspendable output
286    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    // Verify other inputs against prevouts.
296    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
332/// Construct a challenge input with the message
333fn 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        // https://mempool.space/de/address/1FYMZEHnszCHKTBdFZ2DLrUuk3dGwYKQxh
561        // https://bitcoin.stackexchange.com/questions/65969/invalid-public-key-was-spent-how-was-this-possible
562
563        let addr_unspendable_testnet = Address::p2pkh(pkh, Network::Testnet);
564        assert_eq!(
565            addr_unspendable_testnet.to_string(),
566            "mv4JrHNmh1dY6ZfEy7zbAmhEc3Dyr8ULqX"
567        );
568        // this address can be discovered in the transaction in https://ulrichard.ch/blog/?p=2566
569    }
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}