From b4cd7f5e7856efa5dbfb3663c24e4a1fc7e541eb Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 27 May 2026 00:27:51 +0000 Subject: [PATCH 01/16] Only pass TRUC packages as multi-transaction vecs `BroadcasterInterface::broadcast_transactions` requires that any passed vector containing multiple transactions must be a single child together with its parents. We will lean on this contract in upcoming commits, so here we fix a case where we broke this contract. --- src/wallet/mod.rs | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 76f2aa9ce..691ef3646 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -334,32 +334,25 @@ impl Wallet { } } - if !unconfirmed_outbound_txids.is_empty() { - let txs_to_broadcast: Vec = unconfirmed_outbound_txids - .iter() - .filter_map(|txid| { - locked_wallet.tx_details(*txid).map(|d| (*d.tx).clone()) - }) - .collect(); - - if !txs_to_broadcast.is_empty() { - let tx_refs: Vec<( - &Transaction, - lightning::chain::chaininterface::TransactionType, - )> = - txs_to_broadcast - .iter() - .map(|tx| { - (tx, lightning::chain::chaininterface::TransactionType::Sweep { channels: vec![] }) - }) - .collect(); - self.broadcaster.broadcast_transactions(&tx_refs); - log_info!( - self.logger, - "Rebroadcast {} unconfirmed transactions on chain tip change", - txs_to_broadcast.len() - ); - } + let count: usize = unconfirmed_outbound_txids + .into_iter() + .filter_map(|txid| { + let tx = locked_wallet.tx_details(txid).map(|d| d.tx)?; + let transaction_type = + lightning::chain::chaininterface::TransactionType::Sweep { + channels: vec![], + }; + self.broadcaster + .broadcast_transactions(&[(tx.as_ref(), transaction_type)]); + Some(()) + }) + .count(); + if count != 0 { + log_info!( + self.logger, + "Rebroadcast {} unconfirmed transactions on chain tip change", + count, + ); } }, WalletEvent::TxUnconfirmed { txid, tx, old_block_time: None } => { From 18f020e22aacebec12d2febd00a2e42fa388b92c Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 27 May 2026 17:35:10 +0000 Subject: [PATCH 02/16] 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. --- src/tx_broadcaster.rs | 175 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 7084135b0..cf3e59b19 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -45,9 +45,182 @@ where L::Target: LdkLogger, { fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) { - let package = txs.iter().map(|(t, _)| (*t).clone()).collect::>(); + let mut package = txs.iter().map(|(t, _)| (*t).clone()).collect::>(); + sort_parents_child_package_topologically(&mut package); self.queue_sender.try_send(package).unwrap_or_else(|e| { log_error!(self.logger, "Failed to broadcast transactions: {}", e); }); } } + +fn sort_parents_child_package_topologically(txs: &mut [Transaction]) { + if txs.len() == 0 || txs.len() == 1 { + return; + } + let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect(); + let any_spends_from_package = |tx: &Transaction| -> bool { + tx.input.iter().any(|input| txids.contains(&input.previous_output.txid)) + }; + txs.sort_by_key(any_spends_from_package); + + #[cfg(debug_assertions)] + { + let child = txs.last().expect("txs is not empty"); + let child_input_txids: Vec<_> = + child.input.iter().map(|input| input.previous_output.txid).collect(); + let parents = &txs[..txs.len() - 1]; + let parent_txids: Vec<_> = parents.iter().map(|parent| parent.compute_txid()).collect(); + // Make sure all the parent txids are parents of the child transaction + debug_assert!(parent_txids.iter().all(|txid| child_input_txids.contains(&txid))); + // Make sure there are no grandparents + debug_assert_eq!(txs.iter().filter(|tx| any_spends_from_package(tx)).count(), 1); + } +} + +#[cfg(test)] +mod tests { + use bitcoin::hashes::Hash; + use bitcoin::{Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness}; + + use super::sort_parents_child_package_topologically; + + fn txin(txid: Txid, vout: u32) -> TxIn { + TxIn { + previous_output: OutPoint { txid, vout }, + script_sig: ScriptBuf::new(), + sequence: Sequence::MAX, + witness: Witness::new(), + } + } + + fn txout(value_sat: u64) -> TxOut { + TxOut { value: Amount::from_sat(value_sat), script_pubkey: ScriptBuf::new() } + } + + fn parent_tx(seed: u8) -> Transaction { + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![txin(Txid::from_byte_array([seed; 32]), 0)], + output: vec![txout(1_000 + u64::from(seed))], + } + } + + fn child_tx(parents: &[&Transaction]) -> Transaction { + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: parents + .iter() + .enumerate() + .map(|(idx, parent)| txin(parent.compute_txid(), idx as u32)) + .collect(), + output: vec![txout(1_000)], + } + } + + fn assert_parents_before_child( + txs: &[Transaction], expected_child: Txid, expected_parents: &[Txid], + ) { + assert_eq!(txs.last().map(Transaction::compute_txid), Some(expected_child)); + assert_eq!(txs.len(), expected_parents.len() + 1); + + let parent_txids = + txs[..txs.len() - 1].iter().map(Transaction::compute_txid).collect::>(); + for expected_parent in expected_parents { + assert!(parent_txids.contains(expected_parent)); + } + } + + #[test] + fn topological_sort_leaves_sorted_package_unchanged() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let child = child_tx(&[&parent_a, &parent_b]); + + let original_txids = + [parent_a.compute_txid(), parent_b.compute_txid(), child.compute_txid()]; + let mut package = vec![parent_a, parent_b, child]; + + sort_parents_child_package_topologically(&mut package); + + assert_eq!( + package.iter().map(Transaction::compute_txid).collect::>(), + original_txids + ); + } + + #[test] + fn topological_sort_moves_single_parent_child_from_front_to_end() { + let parent = parent_tx(1); + let child = child_tx(&[&parent]); + let parent_txids = [parent.compute_txid()]; + let child_txid = child.compute_txid(); + let mut package = vec![child, parent]; + + sort_parents_child_package_topologically(&mut package); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_moves_child_from_front_to_end() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let child = child_tx(&[&parent_a, &parent_b]); + let parent_txids = [parent_a.compute_txid(), parent_b.compute_txid()]; + let child_txid = child.compute_txid(); + let mut package = vec![child, parent_a, parent_b]; + + sort_parents_child_package_topologically(&mut package); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_moves_child_from_front_with_multiple_parents_to_end() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let parent_c = parent_tx(3); + let child = child_tx(&[&parent_a, &parent_b, &parent_c]); + let parent_txids = + [parent_a.compute_txid(), parent_b.compute_txid(), parent_c.compute_txid()]; + let child_txid = child.compute_txid(); + let mut package = vec![child, parent_a, parent_b, parent_c]; + + sort_parents_child_package_topologically(&mut package); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_moves_child_from_middle_to_end() { + let parent_a = parent_tx(1); + let parent_b = parent_tx(2); + let child = child_tx(&[&parent_a, &parent_b]); + let parent_txids = [parent_a.compute_txid(), parent_b.compute_txid()]; + let child_txid = child.compute_txid(); + let mut package = vec![parent_a, child, parent_b]; + + sort_parents_child_package_topologically(&mut package); + + assert_parents_before_child(&package, child_txid, &parent_txids); + } + + #[test] + fn topological_sort_leaves_single_transaction_package_unchanged() { + let parent = parent_tx(1); + let parent_txid = parent.compute_txid(); + let mut package = vec![parent]; + + sort_parents_child_package_topologically(&mut package); + + assert_eq!(package.len(), 1); + assert_eq!(package[0].compute_txid(), parent_txid); + } + + #[test] + fn topological_sort_accepts_empty_vec() { + sort_parents_child_package_topologically(&mut []); + } +} From 7d43482242e6e6e257ae80a33befd4c1f9cd3827 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 28 May 2026 18:16:49 +0000 Subject: [PATCH 03/16] Use a patched blockstream-electrs in CI The patch adds support for the `broadcast_package` method added in electrum protocol v1.6. Upcoming commits will require this patch to pass CI. --- .github/workflows/benchmarks.yml | 13 ++++++--- .github/workflows/hrn-integration.yml | 13 ++++++--- .github/workflows/postgres-integration.yml | 13 ++++++--- .github/workflows/rust.yml | 19 ++++++++----- .github/workflows/vss-integration.yml | 15 +++++++++++ .github/workflows/vss-no-auth-integration.yml | 15 +++++++++++ scripts/build_electrs.sh | 27 +++++++++++++++++++ ...tcoind_electrs.sh => download_bitcoind.sh} | 19 +++---------- 8 files changed, 100 insertions(+), 34 deletions(-) create mode 100755 scripts/build_electrs.sh rename scripts/{download_bitcoind_electrs.sh => download_bitcoind.sh} (55%) diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index cd3980b9a..1cd39ff69 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -29,13 +29,18 @@ jobs: uses: actions/cache@v5 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "(steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | diff --git a/.github/workflows/hrn-integration.yml b/.github/workflows/hrn-integration.yml index f7ded7bc5..466886eb4 100644 --- a/.github/workflows/hrn-integration.yml +++ b/.github/workflows/hrn-integration.yml @@ -27,13 +27,18 @@ jobs: uses: actions/cache@v4 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true'" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | diff --git a/.github/workflows/postgres-integration.yml b/.github/workflows/postgres-integration.yml index 410136928..3764d454b 100644 --- a/.github/workflows/postgres-integration.yml +++ b/.github/workflows/postgres-integration.yml @@ -43,13 +43,18 @@ jobs: uses: actions/cache@v4 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-esplora_a33e97e1-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true'" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 16064fa45..af7edf366 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -59,23 +59,30 @@ jobs: uses: actions/cache@v5 with: path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "matrix.platform != 'windows-latest' && (steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')" + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind + if: "matrix.platform != 'windows-latest' && steps.cache-bitcoind.outputs.cache-hit != 'true'" run: | - source ./scripts/download_bitcoind_electrs.sh + source ./scripts/download_bitcoind.sh mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "matrix.platform != 'windows-latest' && steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./scripts/build_electrs.sh + mkdir -p bin mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables run: | echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - name: Build on Rust ${{ matrix.toolchain }} - run: cargo build --verbose --color always + run: | + cargo build --verbose --color always - name: Build with UniFFI support on Rust ${{ matrix.toolchain }} if: matrix.build-uniffi - run: cargo build --features uniffi --verbose --color always + run: | + cargo build --features uniffi --verbose --color always - name: Check release build on Rust ${{ matrix.toolchain }} run: cargo check --release --verbose --color always - name: Check release build with UniFFI support on Rust ${{ matrix.toolchain }} diff --git a/.github/workflows/vss-integration.yml b/.github/workflows/vss-integration.yml index c67e9194e..a788644cd 100644 --- a/.github/workflows/vss-integration.yml +++ b/.github/workflows/vss-integration.yml @@ -30,6 +30,21 @@ jobs: uses: actions/checkout@v6 with: path: ldk-node + - name: Enable caching for electrs + id: cache-electrs + uses: actions/cache@v5 + with: + path: bin/electrs-${{ runner.os }}-${{ runner.arch }} + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./ldk-node/scripts/build_electrs.sh + mkdir -p bin + mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} + - name: Set electrs environment variable + run: | + echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - name: Checkout VSS uses: actions/checkout@v6 with: diff --git a/.github/workflows/vss-no-auth-integration.yml b/.github/workflows/vss-no-auth-integration.yml index 35666df03..5d81c1a44 100644 --- a/.github/workflows/vss-no-auth-integration.yml +++ b/.github/workflows/vss-no-auth-integration.yml @@ -30,6 +30,21 @@ jobs: uses: actions/checkout@v6 with: path: ldk-node + - name: Enable caching for electrs + id: cache-electrs + uses: actions/cache@v5 + with: + path: bin/electrs-${{ runner.os }}-${{ runner.arch }} + key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }} + - name: Download electrs + if: "steps.cache-electrs.outputs.cache-hit != 'true'" + run: | + source ./ldk-node/scripts/build_electrs.sh + mkdir -p bin + mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} + - name: Set electrs environment variable + run: | + echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - name: Checkout VSS uses: actions/checkout@v6 with: diff --git a/scripts/build_electrs.sh b/scripts/build_electrs.sh new file mode 100755 index 000000000..1300e87fe --- /dev/null +++ b/scripts/build_electrs.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -eox pipefail + +# Our Esplora-based tests require `electrs` binaries. Here, we +# download the code, build the binaries, and export their location +# via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the +# `electrsd`/`bitcoind` crates in our tests. + +HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" +ELECTRS_GIT_REPO="https://github.com/tankyleo/blockstream-electrs.git" +ELECTRS_TAG="2026-05-26-electrum-submit-package" +if [[ "$HOST_PLATFORM" != *linux* && "$HOST_PLATFORM" != *darwin* ]]; then + printf "\n\n" + echo "Unsupported platform: $HOST_PLATFORM Exiting.." + exit 1 +fi + +DL_TMP_DIR=$(mktemp -d) +trap 'rm -rf -- "$DL_TMP_DIR"' EXIT + +pushd "$DL_TMP_DIR" +git clone --branch $ELECTRS_TAG --depth 1 $ELECTRS_GIT_REPO blockstream-electrs +cd blockstream-electrs +RUSTFLAGS="" cargo build +export ELECTRS_EXE="$DL_TMP_DIR"/blockstream-electrs/target/debug/electrs +chmod +x "$ELECTRS_EXE" +popd diff --git a/scripts/download_bitcoind_electrs.sh b/scripts/download_bitcoind.sh similarity index 55% rename from scripts/download_bitcoind_electrs.sh rename to scripts/download_bitcoind.sh index f94e280e3..102cf826f 100755 --- a/scripts/download_bitcoind_electrs.sh +++ b/scripts/download_bitcoind.sh @@ -1,24 +1,18 @@ #!/bin/bash set -eox pipefail -# Our Esplora-based tests require `electrs` and `bitcoind` -# binaries. Here, we download the binaries, validate them, and export their -# location via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the +# Our Esplora-based tests require `bitcoind` binaries. Here, we +# download the binaries, validate them, and export their location +# via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the # `electrsd`/`bitcoind` crates in our tests. HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" -ELECTRS_DL_ENDPOINT="https://github.com/RCasatta/electrsd/releases/download/electrs_releases" -ELECTRS_VERSION="esplora_a33e97e1a1fc63fa9c20a116bb92579bbf43b254" BITCOIND_DL_ENDPOINT="https://bitcoincore.org/bin/" BITCOIND_VERSION="29.0" if [[ "$HOST_PLATFORM" == *linux* ]]; then - ELECTRS_DL_FILE_NAME=electrs_linux_"$ELECTRS_VERSION".zip - ELECTRS_DL_HASH="865e26a96e8df77df01d96f2f569dcf9622fc87a8d99a9b8fe30861a4db9ddf1" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-linux-gnu.tar.gz BITCOIND_DL_HASH="a681e4f6ce524c338a105f214613605bac6c33d58c31dc5135bbc02bc458bb6c" elif [[ "$HOST_PLATFORM" == *darwin* ]]; then - ELECTRS_DL_FILE_NAME=electrs_macos_"$ELECTRS_VERSION".zip - ELECTRS_DL_HASH="2d5ff149e8a2482d3658e9b386830dfc40c8fbd7c175ca7cbac58240a9505bcd" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-apple-darwin.tar.gz BITCOIND_DL_HASH="5bb824fc86a15318d6a83a1b821ff4cd4b3d3d0e1ec3d162b805ccf7cae6fca8" else @@ -31,13 +25,6 @@ DL_TMP_DIR=$(mktemp -d) trap 'rm -rf -- "$DL_TMP_DIR"' EXIT pushd "$DL_TMP_DIR" -ELECTRS_DL_URL="$ELECTRS_DL_ENDPOINT"/"$ELECTRS_DL_FILE_NAME" -curl -L -o "$ELECTRS_DL_FILE_NAME" "$ELECTRS_DL_URL" -echo "$ELECTRS_DL_HASH $ELECTRS_DL_FILE_NAME"|shasum -a 256 -c -unzip "$ELECTRS_DL_FILE_NAME" -export ELECTRS_EXE="$DL_TMP_DIR"/electrs -chmod +x "$ELECTRS_EXE" - BITCOIND_DL_URL="$BITCOIND_DL_ENDPOINT"/bitcoin-core-"$BITCOIND_VERSION"/"$BITCOIND_DL_FILE_NAME" curl -L -o "$BITCOIND_DL_FILE_NAME" "$BITCOIND_DL_URL" echo "$BITCOIND_DL_HASH $BITCOIND_DL_FILE_NAME"|shasum -a 256 -c From ce4abd07b444340d94ed3b5c989122a24f2fae36 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 24 Jun 2026 19:26:49 +0000 Subject: [PATCH 04/16] f: Assert HEAD matches the expected rev --- scripts/build_electrs.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scripts/build_electrs.sh b/scripts/build_electrs.sh index 1300e87fe..6130ca508 100755 --- a/scripts/build_electrs.sh +++ b/scripts/build_electrs.sh @@ -9,6 +9,7 @@ set -eox pipefail HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" ELECTRS_GIT_REPO="https://github.com/tankyleo/blockstream-electrs.git" ELECTRS_TAG="2026-05-26-electrum-submit-package" +ELECTRS_REV="8c06d8010e43f793b1a65f83695ea846e5cd83ed" if [[ "$HOST_PLATFORM" != *linux* && "$HOST_PLATFORM" != *darwin* ]]; then printf "\n\n" echo "Unsupported platform: $HOST_PLATFORM Exiting.." @@ -19,8 +20,15 @@ DL_TMP_DIR=$(mktemp -d) trap 'rm -rf -- "$DL_TMP_DIR"' EXIT pushd "$DL_TMP_DIR" -git clone --branch $ELECTRS_TAG --depth 1 $ELECTRS_GIT_REPO blockstream-electrs +git clone --branch "$ELECTRS_TAG" --depth 1 "$ELECTRS_GIT_REPO" blockstream-electrs cd blockstream-electrs +CURRENT_HEAD=$(git rev-parse HEAD) +if [ "$CURRENT_HEAD" != "$ELECTRS_REV" ]; then + echo "ERROR: HEAD does not match expected commit" + echo "expected: $ELECTRS_REV" + echo "actual: $CURRENT_HEAD" + exit 1 +fi RUSTFLAGS="" cargo build export ELECTRS_EXE="$DL_TMP_DIR"/blockstream-electrs/target/debug/electrs chmod +x "$ELECTRS_EXE" From 6f7e34ab9c638f491fd0a27ca3dea84391749815 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 24 Jun 2026 05:48:23 +0000 Subject: [PATCH 05/16] Switch cln lnd and eclair interop tests to esplora The mempool/electrs docker image used in those tests only supports submitpackage via the esplora interface, not the electrum interface. --- tests/common/scenarios/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/common/scenarios/mod.rs b/tests/common/scenarios/mod.rs index 7cbf56b8e..6c2564b76 100644 --- a/tests/common/scenarios/mod.rs +++ b/tests/common/scenarios/mod.rs @@ -92,10 +92,10 @@ pub(crate) async fn wait_for_htlcs_settled( pub(crate) fn setup_ldk_node() -> Node { let config = crate::common::random_config(true); let mut builder = ldk_node::Builder::from_config(config.node_config); - let mut sync_config = ldk_node::config::ElectrumSyncConfig::default(); + let mut sync_config = ldk_node::config::EsploraSyncConfig::default(); sync_config.timeouts_config.onchain_wallet_sync_timeout_secs = 180; sync_config.timeouts_config.lightning_wallet_sync_timeout_secs = 120; - builder.set_chain_source_electrum("tcp://127.0.0.1:50001".to_string(), Some(sync_config)); + builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), Some(sync_config)); let node = builder.build(config.node_entropy).unwrap(); node.start().unwrap(); node From f5a260a0b31cfdacf0c08cffbff073204a13a69e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 24 Jun 2026 05:48:54 +0000 Subject: [PATCH 06/16] Bump Bitcoin Core version used in kotlin and python tests We bump the Bitcoin Core version used in kotlin and python tests to support ephemeral dust. This is required for 0FC channels. --- tests/docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index e71fd70fb..5459e8eda 100644 --- a/tests/docker/docker-compose.yml +++ b/tests/docker/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: bitcoin: - image: blockstream/bitcoind:27.2 + image: blockstream/bitcoind:29.1 platform: linux/amd64 command: [ From 68acdd9fa9bdf257e651b1db6ba8d3a6ccf8482e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 28 May 2026 01:28:46 +0000 Subject: [PATCH 07/16] Check that the chain source supports 0FC channels Do this roundtrip at the same time we make a roundtrip to retrieve the feerates to keep startup as fast as possible. --- CHANGELOG.md | 2 ++ src/chain/bitcoind.rs | 49 +++++++++++++++++++++++++++++++++++++++++ src/chain/electrum.rs | 23 +++++++++++++++++++ src/chain/esplora.rs | 10 +++++++++ src/chain/mod.rs | 51 +++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 3 ++- src/error.rs | 5 +++++ src/lib.rs | 23 +++++++++++++++++-- 8 files changed, 163 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f15e61f..dfb785467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated. - Users of the VSS storage backend must upgrade their VSS server to at least version `v0.1.0-alpha.0` before upgrading LDK Node. +- Usage of anchor channels now requires an Esplora or Electrum chain source that supports + `submitpackage`, or a Bitcoin Core RPC/REST chain source against Bitcoin Core v29 and above. # 0.7.0 - Dec. 3, 2025 This seventh minor release introduces numerous new features, bug fixes, and API improvements. In particular, it adds support for channel Splicing, Async Payments, as well as sourcing chain data from a Bitcoin Core REST backend. diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 6bfa8ffd2..582100ad1 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -119,6 +119,30 @@ impl BitcoindChainSource { self.api_client.utxo_source() } + pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + let node_version_result = tokio::time::timeout( + Duration::from_secs(CHAIN_POLLING_TIMEOUT_SECS), + self.api_client.get_node_version(), + ) + .await + .map_err(|e| { + log_error!(self.logger, "Failed to get node version: {:?}", e); + Error::ChainSourceNotSupported + })?; + + let node_version = node_version_result.map_err(|e| { + log_error!(self.logger, "Failed to get node version: {:?}", e); + Error::ChainSourceNotSupported + })?; + + // v26 first shipped the `submitpackage` RPC, but we need v29 to relay ephemeral dust. + if node_version < 290000 { + log_error!(self.logger, "Bitcoin backend MUST be greater than or equal to v29"); + return Err(Error::ChainSourceNotSupported); + } + Ok(()) + } + pub(super) async fn continuously_sync_wallets( &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, onchain_wallet: Arc, channel_manager: Arc, @@ -748,6 +772,31 @@ impl BitcoindClient { } } + pub(crate) async fn get_node_version(&self) -> Result { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_node_version_inner(Arc::clone(rpc_client)) + .await + .map_err(BitcoindClientError::Rpc) + }, + BitcoindClient::Rest { rpc_client, .. } => { + // Bitcoin Core's REST interface does not support `getnetworkinfo` + // so we use the RPC client. + Self::get_node_version_inner(Arc::clone(rpc_client)) + .await + .map_err(BitcoindClientError::Rpc) + }, + } + } + + async fn get_node_version_inner(rpc_client: Arc) -> Result { + rpc_client.call_method::("getnetworkinfo", &[]).await.and_then(|value| { + value["version"].as_u64().ok_or(RpcClientError::InvalidData(String::from( + "The version field in the `getnetworkinfo` response should be a u64", + ))) + }) + } + /// Broadcasts the provided transaction. pub(crate) async fn broadcast_transaction( &self, tx: &Transaction, diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 7406f06b4..28a10051f 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -294,6 +294,29 @@ impl ElectrumChainSource { Ok(()) } + pub(crate) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + let electrum_client: Arc = if let Some(client) = + self.electrum_runtime_status.read().expect("lock").client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before checking submit package support" + ); + return Err(Error::ChainSourceNotSupported); + }; + + electrum_client + .electrum_client + .transaction_broadcast_package(&super::dummy_package()) + .map_err(|e| { + log_error!(self.logger, "Electrum server does not support submit package: {:?}", e); + Error::ChainSourceNotSupported + })?; + Ok(()) + } + pub(crate) async fn process_broadcast_package(&self, package: Vec) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index eb23a395d..e2bdaf232 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -76,6 +76,16 @@ impl EsploraChainSource { }) } + pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { + self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err( + |e| { + log_error!(self.logger, "Esplora server does not support submit package: {:?}", e); + Error::ChainSourceNotSupported + }, + )?; + Ok(()) + } + pub(super) async fn sync_onchain_wallet( &self, onchain_wallet: Arc, ) -> Result<(), Error> { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 5a326be97..db9860608 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -29,6 +29,37 @@ use crate::runtime::Runtime; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; +/// We use this parent-child TRUC package to make sure the configured chain source supports +/// broadcasting packages via the `submitpackage` Bitcoin Core RPC. +const PARENT_TXID: &str = "9a015f93fac6cb203c2b994e18b85176eb0354a22a468255516f3c6002d3f696"; +const PARENT_HEX: &str = + "0300000000010160d0cdb72f2ddf719f40ca32f44614c67577fc75996140544003915683c34a310000000000fd\ + ffffff0201000000000000000451024e73876100000000000022512042731375894dad3b25092cd0f713dc5bee4\ + a71e30a95e1db3d880906d7eba1fa01409327942924218e4eb1635a7cce6706fcb37b8bbb61a2f0b86357356681\ + 4e09419a3501e02252043bb237d479304632282fe9159db9e9a6ae6ec5bedea9f0f115a97b0e00"; +const CHILD_TXID: &str = "d011b3ff78cdfb8b93822639ea87771847936b04bb83afc8763a7c02a386ae26"; +const CHILD_HEX: &str = + "0300000000010296f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0000000000ff\ + ffffff96f6d302603c6f515582462aa25403eb7651b8184e992b3c20cbc6fa935f019a0100000000fdffffff015\ + 660000000000000225120ac18cd599a1be003595854e2eeec18dbe1c92d04b0ba05812d04445e3fcf16bc000140\ + 1462a35808d77a164f0a23a84c4721d1545befd09ad19945bb8aa0ea5576953a9699038725f944b1bc429942ef4\ + 7e6504a554babf022cb15db53be2d8c1dbfe5a97b0e00"; + +fn dummy_package() -> [bitcoin::Transaction; 2] { + use bitcoin::consensus::Decodable; + use bitcoin::hex::FromHex; + use bitcoin::Transaction; + let parent_tx_bytes = Vec::from_hex(PARENT_HEX).expect("read from a constant"); + let child_tx_bytes = Vec::from_hex(CHILD_HEX).expect("read from a constant"); + let parent = + Transaction::consensus_decode(&mut &parent_tx_bytes[..]).expect("read from a constant"); + let child = + Transaction::consensus_decode(&mut &child_tx_bytes[..]).expect("read from a constant"); + assert_eq!(parent.compute_txid().to_string(), PARENT_TXID); + assert_eq!(child.compute_txid().to_string(), CHILD_TXID); + [parent, child] +} + pub(crate) enum WalletSyncStatus { Completed, InProgress { subscribers: tokio::sync::broadcast::Sender> }, @@ -438,6 +469,26 @@ impl ChainSource { } } + pub(crate) async fn validate_zero_fee_commitments_support_if_required( + &self, submit_package_support_required: bool, + ) -> Result<(), Error> { + if !submit_package_support_required { + return Ok(()); + } + + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.validate_zero_fee_commitments_support().await + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.validate_zero_fee_commitments_support().await + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.validate_zero_fee_commitments_support().await + }, + } + } + pub(crate) async fn continuously_process_broadcast_queue( &self, mut stop_tx_bcast_receiver: tokio::sync::watch::Receiver<()>, ) { diff --git a/src/config.rs b/src/config.rs index 558a4d061..0402ebb46 100644 --- a/src/config.rs +++ b/src/config.rs @@ -54,7 +54,8 @@ pub const DEFAULT_LOG_FILENAME: &'static str = "ldk_node.log"; /// The default storage directory. pub const DEFAULT_STORAGE_DIR_PATH: &str = "/tmp/ldk_node"; -// The default Esplora server we're using. +// The default Esplora server we're using. It supports `submitpackage`, check using POST on the +// `/txs/package` endpoint. pub(crate) const DEFAULT_ESPLORA_SERVER_URL: &str = "https://blockstream.info/api"; // The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold diff --git a/src/error.rs b/src/error.rs index d07212b00..8546af0dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -137,6 +137,8 @@ pub enum Error { LnurlAuthTimeout, /// The provided lnurl is invalid. InvalidLnurl, + /// The configured chain source is not supported. + ChainSourceNotSupported, } impl fmt::Display for Error { @@ -222,6 +224,9 @@ impl fmt::Display for Error { Self::LnurlAuthFailed => write!(f, "LNURL-auth authentication failed."), Self::LnurlAuthTimeout => write!(f, "LNURL-auth authentication timed out."), Self::InvalidLnurl => write!(f, "The provided lnurl is invalid."), + Self::ChainSourceNotSupported => { + write!(f, "The configured chain source is not supported.") + }, } } } diff --git a/src/lib.rs b/src/lib.rs index 34fa7f54d..5eb95fd15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -285,9 +285,28 @@ impl Node { e })?; - // Block to ensure we update our fee rate cache once on startup + let any_current_0fc_channels = + self.chain_monitor.list_monitors().into_iter().any(|channel_id| { + self.chain_monitor + .get_monitor(channel_id) + .map(|monitor| { + monitor.channel_type_features().requires_anchor_zero_fee_commitments() + }) + .unwrap_or(false) + }); + + // Block to ensure we update our fee rate cache once on startup. + // Also take this opportunity to make sure our chain source supports any current or + // future 0FC channels. let chain_source = Arc::clone(&self.chain_source); - self.runtime.block_on(async move { chain_source.update_fee_rate_estimates().await })?; + self.runtime.block_on(async move { + tokio::try_join!( + chain_source.update_fee_rate_estimates(), + chain_source.validate_zero_fee_commitments_support_if_required( + any_current_0fc_channels || self.config.anchor_channels_config.is_some() + ) + ) + })?; // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); From 92ca4745ba45e394f46c6c02eeb247e1bae14e11 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 25 Jun 2026 02:05:58 +0000 Subject: [PATCH 08/16] f: cover ChainSourceNotSupported variant --- bindings/ldk_node.udl | 1 + 1 file changed, 1 insertion(+) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 851583c5a..de62f90da 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -230,6 +230,7 @@ enum NodeError { "LnurlAuthFailed", "LnurlAuthTimeout", "InvalidLnurl", + "ChainSourceNotSupported", }; typedef dictionary NodeStatus; From 1031204889220412cb97e4c22390aba80460f644 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 25 Jun 2026 00:33:10 +0000 Subject: [PATCH 09/16] f: Use submitpackage to point to the specific RPC call --- src/chain/electrum.rs | 4 ++-- src/chain/esplora.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 28a10051f..121c98691 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -302,7 +302,7 @@ impl ElectrumChainSource { } else { debug_assert!( false, - "We should have started the chain source before checking submit package support" + "We should have started the chain source before checking submitpackage support" ); return Err(Error::ChainSourceNotSupported); }; @@ -311,7 +311,7 @@ impl ElectrumChainSource { .electrum_client .transaction_broadcast_package(&super::dummy_package()) .map_err(|e| { - log_error!(self.logger, "Electrum server does not support submit package: {:?}", e); + log_error!(self.logger, "Electrum server does not support submitpackage: {:?}", e); Error::ChainSourceNotSupported })?; Ok(()) diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index e2bdaf232..00bf29ce6 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -79,7 +79,7 @@ impl EsploraChainSource { pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> { self.esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err( |e| { - log_error!(self.logger, "Esplora server does not support submit package: {:?}", e); + log_error!(self.logger, "Esplora server does not support submitpackage: {:?}", e); Error::ChainSourceNotSupported }, )?; From a6678882f34af70c54be1d1569f84aac10591b9e Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 25 Jun 2026 01:01:48 +0000 Subject: [PATCH 10/16] f: Add todo to switch electrum to protocol_version API --- src/chain/electrum.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 121c98691..c544a44fd 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -307,6 +307,8 @@ impl ElectrumChainSource { return Err(Error::ChainSourceNotSupported); }; + // TODO: Use `protocol_version` API once shipped in + // https://github.com/bitcoindevkit/rust-electrum-client/pull/213 electrum_client .electrum_client .transaction_broadcast_package(&super::dummy_package()) From 84565fcd29fe5355a23eac32872eeab673d8dc12 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Fri, 24 Oct 2025 06:01:26 +0000 Subject: [PATCH 11/16] 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, and must be broadcasted together as a package using `submitpackage`. 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. --- src/chain/bitcoind.rs | 156 +++++++++++++++++++++++------- src/chain/electrum.rs | 79 +++++++++++++++- src/chain/esplora.rs | 215 ++++++++++++++++++++++++++++++------------ src/tx_broadcaster.rs | 35 +++++-- 4 files changed, 382 insertions(+), 103 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 582100ad1..6bb038ffd 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -41,6 +41,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::tx_broadcaster::TransactionsToBroadcast; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -595,46 +596,88 @@ impl BitcoindChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, package: Vec) { - // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 - // features, we should eventually switch to use `submitpackage` via the - // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual - // transactions. - for tx in &package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), - self.api_client.broadcast_transaction(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(id) => { - debug_assert_eq!(id, txid); - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + pub(crate) async fn process_broadcast_package(&self, txs: TransactionsToBroadcast) { + match txs { + TransactionsToBroadcast::Singleton(tx) => { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), + self.api_client.broadcast_transaction(&tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(id) => { + debug_assert_eq!(id, txid); + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, }, Err(e) => { - log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); log_trace!( self.logger, "Failed broadcast transaction bytes: {}", log_bytes!(tx.encode()) ); }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } + } + }, + TransactionsToBroadcast::Package(package) => { + let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), + self.api_client.submit_package(&package), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(result) => { + log_trace!(self.logger, "Successfully broadcast package {:?}", txids); + log_trace!(self.logger, "Successfully broadcast package {}", result); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast package {:?}: {}", + txids, + e + ); + log_trace!(self.logger, "Failed broadcast package bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast package due to timeout {:?}: {}", + txids, + e + ); + log_trace!(self.logger, "Failed broadcast package bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + } + }, } } } @@ -825,6 +868,38 @@ impl BitcoindClient { rpc_client.call_method::("sendrawtransaction", &[tx_json]).await } + /// Submits the provided package + pub(crate) async fn submit_package( + &self, package: &[Transaction], + ) -> Result { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::submit_package_inner(Arc::clone(rpc_client), package) + .await + .map_err(BitcoindClientError::Rpc) + }, + BitcoindClient::Rest { rpc_client, .. } => { + // Bitcoin Core's REST interface does not support submitting packages + // so we use the RPC client. + Self::submit_package_inner(Arc::clone(rpc_client), package) + .await + .map_err(BitcoindClientError::Rpc) + }, + } + } + + async fn submit_package_inner( + rpc_client: Arc, package: &[Transaction], + ) -> Result { + let package_serialized: Vec<_> = + package.iter().map(|tx| bitcoin::consensus::encode::serialize_hex(tx)).collect(); + let package_json = serde_json::json!(package_serialized); + rpc_client + .call_method::("submitpackage", &[package_json]) + .await + .map(|response| response.0) + } + /// Retrieve the fee estimate needed for a transaction to begin /// confirmation within the provided `num_blocks`. pub(crate) async fn get_fee_estimate_for_target( @@ -1376,6 +1451,23 @@ impl TryInto for JsonResponse { } } +pub struct SubmitPackageResponse(String); + +impl TryInto for JsonResponse { + type Error = String; + fn try_into(self) -> Result { + let response = self.0.to_string(); + let res = self.0.as_object().ok_or("Failed to parse submitpackage response".to_string())?; + + match res["package_msg"].as_str() { + Some("success") => Ok(SubmitPackageResponse(response)), + Some(_) | None => { + return Err(response); + }, + } + } +} + #[derive(Debug, Clone)] pub(crate) struct MempoolEntry { /// The transaction id diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index c544a44fd..006b0dded 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -33,6 +33,7 @@ use crate::fee_estimator::{ use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, LdkLogger, Logger}; use crate::runtime::Runtime; +use crate::tx_broadcaster::TransactionsToBroadcast; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::PersistedNodeMetrics; @@ -319,7 +320,7 @@ impl ElectrumChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, package: Vec) { + pub(crate) async fn process_broadcast_package(&self, txs: TransactionsToBroadcast) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() { @@ -329,8 +330,11 @@ impl ElectrumChainSource { return; }; - for tx in package { - electrum_client.broadcast(tx).await; + match txs { + TransactionsToBroadcast::Singleton(tx) => electrum_client.broadcast(tx).await, + TransactionsToBroadcast::Package(package) => { + electrum_client.submit_package(package).await + }, } } } @@ -588,9 +592,17 @@ impl ElectrumRuntimeClient { match timeout_fut.await { Ok(res) => match res { - Ok(_) => { + Ok(Ok(txid)) => { log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, + Ok(Err(e)) => { + log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx_bytes) + ); + }, Err(e) => { log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); log_trace!( @@ -616,6 +628,65 @@ impl ElectrumRuntimeClient { } } + async fn submit_package(&self, package: Vec) { + let electrum_client = Arc::clone(&self.electrum_client); + + let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect(); + let cloned_package = package.clone(); + + let spawn_fut = self + .runtime + .spawn_blocking(move || electrum_client.transaction_broadcast_package(&cloned_package)); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), + spawn_fut, + ); + + match timeout_fut.await { + Ok(res) => match res { + Ok(Ok(result)) => { + if result.success { + log_trace!(self.logger, "Successfully broadcast package {:?}", txids); + log_trace!(self.logger, "Successfully broadcast package {:?}", result); + } else { + log_error!(self.logger, "Failed to broadcast package {:?}", txids); + log_trace!(self.logger, "Failed to broadcast package {:?}", result); + log_trace!(self.logger, "Failed broadcast package bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + } + }, + Ok(Err(e)) => { + log_error!(self.logger, "Failed to broadcast package {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast package bytes:",); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + Err(e) => { + log_error!(self.logger, "Failed to broadcast package {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast package bytes:",); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast package due to timeout {:?}: {}", + txids, + e + ); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + } + } + async fn get_fee_rate_cache_update( &self, ) -> Result, Error> { diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 00bf29ce6..51831685a 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -10,7 +10,7 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use bdk_esplora::EsploraAsyncExt; -use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; +use bitcoin::{FeeRate, Network, Script, Txid}; use esplora_client::AsyncClient as EsploraAsyncClient; use lightning::chain::{Confirm, Filter, WatchedOutput}; use lightning::util::ser::Writeable; @@ -24,6 +24,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, LdkLogger, Logger}; +use crate::tx_broadcaster::TransactionsToBroadcast; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -365,74 +366,170 @@ impl EsploraChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, package: Vec) { - for tx in &package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), - self.esplora_client.broadcast(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); - }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. + pub(crate) async fn process_broadcast_package(&self, package: TransactionsToBroadcast) { + match package { + TransactionsToBroadcast::Singleton(tx) => { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), + self.esplora_client.broadcast(&tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. + log_trace!( + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + message + ); + log_trace!( + self.logger, + "Failed to broadcast transaction {}", + txid, + ); + } else { + log_error!( + self.logger, + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message + ); + log_error!( + self.logger, + "Failed to broadcast transaction {}", + txid, + ); + } log_trace!( self.logger, - "Failed to broadcast due to HTTP connection error: {}", - message + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) ); - } else { + }, + _ => { log_error!( self.logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, - message + "Failed to broadcast transaction {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + } + }, + TransactionsToBroadcast::Package(package) => { + let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), + self.esplora_client.submit_package(&package, None, None), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(result) => { + if result.package_msg.eq_ignore_ascii_case("success") { + log_trace!( + self.logger, + "Successfully broadcast package {:?}", + txids + ); + log_trace!( + self.logger, + "Successfully broadcast package {:?}", + result ); + } else { + log_error!(self.logger, "Failed to broadcast package {:?}", txids); + log_trace!(self.logger, "Failed to broadcast package {:?}", result); + log_trace!(self.logger, "Failed broadcast package bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } } - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); }, - _ => { - log_error!( - self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); + Err(e) => match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. + log_trace!( + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + message + ); + } else { + log_error!( + self.logger, + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message + ); + } + log_error!(self.logger, "Failed to broadcast package {:?}", txids); + log_trace!(self.logger, "Failed broadcast package bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + _ => { + log_error!( + self.logger, + "Failed to broadcast package {:?}: {}", + txids, + e + ); + log_trace!(self.logger, "Failed broadcast package bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, }, }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast package due to timeout {:?}: {}", + txids, + e + ); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in package { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + } + }, } } } diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index cf3e59b19..2876981f7 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -15,12 +15,30 @@ use crate::logger::{log_error, LdkLogger}; const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; +pub(crate) enum TransactionsToBroadcast { + Singleton(Transaction), + Package(Vec), +} + +impl TransactionsToBroadcast { + fn try_new(mut txs: Vec) -> Option { + match txs.len() { + 1 => Some(TransactionsToBroadcast::Singleton(txs.pop().expect("length of txs is 1"))), + 2.. => { + sort_parents_child_package_topologically(&mut txs); + Some(TransactionsToBroadcast::Package(txs)) + }, + 0 => None, + } + } +} + pub(crate) struct TransactionBroadcaster where L::Target: LdkLogger, { - queue_sender: mpsc::Sender>, - queue_receiver: Mutex>>, + queue_sender: mpsc::Sender, + queue_receiver: Mutex>, logger: L, } @@ -35,7 +53,7 @@ where pub(crate) async fn get_broadcast_queue( &self, - ) -> MutexGuard<'_, mpsc::Receiver>> { + ) -> MutexGuard<'_, mpsc::Receiver> { self.queue_receiver.lock().await } } @@ -45,11 +63,12 @@ where L::Target: LdkLogger, { fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) { - let mut package = txs.iter().map(|(t, _)| (*t).clone()).collect::>(); - sort_parents_child_package_topologically(&mut package); - self.queue_sender.try_send(package).unwrap_or_else(|e| { - log_error!(self.logger, "Failed to broadcast transactions: {}", e); - }); + let package = txs.iter().map(|(t, _)| (*t).clone()).collect::>(); + if let Some(transactions_to_broadcast) = TransactionsToBroadcast::try_new(package) { + self.queue_sender.try_send(transactions_to_broadcast).unwrap_or_else(|e| { + log_error!(self.logger, "Failed to broadcast transactions: {}", e); + }); + } } } From 91872b2353b833b661e66991ab0d765cf16a9ab6 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 24 Jun 2026 22:42:50 +0000 Subject: [PATCH 12/16] f: rework TransactionBroadcast type --- src/chain/bitcoind.rs | 20 +++++++++++--------- src/chain/electrum.rs | 13 ++++++------- src/chain/esplora.rs | 24 ++++++++++++----------- src/tx_broadcaster.rs | 44 ++++++++++++++++++++++--------------------- 4 files changed, 53 insertions(+), 48 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 6bb038ffd..6cb236f79 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -41,7 +41,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; -use crate::tx_broadcaster::TransactionsToBroadcast; +use crate::tx_broadcaster::TransactionBroadcast; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -596,9 +596,11 @@ impl BitcoindChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, txs: TransactionsToBroadcast) { - match txs { - TransactionsToBroadcast::Singleton(tx) => { + pub(crate) async fn process_broadcast_package(&self, txs: TransactionBroadcast) { + match txs.len() { + 0 => (), + 1 => { + let tx = txs.into_inner().pop().expect("The length is 1"); let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), @@ -639,11 +641,11 @@ impl BitcoindChainSource { }, } }, - TransactionsToBroadcast::Package(package) => { - let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect(); + 2.. => { + let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect(); let timeout_fut = tokio::time::timeout( Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), - self.api_client.submit_package(&package), + self.api_client.submit_package(&txs), ); match timeout_fut.await { Ok(res) => match res { @@ -659,7 +661,7 @@ impl BitcoindChainSource { e ); log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in package { + for tx in txs.iter() { log_trace!(self.logger, "{}", log_bytes!(tx.encode())); } }, @@ -672,7 +674,7 @@ impl BitcoindChainSource { e ); log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in package { + for tx in txs.iter() { log_trace!(self.logger, "{}", log_bytes!(tx.encode())); } }, diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 006b0dded..178fd4e7d 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -33,7 +33,7 @@ use crate::fee_estimator::{ use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, LdkLogger, Logger}; use crate::runtime::Runtime; -use crate::tx_broadcaster::TransactionsToBroadcast; +use crate::tx_broadcaster::TransactionBroadcast; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::PersistedNodeMetrics; @@ -320,7 +320,7 @@ impl ElectrumChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, txs: TransactionsToBroadcast) { + pub(crate) async fn process_broadcast_package(&self, txs: TransactionBroadcast) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() { @@ -330,11 +330,10 @@ impl ElectrumChainSource { return; }; - match txs { - TransactionsToBroadcast::Singleton(tx) => electrum_client.broadcast(tx).await, - TransactionsToBroadcast::Package(package) => { - electrum_client.submit_package(package).await - }, + match txs.len() { + 0 => (), + 1 => electrum_client.broadcast(txs.into_inner().pop().expect("The length is 1")).await, + 2.. => electrum_client.submit_package(txs.into_inner()).await, } } } diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 51831685a..ccc43caaa 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -24,7 +24,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::update_and_persist_node_metrics; use crate::logger::{log_bytes, log_debug, log_error, log_trace, LdkLogger, Logger}; -use crate::tx_broadcaster::TransactionsToBroadcast; +use crate::tx_broadcaster::TransactionBroadcast; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, PersistedNodeMetrics}; @@ -366,9 +366,11 @@ impl EsploraChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, package: TransactionsToBroadcast) { - match package { - TransactionsToBroadcast::Singleton(tx) => { + pub(crate) async fn process_broadcast_package(&self, txs: TransactionBroadcast) { + match txs.len() { + 0 => (), + 1 => { + let tx = txs.into_inner().pop().expect("The length is 1"); let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), @@ -446,11 +448,11 @@ impl EsploraChainSource { }, } }, - TransactionsToBroadcast::Package(package) => { - let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect(); + 2.. => { + let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect(); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), - self.esplora_client.submit_package(&package, None, None), + self.esplora_client.submit_package(&txs, None, None), ); match timeout_fut.await { Ok(res) => match res { @@ -470,7 +472,7 @@ impl EsploraChainSource { log_error!(self.logger, "Failed to broadcast package {:?}", txids); log_trace!(self.logger, "Failed to broadcast package {:?}", result); log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in package { + for tx in txs.iter() { log_trace!(self.logger, "{}", log_bytes!(tx.encode())); } } @@ -498,7 +500,7 @@ impl EsploraChainSource { } log_error!(self.logger, "Failed to broadcast package {:?}", txids); log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in package { + for tx in txs.iter() { log_trace!(self.logger, "{}", log_bytes!(tx.encode())); } }, @@ -510,7 +512,7 @@ impl EsploraChainSource { e ); log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in package { + for tx in txs.iter() { log_trace!(self.logger, "{}", log_bytes!(tx.encode())); } }, @@ -524,7 +526,7 @@ impl EsploraChainSource { e ); log_trace!(self.logger, "Failed broadcast transaction bytes:"); - for tx in package { + for tx in txs.iter() { log_trace!(self.logger, "{}", log_bytes!(tx.encode())); } }, diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 2876981f7..207c8b64f 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -15,21 +15,25 @@ use crate::logger::{log_error, LdkLogger}; const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; -pub(crate) enum TransactionsToBroadcast { - Singleton(Transaction), - Package(Vec), +pub(crate) struct TransactionBroadcast(Vec); + +impl TransactionBroadcast { + pub(crate) fn into_inner(self) -> Vec { + self.0 + } } -impl TransactionsToBroadcast { - fn try_new(mut txs: Vec) -> Option { - match txs.len() { - 1 => Some(TransactionsToBroadcast::Singleton(txs.pop().expect("length of txs is 1"))), - 2.. => { - sort_parents_child_package_topologically(&mut txs); - Some(TransactionsToBroadcast::Package(txs)) - }, - 0 => None, - } +impl Deref for TransactionBroadcast { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for TransactionBroadcast { + fn from(mut value: Vec) -> Self { + sort_parents_child_package_topologically(&mut value); + TransactionBroadcast(value) } } @@ -37,8 +41,8 @@ pub(crate) struct TransactionBroadcaster where L::Target: LdkLogger, { - queue_sender: mpsc::Sender, - queue_receiver: Mutex>, + queue_sender: mpsc::Sender, + queue_receiver: Mutex>, logger: L, } @@ -53,7 +57,7 @@ where pub(crate) async fn get_broadcast_queue( &self, - ) -> MutexGuard<'_, mpsc::Receiver> { + ) -> MutexGuard<'_, mpsc::Receiver> { self.queue_receiver.lock().await } } @@ -64,11 +68,9 @@ where { fn broadcast_transactions(&self, txs: &[(&Transaction, TransactionType)]) { let package = txs.iter().map(|(t, _)| (*t).clone()).collect::>(); - if let Some(transactions_to_broadcast) = TransactionsToBroadcast::try_new(package) { - self.queue_sender.try_send(transactions_to_broadcast).unwrap_or_else(|e| { - log_error!(self.logger, "Failed to broadcast transactions: {}", e); - }); - } + self.queue_sender.try_send(package.into()).unwrap_or_else(|e| { + log_error!(self.logger, "Failed to broadcast transactions: {}", e); + }); } } From 11b8b423339ecdd5cc53983c7c5c7566c0d5616d Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 25 Jun 2026 00:18:43 +0000 Subject: [PATCH 13/16] f: DRY the broadcast logic and errors across all chain sources --- src/chain/bitcoind.rs | 64 +++------------ src/chain/electrum.rs | 105 ++++++++---------------- src/chain/esplora.rs | 184 +++++++++++++----------------------------- src/chain/mod.rs | 6 +- 4 files changed, 108 insertions(+), 251 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 6cb236f79..724f9c651 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -596,7 +596,15 @@ impl BitcoindChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, txs: TransactionBroadcast) { + fn log_broadcast_error(&self, e: impl core::fmt::Display, txids: &[Txid], txs: &[Transaction]) { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + } + + pub(crate) async fn process_transaction_broadcast(&self, txs: TransactionBroadcast) { match txs.len() { 0 => (), 1 => { @@ -612,33 +620,9 @@ impl BitcoindChainSource { debug_assert_eq!(id, txid); log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); + Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), }, + Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), } }, 2.. => { @@ -653,31 +637,9 @@ impl BitcoindChainSource { log_trace!(self.logger, "Successfully broadcast package {:?}", txids); log_trace!(self.logger, "Successfully broadcast package {}", result); }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast package {:?}: {}", - txids, - e - ); - log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in txs.iter() { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast package due to timeout {:?}: {}", - txids, - e - ); - log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in txs.iter() { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } + Err(e) => self.log_broadcast_error(e, &txids, &txs), }, + Err(e) => self.log_broadcast_error(e, &txids, &txs), } }, } diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 178fd4e7d..171022210 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -320,7 +320,7 @@ impl ElectrumChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, txs: TransactionBroadcast) { + pub(crate) async fn process_transaction_broadcast(&self, txs: TransactionBroadcast) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() { @@ -576,14 +576,23 @@ impl ElectrumRuntimeClient { }) } + fn log_broadcast_error(&self, e: impl core::fmt::Display, txids: &[Txid], txs: &[Transaction]) { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in txs { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + } + async fn broadcast(&self, tx: Transaction) { let electrum_client = Arc::clone(&self.electrum_client); let txid = tx.compute_txid(); - let tx_bytes = tx.encode(); - let spawn_fut = - self.runtime.spawn_blocking(move || electrum_client.transaction_broadcast(&tx)); + let spawn_fut = self.runtime.spawn_blocking({ + let tx = tx.clone(); + move || electrum_client.transaction_broadcast(&tx) + }); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), spawn_fut, @@ -594,36 +603,10 @@ impl ElectrumRuntimeClient { Ok(Ok(txid)) => { log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, - Ok(Err(e)) => { - log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx_bytes) - ); - }, - Err(e) => { - log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx_bytes) - ); - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx_bytes) - ); + Ok(Err(e)) => self.log_broadcast_error(e, &[txid], &[tx]), + Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), }, + Err(e) => self.log_broadcast_error(e, &[txid], &[tx]), } } @@ -631,11 +614,11 @@ impl ElectrumRuntimeClient { let electrum_client = Arc::clone(&self.electrum_client); let txids: Vec<_> = package.iter().map(|tx| tx.compute_txid()).collect(); - let cloned_package = package.clone(); - let spawn_fut = self - .runtime - .spawn_blocking(move || electrum_client.transaction_broadcast_package(&cloned_package)); + let spawn_fut = self.runtime.spawn_blocking({ + let package = package.clone(); + move || electrum_client.transaction_broadcast_package(&package) + }); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), spawn_fut, @@ -645,44 +628,24 @@ impl ElectrumRuntimeClient { Ok(res) => match res { Ok(Ok(result)) => { if result.success { - log_trace!(self.logger, "Successfully broadcast package {:?}", txids); - log_trace!(self.logger, "Successfully broadcast package {:?}", result); + log_trace!( + self.logger, + "Successfully broadcast transaction(s) {:?}", + txids + ); + log_trace!( + self.logger, + "Successfully broadcast transaction(s) {:?}", + result + ); } else { - log_error!(self.logger, "Failed to broadcast package {:?}", txids); - log_trace!(self.logger, "Failed to broadcast package {:?}", result); - log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in package { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } - } - }, - Ok(Err(e)) => { - log_error!(self.logger, "Failed to broadcast package {:?}: {}", txids, e); - log_trace!(self.logger, "Failed broadcast package bytes:",); - for tx in package { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + self.log_broadcast_error(format!("{:?}", result), &txids, &package); } }, - Err(e) => { - log_error!(self.logger, "Failed to broadcast package {:?}: {}", txids, e); - log_trace!(self.logger, "Failed broadcast package bytes:",); - for tx in package { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast package due to timeout {:?}: {}", - txids, - e - ); - log_trace!(self.logger, "Failed broadcast transaction bytes:"); - for tx in package { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } + Ok(Err(e)) => self.log_broadcast_error(e, &txids, &package), + Err(e) => self.log_broadcast_error(e, &txids, &package), }, + Err(e) => self.log_broadcast_error(e, &txids, &package), } } diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index ccc43caaa..8e51ebdd5 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -366,11 +366,60 @@ impl EsploraChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, txs: TransactionBroadcast) { + fn log_http_error(&self, e: esplora_client::Error, txids: &[Txid], txs: &TransactionBroadcast) { + match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 && txs.len() == 1 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. + log_trace!( + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + message + ); + log_trace!(self.logger, "Failed to broadcast transaction(s) {:?}", txids); + } else { + log_error!( + self.logger, + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message + ); + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}", txids); + } + log_trace!(self.logger, "Failed broadcast transaction(s) bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + _ => { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction(s) bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + }, + } + } + + fn log_broadcast_error( + &self, e: impl core::fmt::Display, txids: &[Txid], txs: &TransactionBroadcast, + ) { + log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e); + log_trace!(self.logger, "Failed broadcast transaction bytes:"); + for tx in txs.iter() { + log_trace!(self.logger, "{}", log_bytes!(tx.encode())); + } + } + + pub(crate) async fn process_transaction_broadcast(&self, txs: TransactionBroadcast) { match txs.len() { 0 => (), 1 => { - let tx = txs.into_inner().pop().expect("The length is 1"); + let tx = txs.first().expect("The length is 1"); let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), @@ -381,71 +430,9 @@ impl EsploraChainSource { Ok(()) => { log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. - log_trace!( - self.logger, - "Failed to broadcast due to HTTP connection error: {}", - message - ); - log_trace!( - self.logger, - "Failed to broadcast transaction {}", - txid, - ); - } else { - log_error!( - self.logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, - message - ); - log_error!( - self.logger, - "Failed to broadcast transaction {}", - txid, - ); - } - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - _ => { - log_error!( - self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); + Err(e) => self.log_http_error(e, &[txid], &txs), }, + Err(e) => self.log_broadcast_error(e, &[txid], &txs), } }, 2.. => { @@ -460,76 +447,21 @@ impl EsploraChainSource { if result.package_msg.eq_ignore_ascii_case("success") { log_trace!( self.logger, - "Successfully broadcast package {:?}", + "Successfully broadcast transactions {:?}", txids ); log_trace!( self.logger, - "Successfully broadcast package {:?}", + "Successfully broadcast transactions {:?}", result ); } else { - log_error!(self.logger, "Failed to broadcast package {:?}", txids); - log_trace!(self.logger, "Failed to broadcast package {:?}", result); - log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in txs.iter() { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } + self.log_broadcast_error(format!("{:?}", result), &txids, &txs); } }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. - log_trace!( - self.logger, - "Failed to broadcast due to HTTP connection error: {}", - message - ); - } else { - log_error!( - self.logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, - message - ); - } - log_error!(self.logger, "Failed to broadcast package {:?}", txids); - log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in txs.iter() { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } - }, - _ => { - log_error!( - self.logger, - "Failed to broadcast package {:?}: {}", - txids, - e - ); - log_trace!(self.logger, "Failed broadcast package bytes:"); - for tx in txs.iter() { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } - }, - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast package due to timeout {:?}: {}", - txids, - e - ); - log_trace!(self.logger, "Failed broadcast transaction bytes:"); - for tx in txs.iter() { - log_trace!(self.logger, "{}", log_bytes!(tx.encode())); - } + Err(e) => self.log_http_error(e, &txids, &txs), }, + Err(e) => self.log_broadcast_error(e, &txids, &txs), } }, } diff --git a/src/chain/mod.rs b/src/chain/mod.rs index db9860608..0fab226af 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -506,13 +506,13 @@ impl ChainSource { Some(next_package) = receiver.recv() => { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { - esplora_chain_source.process_broadcast_package(next_package).await + esplora_chain_source.process_transaction_broadcast(next_package).await }, ChainSourceKind::Electrum(electrum_chain_source) => { - electrum_chain_source.process_broadcast_package(next_package).await + electrum_chain_source.process_transaction_broadcast(next_package).await }, ChainSourceKind::Bitcoind(bitcoind_chain_source) => { - bitcoind_chain_source.process_broadcast_package(next_package).await + bitcoind_chain_source.process_transaction_broadcast(next_package).await }, } } From 3bc0b468c29925f0d2318237734b4ec270ecb32b Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Wed, 29 Oct 2025 07:00:04 +0000 Subject: [PATCH 14/16] Include 0FC channels in anchor channel checks --- src/event.rs | 3 ++- src/lib.rs | 10 ++++++---- src/liquidity/service/lsps2.rs | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/event.rs b/src/event.rs index 80acd0690..393a6d8b1 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1256,7 +1256,8 @@ where } } - let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); + let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx() + || channel_type.requires_anchor_zero_fee_commitments(); if anchor_channel && self.config.anchor_channels_config.is_none() { log_error!( self.logger, diff --git a/src/lib.rs b/src/lib.rs index 5eb95fd15..256759b23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1353,7 +1353,8 @@ impl Node { .peer_by_node_id(peer_node_id) .ok_or(Error::ConnectionFailed)? .init_features; - let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx(); + let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx() + || init_features.requires_anchor_zero_fee_commitments(); Ok(new_channel_anchor_reserve_sats(&self.config, peer_node_id, anchor_channel)) } @@ -2316,9 +2317,10 @@ pub(crate) fn total_anchor_channels_reserve_sats( !anchor_channels_config.trusted_peers_no_reserve.contains(&c.counterparty.node_id) && c.channel_shutdown_state .map_or(true, |s| s != ChannelShutdownState::ShutdownComplete) - && c.channel_type - .as_ref() - .map_or(false, |t| t.requires_anchors_zero_fee_htlc_tx()) + && c.channel_type.as_ref().map_or(false, |t| { + t.requires_anchors_zero_fee_htlc_tx() + || t.requires_anchor_zero_fee_commitments() + }) }) .count() as u64 * anchor_channels_config.per_channel_reserve_sats diff --git a/src/liquidity/service/lsps2.rs b/src/liquidity/service/lsps2.rs index 875438b0f..193482fc3 100644 --- a/src/liquidity/service/lsps2.rs +++ b/src/liquidity/service/lsps2.rs @@ -452,9 +452,11 @@ where total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); let spendable_amount_sats = self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let anchor_channel = init_features.requires_anchors_zero_fee_htlc_tx() + || init_features.requires_anchor_zero_fee_commitments(); let required_funds_sats = channel_amount_sats + self.config.anchor_channels_config.as_ref().map_or(0, |c| { - if init_features.requires_anchors_zero_fee_htlc_tx() + if anchor_channel && !c.trusted_peers_no_reserve.contains(&their_network_key) { c.per_channel_reserve_sats From ebfb6cfe5fb611b2099d2bd3c30ae1ff8776e722 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Thu, 25 Jun 2026 00:55:00 +0000 Subject: [PATCH 15/16] f: Include 0fc channels in ReserveType checks --- src/types.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index 914b5dc15..ba1885e75 100644 --- a/src/types.rs +++ b/src/types.rs @@ -599,7 +599,9 @@ impl ChannelDetails { value: LdkChannelDetails, anchor_channels_config: Option<&AnchorChannelsConfig>, ) -> Self { let reserve_type = value.channel_type.as_ref().map(|channel_type| { - if channel_type.supports_anchors_zero_fee_htlc_tx() { + if channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_anchor_zero_fee_commitments() + { if let Some(config) = anchor_channels_config { if config.trusted_peers_no_reserve.contains(&value.counterparty.node_id) { ReserveType::TrustedPeersNoReserve From fcf54fd360958b58dc1fe157081bd2b523438652 Mon Sep 17 00:00:00 2001 From: Leo Nash Date: Mon, 13 Oct 2025 13:11:21 +0000 Subject: [PATCH 16/16] Negotiate 0FC channels if the anchor config is set --- src/config.rs | 16 ++++++++++------ tests/common/mod.rs | 7 +++---- tests/integration_tests_rust.rs | 14 +++----------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/config.rs b/src/config.rs index 0402ebb46..0b4323208 100644 --- a/src/config.rs +++ b/src/config.rs @@ -171,15 +171,17 @@ pub struct Config { /// used to send pre-flight probes. pub probing_liquidity_limit_multiplier: u64, /// Configuration options pertaining to Anchor channels, i.e., channels for which the - /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. + /// `option_zero_fee_commitments` or `option_anchors_zero_fee_htlc_tx` channel type is + /// negotiated. /// /// Please refer to [`AnchorChannelsConfig`] for further information on Anchor channels. /// /// If set to `Some`, we'll try to open new channels with Anchors enabled, i.e., new channels - /// will be negotiated with the `option_anchors_zero_fee_htlc_tx` channel type if supported by - /// the counterparty. Note that this won't prevent us from opening non-Anchor channels if the - /// counterparty doesn't support `option_anchors_zero_fee_htlc_tx`. If set to `None`, new - /// channels will be negotiated with the legacy `option_static_remotekey` channel type only. + /// will be negotiated with the `option_zero_fee_commitments` channel type first, then the + /// `option_anchors_zero_fee_htlc_tx` channel type if supported by the counterparty. Note + /// that this won't prevent us from opening non-Anchor channels if the counterparty doesn't + /// support `option_anchors_zero_fee_htlc_tx`. If set to `None`, new channels will be + /// negotiated with the legacy `option_static_remotekey` channel type only. /// /// **Note:** If set to `None` *after* some Anchor channels have already been /// opened, no dedicated emergency on-chain reserve will be maintained for these channels, @@ -282,7 +284,7 @@ impl Default for HumanReadableNamesConfig { } /// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the -/// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. +/// `option_zero_fee_commitments` or `option_anchors_zero_fee_htlc_tx` channel type is negotiated. /// /// Prior to the introduction of Anchor channels, the on-chain fees paying for the transactions /// issued on channel closure were pre-determined and locked-in at the time of the channel @@ -404,6 +406,8 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config.channel_handshake_limits.force_announced_channel_preference = false; user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = config.anchor_channels_config.is_some(); + user_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = + config.anchor_channels_config.is_some(); user_config.reject_inbound_splices = false; if may_announce_channel(config).is_err() { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index adeb327bf..e8865a4fb 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1413,10 +1413,9 @@ pub(crate) async fn do_channel_full_cycle( let node_a_outbound_capacity_msat = node_a.list_channels()[0].outbound_capacity_msat; let node_a_reserve_msat = node_a.list_channels()[0].unspendable_punishment_reserve.unwrap() * 1000; - // TODO: Zero-fee commitment channels are anchor channels, but do not allocate any - // funds to the anchor, so this will need to be updated when we ship these channels - // in ldk-node. - let node_a_anchors_msat = if expect_anchor_channel { 2 * 330 * 1000 } else { 0 }; + // If we expect an anchor channel, this will be a 0FC channel, so no funds will be + // allocated to the anchor. + let node_a_anchors_msat = 0; let funding_amount_msat = node_a.list_channels()[0].channel_value_sats * 1000; // Node B does not have any reserve, so we only subtract a few items on node A's // side to arrive at node B's capacity diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fab73ed0c..abe115a0f 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1118,17 +1118,12 @@ async fn splice_channel() { let user_channel_id_b = expect_channel_ready_event!(node_b, node_a.node_id()); let opening_transaction_fee_sat = 156; - let closing_transaction_fee_sat = 614; - let anchor_output_sat = 330; assert_eq!( node_a.list_balances().total_onchain_balance_sats, premine_amount_sat - 4_000_000 - opening_transaction_fee_sat ); - assert_eq!( - node_a.list_balances().total_lightning_balance_sats, - 4_000_000 - closing_transaction_fee_sat - anchor_output_sat - ); + assert_eq!(node_a.list_balances().total_lightning_balance_sats, 4_000_000); assert_eq!(node_b.list_balances().total_lightning_balance_sats, 0); let address = node_a.onchain_payment().new_address().unwrap(); @@ -1208,10 +1203,7 @@ async fn splice_channel() { // Mine a block to give time for the HTLC to resolve generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1).await; - assert_eq!( - node_a.list_balances().total_lightning_balance_sats, - 4_000_000 - closing_transaction_fee_sat - anchor_output_sat + amount_msat / 1000 - ); + assert_eq!(node_a.list_balances().total_lightning_balance_sats, 4_000_000 + amount_msat / 1000); assert_eq!( node_b.list_balances().total_lightning_balance_sats, expected_splice_in_lightning_balance_sat - amount_msat / 1000 @@ -1245,7 +1237,7 @@ async fn splice_channel() { ); assert_eq!( node_a.list_balances().total_lightning_balance_sats, - 4_000_000 - closing_transaction_fee_sat - anchor_output_sat - expected_splice_out_fee_sat + 4_000_000 - expected_splice_out_fee_sat ); }