diff --git a/taptrade-cli-demo/bitcoin-testnet4-node/bitcoin_data/bitcoin.conf b/taptrade-cli-demo/bitcoin-testnet4-node/bitcoin_data/bitcoin.conf new file mode 100644 index 0000000..f7aaf31 --- /dev/null +++ b/taptrade-cli-demo/bitcoin-testnet4-node/bitcoin_data/bitcoin.conf @@ -0,0 +1,15 @@ +[testnet4] +server=1 +txindex=1 +rpcbind=127.0.0.1 +rpcallowip=127.0.0.1 +rpcport=18332 +# Authentication +rpcauth= +# generator: https://jlopp.github.io/bitcoin-core-rpc-auth-generator/ +# Cookie file authentication +rpccookiefile=/home/user/.bitcoin/.cookie +# Increase the number of connections +maxconnections=15 +# Set the maximum number of transactions to keep in the memory pool +maxmempool=300 diff --git a/taptrade-cli-demo/bitcoin-testnet4-node/docker-compose.yml b/taptrade-cli-demo/bitcoin-testnet4-node/docker-compose.yml new file mode 100755 index 0000000..5bc7e44 --- /dev/null +++ b/taptrade-cli-demo/bitcoin-testnet4-node/docker-compose.yml @@ -0,0 +1,14 @@ +services: + bitcoind: + image: mocacinno/btc_testnet4:latest + privileged: true + container_name: bitcoind + volumes: + - ./bitcoin_data:/root/.bitcoin/ + command: ["bitcoind", "-testnet4"] + ports: + - "18332:18332" + - "8333:8333" + - "48332:48332" + +# https://bitcointalk.org/index.php?topic=5496494 diff --git a/taptrade-cli-demo/coordinator/.env b/taptrade-cli-demo/coordinator/.env index fc882e3..adf371c 100644 --- a/taptrade-cli-demo/coordinator/.env +++ b/taptrade-cli-demo/coordinator/.env @@ -1,5 +1,6 @@ -ELECTRUM_BACKEND="ssl://mempool.space:40002" # we need a node with large mempool size limit for monitoring to miss no bond transactions -ESPLORA_BACKEND="https://blockstream.info/testnet/api" # blockstream.info testnet backend +BITCOIN_RPC_ADDRESS_PORT="127.0.0.1:18332" +BITCOIN_RPC_COOKIE_FILE_PATH="~/.bitcoin/.cookie" # path to the cookie file for the bitcoind RPC +BITCOIN_RPC_WALLET_NAME="coordinator_wallet" # not used yet DATABASE_PATH="./dbs/trades.db" # path to the coordinator sqlite database storing the trades BDK_DB_PATH="./dbs/bdk-wallet" # Path to the BDK Sled database (no .db postfix) WALLET_XPRV="tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32" diff --git a/taptrade-cli-demo/coordinator/Cargo.lock b/taptrade-cli-demo/coordinator/Cargo.lock index b03bf75..d97f0ec 100644 --- a/taptrade-cli-demo/coordinator/Cargo.lock +++ b/taptrade-cli-demo/coordinator/Cargo.lock @@ -252,6 +252,7 @@ dependencies = [ "bdk-macros", "bitcoin 0.30.2", "bitcoinconsensus", + "core-rpc", "electrum-client", "esplora-client", "getrandom", @@ -504,6 +505,32 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core-rpc" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d77079e1b71c2778d6e1daf191adadcd4ff5ec3ccad8298a79061d865b235b" +dependencies = [ + "bitcoin-private", + "core-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "core-rpc-json" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581898ed9a83f31c64731b1d8ca2dfffcfec14edf1635afacd5234cddbde3a41" +dependencies = [ + "bitcoin 0.30.2", + "bitcoin-private", + "serde", + "serde_json", +] + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1182,6 +1209,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpc" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8d6b3f301ba426b30feca834a2a18d48d5b54e5065496b5c1b05537bee3639" +dependencies = [ + "base64 0.13.1", + "serde", + "serde_json", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/taptrade-cli-demo/coordinator/Cargo.toml b/taptrade-cli-demo/coordinator/Cargo.toml index 1eae80f..d7921e5 100644 --- a/taptrade-cli-demo/coordinator/Cargo.toml +++ b/taptrade-cli-demo/coordinator/Cargo.toml @@ -9,7 +9,7 @@ bitcoin = "0.29.0" miniscript = "12.0.0" axum = { version = "0.7.5", features = ["tokio", "json"] } # "use-esplora-async", "async-interface", for async esplora -bdk = { version = "0.29.0", default-features = false, features = ["key-value-db", "bitcoinconsensus", "std", "electrum", "use-esplora-ureq","compiler", "verify"] } +bdk = { version = "0.29.0", default-features = false, features = ["key-value-db", "bitcoinconsensus", "std", "electrum", "use-esplora-ureq","compiler", "verify", "rpc"] } # bitcoinconsensus = "0.106.0" dotenv = "0.15.0" @@ -19,7 +19,7 @@ rand = "0.8.5" reqwest = { version = "0.12.4", features = ["blocking", "json"] } serde = "1.0.203" sqlx = { version = "0.7.4", features = ["runtime-tokio", "sqlite"] } -tokio = { version = "1.38.0", features = ["full", "test-util"] } +tokio = { version = "1.38.0", features = ["full", "test-util", "rt"] } tower = "0.4.13" log = "0.4.22" env_logger = "0.11.3" diff --git a/taptrade-cli-demo/coordinator/src/coordinator/verify_bond.rs b/taptrade-cli-demo/coordinator/src/coordinator/_verify_bond.old similarity index 100% rename from taptrade-cli-demo/coordinator/src/coordinator/verify_bond.rs rename to taptrade-cli-demo/coordinator/src/coordinator/_verify_bond.old diff --git a/taptrade-cli-demo/coordinator/src/coordinator/monitoring.rs b/taptrade-cli-demo/coordinator/src/coordinator/monitoring.rs index 67b8bb3..0c92385 100644 --- a/taptrade-cli-demo/coordinator/src/coordinator/monitoring.rs +++ b/taptrade-cli-demo/coordinator/src/coordinator/monitoring.rs @@ -4,16 +4,18 @@ // Also needs to implement punishment logic in case a fraud is detected. use super::*; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Table { Orderbook, ActiveTrades, + Memory, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MonitoringBond { pub bond_tx_hex: String, pub trade_id_hex: String, + pub robot: Vec, pub requirements: BondRequirements, pub table: Table, } @@ -23,11 +25,7 @@ pub struct MonitoringBond { // continue monitoring the bond transaction until a confirmation happens for maximum pain // in case the trader is actively malicious and did not just accidentally invalidate the bond // we could directly forward bond sats to the other parties payout address in case it is a taken trade -async fn punish_trader( - coordinator: &Coordinator, - robohash: Vec, - bond: MonitoringBond, -) -> Result<()> { +async fn punish_trader(coordinator: &Coordinator, bond: &MonitoringBond) -> Result<()> { // publish bond coordinator .coordinator_wallet @@ -43,32 +41,29 @@ pub async fn monitor_bonds(coordinator: Arc) -> Result<()> { loop { // fetch all bonds - let bonds = coordinator_db.fetch_all_bonds().await?; + let bonds = Arc::new(coordinator_db.fetch_all_bonds().await?); + let validation_results = coordinator_wallet + .validate_bonds(Arc::clone(&bonds)) + .await?; debug!("Monitoring active bonds: {}", bonds.len()); // verify all bonds and initiate punishment if necessary - for bond in bonds { - if let Err(e) = coordinator_wallet - .validate_bond_tx_hex(&bond.1.bond_tx_hex, &bond.1.requirements) - .await + for (bond, error) in validation_results { + warn!("Bond validation failed: {:?}", error); + match env::var("PUNISHMENT_ENABLED") + .unwrap_or_else(|_| "0".to_string()) + .as_str() { - warn!("Bond validation failed: {:?}", e); - match env::var("PUNISHMENT_ENABLED") - .unwrap_or_else(|_| "0".to_string()) - .as_str() - { - "1" => { - dbg!("Punishing trader for bond violation: {:?}", e); - punish_trader(&coordinator, bond.0, bond.1).await?; - } - "0" => { - dbg!("Punishment disabled, ignoring bond violation: {:?}", e); - continue; - } - _ => Err(anyhow!("Invalid PUNISHMENT_ENABLED env var"))?, + "1" => { + dbg!("Punishing trader for bond violation: {:?}", error); + punish_trader(&coordinator, &bond).await?; } + "0" => { + dbg!("Punishment disabled, ignoring bond violation: {:?}", error); + continue; + } + _ => Err(anyhow!("Invalid PUNISHMENT_ENABLED env var"))?, } } - // sleep for a while tokio::time::sleep(tokio::time::Duration::from_secs(15)).await; } diff --git a/taptrade-cli-demo/coordinator/src/database/mod.rs b/taptrade-cli-demo/coordinator/src/database/mod.rs index 252ca26..305a3a8 100644 --- a/taptrade-cli-demo/coordinator/src/database/mod.rs +++ b/taptrade-cli-demo/coordinator/src/database/mod.rs @@ -404,8 +404,8 @@ impl CoordinatorDB { // returns a hashmap of RoboHash, MonitoringBond for the monitoring loop // in case this gets a bottleneck (db too large for heap) we can implement in place checking - pub async fn fetch_all_bonds(&self) -> Result, MonitoringBond>> { - let mut bonds = HashMap::new(); + pub async fn fetch_all_bonds(&self) -> Result> { + let mut bonds = Vec::new(); let mut rows_orderbook = sqlx::query( "SELECT offer_id, robohash, bond_address, bond_amount_sat, amount_sat, bond_tx_hex FROM active_maker_offers", ) @@ -422,11 +422,12 @@ impl CoordinatorDB { let bond = MonitoringBond { bond_tx_hex: row.get("bond_tx_hex"), + robot: robohash, trade_id_hex: row.get("offer_id"), requirements, table: Table::Orderbook, }; - bonds.insert(robohash, bond); + bonds.push(bond); } let mut rows_taken = sqlx::query( @@ -453,11 +454,12 @@ impl CoordinatorDB { let bond_maker = MonitoringBond { bond_tx_hex: row.get("bond_tx_hex_maker"), + robot: robohash_maker, trade_id_hex: trade_id_hex.clone(), requirements: requirements_maker, table: Table::ActiveTrades, }; - bonds.insert(robohash_maker, bond_maker); + bonds.push(bond_maker); let requirements_maker = BondRequirements { bond_address: row.get("bond_address_taker"), @@ -465,13 +467,14 @@ impl CoordinatorDB { min_input_sum_sat, }; - let bond_maker = MonitoringBond { + let bond_taker = MonitoringBond { bond_tx_hex: row.get("bond_tx_hex_taker"), trade_id_hex, + robot: robohash_taker, requirements: requirements_maker, table: Table::ActiveTrades, }; - bonds.insert(robohash_taker, bond_maker); + bonds.push(bond_taker); } Ok(bonds) } diff --git a/taptrade-cli-demo/coordinator/src/main.rs b/taptrade-cli-demo/coordinator/src/main.rs index 6bad234..b83972f 100755 --- a/taptrade-cli-demo/coordinator/src/main.rs +++ b/taptrade-cli-demo/coordinator/src/main.rs @@ -10,10 +10,10 @@ use coordinator::monitoring::monitor_bonds; use coordinator::monitoring::*; use database::CoordinatorDB; use dotenv::dotenv; -use log::{debug, error, info, trace, warn}; +use log::{debug, error, info, warn}; use std::time::{SystemTime, UNIX_EPOCH}; use std::{env, sync::Arc}; -use tokio::sync::Mutex; +use tokio::{sync::Mutex, task::spawn_blocking}; use wallet::*; pub struct Coordinator { @@ -37,8 +37,16 @@ async fn main() -> Result<()> { }); // begin monitoring bonds - tokio::spawn(monitor_bonds(Arc::clone(&coordinator))); - + let coordinator_ref = Arc::clone(&coordinator); + tokio::spawn(async move { + loop { + if let Err(e) = monitor_bonds(coordinator_ref.clone()).await { + error!("Error in monitor_bonds: {:?}", e); + // Optionally add a delay before retrying + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + } + }); // Start the API server api_server(coordinator).await?; Ok(()) diff --git a/taptrade-cli-demo/coordinator/src/wallet/mod.rs b/taptrade-cli-demo/coordinator/src/wallet/mod.rs index 027e592..24cd8f6 100644 --- a/taptrade-cli-demo/coordinator/src/wallet/mod.rs +++ b/taptrade-cli-demo/coordinator/src/wallet/mod.rs @@ -5,25 +5,26 @@ use super::*; use anyhow::Context; use bdk::{ bitcoin::{self, bip32::ExtendedPrivKey, consensus::encode::deserialize, Transaction}, - blockchain::{Blockchain, ElectrumBlockchain}, - electrum_client::client::Client, + bitcoincore_rpc::{Client, RawTx, RpcApi}, + blockchain::{rpc::Auth, Blockchain, ConfigurableBlockchain, RpcBlockchain, RpcConfig}, sled::{self, Tree}, template::Bip86, wallet::verify::*, KeychainKind, SyncOptions, Wallet, }; -use std::fmt; use std::str::FromStr; +use std::{fmt, ops::Deref}; use utils::*; // use verify_tx::*; #[derive(Clone)] pub struct CoordinatorWallet { pub wallet: Arc>>, - pub backend: Arc, + pub backend: Arc, + pub json_rpc_client: Arc, } -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub struct BondRequirements { pub bond_address: String, pub locking_amount_sat: u64, @@ -34,10 +35,17 @@ pub fn init_coordinator_wallet() -> Result> { let wallet_xprv = ExtendedPrivKey::from_str( &env::var("WALLET_XPRV").context("loading WALLET_XPRV from .env failed")?, )?; - let backend = ElectrumBlockchain::from(Client::new( - &env::var("ELECTRUM_BACKEND") - .context("Parsing ELECTRUM_BACKEND from .env failed, is it set?")?, - )?); + let rpc_config = RpcConfig { + url: env::var("BITCOIN_RPC_ADDRESS_PORT")?.to_string(), + auth: Auth::Cookie { + file: env::var("BITCOIN_RPC_COOKIE_FILE_PATH")?.into(), + }, + network: bdk::bitcoin::Network::Testnet, + wallet_name: env::var("BITCOIN_RPC_WALLET_NAME")?, + sync_params: None, + }; + let json_rpc_client = Client::new(&rpc_config.url, rpc_config.auth.clone().into())?; + let backend = RpcBlockchain::from_config(&rpc_config)?; // let backend = EsploraBlockchain::new(&env::var("ESPLORA_BACKEND")?, 1000); let sled_db = sled::open(env::var("BDK_DB_PATH")?)?.open_tree("default_wallet")?; let wallet = Wallet::new( @@ -54,6 +62,7 @@ pub fn init_coordinator_wallet() -> Result> { Ok(CoordinatorWallet { wallet: Arc::new(Mutex::new(wallet)), backend: Arc::new(backend), + json_rpc_client: Arc::new(json_rpc_client), }) } @@ -64,61 +73,108 @@ impl CoordinatorWallet { Ok(address.address.to_string()) } - // validate bond (check amounts, valid inputs, correct addresses, valid signature, feerate) - // also check if inputs are confirmed already pub async fn validate_bond_tx_hex( &self, - bond: &str, + bond_tx_hex: &str, requirements: &BondRequirements, ) -> Result<()> { - let input_sum: u64; - let blockchain = &*self.backend; - let tx: Transaction = deserialize(&hex::decode(bond)?)?; - { - debug!("Called validate_bond_tx_hex()"); - let wallet = self.wallet.lock().await; - if let Err(e) = wallet.sync(blockchain, SyncOptions::default()) { - error!("Error syncing wallet: {:?}", e); - return Ok(()); // if the electrum server goes down all bonds will be considered valid. Maybe redundancy should be added. - }; - // we need to test this with signed and invalid/unsigned transactions - // checks signatures and inputs - if let Err(e) = verify_tx(&tx, &*wallet.database(), blockchain) { - return Err(anyhow!(e)); - } - - // check if the tx has the correct input amounts (have to be >= trading amount) - input_sum = match tx.input_sum(blockchain, &*wallet.database()) { - Ok(amount) => { - if amount < requirements.min_input_sum_sat { - return Err(anyhow!("Bond input sum too small")); - } - amount - } - Err(e) => { - return Err(anyhow!(e)); - } - }; - } - // check if bond output to us is big enough - match tx.bond_output_sum(&requirements.bond_address) { - Ok(amount) => { - if amount < requirements.locking_amount_sat { - return Err(anyhow!("Bond output sum too small")); - } - amount - } - Err(e) => { - return Err(anyhow!(e)); - } + debug!("Validating bond in validate_bond_tx_hex()"); + let dummy_monitoring_bond = MonitoringBond { + bond_tx_hex: bond_tx_hex.to_string(), + trade_id_hex: "0".to_string(), + robot: vec![0], + requirements: requirements.clone(), + table: Table::Memory, }; - if ((input_sum - tx.all_output_sum()) / tx.vsize() as u64) < 200 { - return Err(anyhow!("Bond fee rate too low")); + let invalid_bond = self + .validate_bonds(Arc::new(vec![dummy_monitoring_bond])) + .await?; + if !invalid_bond.is_empty() { + return Err(anyhow!(invalid_bond[0].1.to_string())); } - debug!("validate_bond_tx_hex(): Bond validation successful."); Ok(()) } + // validate bond (check amounts, valid inputs, correct addresses, valid signature, feerate) + // also check if inputs are confirmed already + // bdk::blockchain::compact_filters::Mempool::iter_txs() -> Vec(Tx) to check if contained in mempool + // blockchain::get_tx to get input + pub async fn validate_bonds( + &self, + bonds: Arc>, + ) -> Result> { + let mut invalid_bonds: Vec<(MonitoringBond, anyhow::Error)> = Vec::new(); + let blockchain = &*self.backend; + + { + let wallet = self.wallet.lock().await; + for bond in *bonds { + let input_sum: u64; + + let tx: Transaction = deserialize(&hex::decode(&bond.bond_tx_hex)?)?; + debug!("Validating bond in validate_bonds()"); + // we need to test this with signed and invalid/unsigned transactions + // checks signatures and inputs + if let Err(e) = verify_tx(&tx, &*wallet.database(), blockchain) { + invalid_bonds.push((bond.clone(), anyhow!(e))); + continue; + } + + // check if the tx has the correct input amounts (have to be >= trading amount) + input_sum = match tx.input_sum(blockchain, &*wallet.database()) { + Ok(amount) => { + if amount < bond.requirements.min_input_sum_sat { + invalid_bonds.push(( + bond.clone(), + anyhow!("Bond input sum too small: {}", amount), + )); + continue; + } + amount + } + Err(e) => { + return Err(anyhow!(e)); + } + }; + // check if bond output to us is big enough + match tx.bond_output_sum(&bond.requirements.bond_address) { + Ok(amount) => { + if amount < bond.requirements.locking_amount_sat { + invalid_bonds.push(( + bond.clone(), + anyhow!("Bond output sum too small: {}", amount), + )); + continue; + } + amount + } + Err(e) => { + return Err(anyhow!(e)); + } + }; + if ((input_sum - tx.all_output_sum()) / tx.vsize() as u64) < 200 { + invalid_bonds.push(( + bond.clone(), + anyhow!( + "Bond fee rate too low: {}", + (input_sum - tx.all_output_sum()) / tx.vsize() as u64 + ), + )); + continue; + } + } + } + // let invalid_bonds = Arc::new(invalid_bonds); + // let json_rpc_client = self.json_rpc_client.clone(); + // let mempool_accept_future = tokio::task::spawn_blocking(move || { + // test_mempool_accept_bonds(json_rpc_client, bonds, &mut invalid_bonds) + // }); + // mempool_accept_future.await??; + + debug!("validate_bond_tx_hex(): Bond validation done."); + Ok(invalid_bonds) + } + pub fn publish_bond_tx_hex(&self, bond: &str) -> Result<()> { warn!("publish_bond_tx_hex(): publishing cheating bond tx!"); let blockchain = &*self.backend; @@ -129,6 +185,49 @@ impl CoordinatorWallet { } } +fn search_monitoring_bond_by_txid( + // this should not happen often, so the inefficiency is acceptable + monitoring_bonds: &Vec, + txid: &str, +) -> Result { + for bond in monitoring_bonds { + let bond_tx: Transaction = deserialize(&hex::decode(&bond.bond_tx_hex)?)?; + if bond_tx.txid().to_string() == txid { + return Ok(bond.clone()); + } + } + Err(anyhow!("Bond not found in monitoring bonds")) +} + +fn test_mempool_accept_bonds( + json_rpc_client: Arc, + bonds: Arc>, + invalid_bonds: &mut Vec<(MonitoringBond, anyhow::Error)>, +) -> Result<()> { + let raw_bonds: Vec = bonds + .iter() + .map(|bond| bond.bond_tx_hex.clone().raw_hex()) // Assuming `raw_hex()` returns a String or &str + .collect(); + + let test_mempool_accept_res = json_rpc_client.deref().test_mempool_accept(&raw_bonds)?; + + for res in test_mempool_accept_res { + if !res.allowed { + let invalid_bond: MonitoringBond = + search_monitoring_bond_by_txid(&bonds, &res.txid.to_string())?; + invalid_bonds.push(( + invalid_bond, + anyhow!( + "Bond not accepted by testmempoolaccept: {:?}", + res.reject_reason + .unwrap_or("rejected by testmempoolaccept".to_string()) + ), + )); + }; + } + Ok(()) +} + impl fmt::Debug for CoordinatorWallet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("CoordinatorWallet") @@ -145,10 +244,20 @@ mod tests { use super::*; use bdk::bitcoin::Network; use bdk::database::MemoryDatabase; - use bdk::{blockchain::ElectrumBlockchain, Wallet}; - + use bdk::{blockchain::RpcBlockchain, Wallet}; async fn new_test_wallet(wallet_xprv: &str) -> CoordinatorWallet { - let backend = ElectrumBlockchain::from(Client::new("ssl://mempool.space:40002").unwrap()); + dotenv().ok(); + let rpc_config = RpcConfig { + url: env::var("BITCOIN_RPC_ADDRESS_PORT").unwrap().to_string(), + auth: Auth::Cookie { + file: env::var("BITCOIN_RPC_COOKIE_FILE_PATH").unwrap().into(), + }, + network: bdk::bitcoin::Network::Testnet, + wallet_name: env::var("BITCOIN_RPC_WALLET_NAME").unwrap(), + sync_params: None, + }; + let json_rpc_client = Client::new(&rpc_config.url, rpc_config.auth.clone().into()).unwrap(); + let backend = RpcBlockchain::from_config(&rpc_config).unwrap(); let wallet_xprv = ExtendedPrivKey::from_str(wallet_xprv).unwrap(); let wallet = Wallet::new( @@ -163,22 +272,60 @@ mod tests { CoordinatorWallet:: { wallet: Arc::new(Mutex::new(wallet)), backend: Arc::new(backend), + json_rpc_client: Arc::new(json_rpc_client), } } #[tokio::test] async fn test_transaction_without_signature() { - panic!("Not implemented"); + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond_without_signature = "02000000010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb380140000000000fdffffff02998d0000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c6532364f860000"; + let requirements = BondRequirements { + min_input_sum_sat: 51000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" + .to_string(), + }; + + let result = test_wallet + .validate_bond_tx_hex(&bond_without_signature, &requirements) + .await; + assert!(result.is_err()); } #[tokio::test] async fn test_transaction_with_invalid_signature() { - panic!("Not implemented"); + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + // assembled bond tx but with the signature of a different bond = invalid + let bond_with_invalid_signature = "020000000001010127a9d96655011fca55dc2667f30b98655e46da98d0f84df676b53d7fb3801400000000001900000002aa900000000000002251207dd0d1650cdc22537709e35620f3b5cc3249b305bda1209ba4e5e01bc3ad2d8c50c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c65323601401fddcc681a1d0324c8fdeabbc08a3b06c26741872363c0ddfc82f15b6abe43d37815bcdc2ce1fb2f70cac426f7fb269d322ac6a621886208d0c625335bba670800000000"; + let requirements = BondRequirements { + min_input_sum_sat: 51000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" + .to_string(), + }; + + let result = test_wallet + .validate_bond_tx_hex(&bond_with_invalid_signature, &requirements) + .await; + assert!(result.is_err()); } #[tokio::test] - async fn test_transaction_with_spent_input() { - panic!("Not implemented"); + async fn test_bond_with_spent_input() { + let test_wallet = new_test_wallet("tprv8ZgxMBicQKsPdHuCSjhQuSZP1h6ZTeiRqREYS5guGPdtL7D1uNLpnJmb2oJep99Esq1NbNZKVJBNnD2ZhuXSK7G5eFmmcx73gsoa65e2U32").await; + let bond_with_spent_input = "02000000000101f7d992795b0b43227ea83e296a7c2a91771ede3ef54f1eb5664393c79b9399080100000000fdffffff0250c3000000000000225120a12e5d145a4a3ab43f6cc1188435e74f253eace72bd986f1aaf780fd0c653236abc6010000000000225120b83c64b440203fb74a0c672cd829f387b957129835dd3b5c4e33fc71a146b3ae0140afdafbae5b76217f469790b211d7fbda427e5b4379c4603e9ae08c9ef5aaae30bfecfc16e5f636c737bea8e0e27974854d1cd0d094ed737aadfc679a974074574f860000"; + let requirements = BondRequirements { + min_input_sum_sat: 51000, + locking_amount_sat: 50000, + bond_address: "tb1p5yh969z6fgatg0mvcyvggd08fujnat8890vcdud277q06rr9xgmqwfdkcx" + .to_string(), + }; + + let result = test_wallet + .validate_bond_tx_hex(&bond_with_spent_input, &requirements) + .await; + assert!(result.is_err()); } #[tokio::test] diff --git a/taptrade-cli-demo/coordinator/src/wallet/verify_tx.rs b/taptrade-cli-demo/coordinator/src/wallet/verify_tx.rs deleted file mode 100644 index 5ee2402..0000000 --- a/taptrade-cli-demo/coordinator/src/wallet/verify_tx.rs +++ /dev/null @@ -1,160 +0,0 @@ -//////////////////////////////////////////////////////////////////////////// -// copied from bdk as verify_tx does not compile with async esplora backend. -// needs to be updated in case we want to use async esplora -//////////////////////////////////////////////////////////////////////////// - -//! Verify transactions against the consensus rules - -use std::collections::HashMap; -use std::fmt; - -use bdk::bitcoin::consensus::serialize; -use bdk::bitcoin::{OutPoint, Transaction, Txid}; - -use bdk::blockchain::GetTx; -use bdk::database::Database; -// use bdk::error::Error; - -/// Verify a transaction against the consensus rules -/// -/// This function uses [`bitcoinconsensus`] to verify transactions by fetching the required data -/// either from the [`Database`] or using the [`Blockchain`]. -/// -/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the -/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or -/// with unconfirmed transactions that have been evicted from the backend's memory. -/// -/// [`Blockchain`]: crate::blockchain::Blockchain -pub fn verify_tx_mod( - 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))?; - - let res = bitcoinconsensus::verify( - &spent_output.script_pubkey.to_bytes(), - spent_output.value, - &serialized_tx, - index, - ); - - // Since we have a local cache we might as well cache stuff from the db, as it will very - // likely decrease latency compared to reading from disk or performing an SQL query. - tx_cache.insert(prev_tx.txid(), prev_tx); - } - - Ok(()) -} - -/// Error during validation of a tx agains the consensus rules -#[derive(Debug)] -pub enum VerifyError { - /// The transaction being spent is not available in the database or the blockchain client - MissingInputTx(Txid), - /// The transaction being spent doesn't have the requested output - InvalidInput(OutPoint), - - /// Consensus error - Consensus(bitcoinconsensus::Error), - - /// Generic error - /// - /// It has to be wrapped in a `Box` since `Error` has a variant that contains this enum - Global(Box), -} - -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 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, Error> { -// Ok(None) -// } -// } - -// #[test] -// fn test_verify_fail_unsigned_tx() { -// // https://blockstream.info/tx/95da344585fcf2e5f7d6cbf2c3df2dcce84f9196f7a7bb901a43275cd6eb7c3f -// let prev_tx: Transaction = deserialize(&Vec::::from_hex("020000000101192dea5e66d444380e106f8e53acb171703f00d43fb6b3ae88ca5644bdb7e1000000006b48304502210098328d026ce138411f957966c1cf7f7597ccbb170f5d5655ee3e9f47b18f6999022017c3526fc9147830e1340e04934476a3d1521af5b4de4e98baf49ec4c072079e01210276f847f77ec8dd66d78affd3c318a0ed26d89dab33fa143333c207402fcec352feffffff023d0ac203000000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988aca4b956050000000017a91494d5543c74a3ee98e0cf8e8caef5dc813a0f34b48768cb0700").unwrap()).unwrap(); -// // https://blockstream.info/tx/aca326a724eda9a461c10a876534ecd5ae7b27f10f26c3862fb996f80ea2d45d -// let signed_tx: Transaction = deserialize(&Vec::::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_mod(&signed_tx, &database, &blockchain); -// assert_matches!(result, Err(VerifyError::MissingInputTx(txid)) if txid == prev_tx.txid(), -// "Error should be a `MissingInputTx` error" -// ); - -// // insert the prev_tx -// database.set_raw_tx(&prev_tx).unwrap(); - -// let result = verify_tx_mod(&unsigned_tx, &database, &blockchain); -// assert_matches!( -// result, -// Err(VerifyError::Consensus(_)), -// "Error should be a `Consensus` error" -// ); - -// let result = verify_tx_mod(&signed_tx, &database, &blockchain); -// assert!( -// result.is_ok(), -// "Should work since the TX is correctly signed" -// ); -// } -// }