From af19d2a62c9d59b3e32c11115796c5699d2ede8e Mon Sep 17 00:00:00 2001 From: Richard Ulrich Date: Fri, 25 Sep 2026 10:17:21 +0200 Subject: [PATCH 1/2] catch duplicate utxos --- Cargo.toml | 2 +- src/reserves.rs | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2937f41..a32c999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk-reserves" -version = "3.1.0" +version = "3.1.1" authors = ["Richard Ulrich "] edition = "2024" description = "Proof of reserves for bitcoin dev kit" diff --git a/src/reserves.rs b/src/reserves.rs index ba89ce6..ef762bf 100644 --- a/src/reserves.rs +++ b/src/reserves.rs @@ -91,6 +91,8 @@ pub enum ProofError { Wallet(bdk_wallet::descriptor::error::Error), /// Failed to sign a transaction Sign(SignerError), + /// Most likely trying to inflate the amount + DuplicateUtxo, } impl From for ProofError { @@ -229,6 +231,19 @@ pub fn verify_proof( return Err(ProofError::WrongNumberOfInputs); } + // verify that UTXOs are not duplicated + let mut utxos = tx + .input + .iter() + .skip(1) + .map(|inp| inp.previous_output) + .collect::>(); + utxos.sort_unstable(); + utxos.dedup(); + if utxos.len() + 1 != tx.input.len() { + return Err(ProofError::DuplicateUtxo); + } + // verify the challenge txin let challenge_txin = challenge_txin(message); if tx.input[0].previous_output != challenge_txin.previous_output { @@ -282,7 +297,7 @@ pub fn verify_proof( return Err(ProofError::InAndOutValueNotEqual); } - // verify the unspendable output + // verify the unspendable (burn) output let pkh = PubkeyHash::from_raw_hash(hash160::Hash::hash(&[0])); let out_script_unspendable = ScriptBuf::new_p2pkh(&pkh); @@ -597,4 +612,29 @@ mod test { wallet.verify_proof(&psbt, message, None).unwrap(); } + + #[test] + #[should_panic(expected = "DuplicateUtxo")] + fn test_duplicate_utxo() { + let descriptor = "wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"; + let (mut wallet, _) = get_funded_wallet_single(descriptor); + + let message = "This belongs to me."; + let mut psbt = wallet.create_proof(message).unwrap(); + + psbt.inputs.push(psbt.inputs.last().unwrap().clone()); + psbt.unsigned_tx + .input + .push(psbt.unsigned_tx.input.last().unwrap().clone()); + psbt.unsigned_tx.output[0].value = Amount::from_sat(100_000); + + let signopts = SignOptions { + trust_witness_utxo: true, + ..Default::default() + }; + wallet.sign(&mut psbt, signopts).unwrap(); + + let spendable = wallet.verify_proof(&psbt, message, None).unwrap(); + assert_eq!(spendable, Amount::from_sat(100_000)); + } } From 07a517811b36baf4f280c2a129804e479590d08b Mon Sep 17 00:00:00 2001 From: Richard Ulrich Date: Mon, 5 Oct 2026 15:43:19 +0200 Subject: [PATCH 2/2] adding #[allow(deprecated)] to the Wallet::sign calls --- src/reserves.rs | 2 ++ tests/mempool.rs | 4 ++++ tests/multi_sig.rs | 3 +++ tests/regtestenv.rs | 1 + tests/single_sig.rs | 1 + tests/tampering.rs | 2 ++ 6 files changed, 13 insertions(+) diff --git a/src/reserves.rs b/src/reserves.rs index ef762bf..512fc28 100644 --- a/src/reserves.rs +++ b/src/reserves.rs @@ -382,6 +382,7 @@ mod test { trust_witness_utxo: true, ..Default::default() }; + #[allow(deprecated)] wallet.sign(&mut psbt, signopts).unwrap(); let spendable = wallet.verify_proof(&psbt, message, None).unwrap(); @@ -632,6 +633,7 @@ mod test { trust_witness_utxo: true, ..Default::default() }; + #[allow(deprecated)] wallet.sign(&mut psbt, signopts).unwrap(); let spendable = wallet.verify_proof(&psbt, message, None).unwrap(); diff --git a/tests/mempool.rs b/tests/mempool.rs index 89ac345..7166fd7 100644 --- a/tests/mempool.rs +++ b/tests/mempool.rs @@ -41,6 +41,7 @@ fn unconfirmed() -> Result<(), ProofError> { trust_witness_utxo: true, ..Default::default() }; + #[allow(deprecated)] let finalized = wallet.sign(&mut psbt, signopts.clone())?; assert!(finalized); client @@ -53,6 +54,7 @@ fn unconfirmed() -> Result<(), ProofError> { let message = "This belongs to me."; let mut psbt = wallet.create_proof(message)?; + #[allow(deprecated)] let finalized = wallet.sign(&mut psbt, signopts)?; assert!(finalized); @@ -105,6 +107,7 @@ fn confirmed() { trust_witness_utxo: true, ..Default::default() }; + #[allow(deprecated)] let finalized = wallet.sign(&mut psbt, signopts.clone()).unwrap(); assert!(finalized); client @@ -117,6 +120,7 @@ fn confirmed() { let message = "This belongs to me."; let mut psbt = wallet.create_proof(message).unwrap(); + #[allow(deprecated)] let finalized = wallet.sign(&mut psbt, signopts).unwrap(); assert!(finalized); diff --git a/tests/multi_sig.rs b/tests/multi_sig.rs index 33f3bda..fc5b4e7 100644 --- a/tests/multi_sig.rs +++ b/tests/multi_sig.rs @@ -135,10 +135,12 @@ fn test_proof_multisig( //remove_partial_sigs: false, ..Default::default() }; + #[allow(deprecated)] let finalized = wallets[0].sign(&mut psbt, signopts.clone())?; assert_eq!(count_signatures(&psbt), (num_inp - 1, 1, 0)); assert!(!finalized); + #[allow(deprecated)] let finalized = wallets[1].sign(&mut psbt, signopts.clone())?; assert_eq!( count_signatures(&psbt), @@ -147,6 +149,7 @@ fn test_proof_multisig( assert!(finalized); // 2 signatures are enough. Just checking what happens... + #[allow(deprecated)] let finalized = wallets[2].sign(&mut psbt, signopts.clone())?; assert_eq!( count_signatures(&psbt), diff --git a/tests/regtestenv.rs b/tests/regtestenv.rs index 965202a..e131691 100644 --- a/tests/regtestenv.rs +++ b/tests/regtestenv.rs @@ -85,6 +85,7 @@ impl RegTestEnv { let signopts = SignOptions { ..Default::default() }; + #[allow(deprecated)] let finalized = wallets .iter_mut() .any(|wallet| wallet.sign(&mut psbt, signopts.clone()).unwrap()); diff --git a/tests/single_sig.rs b/tests/single_sig.rs index abc5ad7..41863b8 100644 --- a/tests/single_sig.rs +++ b/tests/single_sig.rs @@ -21,6 +21,7 @@ fn test_proof_singlesig(#[case] descriptor: &'static str) -> Result<(), ProofErr num_inp ); + #[allow(deprecated)] let finalized = wallet.sign( &mut psbt, SignOptions { diff --git a/tests/tampering.rs b/tests/tampering.rs index fe58386..6d9844b 100644 --- a/tests/tampering.rs +++ b/tests/tampering.rs @@ -17,6 +17,7 @@ fn tampered_proof_message() { trust_witness_utxo: true, ..Default::default() }; + #[allow(deprecated)] let _finalized = wallet.sign(&mut psbt_alice, signopt).unwrap(); let spendable = wallet @@ -56,6 +57,7 @@ fn tampered_proof_miner_fee() { // reduce the output value to grant a miner fee psbt.unsigned_tx.output[0].value -= Amount::from_sat(100); + #[allow(deprecated)] let _finalized = wallet.sign(&mut psbt, signopt).unwrap(); let _spendable = wallet.verify_proof(&psbt, message, None).unwrap();