Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/benchmarks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 5 additions & 13 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ jobs:
]
include:
- toolchain: stable
check-fmt: true
build-uniffi: true
platform: ubuntu-latest
- toolchain: stable
platform: macos-latest
Expand All @@ -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
Expand All @@ -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
Expand All @@ -69,24 +71,14 @@ 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: |
cargo doc --release --verbose --color always
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
6 changes: 2 additions & 4 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};
Expand All @@ -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};

Expand Down
2 changes: 0 additions & 2 deletions src/payment/unified_qr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
5 changes: 0 additions & 5 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Update>) -> Result<(), Error> {
let mut locked_wallet = self.inner.lock().unwrap();
match locked_wallet.apply_update(update) {
Expand Down
42 changes: 41 additions & 1 deletion tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
152 changes: 101 additions & 51 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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(),
Expand All @@ -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<u64> = 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<u64> = 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]
Expand Down
Loading