Skip to content

Commit

Permalink
Scriptable transaction broadcast (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
shesek committed Sep 23, 2020
1 parent 72cfb45 commit c9d76e8
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 9 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@

- Reproducible builds using Docker

- Scriptable transaction broadcast command via `--tx-broadcast-cmd <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)
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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

Expand Down
2 changes: 1 addition & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

Expand Down
24 changes: 22 additions & 2 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -198,14 +199,24 @@ 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<String>,

#[cfg(unix)]
#[structopt(
long,
short = "U",
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<path::PathBuf>,

Expand All @@ -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<Vec<String>>,
}
Expand Down Expand Up @@ -401,3 +412,12 @@ fn bitcoind_default_dir() -> Option<path::PathBuf> {
#[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(),
}
}
}
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
30 changes: 24 additions & 6 deletions src/query.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::process::Command;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};

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};
Expand All @@ -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<RpcClient>,
indexer: Arc<RwLock<Indexer>>,

Expand All @@ -32,12 +33,17 @@ pub struct Query {
cached_estimates: RwLock<HashMap<u16, (Option<f64>, Instant)>>,
}

pub struct QueryConfig {
pub network: Network,
pub broadcast_cmd: Option<String>,
}

type FeeHistogram = Vec<(f32, u32)>;

impl Query {
pub fn new(network: Network, rpc: Arc<RpcClient>, indexer: Arc<RwLock<Indexer>>) -> Self {
pub fn new(config: QueryConfig, rpc: Arc<RpcClient>, indexer: Arc<RwLock<Indexer>>) -> Self {
Query {
network,
config,
rpc,
indexer,
cached_relayfee: RwLock::new(None),
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -183,7 +189,19 @@ impl Query {
}

pub fn broadcast(&self, tx_hex: &str) -> Result<Txid> {
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<Option<BlockHash>> {
Expand Down

0 comments on commit c9d76e8

Please sign in to comment.