diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index cd3980b9af..1cd39ff698 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 f7ded7bc56..466886eb40 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 410136928a..3764d454b1 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 16064fa45c..af7edf366c 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 c67e9194e1..a788644cd5 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 35666df038..5d81c1a44e 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/CHANGELOG.md b/CHANGELOG.md index 93c7cf59b2..1adc2f1937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Compatibility Notes - Pending JIT-channel payments created before upgrading may fail after upgrade because the prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated. +- 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/scripts/build_electrs.sh b/scripts/build_electrs.sh new file mode 100755 index 0000000000..1300e87fe0 --- /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 f94e280e3b..102cf826f3 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 diff --git a/src/builder.rs b/src/builder.rs index 3df594b7cf..44397c9650 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -190,6 +190,8 @@ pub enum BuildError { LoggerSetupFailed, /// We failed to setup the configured chain source. ChainSourceSetupFailed, + /// The configured chain source is not supported. + ChainSourceNotSupported, /// The given network does not match the node's previously configured network. NetworkMismatch, /// The role of the node in an asynchronous payments context is not compatible with the current configuration. @@ -233,6 +235,9 @@ impl fmt::Display for BuildError { Self::DNSResolverSetupFailed => { write!(f, "An attempt to setup a DNS resolver has failed.") }, + Self::ChainSourceNotSupported => { + write!(f, "The configured chain source is not supported.") + }, } } } @@ -1428,18 +1433,20 @@ fn build_with_store_internal( let (chain_source, chain_tip_opt) = match chain_data_source_config { Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => { let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default()); - ChainSource::new_esplora( - server_url.clone(), - headers.clone(), - sync_config, - Arc::clone(&fee_estimator), - Arc::clone(&tx_broadcaster), - Arc::clone(&kv_store), - Arc::clone(&config), - Arc::clone(&logger), - Arc::clone(&node_metrics), - ) - .map_err(|()| BuildError::ChainSourceSetupFailed)? + runtime.block_on(async { + ChainSource::new_esplora( + server_url.clone(), + headers.clone(), + sync_config, + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + ) + .await + })? }, Some(ChainDataSourceConfig::Electrum { server_url, sync_config }) => { let sync_config = sync_config.unwrap_or(ElectrumSyncConfig::default()); @@ -1476,7 +1483,7 @@ fn build_with_store_internal( Arc::clone(&node_metrics), ) .await - }), + })?, None => runtime.block_on(async { ChainSource::new_bitcoind_rpc( rpc_host.clone(), @@ -1491,25 +1498,27 @@ fn build_with_store_internal( Arc::clone(&node_metrics), ) .await - }), + })?, }, None => { // Default to Esplora client. let server_url = DEFAULT_ESPLORA_SERVER_URL.to_string(); let sync_config = EsploraSyncConfig::default(); - ChainSource::new_esplora( - server_url.clone(), - HashMap::new(), - sync_config, - Arc::clone(&fee_estimator), - Arc::clone(&tx_broadcaster), - Arc::clone(&kv_store), - Arc::clone(&config), - Arc::clone(&logger), - Arc::clone(&node_metrics), - ) - .map_err(|()| BuildError::ChainSourceSetupFailed)? + runtime.block_on(async { + ChainSource::new_esplora( + server_url.clone(), + HashMap::new(), + sync_config, + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + ) + .await + })? }, }; let chain_source = Arc::new(chain_source); diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 6bfa8ffd27..b6e1586913 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -42,7 +42,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::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; -use crate::{Error, PersistedNodeMetrics}; +use crate::{BuildError, Error, PersistedNodeMetrics}; const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; const CHAIN_POLLING_TIMEOUT_SECS: u64 = 10; @@ -59,11 +59,11 @@ pub(super) struct BitcoindChainSource { } impl BitcoindChainSource { - pub(crate) fn new_rpc( + pub(crate) async fn new_rpc( rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, fee_estimator: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc, - ) -> Self { + ) -> Result { let api_client = Arc::new(BitcoindClient::new_rpc( rpc_host.clone(), rpc_port.clone(), @@ -71,9 +71,33 @@ impl BitcoindChainSource { rpc_password.clone(), )); + let node_version_result = tokio::time::timeout( + Duration::from_secs(CHAIN_POLLING_TIMEOUT_SECS), + api_client.get_node_version(), + ) + .await + .map_err(|e| { + log_error!(logger, "Failed to get node version: {:?}", e); + BuildError::ChainSourceSetupFailed + })?; + + let node_version = node_version_result.map_err(|e| { + log_error!(logger, "Failed to get node version: {:?}", e); + BuildError::ChainSourceSetupFailed + })?; + + if config.anchor_channels_config.is_some() { + // v26 first shipped the `submitpackage` RPC, but we need v29 to relay ephemeral + // dust + if node_version < 290000 { + log_error!(logger, "Bitcoin backend MUST be greater than or equal to v29"); + return Err(BuildError::ChainSourceNotSupported); + } + } + let latest_chain_tip = RwLock::new(None); let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - Self { + Ok(Self { api_client, latest_chain_tip, wallet_polling_status, @@ -82,15 +106,15 @@ impl BitcoindChainSource { config, logger: Arc::clone(&logger), node_metrics, - } + }) } - pub(crate) fn new_rest( + pub(crate) async fn new_rest( rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, fee_estimator: Arc, kv_store: Arc, config: Arc, rest_client_config: BitcoindRestClientConfig, logger: Arc, node_metrics: Arc, - ) -> Self { + ) -> Result { let api_client = Arc::new(BitcoindClient::new_rest( rest_client_config.rest_host, rest_client_config.rest_port, @@ -100,10 +124,34 @@ impl BitcoindChainSource { rpc_password, )); + let node_version_result = tokio::time::timeout( + Duration::from_secs(CHAIN_POLLING_TIMEOUT_SECS), + api_client.get_node_version(), + ) + .await + .map_err(|e| { + log_error!(logger, "Failed to get node version: {:?}", e); + BuildError::ChainSourceSetupFailed + })?; + + let node_version = node_version_result.map_err(|e| { + log_error!(logger, "Failed to get node version: {:?}", e); + BuildError::ChainSourceSetupFailed + })?; + + if config.anchor_channels_config.is_some() { + // v26 first shipped the `submitpackage` RPC, but we need v29 to relay ephemeral + // dust + if node_version < 290000 { + log_error!(logger, "Bitcoin backend MUST be greater than or equal to v29"); + return Err(BuildError::ChainSourceNotSupported); + } + } + let latest_chain_tip = RwLock::new(None); let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - Self { + Ok(Self { api_client, latest_chain_tip, wallet_polling_status, @@ -112,7 +160,7 @@ impl BitcoindChainSource { config, logger: Arc::clone(&logger), node_metrics, - } + }) } pub(super) fn as_utxo_source(&self) -> UtxoSourceClient { @@ -572,11 +620,8 @@ impl BitcoindChainSource { } 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 { + if package.len() == 1 { + let tx = &package[0]; let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS), @@ -611,6 +656,48 @@ impl BitcoindChainSource { ); }, } + } else if package.len() > 1 { + 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) => { + if result.contains(r#""package_msg":"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())); + } + } + }, + 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())); + } + }, + } } } } @@ -748,6 +835,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, @@ -776,6 +888,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(|value| value.to_string()) + } + /// 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( diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 7406f06b4b..c3b36ed1d7 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -294,7 +294,7 @@ impl ElectrumChainSource { Ok(()) } - pub(crate) async fn process_broadcast_package(&self, package: Vec) { + pub(crate) async fn process_broadcast_package(&self, mut package: Vec) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().expect("lock").client().as_ref() { @@ -304,8 +304,10 @@ impl ElectrumChainSource { return; }; - for tx in package { - electrum_client.broadcast(tx).await; + if package.len() == 1 { + electrum_client.broadcast(package.pop().expect("Package length is 1")).await + } else if package.len() > 1 { + electrum_client.submit_package(package).await } } } @@ -424,6 +426,14 @@ impl ElectrumRuntimeClient { Error::ConnectionFailed })?, ); + if config.anchor_channels_config.is_some() { + electrum_client.transaction_broadcast_package(&super::dummy_package()).map_err( + |e| { + log_error!(logger, "Electrum server does not support submit package: {:?}", e); + Error::ChainSourceNotSupported + }, + )?; + } let bdk_electrum_client = Arc::new(BdkElectrumClient::new(Arc::clone(&electrum_client))); let tx_sync = Arc::new( ElectrumSyncClient::from_client(Arc::clone(&electrum_client), Arc::clone(&logger)) @@ -563,9 +573,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!( @@ -591,6 +609,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 eb23a395d3..d67f51bcd2 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -25,7 +25,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::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; -use crate::{Error, PersistedNodeMetrics}; +use crate::{BuildError, Error, PersistedNodeMetrics}; pub(super) struct EsploraChainSource { pub(super) sync_config: EsploraSyncConfig, @@ -41,11 +41,11 @@ pub(super) struct EsploraChainSource { } impl EsploraChainSource { - pub(crate) fn new( + pub(crate) async fn new( server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, fee_estimator: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc, - ) -> Result { + ) -> Result { let mut client_builder = esplora_client::Builder::new(&server_url); client_builder = client_builder.timeout(sync_config.timeouts_config.per_request_timeout_secs as u64); @@ -56,7 +56,18 @@ impl EsploraChainSource { let esplora_client = client_builder.build_async().map_err(|e| { log_error!(logger, "Failed to build Esplora client: {}", e); + BuildError::ChainSourceSetupFailed })?; + + if config.anchor_channels_config.is_some() { + esplora_client.submit_package(&super::dummy_package(), None, None).await.map_err( + |e| { + log_error!(logger, "Esplora server does not support submit package: {:?}", e); + BuildError::ChainSourceNotSupported + }, + )?; + } + let tx_sync = Arc::new(EsploraSyncClient::from_client(esplora_client.clone(), Arc::clone(&logger))); @@ -356,7 +367,7 @@ impl EsploraChainSource { } pub(crate) async fn process_broadcast_package(&self, package: Vec) { - for tx in &package { + if let [tx] = &package[..] { let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(self.sync_config.timeouts_config.tx_broadcast_timeout_secs), @@ -380,6 +391,7 @@ impl EsploraChainSource { "Failed to broadcast due to HTTP connection error: {}", message ); + log_trace!(self.logger, "Failed to broadcast transaction {}", txid,); } else { log_error!( self.logger, @@ -387,6 +399,7 @@ impl EsploraChainSource { status, message ); + log_error!(self.logger, "Failed to broadcast transaction {}", txid,); } log_trace!( self.logger, @@ -423,6 +436,81 @@ impl EsploraChainSource { ); }, } + } else if package.len() > 1 { + 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())); + } + } + }, + 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 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/chain/mod.rs b/src/chain/mod.rs index 5a326be97b..a6c559cf33 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -27,7 +27,38 @@ use crate::fee_estimator::OnchainFeeEstimator; use crate::logger::{log_debug, log_info, log_trace, LdkLogger, Logger}; use crate::runtime::Runtime; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; -use crate::{Error, PersistedNodeMetrics}; +use crate::{BuildError, 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, @@ -96,12 +127,12 @@ enum ChainSourceKind { } impl ChainSource { - pub(crate) fn new_esplora( + pub(crate) async fn new_esplora( server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc, - ) -> Result<(Self, Option), ()> { + ) -> Result<(Self, Option), BuildError> { let esplora_chain_source = EsploraChainSource::new( server_url, headers, @@ -111,7 +142,8 @@ impl ChainSource { config, Arc::clone(&logger), node_metrics, - )?; + ) + .await?; let kind = ChainSourceKind::Esplora(esplora_chain_source); let registered_txids = Mutex::new(HashSet::new()); Ok((Self { kind, registered_txids, tx_broadcaster, logger }, None)) @@ -142,7 +174,7 @@ impl ChainSource { fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc, - ) -> (Self, Option) { + ) -> Result<(Self, Option), BuildError> { let bitcoind_chain_source = BitcoindChainSource::new_rpc( rpc_host, rpc_port, @@ -153,11 +185,12 @@ impl ChainSource { config, Arc::clone(&logger), node_metrics, - ); + ) + .await?; let best_block = bitcoind_chain_source.poll_best_block().await.ok(); let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); let registered_txids = Mutex::new(HashSet::new()); - (Self { kind, registered_txids, tx_broadcaster, logger }, best_block) + Ok((Self { kind, registered_txids, tx_broadcaster, logger }, best_block)) } pub(crate) async fn new_bitcoind_rest( @@ -165,7 +198,7 @@ impl ChainSource { fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, rest_client_config: BitcoindRestClientConfig, logger: Arc, node_metrics: Arc, - ) -> (Self, Option) { + ) -> Result<(Self, Option), BuildError> { let bitcoind_chain_source = BitcoindChainSource::new_rest( rpc_host, rpc_port, @@ -177,11 +210,12 @@ impl ChainSource { rest_client_config, Arc::clone(&logger), node_metrics, - ); + ) + .await?; let best_block = bitcoind_chain_source.poll_best_block().await.ok(); let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); let registered_txids = Mutex::new(HashSet::new()); - (Self { kind, registered_txids, tx_broadcaster, logger }, best_block) + Ok((Self { kind, registered_txids, tx_broadcaster, logger }, best_block)) } pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { diff --git a/src/config.rs b/src/config.rs index 558a4d0618..0b43232080 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 @@ -170,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, @@ -281,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 @@ -403,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/src/error.rs b/src/error.rs index d07212b008..8546af0dd2 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/event.rs b/src/event.rs index 80acd0690e..393a6d8b1a 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 b450642879..aa42281519 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1328,7 +1328,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)) } @@ -2291,9 +2292,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 875438b0fb..193482fc3f 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 diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 7084135b00..01d2378211 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -45,9 +45,192 @@ 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]) { + // LDK multi-transaction broadcasts are one child plus its direct parents, and the + // child spends every parent. Thus, checking adjacent pairs is enough to find the + // child, while the already-sorted common case exits after only hashing one transaction. + if txs.len() < 2 { + return; + } + let mut child_pos = txs.len() - 1; + let mut pos = txs.len() - 1; + 'outer: while pos > 0 { + let txid_a = txs[pos - 1].compute_txid(); + for txid in txs[pos].input.iter().map(|input| input.previous_output.txid) { + if txid == txid_a { + child_pos = pos; + break 'outer; + } + } + let txid_b = txs[pos].compute_txid(); + for txid in txs[pos - 1].input.iter().map(|input| input.previous_output.txid) { + if txid == txid_b { + child_pos = pos - 1; + break 'outer; + } + } + if pos == 2 { + pos = 1; + } else { + pos = pos.saturating_sub(2); + } + } + debug_assert!(pos != 0); + txs.swap(child_pos, txs.len() - 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 []); + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 76f2aa9ce6..691ef36463 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 } => { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index adeb327bf0..e8865a4fb1 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/common/scenarios/mod.rs b/tests/common/scenarios/mod.rs index 7cbf56b8e1..6c2564b764 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 diff --git a/tests/docker/docker-compose.yml b/tests/docker/docker-compose.yml index e71fd70fba..5459e8eda7 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: [ diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 5b07ab50d0..58e36c7028 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1023,17 +1023,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(); @@ -1113,10 +1108,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 @@ -1150,7 +1142,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 ); }