use std::collections::HashMap;
use std::fmt;
use bitcoin::consensus::serialize;
use bitcoin::{OutPoint, Transaction, Txid};
use crate::blockchain::GetTx;
use crate::database::Database;
use crate::error::Error;
pub fn verify_tx<D: Database, B: GetTx>(
tx: &Transaction,
database: &D,
blockchain: &B,
) -> Result<(), VerifyError> {
log::debug!("Verifying {}", tx.txid());
let serialized_tx = serialize(tx);
let mut tx_cache = HashMap::<_, Transaction>::new();
for (index, input) in tx.input.iter().enumerate() {
let prev_tx = if let Some(prev_tx) = tx_cache.get(&input.previous_output.txid) {
prev_tx.clone()
} else if let Some(prev_tx) = database.get_raw_tx(&input.previous_output.txid)? {
prev_tx
} else if let Some(prev_tx) = blockchain.get_tx(&input.previous_output.txid)? {
prev_tx
} else {
return Err(VerifyError::MissingInputTx(input.previous_output.txid));
};
let spent_output = prev_tx
.output
.get(input.previous_output.vout as usize)
.ok_or(VerifyError::InvalidInput(input.previous_output))?;
bitcoinconsensus::verify(
&spent_output.script_pubkey.to_bytes(),
spent_output.value,
&serialized_tx,
index,
)?;
tx_cache.insert(prev_tx.txid(), prev_tx);
}
Ok(())
}
#[derive(Debug)]
pub enum VerifyError {
MissingInputTx(Txid),
InvalidInput(OutPoint),
Consensus(bitcoinconsensus::Error),
Global(Box<Error>),
}
impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingInputTx(txid) => write!(f, "The transaction being spent is not available in the database or the blockchain client: {}", txid),
Self::InvalidInput(outpoint) => write!(f, "The transaction being spent doesn't have the requested output: {}", outpoint),
Self::Consensus(err) => write!(f, "Consensus error: {:?}", err),
Self::Global(err) => write!(f, "Generic error: {}", err),
}
}
}
impl std::error::Error for VerifyError {}
impl From<Error> for VerifyError {
fn from(other: Error) -> Self {
VerifyError::Global(Box::new(other))
}
}
impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
#[cfg(test)]
mod test {
use super::*;
use crate::database::{BatchOperations, MemoryDatabase};
use assert_matches::assert_matches;
use bitcoin::consensus::encode::deserialize;
use bitcoin::hashes::hex::FromHex;
use bitcoin::{Transaction, Txid};
struct DummyBlockchain;
impl GetTx for DummyBlockchain {
fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(None)
}
}
#[test]
fn test_verify_fail_unsigned_tx() {
let prev_tx: Transaction = deserialize(&Vec::<u8>::from_hex("020000000101192dea5e66d444380e106f8e53acb171703f00d43fb6b3ae88ca5644bdb7e1000000006b48304502210098328d026ce138411f957966c1cf7f7597ccbb170f5d5655ee3e9f47b18f6999022017c3526fc9147830e1340e04934476a3d1521af5b4de4e98baf49ec4c072079e01210276f847f77ec8dd66d78affd3c318a0ed26d89dab33fa143333c207402fcec352feffffff023d0ac203000000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988aca4b956050000000017a91494d5543c74a3ee98e0cf8e8caef5dc813a0f34b48768cb0700").unwrap()).unwrap();
let signed_tx: Transaction = deserialize(&Vec::<u8>::from_hex("02000000013f7cebd65c27431a90bba7f796914fe8cc2ddfc3f2cbd6f7e5f2fc854534da95000000006b483045022100de1ac3bcdfb0332207c4a91f3832bd2c2915840165f876ab47c5f8996b971c3602201c6c053d750fadde599e6f5c4e1963df0f01fc0d97815e8157e3d59fe09ca30d012103699b464d1d8bc9e47d4fb1cdaa89a1c5783d68363c4dbc4b524ed3d857148617feffffff02836d3c01000000001976a914fc25d6d5c94003bf5b0c7b640a248e2c637fcfb088ac7ada8202000000001976a914fbed3d9b11183209a57999d54d59f67c019e756c88ac6acb0700").unwrap()).unwrap();
let mut database = MemoryDatabase::new();
let blockchain = DummyBlockchain;
let mut unsigned_tx = signed_tx.clone();
for input in &mut unsigned_tx.input {
input.script_sig = Default::default();
input.witness = Default::default();
}
let result = verify_tx(&signed_tx, &database, &blockchain);
assert_matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid(),
"Error should be a `MissingInputTx` error"
);
database.set_raw_tx(&prev_tx).unwrap();
let result = verify_tx(&unsigned_tx, &database, &blockchain);
assert_matches!(
result,
Err(VerifyError::Consensus(_)),
"Error should be a `Consensus` error"
);
let result = verify_tx(&signed_tx, &database, &blockchain);
assert!(
result.is_ok(),
"Should work since the TX is correctly signed"
);
}
}