diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml index ef049ad851..f85ec11419 100644 --- a/.github/workflows/benchmarks.yml +++ b/.github/workflows/benchmarks.yml @@ -34,7 +34,7 @@ jobs: if: "(steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')" run: | source ./scripts/download_bitcoind_electrs.sh - mkdir bin + mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 87249bd720..6ad2b24ef9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -22,8 +22,6 @@ jobs: ] include: - toolchain: stable - check-fmt: true - build-uniffi: true platform: ubuntu-latest - toolchain: stable platform: macos-latest @@ -38,6 +36,10 @@ jobs: - name: Install Rust ${{ matrix.toolchain }} toolchain run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ matrix.toolchain }} + - name: Pin packages to allow for MSRV + if: matrix.msrv + run: | + cargo update -p idna_adapter --precise "1.2.0" --verbose # idna_adapter 1.2.1 uses ICU4X 2.2.0, requiring 1.86 and newer - name: Check formatting on Rust ${{ matrix.toolchain }} if: matrix.check-fmt run: rustup component add rustfmt && cargo fmt --all -- --check @@ -60,7 +62,7 @@ jobs: if: "matrix.platform != 'windows-latest' && (steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')" run: | source ./scripts/download_bitcoind_electrs.sh - mkdir bin + mkdir -p bin mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - name: Set bitcoind/electrs environment variables @@ -69,9 +71,6 @@ jobs: echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - name: Build on Rust ${{ matrix.toolchain }} 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 - name: Build documentation on Rust ${{ matrix.toolchain }} if: "matrix.platform != 'windows-latest' || matrix.toolchain != '1.85.0'" run: | @@ -79,14 +78,7 @@ jobs: cargo doc --document-private-items --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 }} - if: matrix.build-uniffi - run: cargo check --release --features uniffi --verbose --color always - name: Test on Rust ${{ matrix.toolchain }} if: "matrix.platform != 'windows-latest'" run: | RUSTFLAGS="--cfg no_download" cargo test - - name: Test with UniFFI support on Rust ${{ matrix.toolchain }} - if: "matrix.platform != 'windows-latest' && matrix.build-uniffi" - run: | - RUSTFLAGS="--cfg no_download" cargo test --features uniffi diff --git a/src/builder.rs b/src/builder.rs index 5328b368c2..79ce743901 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::convert::TryInto; use std::default::Default; use std::path::PathBuf; @@ -14,12 +14,11 @@ use std::time::SystemTime; use std::{fmt, fs}; use bdk_wallet::template::Bip84; -use bdk_wallet::{KeychainKind, Wallet as BdkWallet, Update}; +use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; use bip39::Mnemonic; use bitcoin::bip32::{ChildNumber, Xpriv}; use bitcoin::secp256k1::PublicKey; use bitcoin::{BlockHash, Network}; -use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; use lightning::chain::{chainmonitor, BestBlock, Watch}; use lightning::io::Cursor; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; @@ -39,7 +38,6 @@ use lightning::util::persist::{ }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; -use lightning_block_sync::BlockSource; use lightning_persister::fs_store::FilesystemStore; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 48995d2e87..a4ae77d766 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -18,8 +18,6 @@ use bip21::de::ParamKind; use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams}; use bitcoin::address::{NetworkChecked, NetworkUnchecked}; use bitcoin::{Amount, Txid}; -use bitcoin_payment_instructions::amount::Amount as BPIAmount; -use bitcoin_payment_instructions::{PaymentInstructions, PaymentMethod}; use lightning::ln::channelmanager::PaymentId; use lightning::offers::offer::Offer; use lightning::routing::router::RouteParametersConfig; diff --git a/src/types.rs b/src/types.rs index 1081e2a188..dfd4cd0bbe 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,8 +9,7 @@ use std::fmt; use std::sync::{Arc, Mutex}; use bitcoin::secp256k1::PublicKey; -use bitcoin::{OutPoint, ScriptBuf}; -use bitcoin_payment_instructions::onion_message_resolver::LDKOnionMessageDNSSECHrnResolver; +use bitcoin::OutPoint; use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 9fb1aabb84..23538ed46d 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -30,7 +30,6 @@ use bitcoin::{ Address, Amount, FeeRate, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, Weight, WitnessProgram, WitnessVersion, }; -use bdk_chain::CheckPoint; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::chain::{BestBlock, Listen}; @@ -114,10 +113,6 @@ impl Wallet { BestBlock { block_hash: checkpoint.hash(), height: checkpoint.height() } } - pub(crate) fn latest_checkpoint(&self) -> CheckPoint { - self.inner.lock().unwrap().latest_checkpoint() - } - pub(crate) fn apply_update(&self, update: impl Into) -> Result<(), Error> { let mut locked_wallet = self.inner.lock().unwrap(); match locked_wallet.apply_update(update) { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 699f8f1d0e..c8ae80cbcf 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -17,7 +17,7 @@ use std::future::Future; use std::path::PathBuf; use std::pin::Pin; use std::sync::{Arc, RwLock}; -use std::time::Duration; +use std::time::{Duration, Instant}; use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::sha256::Hash as Sha256; @@ -204,9 +204,49 @@ pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { electrsd_conf.http_enabled = true; electrsd_conf.network = "regtest"; let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf).unwrap(); + wait_for_esplora_ready(&electrsd); (bitcoind, electrsd) } +/// Block until electrs's esplora REST endpoint is actually serving requests. +/// +/// `ElectrsD::with_conf` returns as soon as the electrum RPC port is listening, but the esplora +/// HTTP server may not be accepting connections yet. Hitting it before it is ready surfaces as a +/// transient fee-estimate fetch failure during `Node::start`. Failing fast there is the right +/// behavior for production, where a real esplora is expected to already be up, so we keep the node +/// strict and instead make the test harness hand back a backend that is ready to serve. +fn wait_for_esplora_ready(electrsd: &ElectrsD) { + use std::io::{Read, Write}; + use std::net::TcpStream; + + let addr = electrsd.esplora_url.as_ref().expect("esplora REST endpoint not enabled"); + let deadline = Instant::now() + Duration::from_secs(30); + loop { + let responded = TcpStream::connect(addr) + .ok() + .and_then(|mut stream| { + stream.set_read_timeout(Some(Duration::from_secs(2))).ok()?; + let request = format!( + "GET /blocks/tip/height HTTP/1.0\r\nHost: {addr}\r\nConnection: close\r\n\r\n" + ); + stream.write_all(request.as_bytes()).ok()?; + let mut response = String::new(); + stream.read_to_string(&mut response).ok()?; + response.lines().next().map(|status| status.contains("200")) + }) + .unwrap_or(false); + + if responded { + return; + } + assert!( + Instant::now() < deadline, + "esplora REST server at {addr} did not become ready in time" + ); + std::thread::sleep(Duration::from_millis(50)); + } +} + pub(crate) fn random_storage_path() -> PathBuf { let mut temp_path = std::env::temp_dir(); let mut rng = rng(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 008b3db006..7514100956 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -2158,7 +2158,7 @@ async fn lsps4_client_service_integration() { } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn lsps4_channel_size_tiers_are_applied() { +async fn lsps4_jit_channel_grows_via_splicing() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let sync_config = EsploraSyncConfig { background_sync_config: None }; @@ -2217,29 +2217,20 @@ async fn lsps4_channel_size_tiers_are_applied() { expect_channel_ready_event!(payer_node, service_node.node_id()); expect_channel_ready_event!(service_node, payer_node.node_id()); - let expect_service_forward = |expected_forward_amount_msat: u64| loop { - match service_node.wait_next_event() { - Event::PaymentForwarded { - skimmed_fee_msat, outbound_amount_forwarded_msat, .. - } => { - assert_eq!(skimmed_fee_msat.unwrap_or(0), 0); - assert_eq!(outbound_amount_forwarded_msat, Some(expected_forward_amount_msat)); - service_node.event_handled().unwrap(); - break; - }, - Event::SendWebhook { .. } => { - service_node.event_handled().unwrap(); - }, - other => panic!("unexpected service event: {:?}", other), - } - }; - - let mut expected_channel_values = Vec::new(); - + // `channel_size_tiers` is keyed by the number of channels already open with the peer, + // so it only influences the *initial* channel open. Once a channel exists, the LSPS4 + // service grows liquidity by splicing the existing channel rather than opening new + // ones, so the client always ends up with exactly one channel that grows monotonically. + let tier0_channel_size_sat = 100_000; + + // Drive a single JIT payment to completion and return the client's resulting channel + // value with the service. A liquidity action (initial open or splice) may be needed; + // splices must confirm on-chain before the channel is usable again, so we mine blocks + // whenever one is pending and keep draining events until the payment is both forwarded + // by the service and received by the client. macro_rules! pay_lsps4_invoice { - ($amount_sat:expr, $expected_new_channel_value_sat:expr) => {{ - let amount_sat = $amount_sat; - let expected_new_channel_value_sat = $expected_new_channel_value_sat; + ($amount_sat:expr) => {{ + let amount_sat: u64 = $amount_sat; let amount_msat = amount_sat * 1000; let invoice_description = Bolt11InvoiceDescription::Direct( Description::new(format!("lsps4 tier test {}", amount_sat)).unwrap(), @@ -2250,51 +2241,110 @@ async fn lsps4_channel_size_tiers_are_applied() { .unwrap(); let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); - if expected_new_channel_value_sat.is_some() { - expect_channel_pending_event!(service_node, client_node.node_id()); - expect_channel_ready_event!(service_node, client_node.node_id()); - expect_channel_pending_event!(client_node, service_node.node_id()); - expect_channel_ready_event!(client_node, service_node.node_id()); + let mut payer_succeeded = false; + let mut forwarded = false; + let mut client_payment_id = None; + let mut liquidity_pending = false; + + 'drive: for _ in 0..60 { + for node in [&service_node, &client_node, &payer_node] { + while let Some(event) = node.next_event() { + match event { + Event::PaymentForwarded { + skimmed_fee_msat, + outbound_amount_forwarded_msat, + .. + } => { + assert_eq!(skimmed_fee_msat.unwrap_or(0), 0); + assert_eq!(outbound_amount_forwarded_msat, Some(amount_msat)); + forwarded = true; + }, + Event::ChannelPending { .. } | Event::SplicePending { .. } => { + liquidity_pending = true; + }, + Event::PaymentSuccessful { payment_id: pid, .. } + if pid == Some(payment_id) => + { + payer_succeeded = true; + }, + Event::PaymentReceived { + payment_id: pid, amount_msat: amt, .. + } => { + assert_eq!(amt, amount_msat); + client_payment_id = pid; + }, + _ => {}, + } + node.event_handled().unwrap(); + } + } + + if payer_succeeded && forwarded && client_payment_id.is_some() { + break 'drive; + } + + // Confirm any pending splice/open so the LSP can retry forwarding. + if liquidity_pending { + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6).await; + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + } + tokio::time::sleep(std::time::Duration::from_millis(200)).await; } - expect_service_forward(amount_msat); - expect_payment_successful_event!(payer_node, Some(payment_id), None); - let client_payment_id = expect_payment_received_event!(client_node, amount_msat).unwrap(); + assert!(payer_succeeded, "payer did not complete {}sat payment", amount_sat); + assert!(forwarded, "service did not forward {}sat payment", amount_sat); + let client_payment_id = client_payment_id.expect("client did not receive the payment"); + let payment = client_node.payment(&client_payment_id).unwrap(); assert!(matches!(payment.kind, PaymentKind::Bolt11Jit { .. })); assert_eq!(payment.amount_msat, Some(amount_msat)); assert_eq!(payment.status, PaymentStatus::Succeeded); - if let Some(channel_value_sat) = expected_new_channel_value_sat { - expected_channel_values.push(channel_value_sat); - } - - let mut actual_channel_values: Vec = client_node + // Liquidity is always added by splicing the existing channel, never by opening + // additional ones, so the client holds exactly one channel with the service. + let channel_values: Vec = client_node .list_channels() .into_iter() .filter(|chan| chan.counterparty_node_id == service_node.node_id()) .map(|chan| chan.channel_value_sats) .collect(); - actual_channel_values.sort_unstable(); - let mut expected_sorted = expected_channel_values.clone(); - expected_sorted.sort_unstable(); - assert_eq!(actual_channel_values, expected_sorted); + assert_eq!( + channel_values.len(), + 1, + "expected a single client<->service channel, got {:?}", + channel_values + ); + channel_values[0] }}; } - pay_lsps4_invoice!(50_000, Some(100_000)); - pay_lsps4_invoice!(100_000, Some(500_000)); - - for _ in 0..5 { - pay_lsps4_invoice!(20_000, None); + // A request below tier[0] still opens a tier[0]-sized channel: the tier is applied. + let mut channel_value_sat = pay_lsps4_invoice!(50_000); + assert_eq!(channel_value_sat, tier0_channel_size_sat); + + // Subsequent liquidity needs grow the same channel via splicing. Exact post-splice + // sizes depend on reserve/capacity, so we only assert the channel never splits and + // never shrinks. + for amount_sat in [100_000u64, 20_000, 400_000] { + let new_value_sat = pay_lsps4_invoice!(amount_sat); + assert!( + new_value_sat >= channel_value_sat, + "channel shrank from {} to {}", + channel_value_sat, + new_value_sat + ); + channel_value_sat = new_value_sat; } - pay_lsps4_invoice!(400_000, Some(1_000_000)); - pay_lsps4_invoice!(100_000, None); - pay_lsps4_invoice!(700_000, Some(1_000_000)); - pay_lsps4_invoice!(900_000, Some(1_000_000)); - - assert_eq!(expected_channel_values, vec![100_000, 500_000, 1_000_000, 1_000_000, 1_000_000]); + // The channel must have grown beyond the initial tier[0] open via splicing. + assert!( + channel_value_sat > tier0_channel_size_sat, + "expected channel to grow beyond tier[0] ({}sat) via splicing, final size {}sat", + tier0_channel_size_sat, + channel_value_sat + ); } #[test]