diff --git a/Cargo.lock b/Cargo.lock index c98bb34..08b276f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,7 @@ dependencies = [ "bech32 0.10.0-beta", "bitcoin-internals", "bitcoin_hashes 0.13.0", + "bitcoinconsensus", "hex-conservative", "hex_lit", "secp256k1 0.28.2", @@ -136,6 +137,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoinconsensus" +version = "0.20.2-0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54505558b77e0aa21b2491a7b39cbae9db22ac8b1bc543ef4600edb762306f9c" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "bitcoincore-rpc" version = "0.18.0" diff --git a/Cargo.toml b/Cargo.toml index 912538f..0818dc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] anyhow = "1.0.79" -bitcoin = "0.31.1" +bitcoin = { version = "0.31.1", features = ["bitcoinconsensus"] } bitcoincore-rpc = "0.18.0" clap = { version = "4.4.18", features = ["derive"] } env_logger = "0.10.0" diff --git a/justfile b/justfile index 7b11309..038062d 100644 --- a/justfile +++ b/justfile @@ -6,12 +6,13 @@ help: RUST_LOG=info ./target/release/btc-dev-utils -h +# get current block height get-block-height: RUST_LOG=info ./target/release/btc-dev-utils get-block-height # get new wallet -new-wallet wallet_name="default_wallet" address_type="bech32m": - RUST_LOG=info ./target/release/btc-dev-utils -w {{ wallet_name }} -z {{ address_type }} new-wallet +new-wallet wallet_name="default_wallet": + RUST_LOG=info ./target/release/btc-dev-utils -w {{ wallet_name }} new-wallet # get wallet info get-wallet-info wallet_name="default_wallet": @@ -26,8 +27,8 @@ new-multisig required_signatures="2" wallet_names="default_wallet1,default_walle RUST_LOG=info ./target/release/btc-dev-utils -n {{ required_signatures }} -v {{ wallet_names }} -m {{ multisig_name }} new-multisig # get new wallet address -get-new-address wallet_name="default_wallet": - RUST_LOG=info ./target/release/btc-dev-utils -w {{ wallet_name }} get-new-address +get-new-address wallet_name="default_wallet" address_type="bech32m": + RUST_LOG=info ./target/release/btc-dev-utils -w {{ wallet_name }} -z {{ address_type }} get-new-address # get address info get-address-info wallet_name="default_wallet" address="address": @@ -61,6 +62,14 @@ list-unspent wallet_name="default_wallet": get-tx txid="txid": RUST_LOG=info ./target/release/btc-dev-utils -i {{ txid }} get-tx +# get details about an unspent transaction output +get-tx-out txid="txid" vout="0": + RUST_LOG=info ./target/release/btc-dev-utils -i {{ txid }} -o {{ vout }} get-tx-out + +# decode raw transaction +decode-raw-tx tx_hex="tx_hex": + RUST_LOG=info ./target/release/btc-dev-utils -t {{ tx_hex }} decode-raw-tx + # create a signed BTC transaction sign-tx wallet_name="default_wallet" recipient="recpient_address" amount="49.99" fee_amount="0.01" utxo_strat="fifo": RUST_LOG=info ./target/release/btc-dev-utils -w {{ wallet_name }} -r {{ recipient }} -x {{ amount }} -f {{ fee_amount }} -y {{ utxo_strat }} sign-tx @@ -101,6 +110,10 @@ finalize-psbt psbt="combined_psbt_hex": finalize-psbt-and-broadcast psbt="combined_psbt_hex": RUST_LOG=info ./target/release/btc-dev-utils -p {{ psbt }} finalize-psbt-and-broadcast +# Verify a signed transaction +verify-signed-tx tx_hex="tx_hex": + RUST_LOG=info ./target/release/btc-dev-utils -t {{ tx_hex }} verify-signed-tx + # create and ordinals inscription inscribe-ord: RUST_LOG=info ./target/release/btc-dev-utils inscribe-ord diff --git a/src/main.rs b/src/main.rs index 9849ffc..4d16234 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,22 @@ use std::{fs, thread, time::Duration, path::Path}; use log::{error, info}; +use modules::verification::verify_signed_tx; use serde_json::Value; use clap::Parser; use modules::bitcoind_client::{ analyze_psbt, broadcast_tx_wrapper, - combine_psbts, - decode_psbt, + combine_psbts, decode_psbt, + decode_raw_tx, finalize_psbt, finalize_psbt_and_broadcast, get_block_height, get_spendable_balance, - get_tx, rescan_blockchain + get_tx_out_wrapper, + get_tx_wrapper, + rescan_blockchain }; use modules::wallet_ops::{ @@ -71,8 +74,10 @@ fn main() -> Result<(), Box> { Action::GetSpendableBalance => get_spendable_balance(&args.address, &settings), Action::MineBlocks => mine_blocks_wrapper(&args.wallet_name, args.blocks, &settings), Action::ListUnspent => list_unspent(&args.wallet_name, &settings), - Action::GetTx => get_tx(&args.txid, &settings), + Action::GetTx => get_tx_wrapper(&args.txid, &settings), + Action::GetTxOut => get_tx_out_wrapper(&args.txid, args.vout, Some(args.confirmations), &settings), Action::SignTx => sign_tx_wrapper(&args.wallet_name, &args.recipient, args.amount, args.fee_amount, args.utxo_strat, &settings), + Action::DecodeRawTx => decode_raw_tx(&args.tx_hex, &settings), Action::BroadcastTx => broadcast_tx_wrapper( &args.tx_hex, args.max_fee_rate, &settings), Action::SendBtc => send_btc(&args.wallet_name, &args.recipient, args.amount, &settings), Action::CreatePsbt => create_psbt(&args.wallet_name, &args.recipient, args.amount, args.fee_amount, args.utxo_strat, &settings), @@ -82,6 +87,7 @@ fn main() -> Result<(), Box> { Action::CombinePsbts => combine_psbts(&args.psbts, &settings), Action::FinalizePsbt => finalize_psbt(&args.psbt_hex, &settings), Action::FinalizePsbtAndBroadcast => finalize_psbt_and_broadcast(&args.psbt_hex, &settings), + Action::VerifySignedTx => verify_signed_tx(&args.tx_hex, &settings), Action::InscribeOrd => regtest_inscribe_ord(&settings) } } diff --git a/src/modules/bitcoind_client.rs b/src/modules/bitcoind_client.rs index 9b28c23..858662f 100644 --- a/src/modules/bitcoind_client.rs +++ b/src/modules/bitcoind_client.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use bitcoin::{Address, Amount}; -use bitcoincore_rpc::json::ScanTxOutRequest; +use bitcoincore_rpc::json::{GetRawTransactionResult, GetTxOutResult, ScanTxOutRequest}; use bitcoincore_rpc::{json::FinalizePsbtResult, RpcApi, RawTx, Client}; use log::info; @@ -40,17 +40,56 @@ pub fn rescan_blockchain(settings: &Settings) -> Result<(), Box Result<(), Box> { - let client: Client = create_rpc_client(settings, None); + +pub fn get_tx(txid: &str, settings: &Settings) -> Result> { + let client: Client = create_rpc_client(settings, None); let txid_converted = bitcoin::Txid::from_str(txid)?; let tx = client.get_raw_transaction_info(&txid_converted, None)?; + + Ok(tx) +} + +pub fn get_tx_wrapper(txid: &str, settings: &Settings) -> Result<(), Box> { + let tx = get_tx(txid, settings)?; + info!("{:#?}", tx); Ok(()) } +pub fn get_tx_out(txid: &str, vout: u32, confirmations: Option, settings: &Settings) -> Result> { + let client: Client = create_rpc_client(settings, None); + + let txid_converted = bitcoin::Txid::from_str(txid)?; + let tx_out = client.get_tx_out(&txid_converted, vout, None)?; // None = include_mempool + + match tx_out { + Some(tx_out) => { + if let Some(confirmations) = confirmations { + if tx_out.confirmations >= confirmations { + Ok(tx_out) + } else { + Err(format!("TxOut not enough confirmations").into()) + } + } else { + Ok(tx_out) + } + }, + None => { + Err(format!("TxOut not found").into()) + }, + } +} + +pub fn get_tx_out_wrapper(txid: &str, vout: u32, confirmations: Option, settings: &Settings) -> Result<(), Box> { + let tx_out = get_tx_out(txid, vout, confirmations,settings)?; + + info!("{:#?}", tx_out); + + Ok(()) +} + pub fn broadcast_tx(client: &Client, tx_hex: &str, max_fee_rate: Option) -> Result> { let max_fee_rate = match max_fee_rate { Some(fee_rate) => { @@ -78,6 +117,15 @@ pub fn broadcast_tx_wrapper(tx_hex: &str, max_fee_rate: f64, settings: &Settings Ok(()) } +pub fn decode_raw_tx(tx_hex: &str, settings: &Settings) -> Result<(), Box> { + let client = create_rpc_client(settings, None); + + let tx = client.decode_raw_transaction(tx_hex, None)?; + info!("{:#?}", tx); + + Ok(()) +} + /// PSBT Ops pub fn decode_psbt(psbt: &str, settings: &Settings) -> Result<(), Box> { diff --git a/src/modules/mod.rs b/src/modules/mod.rs index db300a9..2187510 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,4 +1,5 @@ pub mod bitcoind_client; pub mod bitcoind_conn; pub mod wallet; -pub mod wallet_ops; \ No newline at end of file +pub mod wallet_ops; +pub mod verification; \ No newline at end of file diff --git a/src/modules/verification.rs b/src/modules/verification.rs new file mode 100644 index 0000000..8630c29 --- /dev/null +++ b/src/modules/verification.rs @@ -0,0 +1,57 @@ +use bitcoin::{consensus::deserialize, OutPoint, Transaction, TxOut}; + +use crate::{modules::bitcoind_client::get_tx, settings::Settings}; + +use super::bitcoind_client::get_tx_out; + +pub fn verify_signed_tx(tx_hex: &str, settings: &Settings) -> Result<(), Box> { + let tx: Transaction = deserialize(&hex::decode(tx_hex)?)?; + + println!("Verifying transaction: {}", tx.txid()); + println!("Number of inputs: {}", tx.input.len()); + + // Check if UTXOs are still unspent + for (index, input) in tx.input.iter().enumerate() { + println!("Checking UTXO for input {}", index); + match is_utxo_unspent(&input.previous_output, settings) { + Ok(true) => println!("UTXO for input {} is unspent", index), // UTXO is unspent, continue + Ok(false) => return Err(format!("UTXO for input {} has already been spent", index).into()), + Err(e) => return Err(format!("Error checking UTXO for input {}: {}", index, e).into()), + } + } + + // Create a closure to fetch the TxOut for each input + let mut spent = |outpoint: &OutPoint| -> Option { + match get_tx(&outpoint.txid.to_string(), settings) { + Ok(prev_tx) => prev_tx.vout.get(outpoint.vout as usize).map(|output| { + TxOut { + value: output.value, + script_pubkey: bitcoin::ScriptBuf::from(output.script_pub_key.hex.clone()), + } + }), + Err(_) => None, + } + }; + + // Verify the transaction + tx.verify(&mut spent).map_err(|e| {format!("Transaction verification failed: {:?}", e)})?; + + println!("Transaction verified successfully"); + + Ok(()) +} + +fn is_utxo_unspent(outpoint: &OutPoint, settings: &Settings) -> Result> { + let txid = outpoint.txid.to_string(); + + match get_tx_out(&txid, outpoint.vout, None, settings) { + Ok(_) => Ok(true), // UTXO exists and is unspent + Err(e) => { + if e.to_string().contains("TxOut not found") { + Ok(false) // UTXO doesn't exist (already spent) + } else { + Err(format!("Error checking UTXO: {}", e).into()) + } + } + } +} \ No newline at end of file diff --git a/src/utils/cli.rs b/src/utils/cli.rs index 4fa2ab6..7261fa7 100644 --- a/src/utils/cli.rs +++ b/src/utils/cli.rs @@ -89,6 +89,14 @@ pub struct Cli { #[arg(short='l', long, value_delimiter = ',', default_value = "cHNidP8BAH0CAAAAAbAip9TqQ,cHNidP8BAH0CAAAAAbAip9TqQ")] pub psbts: Vec, + /// Vout + #[arg(short='o', long, default_value = "0")] + pub vout: u32, + + /// Transaction confirmations + #[arg(short='c', long, default_value = "0")] + pub confirmations: u32, + #[command(subcommand)] pub action: Action, } @@ -109,7 +117,9 @@ pub enum Action { MineBlocks, ListUnspent, GetTx, + GetTxOut, SignTx, + DecodeRawTx, BroadcastTx, SendBtc, CreatePsbt, @@ -119,6 +129,7 @@ pub enum Action { CombinePsbts, FinalizePsbt, FinalizePsbtAndBroadcast, + VerifySignedTx, InscribeOrd }