Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,14 @@ mempool_size_mb=100
(system `tor`, not Arti). DNS seeds are not resolved locally on that path —
pass `--connect ADDR` (or reuse a `peers` file). `--proxy-randomize` (default
on) uses a fresh SOCKS username per peer so Tor isolates circuits.
`--proxy` or `--onion` also turns on **isolated local-tx broadcast**:
`sendrawtransaction`, Electrum `transaction.broadcast`, and Esplora
`POST /tx` (and packages) are not INV'd on standing peers. After mempool
accept the node opens a short-lived SOCKS circuit (fresh isolation
credentials), BIP324-handshakes one or two AddrMan peers (onion first),
sends `tx`, and disconnects. This is **not** Dandelion++. If that
one-shot fails, the tx stays in the mempool and is still not INV'd;
confirmation can still arrive in a block.
`--onion HOST:PORT` stores a separate SOCKS endpoint for onion destinations.
`--only-net onion` (repeatable with `ipv4`/`ipv6`/`i2p`) filters dial and learn;
onion requires `--proxy` or `--onion`. `--connect foo.onion:8333` is a start
Expand Down
156 changes: 85 additions & 71 deletions crates/rbitcoin-electrum/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,89 @@
}
}

fn broadcast_raw_tx(
params: &Value,
config: &ElectrumConfig,
mempool: Option<&MempoolHub>,
) -> Result<Value, String> {
let raw_hex = param_str(params, 0)?;
if raw_hex.len() > config.max_broadcast_hex {

Check warning on line 1447 in crates/rbitcoin-electrum/src/server.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace > with >= in broadcast_raw_tx
return Err(format!(
"transaction hex too large (max {} chars)",
config.max_broadcast_hex
));
}
let raw = rbitcoin_primitives::hex_decode(raw_hex).map_err(|e| e.to_string())?;
if raw.len() > 4_000_000 {

Check warning on line 1454 in crates/rbitcoin-electrum/src/server.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace > with >= in broadcast_raw_tx

Check warning on line 1454 in crates/rbitcoin-electrum/src/server.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace > with < in broadcast_raw_tx

Check warning on line 1454 in crates/rbitcoin-electrum/src/server.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace > with == in broadcast_raw_tx
return Err("transaction too large".into());
}
let tx: bitcoin::Transaction =
bitcoin::consensus::deserialize(&raw).map_err(|e| e.to_string())?;
let mp = mempool.ok_or_else(|| "mempool not available".to_string())?;
let r = mp
.accept_tx(&tx)
.map_err(|e| format!("broadcast reject: {e}"))?;
mp.mark_local_origin(r.txid);
Ok(json!(format!("{}", r.txid)))
}

fn broadcast_package(
params: &Value,
config: &ElectrumConfig,
mempool: Option<&MempoolHub>,
) -> Result<Value, String> {
let arr = params
.as_array()
.and_then(|a| a.first())
.and_then(|v| v.as_array())
.ok_or_else(|| "broadcast_package expected array of hex txs".to_string())?;
let verbose = params
.as_array()
.and_then(|a| a.get(1))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut txs = Vec::with_capacity(arr.len());
let mut total_hex = 0usize;
for h in arr {
let raw_hex = h
.as_str()
.ok_or_else(|| "broadcast_package tx must be hex".to_string())?;
total_hex = total_hex.saturating_add(raw_hex.len());
if total_hex > config.max_broadcast_hex {

Check warning on line 1489 in crates/rbitcoin-electrum/src/server.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace > with >= in broadcast_package
return Err("package hex too large".into());
}
let raw = rbitcoin_primitives::hex_decode(raw_hex).map_err(|e| e.to_string())?;
if raw.len() > 4_000_000 {

Check warning on line 1493 in crates/rbitcoin-electrum/src/server.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace > with >= in broadcast_package

Check warning on line 1493 in crates/rbitcoin-electrum/src/server.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace > with == in broadcast_package
return Err("transaction too large".into());
}
let tx: bitcoin::Transaction =
bitcoin::consensus::deserialize(&raw).map_err(|e| e.to_string())?;
txs.push(tx);
}
let mp = mempool.ok_or_else(|| "mempool not available".to_string())?;
let accepted = mp
.accept_package(&txs)
.map_err(|e| format!("broadcast_package reject: {e}"))?;
for r in &accepted {
mp.mark_local_origin(r.txid);
}
if verbose {
let mut tx_results = serde_json::Map::new();
for r in &accepted {
tx_results.insert(
r.txid.to_string(),
json!({"txid": r.txid.to_string(), "allowed": true}),
);
}
Ok(json!({
"package_msg": "success",
"tx-results": tx_results,
}))
} else {
Ok(json!("success"))
}
}

#[allow(clippy::too_many_arguments)] // call-site args stay unbundled
fn dispatch_pinned(
method: &str,
Expand Down Expand Up @@ -1727,77 +1810,8 @@
"pos": proof.pos,
}))
}
"blockchain.transaction.broadcast" => {
let raw_hex = param_str(params, 0)?;
if raw_hex.len() > config.max_broadcast_hex {
return Err(format!(
"transaction hex too large (max {} chars)",
config.max_broadcast_hex
));
}
let raw = rbitcoin_primitives::hex_decode(raw_hex).map_err(|e| e.to_string())?;
// Consensus max block weight is 4M; reject absurd raw sizes early.
if raw.len() > 4_000_000 {
return Err("transaction too large".into());
}
let tx: bitcoin::Transaction =
bitcoin::consensus::deserialize(&raw).map_err(|e| e.to_string())?;
let mp = mempool.ok_or_else(|| "mempool not available".to_string())?;
let r = mp
.accept_tx(&tx)
.map_err(|e| format!("broadcast reject: {e}"))?;
let _ = chain.network;
Ok(json!(format!("{}", r.txid)))
}
"blockchain.transaction.broadcast_package" => {
let arr = params
.as_array()
.and_then(|a| a.first())
.and_then(|v| v.as_array())
.ok_or_else(|| "broadcast_package expected array of hex txs".to_string())?;
let verbose = params
.as_array()
.and_then(|a| a.get(1))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut txs = Vec::with_capacity(arr.len());
let mut total_hex = 0usize;
for h in arr {
let raw_hex = h
.as_str()
.ok_or_else(|| "broadcast_package tx must be hex".to_string())?;
total_hex = total_hex.saturating_add(raw_hex.len());
if total_hex > config.max_broadcast_hex {
return Err("package hex too large".into());
}
let raw = rbitcoin_primitives::hex_decode(raw_hex).map_err(|e| e.to_string())?;
if raw.len() > 4_000_000 {
return Err("transaction too large".into());
}
let tx: bitcoin::Transaction =
bitcoin::consensus::deserialize(&raw).map_err(|e| e.to_string())?;
txs.push(tx);
}
let mp = mempool.ok_or_else(|| "mempool not available".to_string())?;
let accepted = mp
.accept_package(&txs)
.map_err(|e| format!("broadcast_package reject: {e}"))?;
if verbose {
let mut tx_results = serde_json::Map::new();
for r in &accepted {
tx_results.insert(
r.txid.to_string(),
json!({"txid": r.txid.to_string(), "allowed": true}),
);
}
Ok(json!({
"package_msg": "success",
"tx-results": tx_results,
}))
} else {
Ok(json!("success"))
}
}
"blockchain.transaction.broadcast" => broadcast_raw_tx(params, config, mempool),
"blockchain.transaction.broadcast_package" => broadcast_package(params, config, mempool),
"mempool.get_info" => {
let min = MempoolHub::relay_fee_btc_per_kb();
let unbroadcast = mempool.map(|m| m.unbroadcast_count()).unwrap_or(0);
Expand Down
4 changes: 4 additions & 0 deletions crates/rbitcoin-esplora/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1723,6 +1723,7 @@ async fn admit_broadcast(st: AppState, tx: bitcoin::Transaction) -> Response {
};
match mp.accept_tx_async(tx).await {
Ok(r) => {
mp.mark_local_origin(r.txid);
let tid = r.txid.to_byte_array();
plain_ok(block_hash_hex(&tid))
}
Expand Down Expand Up @@ -1959,6 +1960,9 @@ pub async fn post_tx_package(State(st): State<AppState>, body: Bytes) -> Respons
}
match mp.accept_package_async(txs).await {
Ok(results) => {
for r in &results {
mp.mark_local_origin(r.txid);
}
let txids: Vec<String> = results
.iter()
.map(|r| block_hash_hex(&r.txid.to_byte_array()))
Expand Down
Loading
Loading