Support 'blockchain.transaction.broadcast_package'
What changed, and why it matters
This commit adds a new Electrum protocol method that lets a user submit multiple Bitcoin transactions as a package to the connected Bitcoin node. It is a feature addition: it reuses the same hex parsing and RPC forwarding logic already used for single-transaction broadcast, just extended to a list. There is no direct evidence in the commit that this fixes a security vulnerability or introduces a new attack path beyond the existing transaction-broadcast surface.
Review as a normal feature addition. Consider whether the new `broadcast_package` endpoint needs the same operational safeguards (request size limits, rate limiting, authentication) as the existing single-transaction broadcast endpoint, and verify that `submitpackage` responses are passed through without leaking sensitive node information.
Security signals we found
New RPC surface exposed to Electrum clients (transaction broadcast package)
Refactored single-transaction broadcast to share hex deserialization helper
No input length or resource limits added for the package variant in this diff
No vendor security framing or CVE references present in commit
Evidence from the diff
The patch implements Electrum’s blockchain.transaction.broadcast_package by: (1) adding a Daemon::submitpackage helper that serializes a slice of bitcoin::Transactions to hex and calls Bitcoin Core’s submitpackage RPC; (2) adding Rpc::transaction_broadcast_package which parses a Vec<String> of hex transactions via a shared tx_from_hex helper and forwards them; (3) wiring the new method into the Electrum method dispatch and parameter parsing. The single-transaction broadcast path is refactored to use the same tx_from_hex helper. No authentication, authorization, rate-limiting, or input-size changes are visible in the diff.
Changed components
src/daemon.rssrc/electrum.rsElectrum RPC server method dispatchBitcoin Core RPC bridgeInspect captured patch +31 / −4
diff --git a/src/daemon.rs b/src/daemon.rs
index d1d0277..3a0690f 100644
--- a/src/daemon.rs
+++ b/src/daemon.rs
@@ -1,5 +1,6 @@
use anyhow::{Context, Result};
+use bitcoin::consensus::encode::serialize_hex;
use bitcoin::{consensus::deserialize, hashes::hex::FromHex};
use bitcoin::{Amount, BlockHash, Transaction, Txid};
use bitcoincore_rpc::{json, jsonrpc, Auth, Client, RpcApi};
@@ -173,6 +174,13 @@ impl Daemon {
.context("failed to broadcast transaction")
}
+ pub(crate) fn submitpackage(&self, txs: &[Transaction]) -> Result<Value> {
+ let package: Vec<String> = txs.iter().map(serialize_hex).collect();
+ self.rpc
+ .call("submitpackage", &[json!(package)])
+ .context("failed to submitpackage package")
+ }
+
pub(crate) fn get_transaction_info(
&self,
txid: &Txid,
diff --git a/src/electrum.rs b/src/electrum.rs
index c584ce1..31547a5 100644
--- a/src/electrum.rs
+++ b/src/electrum.rs
@@ -3,7 +3,7 @@ use bitcoin::{
consensus::{deserialize, encode::serialize_hex},
hashes::hex::FromHex,
hex::DisplayHex,
- BlockHash, Txid,
+ BlockHash, Transaction, Txid,
};
use crossbeam_channel::Receiver;
use rayon::prelude::*;
@@ -359,12 +359,18 @@ impl Rpc {
}
fn transaction_broadcast(&self, (tx_hex,): &(String,)) -> Result<Value> {
- let tx_bytes = Vec::from_hex(tx_hex).context("non-hex transaction")?;
- let tx = deserialize(&tx_bytes).context("invalid transaction")?;
- let txid = self.daemon.broadcast(&tx)?;
+ let txid = self.daemon.broadcast(&tx_from_hex(tx_hex)?)?;
Ok(json!(txid))
}
+ fn transaction_broadcast_package(&self, (txs_hex,): &(Vec<String>,)) -> Result<Value> {
+ let txs = txs_hex
+ .iter()
+ .map(|s| tx_from_hex(s))
+ .collect::<Result<Vec<_>>>()?;
+ self.daemon.submitpackage(&txs)
+ }
+
fn transaction_get(&self, args: &TxGetArgs) -> Result<Value> {
let (txid, verbose) = args.into();
if verbose {
@@ -562,6 +568,9 @@ impl Rpc {
Params::ScriptHashSubscribe(args) => self.scripthash_subscribe(client, args),
Params::ScriptHashUnsubscribe(args) => self.scripthash_unsubscribe(client, args),
Params::TransactionBroadcast(args) => self.transaction_broadcast(args),
+ Params::TransactionBroadcastPackage(args) => {
+ self.transaction_broadcast_package(args)
+ }
Params::TransactionGet(args) => self.transaction_get(args),
Params::TransactionGetMerkle(args) => self.transaction_get_merkle(args),
Params::TransactionFromPosition(args) => self.transaction_from_pos(*args),
@@ -578,6 +587,7 @@ enum Params {
BlockHeader((usize,)),
BlockHeaders((usize, usize)),
TransactionBroadcast((String,)),
+ TransactionBroadcastPackage((Vec<String>,)),
Donation,
EstimateFee((u16,)),
Features,
@@ -611,6 +621,9 @@ impl Params {
"blockchain.scripthash.subscribe" => Params::ScriptHashSubscribe(convert(params)?),
"blockchain.scripthash.unsubscribe" => Params::ScriptHashUnsubscribe(convert(params)?),
"blockchain.transaction.broadcast" => Params::TransactionBroadcast(convert(params)?),
+ "blockchain.transaction.broadcast_package" => {
+ Params::TransactionBroadcastPackage(convert(params)?)
+ }
"blockchain.transaction.get" => Params::TransactionGet(convert(params)?),
"blockchain.transaction.get_merkle" => Params::TransactionGetMerkle(convert(params)?),
"blockchain.transaction.id_from_pos" => {
@@ -763,6 +776,12 @@ fn check_between(version_str: &str, min_str: &str, max_str: &str) -> Result<()>
Ok(())
}
+fn tx_from_hex(tx_hex: &str) -> Result<Transaction> {
+ let tx_bytes = Vec::from_hex(tx_hex).context("non-hex transaction")?;
+ let tx = deserialize(&tx_bytes).context("invalid transaction")?;
+ Ok(tx)
+}
+
#[cfg(test)]
mod tests {
use super::*;
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.