From c9d76e84827020b407bfa11c20811e5288da3c0c Mon Sep 17 00:00:00 2001 From: Nadav Ivgi Date: Wed, 23 Sep 2020 12:18:02 +0300 Subject: [PATCH] Scriptable transaction broadcast (#7) --- CHANGELOG.md | 15 +++++++++++++++ README.md | 14 ++++++++++++++ src/app.rs | 2 +- src/config.rs | 24 ++++++++++++++++++++++-- src/error.rs | 3 +++ src/query.rs | 30 ++++++++++++++++++++++++------ 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a96884..a5b8612 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ - Reproducible builds using Docker +- Scriptable transaction broadcast command via `--tx-broadcast-cmd ` (#7) + + The command will be used in place of broadcasting transactions using the full node, + which may provide better privacy in some circumstances. + + For example, to broadcast transactions over Tor using the blockstream.info onion service, you can use: + + ``` + --tx-broadcast-cmd '[ $(curl -s -x socks5h://localhost:9050 http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api/tx -d {tx_hex} -o /dev/stderr -w "%{http_code}") -eq 200 ]' + ``` + + (replace port `9050` with `9150` if you're using the Tor browser bundle) + + h/t @chris-belcher's EPS for inspiring this feature! 🎩 + - Electrum plugin: Fix hot wallet test (#47) - Electrum: Fix docker image libssl dependency with the `http` feature (#48) diff --git a/README.md b/README.md index fa726ef..c2a3af7 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,20 @@ It is recommended to use a separate watch-only wallet for bwt (can be created wi *Note that EPS and bwt should not be run on the same bitcoind wallet with the same xpub, they will conflict.* +##### Scriptable transaction broadcast + +You may set a custom command for broadcasting transactions via `--tx-broadcast-cmd `. + +The command will be used in place of broadcasting transactions using the full node, +which may provide better privacy in some circumstances. + +For example, to broadcast transactions over Tor using the blockstream.info onion service, you can use: + +``` +--tx-broadcast-cmd '[ $(curl -s -x socks5h://localhost:9050 http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api/tx -d {tx_hex} -o /dev/stderr -w "%{http_code}") -eq 200 ]' +``` + +(replace port `9050` with `9150` if you're using the Tor browser bundle) ## Electrum plugin diff --git a/src/app.rs b/src/app.rs index ca94ca9..d150393 100644 --- a/src/app.rs +++ b/src/app.rs @@ -50,7 +50,7 @@ impl App { config.bitcoind_auth()?, )?); let indexer = Arc::new(RwLock::new(Indexer::new(rpc.clone(), watcher))); - let query = Arc::new(Query::new(config.network, rpc.clone(), indexer.clone())); + let query = Arc::new(Query::new((&config).into(), rpc.clone(), indexer.clone())); wait_bitcoind(&rpc)?; diff --git a/src/config.rs b/src/config.rs index 89b9e80..c49d127 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ use bitcoincore_rpc::Auth as RpcAuth; use crate::error::{Context, OptionExt, Result}; use crate::hd::XyzPubKey; +use crate::query::QueryConfig; use crate::types::RescanSince; #[derive(StructOpt, Debug)] @@ -198,6 +199,16 @@ pub struct Config { )] pub poll_interval: time::Duration, + #[structopt( + short = "B", + long = "tx-broadcast-cmd", + help = "Custom command for broadcasting transactions. {tx_hex} is replaced with the transaction.", + env, + hide_env_values(true), + display_order(91) + )] + pub broadcast_cmd: Option, + #[cfg(unix)] #[structopt( long, @@ -205,7 +216,7 @@ pub struct Config { help = "Path to bind the sync notification unix socket", env, hide_env_values(true), - display_order(91) + display_order(101) )] pub unix_listener_path: Option, @@ -217,7 +228,7 @@ pub struct Config { env, hide_env_values(true), use_delimiter(true), - display_order(92) + display_order(102) )] pub webhook_urls: Option>, } @@ -401,3 +412,12 @@ fn bitcoind_default_dir() -> Option { #[cfg(windows)] return Some(dirs::data_dir()?.join("Bitcoin")); } + +impl From<&Config> for QueryConfig { + fn from(config: &Config) -> QueryConfig { + QueryConfig { + network: config.network, + broadcast_cmd: config.broadcast_cmd.clone(), + } + } +} diff --git a/src/error.rs b/src/error.rs index b720789..b9bef53 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,6 +24,9 @@ pub enum BwtError { #[error("Blocks unavailable due to pruning")] PrunedBlocks, + #[error("Custom broadcast command failed with {0}")] + BroadcastCmdFailed(std::process::ExitStatus), + #[error("Error communicating with the Bitcoin RPC: {0}")] RpcProtocol(rpc::Error), diff --git a/src/query.rs b/src/query.rs index e145cdc..ace9300 100644 --- a/src/query.rs +++ b/src/query.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::process::Command; use std::sync::{Arc, RwLock}; use std::time::{Duration, Instant}; @@ -6,7 +7,7 @@ use serde::Serialize; use serde_json::Value; use bitcoin::util::bip32::Fingerprint; -use bitcoin::{BlockHash, BlockHeader, Network, OutPoint, Txid}; +use bitcoin::{BlockHash, BlockHeader, Network, OutPoint, Transaction, Txid}; use bitcoincore_rpc::{json as rpcjson, Client as RpcClient, RpcApi}; use crate::error::{BwtError, Context, OptionExt, Result}; @@ -23,7 +24,7 @@ const FEE_HISTOGRAM_TTL: Duration = Duration::from_secs(120); const FEE_ESTIMATES_TTL: Duration = Duration::from_secs(120); pub struct Query { - network: Network, + config: QueryConfig, rpc: Arc, indexer: Arc>, @@ -32,12 +33,17 @@ pub struct Query { cached_estimates: RwLock, Instant)>>, } +pub struct QueryConfig { + pub network: Network, + pub broadcast_cmd: Option, +} + type FeeHistogram = Vec<(f32, u32)>; impl Query { - pub fn new(network: Network, rpc: Arc, indexer: Arc>) -> Self { + pub fn new(config: QueryConfig, rpc: Arc, indexer: Arc>) -> Self { Query { - network, + config, rpc, indexer, cached_relayfee: RwLock::new(None), @@ -104,7 +110,7 @@ impl Query { // regtest typically doesn't have fee estimates, just use the relay fee instead. // this stops electrum from complanining about unavailable dynamic fees. - if self.network == Network::Regtest { + if self.config.network == Network::Regtest { return self.relay_fee().map(Some); } @@ -183,7 +189,19 @@ impl Query { } pub fn broadcast(&self, tx_hex: &str) -> Result { - Ok(self.rpc.send_raw_transaction(tx_hex)?) + if let Some(broadcast_cmd) = &self.config.broadcast_cmd { + // need to deserialize to ensure validity (preventing code injection) and to determine the txid + let tx: Transaction = bitcoin::consensus::deserialize(&hex::decode(tx_hex)?)?; + let cmd = broadcast_cmd.replacen("{tx_hex}", tx_hex, 1); + debug!("broadcasting tx with cmd {}", broadcast_cmd); + let status = Command::new("sh").arg("-c").arg(cmd).status()?; + if !status.success() { + bail!(BwtError::BroadcastCmdFailed(status)) + } + Ok(tx.txid()) + } else { + Ok(self.rpc.send_raw_transaction(tx_hex)?) + } } pub fn find_tx_blockhash(&self, txid: &Txid) -> Result> {