jitash_bdk/wallet/
verify.rs

1// Bitcoin Dev Kit
2// Written in 2021 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//! Verify transactions against the consensus rules
13
14use std::collections::HashMap;
15use std::fmt;
16
17use bitcoin::consensus::serialize;
18use bitcoin::{OutPoint, Transaction, Txid};
19
20use crate::blockchain::GetTx;
21use crate::database::Database;
22use crate::error::Error;
23
24/// Verify a transaction against the consensus rules
25///
26/// This function uses [`bitcoinconsensus`] to verify transactions by fetching the required data
27/// either from the [`Database`] or using the [`Blockchain`].
28///
29/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
30/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
31/// with unconfirmed transactions that have been evicted from the backend's memory.
32///
33/// [`Blockchain`]: crate::blockchain::Blockchain
34pub fn verify_tx<D: Database, B: GetTx>(
35    tx: &Transaction,
36    database: &D,
37    blockchain: &B,
38) -> Result<(), VerifyError> {
39    log::debug!("Verifying {}", tx.txid());
40
41    let serialized_tx = serialize(tx);
42    let mut tx_cache = HashMap::<_, Transaction>::new();
43
44    for (index, input) in tx.input.iter().enumerate() {
45        let prev_tx = if let Some(prev_tx) = tx_cache.get(&input.previous_output.txid) {
46            prev_tx.clone()
47        } else if let Some(prev_tx) = database.get_raw_tx(&input.previous_output.txid)? {
48            prev_tx
49        } else if let Some(prev_tx) = blockchain.get_tx(&input.previous_output.txid)? {
50            prev_tx
51        } else {
52            return Err(VerifyError::MissingInputTx(input.previous_output.txid));
53        };
54
55        let spent_output = prev_tx
56            .output
57            .get(input.previous_output.vout as usize)
58            .ok_or(VerifyError::InvalidInput(input.previous_output))?;
59
60        bitcoinconsensus::verify(
61            &spent_output.script_pubkey.to_bytes(),
62            spent_output.value,
63            &serialized_tx,
64            index,
65        )?;
66
67        // Since we have a local cache we might as well cache stuff from the db, as it will very
68        // likely decrease latency compared to reading from disk or performing an SQL query.
69        tx_cache.insert(prev_tx.txid(), prev_tx);
70    }
71
72    Ok(())
73}
74
75/// Error during validation of a tx agains the consensus rules
76#[derive(Debug)]
77pub enum VerifyError {
78    /// The transaction being spent is not available in the database or the blockchain client
79    MissingInputTx(Txid),
80    /// The transaction being spent doesn't have the requested output
81    InvalidInput(OutPoint),
82
83    /// Consensus error
84    Consensus(bitcoinconsensus::Error),
85
86    /// Generic error
87    ///
88    /// It has to be wrapped in a `Box` since `Error` has a variant that contains this enum
89    Global(Box<Error>),
90}
91
92impl fmt::Display for VerifyError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            Self::MissingInputTx(txid) => write!(f, "The transaction being spent is not available in the database or the blockchain client: {}", txid),
96            Self::InvalidInput(outpoint) => write!(f, "The transaction being spent doesn't have the requested output: {}", outpoint),
97            Self::Consensus(err) => write!(f, "Consensus error: {:?}", err),
98            Self::Global(err) => write!(f, "Generic error: {}", err),
99        }
100    }
101}
102
103impl std::error::Error for VerifyError {}
104
105impl From<Error> for VerifyError {
106    fn from(other: Error) -> Self {
107        VerifyError::Global(Box::new(other))
108    }
109}
110impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
111
112#[cfg(test)]
113mod test {
114    use super::*;
115    use crate::database::{BatchOperations, MemoryDatabase};
116    use assert_matches::assert_matches;
117    use bitcoin::consensus::encode::deserialize;
118    use bitcoin::hashes::hex::FromHex;
119    use bitcoin::{Transaction, Txid};
120
121    struct DummyBlockchain;
122
123    impl GetTx for DummyBlockchain {
124        fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
125            Ok(None)
126        }
127    }
128
129    #[test]
130    fn test_verify_fail_unsigned_tx() {
131        // https://blockstream.info/tx/95da344585fcf2e5f7d6cbf2c3df2dcce84f9196f7a7bb901a43275cd6eb7c3f
132        let prev_tx: Transaction = deserialize(&Vec::<u8>::from_hex("020000000101192dea5e66d444380e106f8e53acb171703f00d43fb6b3ae88ca5644bdb7e1000000006b48304502210098328d026ce138411f957966c1cf7f7597ccbb170f5d5655ee3e9f47b18f6999022017c3526fc9147830e1340e04934476a3d1521af5b4de4e98baf49ec4c072079e01210276f847f77ec8dd66d78affd3c318a0ed26d89dab33fa143333c207402fcec352feffffff023d0ac203000000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988aca4b956050000000017a91494d5543c74a3ee98e0cf8e8caef5dc813a0f34b48768cb0700").unwrap()).unwrap();
133        // https://blockstream.info/tx/aca326a724eda9a461c10a876534ecd5ae7b27f10f26c3862fb996f80ea2d45d
134        let signed_tx: Transaction = deserialize(&Vec::<u8>::from_hex("02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700").unwrap()).unwrap();
135
136        let mut database = MemoryDatabase::new();
137        let blockchain = DummyBlockchain;
138
139        let mut unsigned_tx = signed_tx.clone();
140        for input in &mut unsigned_tx.input {
141            input.script_sig = Default::default();
142            input.witness = Default::default();
143        }
144
145        let result = verify_tx(&signed_tx, &database, &blockchain);
146        assert_matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid(),
147            "Error should be a `MissingInputTx` error"
148        );
149
150        // insert the prev_tx
151        database.set_raw_tx(&prev_tx).unwrap();
152
153        let result = verify_tx(&unsigned_tx, &database, &blockchain);
154        assert_matches!(
155            result,
156            Err(VerifyError::Consensus(_)),
157            "Error should be a `Consensus` error"
158        );
159
160        let result = verify_tx(&signed_tx, &database, &blockchain);
161        assert!(
162            result.is_ok(),
163            "Should work since the TX is correctly signed"
164        );
165    }
166}