Skip to content

Commit a9040a9

Browse files
committed
Sort packages received via BroadcasterInterface
Implementations of `BroadcasterInterface` cannot assume any topological ordering on the transactions received, so here we order the received transactions before adding them to the broadcast queue. Any consumers of the queue can now assume all transactions received to be topologically sorted. Codex wrote the tests.
1 parent 4304326 commit a9040a9

5 files changed

Lines changed: 209 additions & 14 deletions

File tree

src/chain/bitcoind.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ use crate::fee_estimator::{
4141
};
4242
use crate::io::utils::update_and_persist_node_metrics;
4343
use crate::logger::{log_bytes, log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
44+
use crate::tx_broadcaster::SortedTransactions;
4445
use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
4546
use crate::{Error, PersistedNodeMetrics};
4647

@@ -595,12 +596,12 @@ impl BitcoindChainSource {
595596
Ok(())
596597
}
597598

598-
pub(crate) async fn process_broadcast_package(&self, package: Vec<Transaction>) {
599+
pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) {
599600
// While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28
600601
// features, we should eventually switch to use `submitpackage` via the
601602
// `rust-bitcoind-json-rpc` crate rather than just broadcasting individual
602603
// transactions.
603-
for tx in &package {
604+
for tx in txs.iter() {
604605
let txid = tx.compute_txid();
605606
let timeout_fut = tokio::time::timeout(
606607
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),

src/chain/electrum.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use crate::fee_estimator::{
3737
use crate::io::utils::update_and_persist_node_metrics;
3838
use crate::logger::{log_bytes, log_debug, log_error, log_trace, log_warn, LdkLogger, Logger};
3939
use crate::runtime::Runtime;
40+
use crate::tx_broadcaster::SortedTransactions;
4041
use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
4142
use crate::PersistedNodeMetrics;
4243

@@ -353,7 +354,7 @@ impl ElectrumChainSource {
353354
}
354355
}
355356

356-
pub(crate) async fn process_broadcast_package(&self, package: Vec<Transaction>) {
357+
pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) {
357358
let electrum_client: Arc<ElectrumRuntimeClient> = if let Some(client) =
358359
self.electrum_runtime_status.read().expect("lock").client().as_ref()
359360
{
@@ -363,7 +364,7 @@ impl ElectrumChainSource {
363364
return;
364365
};
365366

366-
for tx in package {
367+
for tx in txs.into_inner() {
367368
electrum_client.broadcast(tx).await;
368369
}
369370
}

src/chain/esplora.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +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::{FeeRate, Network, Script, Transaction, Txid};
14+
use bitcoin::{FeeRate, Network, Script, Txid};
1515
use esplora_client::AsyncClient as EsploraAsyncClient;
1616
use lightning::chain::{Confirm, Filter, WatchedOutput};
1717
use lightning::util::ser::Writeable;
@@ -28,6 +28,7 @@ use crate::fee_estimator::{
2828
};
2929
use crate::io::utils::update_and_persist_node_metrics;
3030
use crate::logger::{log_bytes, log_debug, log_error, log_trace, log_warn, LdkLogger, Logger};
31+
use crate::tx_broadcaster::SortedTransactions;
3132
use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
3233
use crate::{Error, PersistedNodeMetrics};
3334

@@ -410,8 +411,8 @@ impl EsploraChainSource {
410411
Ok(())
411412
}
412413

413-
pub(crate) async fn process_broadcast_package(&self, package: Vec<Transaction>) {
414-
for tx in &package {
414+
pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) {
415+
for tx in txs.iter() {
415416
let txid = tx.compute_txid();
416417
let timeout_fut = tokio::time::timeout(
417418
Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs),

src/chain/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet};
1313
use std::sync::{Arc, Mutex};
1414
use std::time::Duration;
1515

16-
use bitcoin::{Script, Transaction, Txid};
16+
use bitcoin::{Script, Txid};
1717
use lightning::chain::{BlockLocator, Filter};
1818

1919
use crate::chain::bitcoind::{BitcoindChainSource, UtxoSourceClient};
@@ -518,16 +518,16 @@ impl ChainSource {
518518
continue;
519519
},
520520
};
521-
let txs: Vec<Transaction> = package.into_transactions();
521+
let package = package.into_sorted_transactions();
522522
match &self.kind {
523523
ChainSourceKind::Esplora(esplora_chain_source) => {
524-
esplora_chain_source.process_broadcast_package(txs).await
524+
esplora_chain_source.process_transaction_broadcast(package).await
525525
},
526526
ChainSourceKind::Electrum(electrum_chain_source) => {
527-
electrum_chain_source.process_broadcast_package(txs).await
527+
electrum_chain_source.process_transaction_broadcast(package).await
528528
},
529529
ChainSourceKind::Bitcoind(bitcoind_chain_source) => {
530-
bitcoind_chain_source.process_broadcast_package(txs).await
530+
bitcoind_chain_source.process_transaction_broadcast(package).await
531531
},
532532
}
533533
}

src/tx_broadcaster.rs

Lines changed: 194 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,52 @@ impl BroadcastPackage {
4343
}
4444

4545
/// Consumes the package into its transactions, ready for the chain client.
46-
pub(crate) fn into_transactions(self) -> Vec<Transaction> {
47-
self.0.into_iter().map(|(tx, _)| tx).collect()
46+
pub(crate) fn into_sorted_transactions(self) -> SortedTransactions {
47+
let txs = self.0.into_iter().map(|(tx, _)| tx).collect();
48+
SortedTransactions::sort_parents_child_package_topologically(txs)
49+
}
50+
}
51+
52+
pub(crate) struct SortedTransactions(Vec<Transaction>);
53+
54+
impl SortedTransactions {
55+
pub(crate) fn sort_parents_child_package_topologically(
56+
mut txs: Vec<Transaction>,
57+
) -> SortedTransactions {
58+
if txs.len() == 0 || txs.len() == 1 {
59+
return SortedTransactions(txs);
60+
}
61+
let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect();
62+
let any_spends_from_package = |tx: &Transaction| -> bool {
63+
tx.input.iter().any(|input| txids.contains(&input.previous_output.txid))
64+
};
65+
txs.sort_by_key(any_spends_from_package);
66+
67+
#[cfg(debug_assertions)]
68+
{
69+
let child = txs.last().expect("txs is not empty");
70+
let child_input_txids: Vec<_> =
71+
child.input.iter().map(|input| input.previous_output.txid).collect();
72+
let parents = &txs[..txs.len() - 1];
73+
let parent_txids: Vec<_> = parents.iter().map(|parent| parent.compute_txid()).collect();
74+
// Make sure all the parent txids are parents of the child transaction
75+
debug_assert!(parent_txids.iter().all(|txid| child_input_txids.contains(&txid)));
76+
// Make sure there are no grandparents
77+
debug_assert_eq!(txs.iter().filter(|tx| any_spends_from_package(tx)).count(), 1);
78+
}
79+
80+
SortedTransactions(txs)
81+
}
82+
83+
pub(crate) fn into_inner(self) -> Vec<Transaction> {
84+
self.0
85+
}
86+
}
87+
88+
impl Deref for SortedTransactions {
89+
type Target = Vec<Transaction>;
90+
fn deref(&self) -> &Self::Target {
91+
&self.0
4892
}
4993
}
5094

@@ -123,3 +167,151 @@ where
123167
});
124168
}
125169
}
170+
171+
#[cfg(test)]
172+
mod tests {
173+
use bitcoin::hashes::Hash;
174+
use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness};
175+
176+
use super::SortedTransactions;
177+
178+
fn txin(txid: Txid, vout: u32) -> TxIn {
179+
TxIn {
180+
previous_output: OutPoint { txid, vout },
181+
script_sig: ScriptBuf::new(),
182+
sequence: Sequence::MAX,
183+
witness: Witness::new(),
184+
}
185+
}
186+
187+
fn txout(value_sat: u64) -> TxOut {
188+
TxOut { value: Amount::from_sat(value_sat), script_pubkey: ScriptBuf::new() }
189+
}
190+
191+
fn parent_tx(seed: u8) -> Transaction {
192+
Transaction {
193+
version: bitcoin::transaction::Version::TWO,
194+
lock_time: bitcoin::absolute::LockTime::ZERO,
195+
input: vec![txin(Txid::from_byte_array([seed; 32]), 0)],
196+
output: vec![txout(1_000 + u64::from(seed))],
197+
}
198+
}
199+
200+
fn child_tx(parents: &[&Transaction]) -> Transaction {
201+
Transaction {
202+
version: bitcoin::transaction::Version::TWO,
203+
lock_time: bitcoin::absolute::LockTime::ZERO,
204+
input: parents
205+
.iter()
206+
.enumerate()
207+
.map(|(idx, parent)| txin(parent.compute_txid(), idx as u32))
208+
.collect(),
209+
output: vec![txout(1_000)],
210+
}
211+
}
212+
213+
fn assert_parents_before_child(
214+
txs: &[Transaction], expected_child: Txid, expected_parents: &[Txid],
215+
) {
216+
assert_eq!(txs.last().map(Transaction::compute_txid), Some(expected_child));
217+
assert_eq!(txs.len(), expected_parents.len() + 1);
218+
219+
let parent_txids =
220+
txs[..txs.len() - 1].iter().map(Transaction::compute_txid).collect::<Vec<_>>();
221+
for expected_parent in expected_parents {
222+
assert!(parent_txids.contains(expected_parent));
223+
}
224+
}
225+
226+
#[test]
227+
fn topological_sort_leaves_sorted_package_unchanged() {
228+
let parent_a = parent_tx(1);
229+
let parent_b = parent_tx(2);
230+
let child = child_tx(&[&parent_a, &parent_b]);
231+
232+
let original_txids =
233+
[parent_a.compute_txid(), parent_b.compute_txid(), child.compute_txid()];
234+
let txs = vec![parent_a, parent_b, child];
235+
236+
let package = SortedTransactions::sort_parents_child_package_topologically(txs);
237+
238+
assert_eq!(
239+
package.iter().map(Transaction::compute_txid).collect::<Vec<_>>(),
240+
original_txids
241+
);
242+
}
243+
244+
#[test]
245+
fn topological_sort_moves_single_parent_child_from_front_to_end() {
246+
let parent = parent_tx(1);
247+
let child = child_tx(&[&parent]);
248+
let parent_txids = [parent.compute_txid()];
249+
let child_txid = child.compute_txid();
250+
let txs = vec![child, parent];
251+
252+
let package = SortedTransactions::sort_parents_child_package_topologically(txs);
253+
254+
assert_parents_before_child(&package, child_txid, &parent_txids);
255+
}
256+
257+
#[test]
258+
fn topological_sort_moves_child_from_front_to_end() {
259+
let parent_a = parent_tx(1);
260+
let parent_b = parent_tx(2);
261+
let child = child_tx(&[&parent_a, &parent_b]);
262+
let parent_txids = [parent_a.compute_txid(), parent_b.compute_txid()];
263+
let child_txid = child.compute_txid();
264+
let txs = vec![child, parent_a, parent_b];
265+
266+
let package = SortedTransactions::sort_parents_child_package_topologically(txs);
267+
268+
assert_parents_before_child(&package, child_txid, &parent_txids);
269+
}
270+
271+
#[test]
272+
fn topological_sort_moves_child_from_front_with_multiple_parents_to_end() {
273+
let parent_a = parent_tx(1);
274+
let parent_b = parent_tx(2);
275+
let parent_c = parent_tx(3);
276+
let child = child_tx(&[&parent_a, &parent_b, &parent_c]);
277+
let parent_txids =
278+
[parent_a.compute_txid(), parent_b.compute_txid(), parent_c.compute_txid()];
279+
let child_txid = child.compute_txid();
280+
let txs = vec![child, parent_a, parent_b, parent_c];
281+
282+
let package = SortedTransactions::sort_parents_child_package_topologically(txs);
283+
284+
assert_parents_before_child(&package, child_txid, &parent_txids);
285+
}
286+
287+
#[test]
288+
fn topological_sort_moves_child_from_middle_to_end() {
289+
let parent_a = parent_tx(1);
290+
let parent_b = parent_tx(2);
291+
let child = child_tx(&[&parent_a, &parent_b]);
292+
let parent_txids = [parent_a.compute_txid(), parent_b.compute_txid()];
293+
let child_txid = child.compute_txid();
294+
let txs = vec![parent_a, child, parent_b];
295+
296+
let package = SortedTransactions::sort_parents_child_package_topologically(txs);
297+
298+
assert_parents_before_child(&package, child_txid, &parent_txids);
299+
}
300+
301+
#[test]
302+
fn topological_sort_leaves_single_transaction_package_unchanged() {
303+
let parent = parent_tx(1);
304+
let parent_txid = parent.compute_txid();
305+
let txs = vec![parent];
306+
307+
let package = SortedTransactions::sort_parents_child_package_topologically(txs);
308+
309+
assert_eq!(package.len(), 1);
310+
assert_eq!(package[0].compute_txid(), parent_txid);
311+
}
312+
313+
#[test]
314+
fn topological_sort_accepts_empty_vec() {
315+
SortedTransactions::sort_parents_child_package_topologically(Vec::new());
316+
}
317+
}

0 commit comments

Comments
 (0)