Skip to content

Commit be7c507

Browse files
committed
Scriptable transaction broadcast (#7)
1 parent 72cfb45 commit be7c507

File tree

6 files changed

+81
-9
lines changed

6 files changed

+81
-9
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@
44

55
- Reproducible builds using Docker
66

7+
- Scriptable transaction broadcast command via `--tx-broadcast-cmd <cmd>` (#7)
8+
9+
The command will be used in place of broadcasting transactions using the full node,
10+
which may provide better privacy in some circumstances.
11+
12+
The string `{tx_hex}` will be replaced with the hex-encoded transaction.
13+
14+
For example, to broadcast transactions over Tor using the blockstream.info onion service, you can use:
15+
16+
```
17+
--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 ]'
18+
```
19+
20+
(replace port `9050` with `9150` if you're using the Tor browser bundle)
21+
22+
h/t @chris-belcher's EPS for inspiring this feature! 🎩
23+
724
- Electrum plugin: Fix hot wallet test (#47)
825

926
- Electrum: Fix docker image libssl dependency with the `http` feature (#48)

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,20 @@ It is recommended to use a separate watch-only wallet for bwt (can be created wi
236236

237237
*Note that EPS and bwt should not be run on the same bitcoind wallet with the same xpub, they will conflict.*
238238

239+
##### Scriptable transaction broadcast
240+
241+
You may set a custom command for broadcasting transactions via `--tx-broadcast-cmd <cmd>`. The string `{tx_hex}` will be replaced with the hex-encoded transaction.
242+
243+
The command will be used in place of broadcasting transactions using the full node,
244+
which may provide better privacy in some circumstances.
245+
246+
For example, to broadcast transactions over Tor using the blockstream.info onion service, you can use:
247+
248+
```
249+
--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 ]'
250+
```
251+
252+
(replace port `9050` with `9150` if you're using the Tor browser bundle)
239253

240254
## Electrum plugin
241255

src/app.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl App {
5050
config.bitcoind_auth()?,
5151
)?);
5252
let indexer = Arc::new(RwLock::new(Indexer::new(rpc.clone(), watcher)));
53-
let query = Arc::new(Query::new(config.network, rpc.clone(), indexer.clone()));
53+
let query = Arc::new(Query::new((&config).into(), rpc.clone(), indexer.clone()));
5454

5555
wait_bitcoind(&rpc)?;
5656

src/config.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use bitcoincore_rpc::Auth as RpcAuth;
1010

1111
use crate::error::{Context, OptionExt, Result};
1212
use crate::hd::XyzPubKey;
13+
use crate::query::QueryConfig;
1314
use crate::types::RescanSince;
1415

1516
#[derive(StructOpt, Debug)]
@@ -198,14 +199,24 @@ pub struct Config {
198199
)]
199200
pub poll_interval: time::Duration,
200201

202+
#[structopt(
203+
short = "B",
204+
long = "tx-broadcast-cmd",
205+
help = "Custom command for broadcasting transactions. {tx_hex} is replaced with the transaction.",
206+
env,
207+
hide_env_values(true),
208+
display_order(91)
209+
)]
210+
pub broadcast_cmd: Option<String>,
211+
201212
#[cfg(unix)]
202213
#[structopt(
203214
long,
204215
short = "U",
205216
help = "Path to bind the sync notification unix socket",
206217
env,
207218
hide_env_values(true),
208-
display_order(91)
219+
display_order(101)
209220
)]
210221
pub unix_listener_path: Option<path::PathBuf>,
211222

@@ -217,7 +228,7 @@ pub struct Config {
217228
env,
218229
hide_env_values(true),
219230
use_delimiter(true),
220-
display_order(92)
231+
display_order(102)
221232
)]
222233
pub webhook_urls: Option<Vec<String>>,
223234
}
@@ -401,3 +412,12 @@ fn bitcoind_default_dir() -> Option<path::PathBuf> {
401412
#[cfg(windows)]
402413
return Some(dirs::data_dir()?.join("Bitcoin"));
403414
}
415+
416+
impl From<&Config> for QueryConfig {
417+
fn from(config: &Config) -> QueryConfig {
418+
QueryConfig {
419+
network: config.network,
420+
broadcast_cmd: config.broadcast_cmd.clone(),
421+
}
422+
}
423+
}

src/error.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ pub enum BwtError {
2424
#[error("Blocks unavailable due to pruning")]
2525
PrunedBlocks,
2626

27+
#[error("Custom broadcast command failed with {0}")]
28+
BroadcastCmdFailed(std::process::ExitStatus),
29+
2730
#[error("Error communicating with the Bitcoin RPC: {0}")]
2831
RpcProtocol(rpc::Error),
2932

src/query.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use std::collections::HashMap;
2+
use std::process::Command;
23
use std::sync::{Arc, RwLock};
34
use std::time::{Duration, Instant};
45

56
use serde::Serialize;
67
use serde_json::Value;
78

89
use bitcoin::util::bip32::Fingerprint;
9-
use bitcoin::{BlockHash, BlockHeader, Network, OutPoint, Txid};
10+
use bitcoin::{BlockHash, BlockHeader, Network, OutPoint, Transaction, Txid};
1011
use bitcoincore_rpc::{json as rpcjson, Client as RpcClient, RpcApi};
1112

1213
use crate::error::{BwtError, Context, OptionExt, Result};
@@ -23,7 +24,7 @@ const FEE_HISTOGRAM_TTL: Duration = Duration::from_secs(120);
2324
const FEE_ESTIMATES_TTL: Duration = Duration::from_secs(120);
2425

2526
pub struct Query {
26-
network: Network,
27+
config: QueryConfig,
2728
rpc: Arc<RpcClient>,
2829
indexer: Arc<RwLock<Indexer>>,
2930

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

36+
pub struct QueryConfig {
37+
pub network: Network,
38+
pub broadcast_cmd: Option<String>,
39+
}
40+
3541
type FeeHistogram = Vec<(f32, u32)>;
3642

3743
impl Query {
38-
pub fn new(network: Network, rpc: Arc<RpcClient>, indexer: Arc<RwLock<Indexer>>) -> Self {
44+
pub fn new(config: QueryConfig, rpc: Arc<RpcClient>, indexer: Arc<RwLock<Indexer>>) -> Self {
3945
Query {
40-
network,
46+
config,
4147
rpc,
4248
indexer,
4349
cached_relayfee: RwLock::new(None),
@@ -104,7 +110,7 @@ impl Query {
104110

105111
// regtest typically doesn't have fee estimates, just use the relay fee instead.
106112
// this stops electrum from complanining about unavailable dynamic fees.
107-
if self.network == Network::Regtest {
113+
if self.config.network == Network::Regtest {
108114
return self.relay_fee().map(Some);
109115
}
110116

@@ -183,7 +189,19 @@ impl Query {
183189
}
184190

185191
pub fn broadcast(&self, tx_hex: &str) -> Result<Txid> {
186-
Ok(self.rpc.send_raw_transaction(tx_hex)?)
192+
if let Some(broadcast_cmd) = &self.config.broadcast_cmd {
193+
// need to deserialize to ensure validity (preventing code injection) and to determine the txid
194+
let tx: Transaction = bitcoin::consensus::deserialize(&hex::decode(tx_hex)?)?;
195+
let cmd = broadcast_cmd.replacen("{tx_hex}", tx_hex, 1);
196+
debug!("broadcasting tx with cmd {}", broadcast_cmd);
197+
let status = Command::new("sh").arg("-c").arg(cmd).status()?;
198+
if !status.success() {
199+
bail!(BwtError::BroadcastCmdFailed(status))
200+
}
201+
Ok(tx.txid())
202+
} else {
203+
Ok(self.rpc.send_raw_transaction(tx_hex)?)
204+
}
187205
}
188206

189207
pub fn find_tx_blockhash(&self, txid: &Txid) -> Result<Option<BlockHash>> {

0 commit comments

Comments
 (0)