Skip to content

Commit 7d26c23

Browse files
authored
Merge pull request #660 from tankyleo/25-10-0fc-channel-config
Zero-fee commitments support
2 parents fca1614 + a868de8 commit 7d26c23

27 files changed

Lines changed: 1156 additions & 458 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: CI Checks - 0FC Integration Tests
2+
3+
on: [push, pull_request]
4+
5+
concurrency:
6+
group: ${{ github.workflow }}-${{ github.ref }}
7+
cancel-in-progress: true
8+
9+
jobs:
10+
build-and-test:
11+
timeout-minutes: 60
12+
runs-on: self-hosted
13+
steps:
14+
- name: Checkout source code
15+
uses: actions/checkout@v4
16+
- name: Install Rust stable toolchain
17+
run: |
18+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable
19+
- name: Enable caching for bitcoind
20+
id: cache-bitcoind
21+
uses: actions/cache@v4
22+
with:
23+
path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
24+
key: bitcoind-29.0-${{ runner.os }}-${{ runner.arch }}
25+
- name: Enable caching for electrs
26+
id: cache-electrs
27+
uses: actions/cache@v4
28+
with:
29+
path: bin/electrs-${{ runner.os }}-${{ runner.arch }}
30+
key: electrs-submit-package-${{ runner.os }}-${{ runner.arch }}
31+
- name: Download bitcoind
32+
if: "steps.cache-bitcoind.outputs.cache-hit != 'true'"
33+
run: |
34+
source ./scripts/download_bitcoind_electrs.sh
35+
mkdir -p bin
36+
mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }}
37+
- name: Download electrs
38+
if: "steps.cache-electrs.outputs.cache-hit != 'true'"
39+
run: |
40+
source ./scripts/build_electrs.sh
41+
mkdir -p bin
42+
mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }}
43+
- name: Set bitcoind/electrs environment variables
44+
run: |
45+
echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
46+
echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV"
47+
- name: Test with 0FC enabled
48+
run: |
49+
RUSTFLAGS="--cfg no_download --cfg cycle_tests --cfg tokio_unstable --cfg zero_fee_commitment_tests" cargo test -- --test-threads=1

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
- `EsploraSyncConfig` and `ElectrumSyncConfig` now support `force_wallet_full_scan`. When set,
1919
the on-chain wallet keeps using BDK `full_scan` instead of incremental sync until a full scan
2020
succeeds, allowing restored wallets to rediscover funds sent to previously-unknown addresses.
21+
- `Config::anchor_channels_config` is no longer optional, hence anchor channels can no longer be
22+
disabled. We still negotiate legacy channels if the peer does not support anchor channels.
2123

2224
## Bug Fixes and Improvements
2325
- Building a fresh node against a Bitcoin Core RPC or REST chain source that fails to return the

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ check-cfg = [
135135
"cfg(eclair_test)",
136136
"cfg(cycle_tests)",
137137
"cfg(hrn_tests)",
138+
"cfg(zero_fee_commitment_tests)",
138139
]
139140

140141
[[bench]]

benches/payments.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,8 @@ fn payment_benchmark(c: &mut Criterion) {
121121
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
122122
let chain_source = random_chain_source(&bitcoind, &electrsd);
123123

124-
let (node_a, node_b) = setup_two_nodes_with_store(
125-
&chain_source,
126-
false,
127-
true,
128-
false,
129-
common::TestStoreType::Sqlite,
130-
);
124+
let (node_a, node_b) =
125+
setup_two_nodes_with_store(&chain_source, false, false, common::TestStoreType::Sqlite);
131126

132127
let runtime =
133128
tokio::runtime::Builder::new_multi_thread().worker_threads(4).enable_all().build().unwrap();

bindings/ldk_node.udl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ enum NodeError {
246246
"LnurlAuthFailed",
247247
"LnurlAuthTimeout",
248248
"InvalidLnurl",
249+
"ChainSourceNotSupported",
249250
};
250251

251252
typedef dictionary NodeStatus;

scripts/build_electrs.sh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/bin/bash
2+
set -eox pipefail
3+
4+
# Our Esplora-based tests require `electrs` binaries. Here, we
5+
# download the code, build the binaries, and export their location
6+
# via `ELECTRS_EXE` which will be used by the `electrsd` crates in
7+
# our tests.
8+
9+
HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')"
10+
ELECTRS_GIT_REPO="https://github.com/tankyleo/blockstream-electrs.git"
11+
ELECTRS_TAG="2026-05-26-electrum-submit-package"
12+
ELECTRS_REV="8c06d8010e43f793b1a65f83695ea846e5cd83ed"
13+
if [[ "$HOST_PLATFORM" != *linux* && "$HOST_PLATFORM" != *darwin* ]]; then
14+
printf "\n\n"
15+
echo "Unsupported platform: $HOST_PLATFORM Exiting.."
16+
exit 1
17+
fi
18+
19+
DL_TMP_DIR=$(mktemp -d)
20+
trap 'rm -rf -- "$DL_TMP_DIR"' EXIT
21+
22+
pushd "$DL_TMP_DIR"
23+
git clone --branch "$ELECTRS_TAG" --depth 1 "$ELECTRS_GIT_REPO" blockstream-electrs
24+
cd blockstream-electrs
25+
CURRENT_HEAD=$(git rev-parse HEAD)
26+
if [ "$CURRENT_HEAD" != "$ELECTRS_REV" ]; then
27+
echo "ERROR: HEAD does not match expected commit"
28+
echo "expected: $ELECTRS_REV"
29+
echo "actual: $CURRENT_HEAD"
30+
exit 1
31+
fi
32+
RUSTFLAGS="" cargo build
33+
export ELECTRS_EXE="$DL_TMP_DIR"/blockstream-electrs/target/debug/electrs
34+
chmod +x "$ELECTRS_EXE"
35+
popd

src/chain/bitcoind.rs

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

1515
use base64::prelude::BASE64_STANDARD;
1616
use base64::Engine;
17+
use bitcoin::transaction::Version;
1718
use bitcoin::{BlockHash, FeeRate, Network, OutPoint, Transaction, Txid};
1819
use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget;
1920
use lightning::chain::{BlockLocator, Listen};
@@ -41,6 +42,7 @@ use crate::fee_estimator::{
4142
};
4243
use crate::io::utils::update_and_persist_node_metrics;
4344
use crate::logger::{log_bytes, log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
45+
use crate::tx_broadcaster::SortedTransactions;
4446
use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
4547
use crate::{Error, PersistedNodeMetrics};
4648

@@ -119,6 +121,30 @@ impl BitcoindChainSource {
119121
self.api_client.utxo_source()
120122
}
121123

124+
pub(super) async fn validate_zero_fee_commitments_support(&self) -> Result<(), Error> {
125+
let node_version_result = tokio::time::timeout(
126+
Duration::from_secs(CHAIN_POLLING_TIMEOUT_SECS),
127+
self.api_client.get_node_version(),
128+
)
129+
.await
130+
.map_err(|e| {
131+
log_error!(self.logger, "Failed to get node version: {:?}", e);
132+
Error::ConnectionFailed
133+
})?;
134+
135+
let node_version = node_version_result.map_err(|e| {
136+
log_error!(self.logger, "Failed to get node version: {:?}", e);
137+
Error::ConnectionFailed
138+
})?;
139+
140+
// v26 first shipped the `submitpackage` RPC, but we need v29 to relay ephemeral dust
141+
if node_version < 290000 {
142+
log_error!(self.logger, "Bitcoin backend MUST be greater than or equal to v29");
143+
return Err(Error::ChainSourceNotSupported);
144+
}
145+
Ok(())
146+
}
147+
122148
pub(super) async fn continuously_sync_wallets(
123149
&self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>,
124150
onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
@@ -571,46 +597,59 @@ impl BitcoindChainSource {
571597
Ok(())
572598
}
573599

574-
pub(crate) async fn process_broadcast_package(&self, package: Vec<Transaction>) {
575-
// While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28
576-
// features, we should eventually switch to use `submitpackage` via the
577-
// `rust-bitcoind-json-rpc` crate rather than just broadcasting individual
578-
// transactions.
579-
for tx in &package {
580-
let txid = tx.compute_txid();
581-
let timeout_fut = tokio::time::timeout(
582-
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
583-
self.api_client.broadcast_transaction(tx),
584-
);
585-
match timeout_fut.await {
586-
Ok(res) => match res {
587-
Ok(id) => {
588-
debug_assert_eq!(id, txid);
589-
log_trace!(self.logger, "Successfully broadcast transaction {}", txid);
590-
},
591-
Err(e) => {
592-
log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e);
593-
log_trace!(
594-
self.logger,
595-
"Failed broadcast transaction bytes: {}",
596-
log_bytes!(tx.encode())
597-
);
600+
fn log_broadcast_error(
601+
&self, e: impl core::fmt::Display, txids: &[Txid], txs: &SortedTransactions,
602+
) {
603+
log_error!(self.logger, "Failed to broadcast transaction(s) {:?}: {}", txids, e);
604+
log_trace!(self.logger, "Failed broadcast transaction bytes:");
605+
for tx in txs.iter() {
606+
log_trace!(self.logger, "{}", log_bytes!(tx.encode()));
607+
}
608+
}
609+
610+
pub(crate) async fn process_transaction_broadcast(&self, txs: SortedTransactions) {
611+
let all_txs_are_v3 = txs.iter().all(|tx| tx.version == Version::non_standard(3));
612+
match txs.len() {
613+
2.. if all_txs_are_v3 => {
614+
let txids: Vec<_> = txs.iter().map(|tx| tx.compute_txid()).collect();
615+
let timeout_fut = tokio::time::timeout(
616+
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
617+
self.api_client.submit_package(&txs),
618+
);
619+
match timeout_fut.await {
620+
Ok(res) => match res {
621+
Ok(result) => {
622+
log_trace!(self.logger, "Successfully broadcast package {:?}", txids);
623+
log_trace!(self.logger, "Successfully broadcast package {}", result);
624+
},
625+
Err(e) => self.log_broadcast_error(e, &txids, &txs),
598626
},
599-
},
600-
Err(e) => {
601-
log_error!(
602-
self.logger,
603-
"Failed to broadcast transaction due to timeout {}: {}",
604-
txid,
605-
e
606-
);
607-
log_trace!(
608-
self.logger,
609-
"Failed broadcast transaction bytes: {}",
610-
log_bytes!(tx.encode())
627+
Err(e) => self.log_broadcast_error(e, &txids, &txs),
628+
}
629+
},
630+
_ => {
631+
for tx in txs.iter() {
632+
let txid = tx.compute_txid();
633+
let timeout_fut = tokio::time::timeout(
634+
Duration::from_secs(DEFAULT_TX_BROADCAST_TIMEOUT_SECS),
635+
self.api_client.broadcast_transaction(tx),
611636
);
612-
},
613-
}
637+
match timeout_fut.await {
638+
Ok(res) => match res {
639+
Ok(id) => {
640+
debug_assert_eq!(id, txid);
641+
log_trace!(
642+
self.logger,
643+
"Successfully broadcast transaction {}",
644+
txid
645+
);
646+
},
647+
Err(e) => self.log_broadcast_error(e, &[txid], &txs),
648+
},
649+
Err(e) => self.log_broadcast_error(e, &[txid], &txs),
650+
}
651+
}
652+
},
614653
}
615654
}
616655
}
@@ -748,6 +787,31 @@ impl BitcoindClient {
748787
}
749788
}
750789

790+
pub(crate) async fn get_node_version(&self) -> Result<u64, BitcoindClientError> {
791+
match self {
792+
BitcoindClient::Rpc { rpc_client, .. } => {
793+
Self::get_node_version_inner(Arc::clone(rpc_client))
794+
.await
795+
.map_err(BitcoindClientError::Rpc)
796+
},
797+
BitcoindClient::Rest { rpc_client, .. } => {
798+
// Bitcoin Core's REST interface does not support `getnetworkinfo`
799+
// so we use the RPC client.
800+
Self::get_node_version_inner(Arc::clone(rpc_client))
801+
.await
802+
.map_err(BitcoindClientError::Rpc)
803+
},
804+
}
805+
}
806+
807+
async fn get_node_version_inner(rpc_client: Arc<RpcClient>) -> Result<u64, RpcClientError> {
808+
rpc_client.call_method::<serde_json::Value>("getnetworkinfo", &[]).await.and_then(|value| {
809+
value["version"].as_u64().ok_or(RpcClientError::InvalidData(String::from(
810+
"The version field in the `getnetworkinfo` response should be a u64",
811+
)))
812+
})
813+
}
814+
751815
/// Broadcasts the provided transaction.
752816
pub(crate) async fn broadcast_transaction(
753817
&self, tx: &Transaction,
@@ -776,6 +840,38 @@ impl BitcoindClient {
776840
rpc_client.call_method::<Txid>("sendrawtransaction", &[tx_json]).await
777841
}
778842

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

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

0 commit comments

Comments
 (0)