mirror of
https://github.com/RoboSats/taptrade-core.git
synced 2025-07-19 09:13:39 +00:00
Merge branch 'research' of github.com:RoboSats/taptrade-core into research
This commit is contained in:
@ -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
|
14
taptrade-cli-demo/bitcoin-testnet4-node/docker-compose.yml
Executable file
14
taptrade-cli-demo/bitcoin-testnet4-node/docker-compose.yml
Executable file
@ -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
|
@ -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"
|
||||
|
38
taptrade-cli-demo/coordinator/Cargo.lock
generated
38
taptrade-cli-demo/coordinator/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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<u8>,
|
||||
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<u8>,
|
||||
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<Coordinator>) -> 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;
|
||||
}
|
||||
|
@ -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<HashMap<Vec<u8>, MonitoringBond>> {
|
||||
let mut bonds = HashMap::new();
|
||||
pub async fn fetch_all_bonds(&self) -> Result<Vec<MonitoringBond>> {
|
||||
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)
|
||||
}
|
||||
|
@ -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(())
|
||||
|
@ -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<D: bdk::database::BatchDatabase> {
|
||||
pub wallet: Arc<Mutex<Wallet<D>>>,
|
||||
pub backend: Arc<ElectrumBlockchain>,
|
||||
pub backend: Arc<RpcBlockchain>,
|
||||
pub json_rpc_client: Arc<bdk::bitcoincore_rpc::Client>,
|
||||
}
|
||||
|
||||
#[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<CoordinatorWallet<sled::Tree>> {
|
||||
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<CoordinatorWallet<sled::Tree>> {
|
||||
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<D: bdk::database::BatchDatabase> CoordinatorWallet<D> {
|
||||
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<Vec<MonitoringBond>>,
|
||||
) -> Result<Vec<(MonitoringBond, anyhow::Error)>> {
|
||||
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<D: bdk::database::BatchDatabase> CoordinatorWallet<D> {
|
||||
}
|
||||
}
|
||||
|
||||
fn search_monitoring_bond_by_txid(
|
||||
// this should not happen often, so the inefficiency is acceptable
|
||||
monitoring_bonds: &Vec<MonitoringBond>,
|
||||
txid: &str,
|
||||
) -> Result<MonitoringBond> {
|
||||
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<Client>,
|
||||
bonds: Arc<Vec<MonitoringBond>>,
|
||||
invalid_bonds: &mut Vec<(MonitoringBond, anyhow::Error)>,
|
||||
) -> Result<()> {
|
||||
let raw_bonds: Vec<String> = 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<Tree> {
|
||||
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<MemoryDatabase> {
|
||||
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::<MemoryDatabase> {
|
||||
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]
|
||||
|
@ -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<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))?;
|
||||
|
||||
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<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() {
|
||||
// // https://blockstream.info/tx/95da344585fcf2e5f7d6cbf2c3df2dcce84f9196f7a7bb901a43275cd6eb7c3f
|
||||
// let prev_tx: Transaction = deserialize(&Vec::<u8>::from_hex("020000000101192dea5e66d444380e106f8e53acb171703f00d43fb6b3ae88ca5644bdb7e1000000006b48304502210098328d026ce138411f957966c1cf7f7597ccbb170f5d5655ee3e9f47b18f6999022017c3526fc9147830e1340e04934476a3d1521af5b4de4e98baf49ec4c072079e01210276f847f77ec8dd66d78affd3c318a0ed26d89dab33fa143333c207402fcec352feffffff023d0ac203000000001976a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988aca4b956050000000017a91494d5543c74a3ee98e0cf8e8caef5dc813a0f34b48768cb0700").unwrap()).unwrap();
|
||||
// // https://blockstream.info/tx/aca326a724eda9a461c10a876534ecd5ae7b27f10f26c3862fb996f80ea2d45d
|
||||
// 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_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"
|
||||
// );
|
||||
// }
|
||||
// }
|
Reference in New Issue
Block a user