diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index 6d0056e9aa..1ff1b0b26a 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/rust.yml b/.github/workflows/rust.yml index b2575aca1f..7c685355ce 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -59,13 +59,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: "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: | diff --git a/.github/workflows/vss-integration.yml b/.github/workflows/vss-integration.yml index 9591751628..cf3cdf3f74 100644 --- a/.github/workflows/vss-integration.yml +++ b/.github/workflows/vss-integration.yml @@ -26,6 +26,21 @@ jobs: --health-retries 5 steps: + - 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 ./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 code uses: actions/checkout@v6 with: diff --git a/.github/workflows/vss-no-auth-integration.yml b/.github/workflows/vss-no-auth-integration.yml index 950ff3e5f0..48a410a203 100644 --- a/.github/workflows/vss-no-auth-integration.yml +++ b/.github/workflows/vss-no-auth-integration.yml @@ -26,6 +26,21 @@ jobs: --health-retries 5 steps: + - 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 ./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 code uses: actions/checkout@v6 with: 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 5f616c4ce8..be0fd1b741 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1337,18 +1337,22 @@ 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 + }) + .map_err(|()| BuildError::ChainSourceSetupFailed)? }, Some(ChainDataSourceConfig::Electrum { server_url, sync_config }) => { let sync_config = sync_config.unwrap_or(ElectrumSyncConfig::default()); @@ -1370,55 +1374,63 @@ fn build_with_store_internal( rpc_password, rest_client_config, }) => match rest_client_config { - Some(rest_client_config) => runtime.block_on(async { - ChainSource::new_bitcoind_rest( - rpc_host.clone(), - *rpc_port, - rpc_user.clone(), - rpc_password.clone(), - Arc::clone(&fee_estimator), - Arc::clone(&tx_broadcaster), - Arc::clone(&kv_store), - Arc::clone(&config), - rest_client_config.clone(), - Arc::clone(&logger), - Arc::clone(&node_metrics), - ) - .await - }), - None => runtime.block_on(async { - ChainSource::new_bitcoind_rpc( - rpc_host.clone(), - *rpc_port, - rpc_user.clone(), - rpc_password.clone(), - Arc::clone(&fee_estimator), - Arc::clone(&tx_broadcaster), - Arc::clone(&kv_store), - Arc::clone(&config), - Arc::clone(&logger), - Arc::clone(&node_metrics), - ) - .await - }), + Some(rest_client_config) => runtime + .block_on(async { + ChainSource::new_bitcoind_rest( + rpc_host.clone(), + *rpc_port, + rpc_user.clone(), + rpc_password.clone(), + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + rest_client_config.clone(), + Arc::clone(&logger), + Arc::clone(&node_metrics), + ) + .await + }) + .map_err(|()| BuildError::ChainSourceSetupFailed)?, + None => runtime + .block_on(async { + ChainSource::new_bitcoind_rpc( + rpc_host.clone(), + *rpc_port, + rpc_user.clone(), + rpc_password.clone(), + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + ) + .await + }) + .map_err(|()| BuildError::ChainSourceSetupFailed)?, }, 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 + }) + .map_err(|()| BuildError::ChainSourceSetupFailed)? }, }; let chain_source = Arc::new(chain_source); diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 2582f32f64..ff0f94f074 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -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,22 @@ impl BitcoindChainSource { rpc_password.clone(), )); + let node_version = api_client.get_node_version().await.map_err(|e| { + log_error!(logger, "Failed to get node version: {:?}", e); + })?; + + 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(()); + } + } + 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 +95,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 +113,23 @@ impl BitcoindChainSource { rpc_password, )); + let node_version = api_client.get_node_version().await.map_err(|e| { + log_error!(logger, "Failed to get node version: {:?}", e); + })?; + + 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(()); + } + } + 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 +138,7 @@ impl BitcoindChainSource { config, logger: Arc::clone(&logger), node_metrics, - } + }) } pub(super) fn as_utxo_source(&self) -> UtxoSourceClient { @@ -569,11 +595,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), @@ -608,6 +631,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())); + } + }, + } } } } @@ -745,6 +810,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, @@ -773,6 +863,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 54e7fff0ca..bc425089ad 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -275,7 +275,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() { @@ -285,8 +285,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 } } } @@ -387,6 +389,28 @@ struct ElectrumRuntimeClient { logger: Arc, } +fn dummy_transaction() -> Transaction { + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn { + previous_output: bitcoin::OutPoint { + txid: bitcoin::Txid::from_raw_hash(bitcoin::hashes::Hash::from_byte_array( + [0u8; 32], + )), + vout: 0, + }, + sequence: bitcoin::Sequence::MAX, + script_sig: bitcoin::ScriptBuf::new(), + witness: bitcoin::Witness::new(), + }], + output: vec![bitcoin::TxOut { + script_pubkey: bitcoin::ScriptBuf::new(), + value: bitcoin::Amount::ZERO, + }], + } +} + impl ElectrumRuntimeClient { fn new( server_url: String, sync_config: ElectrumSyncConfig, runtime: Arc, @@ -405,6 +429,12 @@ impl ElectrumRuntimeClient { Error::ConnectionFailed })?, ); + if config.anchor_channels_config.is_some() { + electrum_client.transaction_broadcast_package(&[dummy_transaction()]).map_err(|e| { + log_error!(logger, "Electrum server does not support submit package: {:?}", e); + Error::ConnectionFailed + })?; + } let bdk_electrum_client = Arc::new(BdkElectrumClient::new(Arc::clone(&electrum_client))); let tx_sync = Arc::new( ElectrumSyncClient::new(server_url.clone(), Arc::clone(&logger)).map_err(|e| { @@ -543,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!( @@ -571,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 5825a09849..43d482733b 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -40,8 +40,30 @@ pub(super) struct EsploraChainSource { node_metrics: Arc>, } +fn dummy_transaction() -> Transaction { + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn { + previous_output: bitcoin::OutPoint { + txid: bitcoin::Txid::from_raw_hash(bitcoin::hashes::Hash::from_byte_array( + [0u8; 32], + )), + vout: 0, + }, + sequence: bitcoin::Sequence::MAX, + script_sig: bitcoin::ScriptBuf::new(), + witness: bitcoin::Witness::new(), + }], + output: vec![bitcoin::TxOut { + script_pubkey: bitcoin::ScriptBuf::new(), + value: bitcoin::Amount::ZERO, + }], + } +} + 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>, @@ -57,6 +79,15 @@ impl EsploraChainSource { let esplora_client = client_builder.build_async().map_err(|e| { log_error!(logger, "Failed to build Esplora client: {}", e); })?; + + if config.anchor_channels_config.is_some() { + esplora_client.submit_package(&[dummy_transaction()], None, None).await.map_err( + |e| { + log_error!(logger, "Esplora server does not support submit package: {:?}", e); + }, + )?; + } + let tx_sync = Arc::new(EsploraSyncClient::from_client(esplora_client.clone(), Arc::clone(&logger))); @@ -353,7 +384,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), @@ -377,6 +408,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, @@ -384,6 +416,7 @@ impl EsploraChainSource { status, message ); + log_error!(self.logger, "Failed to broadcast transaction {}", txid,); } log_trace!( self.logger, @@ -420,6 +453,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 == "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 cb8541be6a..add86d2d84 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -96,7 +96,7 @@ 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, @@ -111,7 +111,8 @@ impl ChainSource { config, Arc::clone(&logger), node_metrics, - )?; + ) + .await?; let kind = ChainSourceKind::Esplora(esplora_chain_source); let registered_txids = Mutex::new(Vec::new()); Ok((Self { kind, registered_txids, tx_broadcaster, logger }, None)) @@ -142,7 +143,7 @@ impl ChainSource { fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, - ) -> (Self, Option) { + ) -> Result<(Self, Option), ()> { let bitcoind_chain_source = BitcoindChainSource::new_rpc( rpc_host, rpc_port, @@ -153,11 +154,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(Vec::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 +167,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), ()> { let bitcoind_chain_source = BitcoindChainSource::new_rest( rpc_host, rpc_port, @@ -177,11 +179,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(Vec::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..5a620055ba 100644 --- a/src/config.rs +++ b/src/config.rs @@ -170,15 +170,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 +283,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 +405,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/event.rs b/src/event.rs index d672a90f6d..939435adc8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1231,7 +1231,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 2d1a0d958f..46850a66ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1266,7 +1266,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)) } @@ -2170,9 +2171,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.rs b/src/liquidity.rs index 66e3edd334..b2742a60ae 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -758,9 +758,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..285d837903 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -45,9 +45,187 @@ 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); + } +} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 13b1f384f5..97be9b4b30 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -324,32 +324,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(1) + }) + .sum(); + 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 b701028c54..c61bf64c08 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1407,7 +1407,7 @@ pub(crate) async fn do_channel_full_cycle( // 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 }; + let node_a_anchors_msat = if expect_anchor_channel { 0 } else { 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 a3970b1c24..942ca2ca26 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); // Test that splicing and payments fail when there are insufficient funds @@ -1101,10 +1096,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 @@ -1138,7 +1130,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 ); }