Skip to content

Commit 195df39

Browse files
committed
Submit TRUC packages via all chain sources
We rely on the `BroadcasterInterface` contract whereby any multi-transaction vector must be a single child and its parents. In a prior commit, we added the guarantee that any packages received from the broadcast queue are already topologically sorted, and hence can be passed directly to the `submit_package` Bitcoin Core RPC. We avoid broadcasting non-TRUC parents-child packages via `submitpackage` for now to avoid adding a requirement to support `submitpackage` for users that don't enable zero-fee commitment channels. We will do so once support for `submitpackage` is more ubiquitous.
1 parent cc85b4d commit 195df39

3 files changed

Lines changed: 198 additions & 37 deletions

File tree

src/chain/bitcoind.rs

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
1414

1515
use base64::prelude::BASE64_STANDARD;
1616
use base64::Engine;
17+
use bitcoin::transaction::Version;
1718
use bitcoin::{BlockHash, FeeRate, Network, OutPoint, Transaction, Txid};
1819
use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget;
1920
use lightning::chain::{BlockLocator, Listen};
@@ -596,7 +597,9 @@ impl BitcoindChainSource {
596597
Ok(())
597598
}
598599

599-
fn log_broadcast_error(&self, e: impl core::fmt::Display, txids: &[Txid], txs: &[Transaction]) {
600+
fn log_broadcast_error(
601+
&self, e: impl core::fmt::Display, txids: &[Txid], txs: &SortedTransactions,
602+
) {
600603
log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e);
601604
log_trace!(self.logger, "Failed broadcast transaction bytes:");
602605
for tx in txs.iter() {
@@ -605,27 +608,48 @@ impl BitcoindChainSource {
605608
}
606609

607610
pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) {
608-
// While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28
609-
// features, we should eventually switch to use `submitpackage` via the
610-
// `rust-bitcoind-json-rpc` crate rather than just broadcasting individual
611-
// transactions.
612-
let txs = txs.into_inner();
613-
for tx in txs {
614-
let txid = tx.compute_txid();
615-
let timeout_fut = tokio::time::timeout(
616-
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
617-
self.api_client.broadcast_transaction(&tx),
618-
);
619-
match timeout_fut.await {
620-
Ok(res) => match res {
621-
Ok(id) => {
622-
debug_assert_eq!(id, txid);
623-
log_trace!(self.logger, "Successfully broadcast transaction {}", txid);
611+
let all_txs_are_v3 = txs.iter().all(|tx| tx.version == Version::non_standard(3));
612+
match txs.len() {
613+
2.. if all_txs_are_v3 => {
614+
let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect();
615+
let timeout_fut = tokio::time::timeout(
616+
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
617+
self.api_client.submit_package(&txs),
618+
);
619+
match timeout_fut.await {
620+
Ok(res) => match res {
621+
Ok(result) => {
622+
log_trace!(self.logger, "Successfully broadcast package {:?}", txids);
623+
log_trace!(self.logger, "Successfully broadcast package {}", result);
624+
},
625+
Err(e) => self.log_broadcast_error(e, &txids, &txs),
624626
},
625-
Err(e) => self.log_broadcast_error(e, &[txid], &[tx]),
626-
},
627-
Err(e) => self.log_broadcast_error(e, &[txid], &[tx]),
628-
}
627+
Err(e) => self.log_broadcast_error(e, &txids, &txs),
628+
}
629+
},
630+
_ => {
631+
for tx in txs.iter() {
632+
let txid = tx.compute_txid();
633+
let timeout_fut = tokio::time::timeout(
634+
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
635+
self.api_client.broadcast_transaction(tx),
636+
);
637+
match timeout_fut.await {
638+
Ok(res) => match res {
639+
Ok(id) => {
640+
debug_assert_eq!(id, txid);
641+
log_trace!(
642+
self.logger,
643+
"Successfully broadcast transaction {}",
644+
txid
645+
);
646+
},
647+
Err(e) => self.log_broadcast_error(e, &[txid], &txs),
648+
},
649+
Err(e) => self.log_broadcast_error(e, &[txid], &txs),
650+
}
651+
}
652+
},
629653
}
630654
}
631655
}
@@ -816,6 +840,38 @@ impl BitcoindClient {
816840
rpc_client.call_method::<Txid>("sendrawtransaction", &[tx_json]).await
817841
}
818842

843+
/// Submits the provided package
844+
pub(crate) async fn submit_package(
845+
&self, package: &SortedTransactions,
846+
) -> Result<String, BitcoindClientError> {
847+
match self {
848+
BitcoindClient::Rpc { rpc_client, .. } => {
849+
Self::submit_package_inner(Arc::clone(rpc_client), package)
850+
.await
851+
.map_err(BitcoindClientError::Rpc)
852+
},
853+
BitcoindClient::Rest { rpc_client, .. } => {
854+
// Bitcoin Core's REST interface does not support submitting packages
855+
// so we use the RPC client.
856+
Self::submit_package_inner(Arc::clone(rpc_client), package)
857+
.await
858+
.map_err(BitcoindClientError::Rpc)
859+
},
860+
}
861+
}
862+
863+
async fn submit_package_inner(
864+
rpc_client: Arc<RpcClient>, package: &SortedTransactions,
865+
) -> Result<String, RpcClientError> {
866+
let package_serialized: Vec<_> =
867+
package.iter().map(|tx| bitcoin::consensus::encode::serialize_hex(tx)).collect();
868+
let package_json = serde_json::json!(package_serialized);
869+
rpc_client
870+
.call_method::<SubmitPackageResponse>("submitpackage", &[package_json])
871+
.await
872+
.map(|response| response.0)
873+
}
874+
819875
/// Retrieve the fee estimate needed for a transaction to begin
820876
/// confirmation within the provided `num_blocks`.
821877
pub(crate) async fn get_fee_estimate_for_target(
@@ -1367,6 +1423,23 @@ impl TryInto<GetMempoolEntryResponse> for JsonResponse {
13671423
}
13681424
}
13691425

1426+
pub struct SubmitPackageResponse(String);
1427+
1428+
impl TryInto<SubmitPackageResponse> for JsonResponse {
1429+
type Error = String;
1430+
fn try_into(self) -> Result<SubmitPackageResponse, String> {
1431+
let response = self.0.to_string();
1432+
let res = self.0.as_object().ok_or("Failed to parse submitpackage response".to_string())?;
1433+
1434+
match res["package_msg"].as_str() {
1435+
Some("success") => Ok(SubmitPackageResponse(response)),
1436+
Some(_) | None => {
1437+
return Err(response);
1438+
},
1439+
}
1440+
}
1441+
}
1442+
13701443
#[derive(Debug, Clone)]
13711444
pub(crate) struct MempoolEntry {
13721445
/// The transaction id

src/chain/electrum.rs

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use bdk_chain::bdk_core::spk_client::{
1616
};
1717
use bdk_electrum::BdkElectrumClient;
1818
use bdk_wallet::{KeychainKind as BdkKeyChainKind, Update as BdkUpdate};
19+
use bitcoin::transaction::Version;
1920
use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid};
2021
use electrum_client::{
2122
Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi,
@@ -364,8 +365,14 @@ impl ElectrumChainSource {
364365
return;
365366
};
366367

367-
for tx in txs.into_inner() {
368-
electrum_client.broadcast(tx).await;
368+
let all_txs_are_v3 = txs.iter().all(|tx| tx.version == Version::non_standard(3));
369+
match txs.len() {
370+
2.. if all_txs_are_v3 => electrum_client.submit_package(txs).await,
371+
_ => {
372+
for tx in txs.into_inner() {
373+
electrum_client.broadcast(tx).await
374+
}
375+
},
369376
}
370377
}
371378
}
@@ -662,6 +669,46 @@ impl ElectrumRuntimeClient {
662669
}
663670
}
664671

672+
async fn submit_package(&self, package: SortedTransactions) {
673+
let electrum_client = Arc::clone(&self.electrum_client);
674+
675+
let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect();
676+
let package = Arc::new(package);
677+
678+
let spawn_fut = self.runtime.spawn_blocking({
679+
let package = Arc::clone(&package);
680+
move || electrum_client.transaction_broadcast_package(&package)
681+
});
682+
let timeout_fut = tokio::time::timeout(
683+
Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs),
684+
spawn_fut,
685+
);
686+
687+
match timeout_fut.await {
688+
Ok(res) => match res {
689+
Ok(Ok(result)) => {
690+
if result.success {
691+
log_trace!(
692+
self.logger,
693+
"Successfully broadcast transaction(s) {:?}",
694+
txids
695+
);
696+
log_trace!(
697+
self.logger,
698+
"Successfully broadcast transaction(s) {:?}",
699+
result
700+
);
701+
} else {
702+
self.log_broadcast_error(format!("{:?}", result), &txids, &package);
703+
}
704+
},
705+
Ok(Err(e)) => self.log_broadcast_error(e, &txids, &package),
706+
Err(e) => self.log_broadcast_error(e, &txids, &package),
707+
},
708+
Err(e) => self.log_broadcast_error(e, &txids, &package),
709+
}
710+
}
711+
665712
async fn get_fee_rate_cache_update(
666713
&self,
667714
) -> Result<HashMap<ConfirmationTarget, FeeRate>, Error> {

src/chain/esplora.rs

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::sync::{Arc, Mutex};
1111
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
1212

1313
use bdk_esplora::EsploraAsyncExt;
14+
use bitcoin::transaction::Version;
1415
use bitcoin::{FeeRate, Network, Script, Txid};
1516
use esplora_client::AsyncClient as EsploraAsyncClient;
1617
use lightning::chain::{Confirm, Filter, WatchedOutput};
@@ -461,21 +462,61 @@ impl EsploraChainSource {
461462
}
462463

463464
pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) {
464-
for tx in txs.iter() {
465-
let txid = tx.compute_txid();
466-
let timeout_fut = tokio::time::timeout(
467-
Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs),
468-
self.esplora_client.broadcast(tx),
469-
);
470-
match timeout_fut.await {
471-
Ok(res) => match res {
472-
Ok(()) => {
473-
log_trace!(self.logger, "Successfully broadcast transaction {}", txid);
465+
let all_txs_are_v3 = txs.iter().all(|tx| tx.version == Version::non_standard(3));
466+
match txs.len() {
467+
2.. if all_txs_are_v3 => {
468+
let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect();
469+
let timeout_fut = tokio::time::timeout(
470+
Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs),
471+
self.esplora_client.submit_package(&txs, None, None),
472+
);
473+
match timeout_fut.await {
474+
Ok(res) => match res {
475+
Ok(result) => {
476+
if result.package_msg.eq_ignore_ascii_case("success") {
477+
log_trace!(
478+
self.logger,
479+
"Successfully broadcast transactions {:?}",
480+
txids
481+
);
482+
log_trace!(
483+
self.logger,
484+
"Successfully broadcast transactions {:?}",
485+
result
486+
);
487+
} else {
488+
self.log_broadcast_error(format!("{:?}", result), &txids, &txs);
489+
}
490+
},
491+
Err(e) => self.log_http_error(e, &txids, &txs),
474492
},
475-
Err(e) => self.log_http_error(e, &[txid], &txs),
476-
},
477-
Err(e) => self.log_broadcast_error(e, &[txid], &txs),
478-
}
493+
Err(e) => self.log_broadcast_error(e, &txids, &txs),
494+
}
495+
},
496+
_ => {
497+
for tx in txs.iter() {
498+
let txid = tx.compute_txid();
499+
let timeout_fut = tokio::time::timeout(
500+
Duration::from_secs(
501+
self.sync_config.timeouts_config.tx_broadcast_timeout_secs,
502+
),
503+
self.esplora_client.broadcast(tx),
504+
);
505+
match timeout_fut.await {
506+
Ok(res) => match res {
507+
Ok(()) => {
508+
log_trace!(
509+
self.logger,
510+
"Successfully broadcast transaction {}",
511+
txid
512+
);
513+
},
514+
Err(e) => self.log_http_error(e, &[txid], &txs),
515+
},
516+
Err(e) => self.log_broadcast_error(e, &[txid], &txs),
517+
}
518+
}
519+
},
479520
}
480521
}
481522
}

0 commit comments

Comments
 (0)