Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cont_integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
- name: Update Cargo.lock
run: cargo update
- name: Build
run: cargo build
run: cargo build --all-features
- name: Clippy
run: cargo clippy --all-targets -- -D warnings
- name: Test
Expand Down
13 changes: 11 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bdk-reserves"
version = "3.2.0"
version = "3.2.1"
authors = ["Richard Ulrich <richard.ulrich@aminagroup.com>"]
edition = "2024"
description = "Proof of reserves for bitcoin dev kit"
Expand All @@ -9,16 +9,25 @@ readme = "README.md"
license = "MIT OR Apache-2.0"
repository = "https://github.com/bitcoindevkit/bdk-reserves"

[features]
esplora = ["esplora-client", "rustls", "chrono"]

[dependencies]
bdk_wallet = { version = "3.2", default-features = false, features = ["std"] }
bitcoinconsensus = "0.106"
log = "^0.4"
esplora-client = { version = "0.13", optional = true, default-features = false, features = [ "async", "async-https", "tokio"] }
chrono = { version = "0.4", optional = true }

# same version as in rust-bitcoin - I couldn't find a re-export
units = { package = "bitcoin-units", version = "0.1", default-features = false, features = ["alloc"] }

# has to be manually synchronized with bdk_esplora until they add a re-export
rustls = { version = "0.23", optional = true }

[dev-dependencies]
rstest = "^0.11"
bdk_wallet = { version = "3.2", default-features = true, features = ["test-utils"] }
bdk_electrum = "0.24"
electrsd = { version = "0.41", features = ["download", "bitcoind_download", "bitcoind_29_0", "electrs_0_10_6"] }
electrsd = { version = "0.41", features = ["download", "bitcoind_download", "bitcoind_29_0", "electrs_0_10_6"] }
tokio = { version = "1", features = ["full"] }
102 changes: 99 additions & 3 deletions src/reserves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ use bdk_wallet::bitcoin::{Amount, PubkeyHash, Sequence};
use bdk_wallet::chain::ChainPosition;
use bdk_wallet::{AddForeignUtxoError, TxOrdering, Wallet};
use bdk_wallet::{error::CreateTxError, signer::SignerError};
#[cfg(feature = "esplora")]
use chrono::{DateTime, Utc};
#[cfg(feature = "esplora")]
use esplora_client;
use units::weight::Weight;

/// The API for proof of reserves
Expand Down Expand Up @@ -93,6 +97,10 @@ pub enum ProofError {
Sign(SignerError),
/// Most likely trying to inflate the amount
DuplicateUtxo,
/// Error with the Esplora connection
EsploraError,
/// For the moment only supporting witness
WitnessMissing,
}

impl From<AddForeignUtxoError> for ProofError {
Expand Down Expand Up @@ -205,9 +213,6 @@ impl ProofOfReserves for Wallet {

/// Make sure this is a proof, and not a spendable transaction.
/// Make sure the proof is valid.
/// Currently proofs can only be validated against the tip of the chain.
/// If some of the UTXOs in the proof were spent in the meantime, the proof will fail.
/// We can currently not validate whether it was valid at a certain block height.
/// Since the caller provides the outpoints, he is also responsible to make sure they have enough confirmations.
/// Returns the spendable amount of the proof.
pub fn verify_proof(
Expand Down Expand Up @@ -356,6 +361,97 @@ fn challenge_txin(message: &str) -> TxIn {
}
}

#[cfg(feature = "esplora")]
pub struct ProofResult {
pub value: Amount,
pub timestamp: DateTime<Utc>,
}

/// Make sure this is a proof, and not a spendable transaction.
/// Make sure the proof is valid.
/// Verifies the outpoints online with an esplora server.
#[cfg(feature = "esplora")]
pub async fn verify_proof_online(
psbt: &Psbt,
message: &str,
url: &str,
) -> Result<ProofResult, ProofError> {
let outheight = verify_prevouts(psbt, url).await?;
let amount = verify_proof(psbt, message, outheight.outpoints)?;
Ok(ProofResult {
value: amount,
timestamp: outheight.timestamp,
})
}

#[cfg(feature = "esplora")]
struct OutpointsHeight {
pub outpoints: Vec<(OutPoint, TxOut)>,
pub timestamp: DateTime<Utc>,
}

/// collect the information necessary to validate a proof
/// currently only works with witness transactions
/// In addition to the UTXOs, also returns the last block before the first UTXO was spent
/// This allows to validate proofs that no longer hold, but were valid in the past
#[cfg(feature = "esplora")]
async fn verify_prevouts(psbt: &Psbt, url: &str) -> Result<OutpointsHeight, ProofError> {
let client = esplora_client::Builder::new(url)
.header("user-agent", "bdk-reserves/3")
.build_async()
.map_err(|_| ProofError::EsploraError)?;
let current_height = client
.get_height()
.await
.map_err(|_| ProofError::EsploraError)?;

let mut utxos = Vec::<(OutPoint, TxOut)>::new();
let mut unspent_height = current_height;

for (txin, inp) in psbt.unsigned_tx.input.iter().zip(psbt.inputs.iter()) {
let ostat = client
.get_output_status(&txin.previous_output.txid, txin.previous_output.vout.into())
.await
.map_err(|_| ProofError::EsploraError)?
.ok_or(ProofError::EsploraError)?;
if ostat.spent {
let spent_state = ostat.status.ok_or(ProofError::MissingConfirmationInfo)?;
if spent_state.confirmed {
unspent_height = std::cmp::min(
unspent_height,
spent_state.block_height.ok_or(ProofError::EsploraError)? - 1,
);
}
}

let op = txin.previous_output;
if let Some(wutxo) = &inp.witness_utxo {
utxos.push((op, wutxo.clone()));
}
}

let blockinfo = &client
.get_block_infos(Some(unspent_height))
.await
.map_err(|_| ProofError::EsploraError)?[0];
if blockinfo.height != unspent_height {
return Err(ProofError::EsploraError);
}
let timestamp = DateTime::from_timestamp(
blockinfo
.timestamp
.try_into()
.map_err(|_| ProofError::EsploraError)?,
0,
)
.ok_or(ProofError::EsploraError)?;

Ok(OutpointsHeight {
outpoints: utxos,
timestamp,
})
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Loading
Loading