diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4b77edca9..ddfaf8afd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -44,7 +44,8 @@ jobs: cargo update -p hashlink --precise "0.8.2" --verbose # hashlink 0.8.3 requires hashbrown 0.14, requiring 1.64.0 cargo update -p proptest --precise "1.2.0" --verbose # proptest 1.3.0 requires rustc 1.64.0 cargo update -p regex --precise "1.9.6" --verbose # regex 1.10.0 requires rustc 1.65.0 - cargo update -p home --precise "0.5.5" --verbose # home v0.5.9, requires rustc 1.70 or newer + cargo update -p home --precise "0.5.5" --verbose # home v0.5.9 requires rustc 1.70 or newer + cargo update -p reqwest --precise "0.11.24" --verbose # reqwest v0.11.25 requires rustc 1.64 or newer - name: Set RUSTFLAGS to deny warnings if: "matrix.toolchain == 'stable'" run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" diff --git a/Cargo.toml b/Cargo.toml index 1d6bf9c1a..8d9615c20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,14 +28,23 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.0.121", features = ["std"] } -lightning-invoice = { version = "0.29.0" } -lightning-net-tokio = { version = "0.0.121" } -lightning-persister = { version = "0.0.121" } -lightning-background-processor = { version = "0.0.121", features = ["futures"] } -lightning-rapid-gossip-sync = { version = "0.0.121" } -lightning-transaction-sync = { version = "0.0.121", features = ["esplora-async-https", "time"] } -lightning-liquidity = { version = "0.1.0-alpha.1", features = ["std"] } +#lightning = { version = "0.0.121", features = ["std"] } +#lightning-invoice = { version = "0.29.0" } +#lightning-net-tokio = { version = "0.0.121" } +#lightning-persister = { version = "0.0.121" } +#lightning-background-processor = { version = "0.0.121", features = ["futures"] } +#lightning-rapid-gossip-sync = { version = "0.0.121" } +#lightning-transaction-sync = { version = "0.0.121", features = ["esplora-async-https", "time"] } +#lightning-liquidity = { version = "0.1.0-alpha.1", features = ["std"] } + +lightning = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event", features = ["std"] } +lightning-invoice = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event" } +lightning-net-tokio = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event" } +lightning-persister = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event" } +lightning-background-processor = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event", features = ["futures"] } +lightning-rapid-gossip-sync = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event" } +lightning-transaction-sync = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event", features = ["esplora-async"] } +lightning-liquidity = { git = "https://github.com/tnull/lightning-liquidity", branch = "2024-03-invoice-generated-event", features = ["std"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] } #lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" } @@ -64,7 +73,6 @@ bip39 = "2.0.0" rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } -futures = "0.3" tokio = { version = "1", default-features = false, features = [ "rt-multi-thread", "time", "sync" ] } esplora-client = { version = "0.6", default-features = false } libc = "0.2" @@ -78,8 +86,9 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { version = "0.0.121", features = ["std", "_test_utils"] } +#lightning = { version = "0.0.121", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/tnull/rust-lightning", branch = "2024-03-invoice-generated-event", features = ["std", "_test_utils"] } electrum-client = { version = "0.15.1", default-features = true } bitcoincore-rpc = { version = "0.17.0", default-features = false } proptest = "1.0.0" diff --git a/README.md b/README.md index 270bf25a7..4078ce67b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A ready-to-go Lightning node library built using [LDK][ldk] and [BDK][bdk]. LDK Node is a self-custodial Lightning node in library form. Its central goal is to provide a small, simple, and straightforward interface that enables users to easily set up and run a Lightning node with an integrated on-chain wallet. While minimalism is at its core, LDK Node aims to be sufficiently modular and configurable to be useful for a variety of use cases. ## Getting Started -The primary abstraction of the library is the [`Node`][api_docs_node], which can be retrieved by setting up and configuring a [`Builder`][api_docs_builder] to your liking and calling one of the `build` methods. `Node` can then be controlled via commands such as `start`, `stop`, `connect_open_channel`, `send_payment`, etc. +The primary abstraction of the library is the [`Node`][api_docs_node], which can be retrieved by setting up and configuring a [`Builder`][api_docs_builder] to your liking and calling one of the `build` methods. `Node` can then be controlled via commands such as `start`, `stop`, `connect_open_channel`, `send`, etc. ```rust use ldk_node::Builder; @@ -31,7 +31,7 @@ fn main() { node.start().unwrap(); - let funding_address = node.new_onchain_address(); + let funding_address = node.onchain_payment().new_address(); // .. fund address .. @@ -44,7 +44,7 @@ fn main() { node.event_handled(); let invoice = Bolt11Invoice::from_str("INVOICE_STR").unwrap(); - node.send_payment(&invoice).unwrap(); + node.bolt11_payment().send(&invoice).unwrap(); node.stop().unwrap(); } diff --git a/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt index a5ca6eac0..763862a33 100644 --- a/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt +++ b/bindings/kotlin/ldk-node-android/lib/src/androidTest/kotlin/org/lightningdevkit/ldknode/AndroidLibTest.kt @@ -51,10 +51,10 @@ class AndroidLibTest { val nodeId2 = node2.nodeId() println("Node Id 2: $nodeId2") - val address1 = node1.newOnchainAddress() + val address1 = node1.onchain_payment().newOnchainAddress() println("Funding address 1: $address1") - val address2 = node2.newOnchainAddress() + val address2 = node2.onchain_payment().newOnchainAddress() println("Funding address 2: $address2") node1.stop() diff --git a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt index 39c19821d..55b3e310f 100644 --- a/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt +++ b/bindings/kotlin/ldk-node-jvm/lib/src/test/kotlin/org/lightningdevkit/ldknode/LibraryTest.kt @@ -146,10 +146,10 @@ class LibraryTest { val nodeId2 = node2.nodeId() println("Node Id 2: $nodeId2") - val address1 = node1.newOnchainAddress() + val address1 = node1.onchainPayment().newAddress() println("Funding address 1: $address1") - val address2 = node2.newOnchainAddress() + val address2 = node2.onchainPayment().newAddress() println("Funding address 2: $address2") val txid1 = sendToAddress(address1, 100000u) @@ -203,9 +203,9 @@ class LibraryTest { val spendableBalance2AfterOpen = node2.listBalances().spendableOnchainBalanceSats println("Spendable balance 1 after open: $spendableBalance1AfterOpen") println("Spendable balance 2 after open: $spendableBalance2AfterOpen") - assert(spendableBalance1AfterOpen > 49000u) - assert(spendableBalance1AfterOpen < 50000u) - assertEquals(100000uL, spendableBalance2AfterOpen) + assert(spendableBalance1AfterOpen > 24000u) + assert(spendableBalance1AfterOpen < 25000u) + assertEquals(75000uL, spendableBalance2AfterOpen) val channelReadyEvent1 = node1.waitNextEvent() println("Got event: $channelReadyEvent1") @@ -222,9 +222,9 @@ class LibraryTest { else -> return } - val invoice = node2.receivePayment(2500000u, "asdf", 9217u) + val invoice = node2.bolt11Payment().receive(2500000u, "asdf", 9217u) - node1.sendPayment(invoice) + node1.bolt11Payment().send(invoice) val paymentSuccessfulEvent = node1.waitNextEvent() println("Got event: $paymentSuccessfulEvent") @@ -239,7 +239,7 @@ class LibraryTest { assert(node1.listPayments().size == 1) assert(node2.listPayments().size == 1) - node2.closeChannel(userChannelId, nodeId1) + node2.closeChannel(userChannelId, nodeId1, false) val channelClosedEvent1 = node1.waitNextEvent() println("Got event: $channelClosedEvent1") diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b5d18b95c..fef7a48e4 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -15,6 +15,12 @@ dictionary Config { sequence trusted_peers_0conf; u64 probing_liquidity_limit_multiplier; LogLevel log_level; + AnchorChannelsConfig? anchor_channels_config; +}; + +dictionary AnchorChannelsConfig { + sequence trusted_peers_no_reserve; + u64 per_channel_reserve_sats; }; interface Builder { @@ -34,14 +40,18 @@ interface Builder { [Throws=BuildError] void set_listening_addresses(sequence listening_addresses); [Throws=BuildError] - LDKNode build(); + Node build(); + [Throws=BuildError] + Node build_with_fs_store(); }; -interface LDKNode { +interface Node { [Throws=NodeError] void start(); [Throws=NodeError] void stop(); + NodeStatus status(); + Config config(); Event? next_event(); Event wait_next_event(); // [Async] @@ -49,12 +59,9 @@ interface LDKNode { void event_handled(); PublicKey node_id(); sequence? listening_addresses(); - [Throws=NodeError] - Address new_onchain_address(); - [Throws=NodeError] - Txid send_to_onchain_address([ByRef]Address address, u64 amount_msat); - [Throws=NodeError] - Txid send_all_to_onchain_address([ByRef]Address address); + Bolt11Payment bolt11_payment(); + SpontaneousPayment spontaneous_payment(); + OnchainPayment onchain_payment(); [Throws=NodeError] void connect(PublicKey node_id, SocketAddress address, boolean persist); [Throws=NodeError] @@ -62,44 +69,58 @@ interface LDKNode { [Throws=NodeError] UserChannelId connect_open_channel(PublicKey node_id, SocketAddress address, u64 channel_amount_sats, u64? push_to_counterparty_msat, ChannelConfig? channel_config, boolean announce_channel); [Throws=NodeError] - void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id); + void close_channel([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, boolean force); [Throws=NodeError] void update_channel_config([ByRef]UserChannelId user_channel_id, PublicKey counterparty_node_id, ChannelConfig channel_config); [Throws=NodeError] void sync_wallets(); + PaymentDetails? payment([ByRef]PaymentId payment_id); [Throws=NodeError] - PaymentHash send_payment([ByRef]Bolt11Invoice invoice); + void remove_payment([ByRef]PaymentId payment_id); + BalanceDetails list_balances(); + sequence list_payments(); + sequence list_peers(); + sequence list_channels(); [Throws=NodeError] - PaymentHash send_payment_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + string sign_message([ByRef]sequence msg); + boolean verify_signature([ByRef]sequence msg, [ByRef]string sig, [ByRef]PublicKey pkey); [Throws=NodeError] - PaymentHash send_spontaneous_payment(u64 amount_msat, PublicKey node_id, sequence custom_tlvs); + void reset_router(); +}; + +interface Bolt11Payment { [Throws=NodeError] - void send_payment_probes([ByRef]Bolt11Invoice invoice); + PaymentId send([ByRef]Bolt11Invoice invoice); [Throws=NodeError] - void send_spontaneous_payment_probes(u64 amount_msat, PublicKey node_id); + PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); [Throws=NodeError] - void send_payment_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + void send_probes([ByRef]Bolt11Invoice invoice); [Throws=NodeError] - Bolt11Invoice receive_payment(u64 amount_msat, [ByRef]string description, u32 expiry_secs); + void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); [Throws=NodeError] - Bolt11Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs); + Bolt11Invoice receive(u64 amount_msat, [ByRef]string description, u32 expiry_secs); [Throws=NodeError] - Bolt11Invoice receive_payment_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); + Bolt11Invoice receive_variable_amount([ByRef]string description, u32 expiry_secs); [Throws=NodeError] - Bolt11Invoice receive_variable_amount_payment_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); - PaymentDetails? payment([ByRef]PaymentHash payment_hash); + Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); [Throws=NodeError] - void remove_payment([ByRef]PaymentHash payment_hash); - BalanceDetails list_balances(); - sequence list_payments(); - sequence list_peers(); - sequence list_channels(); + Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]string description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); +}; + +interface SpontaneousPayment { [Throws=NodeError] - string sign_message([ByRef]sequence msg); - boolean verify_signature([ByRef]sequence msg, [ByRef]string sig, [ByRef]PublicKey pkey); - boolean is_running(); + PaymentId send(u64 amount_msat, PublicKey node_id, sequence custom_tlvs); [Throws=NodeError] - void reset_router(); + void send_probes(u64 amount_msat, PublicKey node_id); +}; + +interface OnchainPayment { + [Throws=NodeError] + Address new_address(); + [Throws=NodeError] + Txid send_to_address([ByRef]Address address, u64 amount_msat); + [Throws=NodeError] + Txid send_all_to_address([ByRef]Address address); }; [Error] @@ -126,6 +147,7 @@ enum NodeError { "InvalidSocketAddress", "InvalidPublicKey", "InvalidSecretKey", + "InvalidPaymentId", "InvalidPaymentHash", "InvalidPaymentPreimage", "InvalidPaymentSecret", @@ -140,6 +162,22 @@ enum NodeError { "LiquidityFeeTooHigh", }; +dictionary NodeStatus { + boolean is_running; + boolean is_listening; + BestBlock current_best_block; + u64? latest_wallet_sync_timestamp; + u64? latest_onchain_wallet_sync_timestamp; + u64? latest_fee_rate_cache_update_timestamp; + u64? latest_rgs_snapshot_timestamp; + u64? latest_node_announcement_broadcast_timestamp; +}; + +dictionary BestBlock { + BlockHash block_hash; + u32 height; +}; + [Error] enum BuildError { "InvalidSeedBytes", @@ -157,9 +195,9 @@ enum BuildError { [Enum] interface Event { - PaymentSuccessful(PaymentHash payment_hash); - PaymentFailed(PaymentHash payment_hash, PaymentFailureReason? reason); - PaymentReceived(PaymentHash payment_hash, u64 amount_msat); + PaymentSuccessful(PaymentId? payment_id, PaymentHash payment_hash, u64? fee_paid_msat); + PaymentFailed(PaymentId? payment_id, PaymentHash payment_hash, PaymentFailureReason? reason); + PaymentReceived(PaymentId? payment_id, PaymentHash payment_hash, u64 amount_msat); ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo); ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id); ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason); @@ -176,16 +214,26 @@ enum PaymentFailureReason { [Enum] interface ClosureReason { - CounterpartyForceClosed ( UntrustedString peer_msg ); - HolderForceClosed (); - CooperativeClosure (); - CommitmentTxConfirmed (); - FundingTimedOut (); - ProcessingError ( string err ); - DisconnectedPeer (); - OutdatedChannelManager (); - CounterpartyCoopClosedUnfundedChannel (); - FundingBatchClosure (); + CounterpartyForceClosed(UntrustedString peer_msg); + HolderForceClosed(); + LegacyCooperativeClosure(); + CounterpartyInitiatedCooperativeClosure(); + LocallyInitiatedCooperativeClosure(); + CommitmentTxConfirmed(); + FundingTimedOut(); + ProcessingError(string err); + DisconnectedPeer(); + OutdatedChannelManager(); + CounterpartyCoopClosedUnfundedChannel(); + FundingBatchClosure(); +}; + +[Enum] +interface PaymentKind { + Onchain(); + Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, string? bolt11_invoice); + Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, LSPFeeLimits lsp_fee_limits); + Spontaneous(PaymentHash hash, PaymentPreimage? preimage); }; enum PaymentDirection { @@ -205,14 +253,11 @@ dictionary LSPFeeLimits { }; dictionary PaymentDetails { - PaymentHash hash; - PaymentPreimage? preimage; - PaymentSecret? secret; + PaymentId id; + PaymentKind kind; u64? amount_msat; PaymentDirection direction; PaymentStatus status; - LSPFeeLimits? lsp_fee_limits; - string? bolt11_invoice; u64 last_update; }; @@ -232,10 +277,16 @@ dictionary OutPoint { u32 vout; }; +enum ChannelType { + "StaticRemoteKey", + "Anchors", +}; + dictionary ChannelDetails { ChannelId channel_id; PublicKey counterparty_node_id; OutPoint? funding_txo; + ChannelType? channel_type; u64 channel_value_sats; u64? unspendable_punishment_reserve; UserChannelId user_channel_id; @@ -290,6 +341,7 @@ interface PendingSweepBalance { dictionary BalanceDetails { u64 total_onchain_balance_sats; u64 spendable_onchain_balance_sats; + u64 total_anchor_channels_reserve_sats; u64 total_lightning_balance_sats; sequence lightning_balances; sequence pending_balances_from_channel_closures; @@ -343,6 +395,9 @@ typedef string Address; [Custom] typedef string Bolt11Invoice; +[Custom] +typedef string PaymentId; + [Custom] typedef string PaymentHash; diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 864ef7b43..7a384d1ff 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -125,9 +125,9 @@ def test_channel_full_cycle(self): node_id_2 = node_2.node_id() print("Node ID 2:", node_id_2) - address_1 = node_1.new_onchain_address() + address_1 = node_1.onchain_payment().new_address() txid_1 = send_to_address(address_1, 100000) - address_2 = node_2.new_onchain_address() + address_2 = node_2.onchain_payment().new_address() txid_2 = send_to_address(address_2, 100000) wait_for_tx(esplora_endpoint, txid_1) @@ -185,8 +185,8 @@ def test_channel_full_cycle(self): print("EVENT:", channel_ready_event_2) node_2.event_handled() - invoice = node_2.receive_payment(2500000, "asdf", 9217) - node_1.send_payment(invoice) + invoice = node_2.bolt11_payment().receive(2500000, "asdf", 9217) + node_1.bolt11_payment().send(invoice) payment_successful_event_1 = node_1.wait_next_event() assert isinstance(payment_successful_event_1, Event.PAYMENT_SUCCESSFUL) @@ -198,7 +198,7 @@ def test_channel_full_cycle(self): print("EVENT:", payment_received_event_2) node_2.event_handled() - node_2.close_channel(channel_ready_event_2.user_channel_id, node_id_1) + node_2.close_channel(channel_ready_event_2.user_channel_id, node_id_1, false) channel_closed_event_1 = node_1.wait_next_event() assert isinstance(channel_closed_event_1, Event.CHANNEL_CLOSED) diff --git a/docker-compose-cln.yml b/docker-compose-cln.yml index 6628636b9..5fb1f2dcd 100644 --- a/docker-compose-cln.yml +++ b/docker-compose-cln.yml @@ -63,6 +63,7 @@ services: "--bitcoin-rpcuser=user", "--bitcoin-rpcpassword=pass", "--regtest", + "--experimental-anchors", ] ports: - "19846:19846" diff --git a/src/balance.rs b/src/balance.rs index f5a52073d..f688c27d3 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -14,7 +14,15 @@ pub struct BalanceDetails { /// The total balance of our on-chain wallet. pub total_onchain_balance_sats: u64, /// The currently spendable balance of our on-chain wallet. + /// + /// This includes any sufficiently confirmed funds, minus + /// [`total_anchor_channels_reserve_sats`]. + /// + /// [`total_anchor_channels_reserve_sats`]: Self::total_anchor_channels_reserve_sats pub spendable_onchain_balance_sats: u64, + /// The share of our total balance which we retain as an emergency reserve to (hopefully) be + /// able to spend the Anchor outputs when one of our channels is closed. + pub total_anchor_channels_reserve_sats: u64, /// The total balance that we would be able to claim across all our Lightning channels. /// /// Note this excludes balances that we are unsure if we are able to claim (e.g., as we are diff --git a/src/builder.rs b/src/builder.rs index cc0c9adeb..6f56c40f2 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -2,6 +2,7 @@ use crate::config::{ Config, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, DEFAULT_ESPLORA_SERVER_URL, WALLET_KEYS_SEED_LEN, }; +use crate::connection::ConnectionManager; use crate::event::EventQueue; use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::GossipSource; @@ -10,13 +11,13 @@ use crate::io::sqlite_store::SqliteStore; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, FilesystemLogger, Logger}; use crate::message_handler::NodeCustomMessageHandler; -use crate::payment_store::PaymentStore; +use crate::payment::payment_store::PaymentStore; use crate::peer_store::PeerStore; use crate::sweep::OutputSweeper; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ - ChainMonitor, ChannelManager, FakeMessageRouter, GossipSync, KeysManager, NetworkGraph, - OnionMessenger, PeerManager, + ChainMonitor, ChannelManager, DynStore, FakeMessageRouter, GossipSync, KeysManager, + NetworkGraph, OnionMessenger, PeerManager, }; use crate::wallet::Wallet; use crate::{LogLevel, Node}; @@ -33,7 +34,7 @@ use lightning::sign::EntropySource; use lightning::util::config::UserConfig; use lightning::util::persist::{ - read_channel_monitors, KVStore, CHANNEL_MANAGER_PERSISTENCE_KEY, + read_channel_monitors, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::ReadableArgs; @@ -65,6 +66,7 @@ use std::fmt; use std::fs; use std::io::Cursor; use std::path::PathBuf; +use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex, RwLock}; use std::time::SystemTime; @@ -114,12 +116,18 @@ pub enum BuildError { /// The given listening addresses are invalid, e.g. too many were passed. InvalidListeningAddresses, /// We failed to read data from the [`KVStore`]. + /// + /// [`KVStore`]: lightning::util::persist::KVStore ReadFailed, /// We failed to write data to the [`KVStore`]. + /// + /// [`KVStore`]: lightning::util::persist::KVStore WriteFailed, /// We failed to access the given `storage_dir_path`. StoragePathAccessFailed, /// We failed to setup our [`KVStore`]. + /// + /// [`KVStore`]: lightning::util::persist::KVStore KVStoreSetupFailed, /// We failed to setup the onchain wallet. WalletSetupFailed, @@ -298,7 +306,7 @@ impl NodeBuilder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. - pub fn build(&self) -> Result, BuildError> { + pub fn build(&self) -> Result { let storage_dir_path = self.config.storage_dir_path.clone(); fs::create_dir_all(storage_dir_path.clone()) .map_err(|_| BuildError::StoragePathAccessFailed)?; @@ -315,7 +323,7 @@ impl NodeBuilder { /// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options /// previously configured. - pub fn build_with_fs_store(&self) -> Result, BuildError> { + pub fn build_with_fs_store(&self) -> Result { let mut storage_dir_path: PathBuf = self.config.storage_dir_path.clone().into(); storage_dir_path.push("fs_store"); @@ -328,9 +336,7 @@ impl NodeBuilder { /// Builds a [`Node`] instance with a [`VssStore`] backend and according to the options /// previously configured. #[cfg(any(vss, vss_test))] - pub fn build_with_vss_store( - &self, url: String, store_id: String, - ) -> Result, BuildError> { + pub fn build_with_vss_store(&self, url: String, store_id: String) -> Result { let logger = setup_logger(&self.config)?; let seed_bytes = seed_bytes_from_config( @@ -368,9 +374,7 @@ impl NodeBuilder { } /// Builds a [`Node`] instance according to the options previously configured. - pub fn build_with_store( - &self, kv_store: Arc, - ) -> Result, BuildError> { + pub fn build_with_store(&self, kv_store: Arc) -> Result { let logger = setup_logger(&self.config)?; let seed_bytes = seed_bytes_from_config( &self.config, @@ -499,31 +503,29 @@ impl ArcedNodeBuilder { /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. - pub fn build(&self) -> Result>, BuildError> { + pub fn build(&self) -> Result, BuildError> { self.inner.read().unwrap().build().map(Arc::new) } /// Builds a [`Node`] instance with a [`FilesystemStore`] backend and according to the options /// previously configured. - pub fn build_with_fs_store(&self) -> Result>, BuildError> { + pub fn build_with_fs_store(&self) -> Result, BuildError> { self.inner.read().unwrap().build_with_fs_store().map(Arc::new) } /// Builds a [`Node`] instance according to the options previously configured. - pub fn build_with_store( - &self, kv_store: Arc, - ) -> Result>, BuildError> { + pub fn build_with_store(&self, kv_store: Arc) -> Result, BuildError> { self.inner.read().unwrap().build_with_store(kv_store).map(Arc::new) } } /// Builds a [`Node`] instance according to the options previously configured. -fn build_with_store_internal( +fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - logger: Arc, kv_store: Arc, -) -> Result, BuildError> { + logger: Arc, kv_store: Arc, +) -> Result { // Initialize the on-chain wallet and chain access let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes) .map_err(|e| { @@ -603,7 +605,7 @@ fn build_with_store_internal( )); // Initialize the ChainMonitor - let chain_monitor: Arc> = Arc::new(chainmonitor::ChainMonitor::new( + let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( Some(Arc::clone(&tx_sync)), Arc::clone(&tx_broadcaster), Arc::clone(&logger), @@ -663,7 +665,7 @@ fn build_with_store_internal( let router = Arc::new(DefaultRouter::new( Arc::clone(&network_graph), Arc::clone(&logger), - keys_manager.get_secure_random_bytes(), + Arc::clone(&keys_manager), Arc::clone(&scorer), scoring_fee_params, )); @@ -692,12 +694,11 @@ fn build_with_store_internal( // for inbound channels. let mut user_config = UserConfig::default(); user_config.channel_handshake_limits.force_announced_channel_preference = false; - - if !config.trusted_peers_0conf.is_empty() { - // Manually accept inbound channels if we expect 0conf channel requests, avoid - // generating the events otherwise. - user_config.manually_accept_inbound_channels = true; - } + user_config.manually_accept_inbound_channels = true; + // Note the channel_handshake_config will be overwritten in `connect_open_channel`, but we + // still set a default here. + user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = + config.anchor_channels_config.is_some(); if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll @@ -734,7 +735,7 @@ fn build_with_store_internal( channel_monitor_references, ); let (_hash, channel_manager) = - <(BlockHash, ChannelManager)>::read(&mut reader, read_args).map_err(|e| { + <(BlockHash, ChannelManager)>::read(&mut reader, read_args).map_err(|e| { log_error!(logger, "Failed to read channel manager from KVStore: {}", e); BuildError::ReadFailed })?; @@ -891,6 +892,9 @@ fn build_with_store_internal( liquidity_source.as_ref().map(|l| l.set_peer_manager(Arc::clone(&peer_manager))); + let connection_manager = + Arc::new(ConnectionManager::new(Arc::clone(&peer_manager), Arc::clone(&logger))); + // Init payment info storage let payment_store = match io::utils::read_payments(Arc::clone(&kv_store), Arc::clone(&logger)) { Ok(payments) => { @@ -943,12 +947,18 @@ fn build_with_store_internal( }, }; - let (stop_sender, stop_receiver) = tokio::sync::watch::channel(()); + let (stop_sender, _) = tokio::sync::watch::channel(()); + + let is_listening = Arc::new(AtomicBool::new(false)); + let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None)); + let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None)); + let latest_fee_rate_cache_update_timestamp = Arc::new(RwLock::new(None)); + let latest_rgs_snapshot_timestamp = Arc::new(RwLock::new(None)); + let latest_node_announcement_broadcast_timestamp = Arc::new(RwLock::new(None)); Ok(Node { runtime, stop_sender, - stop_receiver, config, wallet, tx_sync, @@ -959,6 +969,7 @@ fn build_with_store_internal( chain_monitor, output_sweeper, peer_manager, + connection_manager, keys_manager, network_graph, gossip_source, @@ -969,6 +980,12 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + is_listening, + latest_wallet_sync_timestamp, + latest_onchain_wallet_sync_timestamp, + latest_fee_rate_cache_update_timestamp, + latest_rgs_snapshot_timestamp, + latest_node_announcement_broadcast_timestamp, }) } diff --git a/src/config.rs b/src/config.rs index 945d712c9..fa56940d4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ const DEFAULT_LDK_WALLET_SYNC_INTERVAL_SECS: u64 = 30; const DEFAULT_FEE_RATE_CACHE_UPDATE_INTERVAL_SECS: u64 = 60 * 10; const DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER: u64 = 3; const DEFAULT_LOG_LEVEL: LogLevel = LogLevel::Debug; +const DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS: u64 = 25_000; // The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold // number of derivation indexes after which BDK stops looking for new scripts belonging to the wallet. @@ -62,6 +63,9 @@ pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64; /// | `trusted_peers_0conf` | [] | /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | +/// | `anchor_channels_config` | Some(..) | +/// +/// See [`AnchorChannelsConfig`] for more information on its respective default values. /// /// [`Node`]: crate::Node pub struct Config { @@ -104,6 +108,21 @@ pub struct Config { /// /// Any messages below this level will be excluded from the logs. pub log_level: LogLevel, + /// Configuration options pertaining to Anchor channels, i.e., channels for which the + /// `option_anchors_zero_fee_htlc_tx` channel type is negotiated. + /// + /// Please refer to [`AnchorChannelsConfig`] for further information on Anchor channels. + /// + /// If set to `Some`, new channels will have Anchors enabled, i.e., will be negotiated with the + /// `option_anchors_zero_fee_htlc_tx` channel type. If set to `None`, new channels will be + /// negotiated with the legacy `option_static_remotekey` channel type. + /// + /// **Note:** Please note that if set to `None` *after* some Anchor channels have already been + /// opened, no dedicated emergency on-chain reserve will be maintained for these channels, + /// which can be dangerous if only insufficient funds are available at the time of channel + /// closure. We *will* however still try to get the Anchor spending transactions confirmed + /// on-chain with the funds available. + pub anchor_channels_config: Option, } impl Default for Config { @@ -120,6 +139,66 @@ impl Default for Config { trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, log_level: DEFAULT_LOG_LEVEL, + anchor_channels_config: Some(AnchorChannelsConfig::default()), + } + } +} + +/// Configuration options pertaining to 'Anchor' channels, i.e., channels for which the +/// `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 +/// opening. This required to estimate what fee rate would be sufficient to still have the +/// closing transactions be spendable on-chain (i.e., not be considered dust). This legacy +/// design of pre-anchor channels proved inadequate in the unpredictable, often turbulent, fee +/// markets we experience today. In contrast, Anchor channels allow to determine an adequate +/// fee rate *at the time of channel closure*, making them much more robust in the face of fee +/// spikes. In turn, they require to maintain a reserve of on-chain funds to be able to get the +/// channel closure transactions confirmed on-chain, at least if the channel counterparty can't +/// be trusted to do this for us. +/// +/// See [BOLT 3] for more technical details on Anchor channels. +/// +/// +/// ### Defaults +/// +/// | Parameter | Value | +/// |----------------------------|--------| +/// | `trusted_peers_no_reserve` | [] | +/// | `per_channel_reserve_sats` | 25000 | +/// +/// +/// [BOLT 3]: https://github.com/lightning/bolts/blob/master/03-transactions.md#htlc-timeout-and-htlc-success-transactions +#[derive(Debug, Clone)] +pub struct AnchorChannelsConfig { + /// A list of peers which we trust to get the required channel closing transactions confirmed + /// on-chain. + /// + /// Channels with these peers won't count towards the retained on-chain reserve and we won't + /// take any action to get the required transactions confirmed ourselves. + /// + /// **Note:** Trusting the channel counterparty to take the necessary actions to get the + /// required Anchor spending and HTLC transactions confirmed on-chain is potentially insecure + /// as the channel may not be closed if they refuse to do so, potentially leaving the user + /// funds stuck *or* even allow the counterparty to steal any in-flight funds after the + /// corresponding HTLCs time out. + pub trusted_peers_no_reserve: Vec, + /// The amount of satoshis we keep as an emergency reserve in our on-chain wallet in order to + /// be able to get the required Anchor output spending and HTLC transactions confirmed when the + /// channel is closed. + /// + /// **Note:** Depending on the fee market at the time of closure, this reserve amount might or + /// might not suffice to successfully spend the Anchor output and have the HTLC transactions + /// confirmed on-chain, i.e., you may want to adjust this value accordingly. + pub per_channel_reserve_sats: u64, +} + +impl Default for AnchorChannelsConfig { + fn default() -> Self { + Self { + trusted_peers_no_reserve: Vec::new(), + per_channel_reserve_sats: DEFAULT_ANCHOR_PER_CHANNEL_RESERVE_SATS, } } } diff --git a/src/connection.rs b/src/connection.rs new file mode 100644 index 000000000..4c0f7a47f --- /dev/null +++ b/src/connection.rs @@ -0,0 +1,148 @@ +use crate::logger::{log_error, log_info, Logger}; +use crate::types::PeerManager; +use crate::Error; + +use lightning::ln::msgs::SocketAddress; + +use bitcoin::secp256k1::PublicKey; + +use std::net::ToSocketAddrs; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +pub(crate) struct ConnectionManager +where + L::Target: Logger, +{ + pending_connections: + Mutex>>)>>, + peer_manager: Arc, + logger: L, +} + +impl ConnectionManager +where + L::Target: Logger, +{ + pub(crate) fn new(peer_manager: Arc, logger: L) -> Self { + let pending_connections = Mutex::new(Vec::new()); + Self { pending_connections, peer_manager, logger } + } + + pub(crate) async fn connect_peer_if_necessary( + &self, node_id: PublicKey, addr: SocketAddress, + ) -> Result<(), Error> { + if self.peer_manager.peer_by_node_id(&node_id).is_some() { + return Ok(()); + } + + self.do_connect_peer(node_id, addr).await + } + + pub(crate) async fn do_connect_peer( + &self, node_id: PublicKey, addr: SocketAddress, + ) -> Result<(), Error> { + // First, we check if there is already an outbound connection in flight, if so, we just + // await on the corresponding watch channel. The task driving the connection future will + // send us the result.. + let pending_ready_receiver_opt = self.register_or_subscribe_pending_connection(&node_id); + if let Some(pending_connection_ready_receiver) = pending_ready_receiver_opt { + return pending_connection_ready_receiver.await.map_err(|e| { + debug_assert!(false, "Failed to receive connection result: {:?}", e); + log_error!(self.logger, "Failed to receive connection result: {:?}", e); + Error::ConnectionFailed + })?; + } + + log_info!(self.logger, "Connecting to peer: {}@{}", node_id, addr); + + let socket_addr = addr + .to_socket_addrs() + .map_err(|e| { + log_error!(self.logger, "Failed to resolve network address: {}", e); + self.propagate_result_to_subscribers(&node_id, Err(Error::InvalidSocketAddress)); + Error::InvalidSocketAddress + })? + .next() + .ok_or_else(|| { + self.propagate_result_to_subscribers(&node_id, Err(Error::ConnectionFailed)); + Error::ConnectionFailed + })?; + + let connection_future = lightning_net_tokio::connect_outbound( + Arc::clone(&self.peer_manager), + node_id, + socket_addr, + ); + + let res = match connection_future.await { + Some(connection_closed_future) => { + let mut connection_closed_future = Box::pin(connection_closed_future); + loop { + tokio::select! { + _ = &mut connection_closed_future => { + log_info!(self.logger, "Peer connection closed: {}@{}", node_id, addr); + break Err(Error::ConnectionFailed); + }, + _ = tokio::time::sleep(Duration::from_millis(10)) => {}, + }; + + match self.peer_manager.peer_by_node_id(&node_id) { + Some(_) => break Ok(()), + None => continue, + } + } + }, + None => { + log_error!(self.logger, "Failed to connect to peer: {}@{}", node_id, addr); + Err(Error::ConnectionFailed) + }, + }; + + self.propagate_result_to_subscribers(&node_id, res); + + res + } + + fn register_or_subscribe_pending_connection( + &self, node_id: &PublicKey, + ) -> Option>> { + let mut pending_connections_lock = self.pending_connections.lock().unwrap(); + if let Some((_, connection_ready_senders)) = + pending_connections_lock.iter_mut().find(|(id, _)| id == node_id) + { + let (tx, rx) = tokio::sync::oneshot::channel(); + connection_ready_senders.push(tx); + Some(rx) + } else { + pending_connections_lock.push((*node_id, Vec::new())); + None + } + } + + fn propagate_result_to_subscribers(&self, node_id: &PublicKey, res: Result<(), Error>) { + // Send the result to any other tasks that might be waiting on it by now. + let mut pending_connections_lock = self.pending_connections.lock().unwrap(); + if let Some((_, connection_ready_senders)) = pending_connections_lock + .iter() + .position(|(id, _)| id == node_id) + .map(|i| pending_connections_lock.remove(i)) + { + for sender in connection_ready_senders { + let _ = sender.send(res).map_err(|e| { + debug_assert!( + false, + "Failed to send connection result to subscribers: {:?}", + e + ); + log_error!( + self.logger, + "Failed to send connection result to subscribers: {:?}", + e + ); + }); + } + } + } +} diff --git a/src/error.rs b/src/error.rs index 730e95460..bc6fbf23e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,6 @@ use std::fmt; -#[derive(Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] /// An error that possibly needs to be handled by the user. pub enum Error { /// Returned when trying to start [`crate::Node`] while it is already running. @@ -47,6 +47,8 @@ pub enum Error { InvalidPublicKey, /// The given secret key is invalid. InvalidSecretKey, + /// The given payment id is invalid. + InvalidPaymentId, /// The given payment hash is invalid. InvalidPaymentHash, /// The given payment pre-image is invalid. @@ -102,6 +104,7 @@ impl fmt::Display for Error { Self::InvalidSocketAddress => write!(f, "The given network address is invalid."), Self::InvalidPublicKey => write!(f, "The given public key is invalid."), Self::InvalidSecretKey => write!(f, "The given secret key is invalid."), + Self::InvalidPaymentId => write!(f, "The given payment id is invalid."), Self::InvalidPaymentHash => write!(f, "The given payment hash is invalid."), Self::InvalidPaymentPreimage => write!(f, "The given payment preimage is invalid."), Self::InvalidPaymentSecret => write!(f, "The given payment secret is invalid."), diff --git a/src/event.rs b/src/event.rs index 4d3955f7b..062ff1573 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,26 +1,29 @@ -use crate::types::{Sweeper, Wallet}; +use crate::types::{DynStore, Sweeper, Wallet}; use crate::{ - hex_utils, ChannelManager, Config, Error, NetworkGraph, PeerInfo, PeerStore, UserChannelId, + hex_utils, BumpTransactionEventHandler, ChannelManager, Config, Error, NetworkGraph, PeerInfo, + PeerStore, UserChannelId, }; -use crate::payment_store::{ - PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentStatus, PaymentStore, +use crate::payment::payment_store::{ + PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, + PaymentStore, }; use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, }; -use crate::logger::{log_error, log_info, Logger}; +use crate::logger::{log_debug, log_error, log_info, Logger}; use lightning::chain::chaininterface::ConfirmationTarget; +use lightning::events::bump_transaction::BumpTransactionEvent; use lightning::events::{ClosureReason, PaymentPurpose}; use lightning::events::{Event as LdkEvent, PaymentFailureReason}; use lightning::impl_writeable_tlv_based_enum; +use lightning::ln::channelmanager::PaymentId; use lightning::ln::{ChannelId, PaymentHash}; use lightning::routing::gossip::NodeId; use lightning::util::errors::APIError; -use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use lightning_liquidity::lsps2::utils::compute_opening_fee; @@ -45,11 +48,21 @@ use std::time::Duration; pub enum Event { /// A sent payment was successful. PaymentSuccessful { + /// A local identifier used to track the payment. + /// + /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. + payment_id: Option, /// The hash of the payment. payment_hash: PaymentHash, + /// The total fee which was spent at intermediate hops in this payment. + fee_paid_msat: Option, }, /// A sent payment has failed. PaymentFailed { + /// A local identifier used to track the payment. + /// + /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. + payment_id: Option, /// The hash of the payment. payment_hash: PaymentHash, /// The reason why the payment failed. @@ -59,6 +72,10 @@ pub enum Event { }, /// A payment has been received. PaymentReceived { + /// A local identifier used to track the payment. + /// + /// Will only be `None` for events serialized with LDK Node v0.2.1 or prior. + payment_id: Option, /// The hash of the payment. payment_hash: PaymentHash, /// The value, in thousandths of a satoshi, that has been received. @@ -106,13 +123,17 @@ pub enum Event { impl_writeable_tlv_based_enum!(Event, (0, PaymentSuccessful) => { (0, payment_hash, required), + (1, fee_paid_msat, option), + (3, payment_id, option), }, (1, PaymentFailed) => { (0, payment_hash, required), (1, reason, option), + (3, payment_id, option), }, (2, PaymentReceived) => { (0, payment_hash, required), + (1, payment_id, option), (2, amount_msat, required), }, (3, ChannelReady) => { @@ -135,22 +156,22 @@ impl_writeable_tlv_based_enum!(Event, }; ); -pub struct EventQueue +pub struct EventQueue where L::Target: Logger, { queue: Arc>>, waker: Arc>>, notifier: Condvar, - kv_store: Arc, + kv_store: Arc, logger: L, } -impl EventQueue +impl EventQueue where L::Target: Logger, { - pub(crate) fn new(kv_store: Arc, logger: L) -> Self { + pub(crate) fn new(kv_store: Arc, logger: L) -> Self { let queue = Arc::new(Mutex::new(VecDeque::new())); let waker = Arc::new(Mutex::new(None)); let notifier = Condvar::new(); @@ -225,13 +246,13 @@ where } } -impl ReadableArgs<(Arc, L)> for EventQueue +impl ReadableArgs<(Arc, L)> for EventQueue where L::Target: Logger, { #[inline] fn read( - reader: &mut R, args: (Arc, L), + reader: &mut R, args: (Arc, L), ) -> Result { let (kv_store, logger) = args; let read_queue: EventQueueDeserWrapper = Readable::read(reader)?; @@ -289,36 +310,39 @@ impl Future for EventFuture { } } -pub(crate) struct EventHandler +pub(crate) struct EventHandler where L::Target: Logger, { - event_queue: Arc>, + event_queue: Arc>, wallet: Arc, - channel_manager: Arc>, - output_sweeper: Arc>, + bump_tx_event_handler: Arc, + channel_manager: Arc, + output_sweeper: Arc, network_graph: Arc, - payment_store: Arc>, - peer_store: Arc>, + payment_store: Arc>, + peer_store: Arc>, runtime: Arc>>, logger: L, config: Arc, } -impl EventHandler +impl EventHandler where L::Target: Logger, { pub fn new( - event_queue: Arc>, wallet: Arc, - channel_manager: Arc>, output_sweeper: Arc>, - network_graph: Arc, payment_store: Arc>, - peer_store: Arc>, runtime: Arc>>, + event_queue: Arc>, wallet: Arc, + bump_tx_event_handler: Arc, + channel_manager: Arc, output_sweeper: Arc, + network_graph: Arc, payment_store: Arc>, + peer_store: Arc>, runtime: Arc>>, logger: L, config: Arc, ) -> Self { Self { event_queue, wallet, + bump_tx_event_handler, channel_manager, output_sweeper, network_graph, @@ -344,7 +368,7 @@ where let confirmation_target = ConfirmationTarget::NonAnchorChannelFee; // We set nLockTime to the current height to discourage fee sniping. - let cur_height = self.channel_manager.current_best_block().height(); + let cur_height = self.channel_manager.current_best_block().height; let locktime = LockTime::from_height(cur_height).unwrap_or(LockTime::ZERO); // Sign the final funding transaction and broadcast it. @@ -409,7 +433,8 @@ where onion_fields: _, counterparty_skimmed_fee_msat, } => { - if let Some(info) = self.payment_store.get(&payment_hash) { + let payment_id = PaymentId(payment_hash.0); + if let Some(info) = self.payment_store.get(&payment_id) { if info.status == PaymentStatus::Succeeded { log_info!( self.logger, @@ -421,7 +446,7 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); @@ -430,17 +455,22 @@ where return; } - let max_total_opening_fee_msat = info - .lsp_fee_limits - .and_then(|l| { - l.max_total_opening_fee_msat.or_else(|| { - l.max_proportional_opening_fee_ppm_msat.and_then(|max_prop_fee| { - // If it's a variable amount payment, compute the actual fee. - compute_opening_fee(amount_msat, 0, max_prop_fee) + let max_total_opening_fee_msat = match info.kind { + PaymentKind::Bolt11Jit { lsp_fee_limits, .. } => { + lsp_fee_limits + .max_total_opening_fee_msat + .or_else(|| { + lsp_fee_limits.max_proportional_opening_fee_ppm_msat.and_then( + |max_prop_fee| { + // If it's a variable amount payment, compute the actual fee. + compute_opening_fee(amount_msat, 0, max_prop_fee) + }, + ) }) - }) - }) - .unwrap_or(0); + .unwrap_or(0) + }, + _ => 0, + }; if counterparty_skimmed_fee_msat > max_total_opening_fee_msat { log_info!( @@ -454,7 +484,7 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); @@ -495,7 +525,7 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); @@ -517,6 +547,7 @@ where hex_utils::to_string(&payment_hash.0), amount_msat, ); + let payment_id = PaymentId(payment_hash.0); match purpose { PaymentPurpose::InvoicePayment { payment_preimage, payment_secret, .. } => { let update = PaymentDetailsUpdate { @@ -524,7 +555,7 @@ where secret: Some(Some(payment_secret)), amount_msat: Some(Some(amount_msat)), status: Some(PaymentStatus::Succeeded), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; match self.payment_store.update(&update) { Ok(true) => (), @@ -548,15 +579,16 @@ where } }, PaymentPurpose::SpontaneousPayment(preimage) => { - let payment = PaymentDetails { - preimage: Some(preimage), + let kind = PaymentKind::Spontaneous { hash: payment_hash, - secret: None, + preimage: Some(preimage), + }; + let payment = PaymentDetails { + id: payment_id, + kind, amount_msat: Some(amount_msat), direction: PaymentDirection::Inbound, status: PaymentStatus::Succeeded, - lsp_fee_limits: None, - bolt11_invoice: None, last_update: 0, }; @@ -584,20 +616,42 @@ where }; self.event_queue - .add_event(Event::PaymentReceived { payment_hash, amount_msat }) + .add_event(Event::PaymentReceived { + payment_id: Some(payment_id), + payment_hash, + amount_msat, + }) .unwrap_or_else(|e| { log_error!(self.logger, "Failed to push to event queue: {}", e); panic!("Failed to push to event queue"); }); }, - LdkEvent::PaymentSent { payment_preimage, payment_hash, fee_paid_msat, .. } => { - if let Some(mut payment) = self.payment_store.get(&payment_hash) { - payment.preimage = Some(payment_preimage); - payment.status = PaymentStatus::Succeeded; - self.payment_store.insert(payment.clone()).unwrap_or_else(|e| { - log_error!(self.logger, "Failed to access payment store: {}", e); - panic!("Failed to access payment store"); - }); + LdkEvent::PaymentSent { + payment_id, + payment_preimage, + payment_hash, + fee_paid_msat, + .. + } => { + let payment_id = if let Some(id) = payment_id { + id + } else { + debug_assert!(false, "payment_id should always be set."); + return; + }; + + let update = PaymentDetailsUpdate { + preimage: Some(Some(payment_preimage)), + status: Some(PaymentStatus::Succeeded), + ..PaymentDetailsUpdate::new(payment_id) + }; + + self.payment_store.update(&update).unwrap_or_else(|e| { + log_error!(self.logger, "Failed to access payment store: {}", e); + panic!("Failed to access payment store"); + }); + + self.payment_store.get(&payment_id).map(|payment| { log_info!( self.logger, "Successfully sent payment of {}msat{} from \ @@ -611,15 +665,20 @@ where hex_utils::to_string(&payment_hash.0), hex_utils::to_string(&payment_preimage.0) ); - } + }); + self.event_queue - .add_event(Event::PaymentSuccessful { payment_hash }) + .add_event(Event::PaymentSuccessful { + payment_id: Some(payment_id), + payment_hash, + fee_paid_msat, + }) .unwrap_or_else(|e| { log_error!(self.logger, "Failed to push to event queue: {}", e); panic!("Failed to push to event queue"); }); }, - LdkEvent::PaymentFailed { payment_hash, reason, .. } => { + LdkEvent::PaymentFailed { payment_id, payment_hash, reason, .. } => { log_info!( self.logger, "Failed to send payment to payment hash {:?} due to {:?}.", @@ -629,14 +688,18 @@ where let update = PaymentDetailsUpdate { status: Some(PaymentStatus::Failed), - ..PaymentDetailsUpdate::new(payment_hash) + ..PaymentDetailsUpdate::new(payment_id) }; self.payment_store.update(&update).unwrap_or_else(|e| { log_error!(self.logger, "Failed to access payment store: {}", e); panic!("Failed to access payment store"); }); self.event_queue - .add_event(Event::PaymentFailed { payment_hash, reason }) + .add_event(Event::PaymentFailed { + payment_id: Some(payment_id), + payment_hash, + reason, + }) .unwrap_or_else(|e| { log_error!(self.logger, "Failed to push to event queue: {}", e); panic!("Failed to push to event queue"); @@ -671,9 +734,68 @@ where temporary_channel_id, counterparty_node_id, funding_satoshis, - channel_type: _, + channel_type, push_msat: _, } => { + let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); + + if anchor_channel { + if let Some(anchor_channels_config) = + self.config.anchor_channels_config.as_ref() + { + let cur_anchor_reserve_sats = crate::total_anchor_channels_reserve_sats( + &self.channel_manager, + &self.config, + ); + let spendable_amount_sats = self + .wallet + .get_balances(cur_anchor_reserve_sats) + .map(|(_, s)| s) + .unwrap_or(0); + + let required_amount_sats = if anchor_channels_config + .trusted_peers_no_reserve + .contains(&counterparty_node_id) + { + 0 + } else { + anchor_channels_config.per_channel_reserve_sats + }; + + if spendable_amount_sats < required_amount_sats { + log_error!( + self.logger, + "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves.", + counterparty_node_id, + ); + self.channel_manager + .force_close_without_broadcasting_txn( + &temporary_channel_id, + &counterparty_node_id, + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return; + } + } else { + log_error!( + self.logger, + "Rejecting inbound channel from peer {} due to Anchor channels being disabled.", + counterparty_node_id, + ); + self.channel_manager + .force_close_without_broadcasting_txn( + &temporary_channel_id, + &counterparty_node_id, + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return; + } + } + let user_channel_id: u128 = rand::thread_rng().gen::(); let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id); let res = if allow_0conf { @@ -694,8 +816,9 @@ where Ok(()) => { log_info!( self.logger, - "Accepting inbound{} channel of {}sats from{} peer {}", + "Accepting inbound{}{} channel of {}sats from{} peer {}", if allow_0conf { " 0conf" } else { "" }, + if anchor_channel { " Anchor" } else { "" }, funding_satoshis, if allow_0conf { " trusted" } else { "" }, counterparty_node_id, @@ -704,8 +827,9 @@ where Err(e) => { log_error!( self.logger, - "Error while accepting inbound{} channel from{} peer {}: {:?}", + "Error while accepting inbound{}{} channel from{} peer {}: {:?}", if allow_0conf { " 0conf" } else { "" }, + if anchor_channel { " Anchor" } else { "" }, counterparty_node_id, if allow_0conf { " trusted" } else { "" }, e, @@ -716,9 +840,10 @@ where LdkEvent::PaymentForwarded { prev_channel_id, next_channel_id, - fee_earned_msat, + total_fee_earned_msat, claim_from_onchain_tx, outbound_amount_forwarded_msat, + .. } => { let read_only_network_graph = self.network_graph.read_only(); let nodes = read_only_network_graph.nodes(); @@ -751,7 +876,7 @@ where let to_next_str = format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); - let fee_earned = fee_earned_msat.unwrap_or(0); + let fee_earned = total_fee_earned_msat.unwrap_or(0); let outbound_amount_forwarded_msat = outbound_amount_forwarded_msat.unwrap_or(0); if claim_from_onchain_tx { log_info!( @@ -779,6 +904,7 @@ where former_temporary_channel_id, counterparty_node_id, funding_txo, + .. } => { log_info!( self.logger, @@ -872,8 +998,37 @@ where }, LdkEvent::DiscardFunding { .. } => {}, LdkEvent::HTLCIntercepted { .. } => {}, - LdkEvent::BumpTransaction(_) => {}, + LdkEvent::BumpTransaction(bte) => { + let (channel_id, counterparty_node_id) = match bte { + BumpTransactionEvent::ChannelClose { + ref channel_id, + ref counterparty_node_id, + .. + } => (channel_id, counterparty_node_id), + BumpTransactionEvent::HTLCResolution { + ref channel_id, + ref counterparty_node_id, + .. + } => (channel_id, counterparty_node_id), + }; + + if let Some(anchor_channels_config) = self.config.anchor_channels_config.as_ref() { + if anchor_channels_config + .trusted_peers_no_reserve + .contains(counterparty_node_id) + { + log_debug!(self.logger, + "Ignoring BumpTransactionEvent for channel {} due to trusted counterparty {}", + channel_id, counterparty_node_id + ); + return; + } + } + + self.bump_tx_event_handler.handle_event(&bte); + }, LdkEvent::InvoiceRequestFailed { .. } => {}, + LdkEvent::InvoiceGenerated { .. } => {}, LdkEvent::ConnectionNeeded { .. } => {}, } } @@ -888,7 +1043,7 @@ mod tests { #[tokio::test] async fn event_queue_persistence() { - let store = Arc::new(TestStore::new(false)); + let store: Arc = Arc::new(TestStore::new(false)); let logger = Arc::new(TestLogger::new()); let event_queue = Arc::new(EventQueue::new(Arc::clone(&store), Arc::clone(&logger))); assert_eq!(event_queue.next_event(), None); @@ -925,7 +1080,7 @@ mod tests { #[tokio::test] async fn event_queue_concurrency() { - let store = Arc::new(TestStore::new(false)); + let store: Arc = Arc::new(TestStore::new(false)); let logger = Arc::new(TestLogger::new()); let event_queue = Arc::new(EventQueue::new(Arc::clone(&store), Arc::clone(&logger))); assert_eq!(event_queue.next_event(), None); diff --git a/src/io/utils.rs b/src/io/utils.rs index f486dda8b..7913600f3 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -4,16 +4,17 @@ use crate::config::WALLET_KEYS_SEED_LEN; use crate::logger::log_error; use crate::peer_store::PeerStore; use crate::sweep::SpendableOutputInfo; +use crate::types::DynStore; use crate::{Error, EventQueue, PaymentDetails}; use lightning::routing::gossip::NetworkGraph; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringDecayParameters}; use lightning::util::logger::Logger; use lightning::util::persist::{ - KVStore, KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, - NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, - NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY, - SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, + KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, NETWORK_GRAPH_PERSISTENCE_KEY, + NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, + SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, + SCORER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; use lightning::util::string::PrintableString; @@ -93,8 +94,8 @@ where } /// Read a previously persisted [`NetworkGraph`] from the store. -pub(crate) fn read_network_graph( - kv_store: Arc, logger: L, +pub(crate) fn read_network_graph( + kv_store: Arc, logger: L, ) -> Result, std::io::Error> where L::Target: Logger, @@ -111,12 +112,8 @@ where } /// Read a previously persisted [`ProbabilisticScorer`] from the store. -pub(crate) fn read_scorer< - K: KVStore + Send + Sync, - G: Deref>, - L: Deref + Clone, ->( - kv_store: Arc, network_graph: G, logger: L, +pub(crate) fn read_scorer>, L: Deref + Clone>( + kv_store: Arc, network_graph: G, logger: L, ) -> Result, std::io::Error> where L::Target: Logger, @@ -135,9 +132,9 @@ where } /// Read previously persisted events from the store. -pub(crate) fn read_event_queue( - kv_store: Arc, logger: L, -) -> Result, std::io::Error> +pub(crate) fn read_event_queue( + kv_store: Arc, logger: L, +) -> Result, std::io::Error> where L::Target: Logger, { @@ -153,9 +150,9 @@ where } /// Read previously persisted peer info from the store. -pub(crate) fn read_peer_info( - kv_store: Arc, logger: L, -) -> Result, std::io::Error> +pub(crate) fn read_peer_info( + kv_store: Arc, logger: L, +) -> Result, std::io::Error> where L::Target: Logger, { @@ -171,8 +168,8 @@ where } /// Read previously persisted payments information from the store. -pub(crate) fn read_payments( - kv_store: Arc, logger: L, +pub(crate) fn read_payments( + kv_store: Arc, logger: L, ) -> Result, std::io::Error> where L::Target: Logger, @@ -201,8 +198,8 @@ where } /// Read previously persisted spendable output information from the store. -pub(crate) fn read_spendable_outputs( - kv_store: Arc, logger: L, +pub(crate) fn read_spendable_outputs( + kv_store: Arc, logger: L, ) -> Result, std::io::Error> where L::Target: Logger, @@ -230,8 +227,8 @@ where Ok(res) } -pub(crate) fn read_latest_rgs_sync_timestamp( - kv_store: Arc, logger: L, +pub(crate) fn read_latest_rgs_sync_timestamp( + kv_store: Arc, logger: L, ) -> Result where L::Target: Logger, @@ -250,8 +247,8 @@ where }) } -pub(crate) fn write_latest_rgs_sync_timestamp( - updated_timestamp: u32, kv_store: Arc, logger: L, +pub(crate) fn write_latest_rgs_sync_timestamp( + updated_timestamp: u32, kv_store: Arc, logger: L, ) -> Result<(), Error> where L::Target: Logger, @@ -277,8 +274,8 @@ where }) } -pub(crate) fn read_latest_node_ann_bcast_timestamp( - kv_store: Arc, logger: L, +pub(crate) fn read_latest_node_ann_bcast_timestamp( + kv_store: Arc, logger: L, ) -> Result where L::Target: Logger, @@ -301,8 +298,8 @@ where }) } -pub(crate) fn write_latest_node_ann_bcast_timestamp( - updated_timestamp: u64, kv_store: Arc, logger: L, +pub(crate) fn write_latest_node_ann_bcast_timestamp( + updated_timestamp: u64, kv_store: Arc, logger: L, ) -> Result<(), Error> where L::Target: Logger, diff --git a/src/lib.rs b/src/lib.rs index 9c7c3dcc6..d51a2e0fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ //! The primary abstraction of the library is the [`Node`], which can be retrieved by setting up //! and configuring a [`Builder`] to your liking and calling [`build`]. `Node` can then be //! controlled via commands such as [`start`], [`stop`], [`connect_open_channel`], -//! [`send_payment`], etc.: +//! [`send`], etc.: //! //! ```no_run //! use ldk_node::Builder; @@ -43,7 +43,7 @@ //! //! node.start().unwrap(); //! -//! let funding_address = node.new_onchain_address(); +//! let funding_address = node.onchain_payment().new_address(); //! //! // .. fund address .. //! @@ -56,7 +56,7 @@ //! node.event_handled(); //! //! let invoice = Bolt11Invoice::from_str("INVOICE_STR").unwrap(); -//! node.send_payment(&invoice).unwrap(); +//! node.bolt11_payment().send(&invoice).unwrap(); //! //! node.stop().unwrap(); //! } @@ -66,7 +66,7 @@ //! [`start`]: Node::start //! [`stop`]: Node::stop //! [`connect_open_channel`]: Node::connect_open_channel -//! [`send_payment`]: Node::send_payment +//! [`send`]: Bolt11Payment::send //! #![cfg_attr(not(feature = "uniffi"), deny(missing_docs))] #![deny(rustdoc::broken_intra_doc_links)] @@ -78,6 +78,7 @@ mod balance; mod builder; mod config; +mod connection; mod error; mod event; mod fee_estimator; @@ -87,7 +88,7 @@ pub mod io; mod liquidity; mod logger; mod message_handler; -mod payment_store; +pub mod payment; mod peer_store; mod sweep; mod tx_broadcaster; @@ -102,7 +103,7 @@ pub use lightning; pub use lightning_invoice; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; -pub use config::{default_config, Config}; +pub use config::{default_config, AnchorChannelsConfig, Config}; pub use error::Error as NodeError; use error::Error; @@ -121,32 +122,31 @@ pub use builder::BuildError; pub use builder::NodeBuilder as Builder; use config::{ - LDK_PAYMENT_RETRY_TIMEOUT, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, - RGS_SYNC_INTERVAL, WALLET_SYNC_INTERVAL_MINIMUM_SECS, + NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, + WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; +use connection::ConnectionManager; use event::{EventHandler, EventQueue}; use gossip::GossipSource; use liquidity::LiquiditySource; -use payment_store::PaymentStore; -pub use payment_store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus}; +use payment::payment_store::PaymentStore; +use payment::{Bolt11Payment, OnchainPayment, PaymentDetails, SpontaneousPayment}; use peer_store::{PeerInfo, PeerStore}; use types::{ - Broadcaster, ChainMonitor, ChannelManager, FeeEstimator, KeysManager, NetworkGraph, - PeerManager, Router, Scorer, Sweeper, Wallet, + Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator, + KeysManager, NetworkGraph, PeerManager, Router, Scorer, Sweeper, Wallet, }; -pub use types::{ChannelDetails, PeerDetails, TlvEntry, UserChannelId}; +pub use types::{ChannelDetails, ChannelType, PeerDetails, TlvEntry, UserChannelId}; use logger::{log_error, log_info, log_trace, FilesystemLogger, Logger}; -use lightning::chain::Confirm; -use lightning::ln::channelmanager::{self, PaymentId, RecipientOnionFields, Retry}; +use lightning::chain::{BestBlock, Confirm}; +use lightning::events::bump_transaction::Wallet as LdkWallet; +use lightning::ln::channelmanager::{ChannelShutdownState, PaymentId}; use lightning::ln::msgs::SocketAddress; -use lightning::ln::{PaymentHash, PaymentPreimage}; - -use lightning::sign::EntropySource; use lightning::util::persist::{ - KVStore, NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, + NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, }; @@ -158,15 +158,8 @@ use lightning_background_processor::process_events_async; use lightning_transaction_sync::EsploraSyncClient; -use lightning::routing::router::{PaymentParameters, RouteParameters}; -use lightning_invoice::{payment, Bolt11Invoice, Currency}; - -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Address, Txid}; - use rand::Rng; use crate::io::{ @@ -175,8 +168,9 @@ use crate::io::{ }; use std::default::Default; use std::net::ToSocketAddrs; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; #[cfg(feature = "uniffi")] uniffi::include_scaffolding!("ldk_node"); @@ -184,33 +178,39 @@ uniffi::include_scaffolding!("ldk_node"); /// The main interface object of LDK Node, wrapping the necessary LDK and BDK functionalities. /// /// Needs to be initialized and instantiated through [`Builder::build`]. -pub struct Node { +pub struct Node { runtime: Arc>>, stop_sender: tokio::sync::watch::Sender<()>, - stop_receiver: tokio::sync::watch::Receiver<()>, config: Arc, wallet: Arc, tx_sync: Arc>>, tx_broadcaster: Arc, fee_estimator: Arc, - event_queue: Arc>>, - channel_manager: Arc>, - chain_monitor: Arc>, - output_sweeper: Arc>, - peer_manager: Arc>, + event_queue: Arc>>, + channel_manager: Arc, + chain_monitor: Arc, + output_sweeper: Arc, + peer_manager: Arc, + connection_manager: Arc>>, keys_manager: Arc, network_graph: Arc, gossip_source: Arc, - liquidity_source: Option>>>, - kv_store: Arc, + liquidity_source: Option>>>, + kv_store: Arc, logger: Arc, _router: Arc, scorer: Arc>, - peer_store: Arc>>, - payment_store: Arc>>, + peer_store: Arc>>, + payment_store: Arc>>, + is_listening: Arc, + latest_wallet_sync_timestamp: Arc>>, + latest_onchain_wallet_sync_timestamp: Arc>>, + latest_fee_rate_cache_update_timestamp: Arc>>, + latest_rgs_snapshot_timestamp: Arc>>, + latest_node_announcement_broadcast_timestamp: Arc>>, } -impl Node { +impl Node { /// Starts the necessary background tasks, such as handling events coming from user input, /// LDK/BDK, and the peer-to-peer network. /// @@ -231,6 +231,8 @@ impl Node { // Block to ensure we update our fee rate cache once on startup let fee_estimator = Arc::clone(&self.fee_estimator); let sync_logger = Arc::clone(&self.logger); + let sync_fee_rate_update_timestamp = + Arc::clone(&self.latest_fee_rate_cache_update_timestamp); let runtime_ref = &runtime; tokio::task::block_in_place(move || { runtime_ref.block_on(async move { @@ -242,6 +244,9 @@ impl Node { "Initial fee rate cache update finished in {}ms.", now.elapsed().as_millis() ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + *sync_fee_rate_update_timestamp.write().unwrap() = unix_time_secs_opt; Ok(()) }, Err(e) => { @@ -255,7 +260,8 @@ impl Node { // Setup wallet sync let wallet = Arc::clone(&self.wallet); let sync_logger = Arc::clone(&self.logger); - let mut stop_sync = self.stop_receiver.clone(); + let sync_onchain_wallet_timestamp = Arc::clone(&self.latest_onchain_wallet_sync_timestamp); + let mut stop_sync = self.stop_sender.subscribe(); let onchain_wallet_sync_interval_secs = self .config .onchain_wallet_sync_interval_secs @@ -276,11 +282,16 @@ impl Node { _ = onchain_wallet_sync_interval.tick() => { let now = Instant::now(); match wallet.sync().await { - Ok(()) => log_trace!( + Ok(()) => { + log_trace!( sync_logger, "Background sync of on-chain wallet finished in {}ms.", now.elapsed().as_millis() - ), + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + *sync_onchain_wallet_timestamp.write().unwrap() = unix_time_secs_opt; + } Err(err) => { log_error!( sync_logger, @@ -296,8 +307,9 @@ impl Node { ); }); - let mut stop_fee_updates = self.stop_receiver.clone(); + let mut stop_fee_updates = self.stop_sender.subscribe(); let fee_update_logger = Arc::clone(&self.logger); + let fee_update_timestamp = Arc::clone(&self.latest_fee_rate_cache_update_timestamp); let fee_estimator = Arc::clone(&self.fee_estimator); let fee_rate_cache_update_interval_secs = self.config.fee_rate_cache_update_interval_secs.max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); @@ -316,11 +328,16 @@ impl Node { _ = fee_rate_update_interval.tick() => { let now = Instant::now(); match fee_estimator.update_fee_estimates().await { - Ok(()) => log_trace!( + Ok(()) => { + log_trace!( fee_update_logger, "Background update of fee rate cache finished in {}ms.", now.elapsed().as_millis() - ), + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + *fee_update_timestamp.write().unwrap() = unix_time_secs_opt; + } Err(err) => { log_error!( fee_update_logger, @@ -339,7 +356,8 @@ impl Node { let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); let sync_logger = Arc::clone(&self.logger); - let mut stop_sync = self.stop_receiver.clone(); + let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp); + let mut stop_sync = self.stop_sender.subscribe(); let wallet_sync_interval_secs = self.config.wallet_sync_interval_secs.max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); runtime.spawn(async move { @@ -359,11 +377,16 @@ impl Node { ]; let now = Instant::now(); match tx_sync.sync(confirmables).await { - Ok(()) => log_trace!( + Ok(()) => { + log_trace!( sync_logger, "Background sync of Lightning wallet finished in {}ms.", now.elapsed().as_millis() - ), + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + *sync_wallet_timestamp.write().unwrap() = unix_time_secs_opt; + } Err(e) => { log_error!(sync_logger, "Background sync of Lightning wallet failed: {}", e) } @@ -377,7 +400,8 @@ impl Node { let gossip_source = Arc::clone(&self.gossip_source); let gossip_sync_store = Arc::clone(&self.kv_store); let gossip_sync_logger = Arc::clone(&self.logger); - let mut stop_gossip_sync = self.stop_receiver.clone(); + let gossip_rgs_sync_timestamp = Arc::clone(&self.latest_rgs_snapshot_timestamp); + let mut stop_gossip_sync = self.stop_sender.subscribe(); runtime.spawn(async move { let mut interval = tokio::time::interval(RGS_SYNC_INTERVAL); loop { @@ -404,6 +428,7 @@ impl Node { log_error!(gossip_sync_logger, "Persistence failed: {}", e); panic!("Persistence failed"); }); + *gossip_rgs_sync_timestamp.write().unwrap() = Some(updated_timestamp as u64); } Err(e) => log_error!( gossip_sync_logger, @@ -420,8 +445,9 @@ impl Node { if let Some(listening_addresses) = &self.config.listening_addresses { // Setup networking let peer_manager_connection_handler = Arc::clone(&self.peer_manager); - let mut stop_listen = self.stop_receiver.clone(); + let mut stop_listen = self.stop_sender.subscribe(); let listening_logger = Arc::clone(&self.logger); + let listening_indicator = Arc::clone(&self.is_listening); let mut bind_addrs = Vec::with_capacity(listening_addresses.len()); @@ -440,6 +466,7 @@ impl Node { } runtime.spawn(async move { + { let listener = tokio::net::TcpListener::bind(&*bind_addrs).await .unwrap_or_else(|e| { @@ -449,11 +476,13 @@ impl Node { ); }); + listening_indicator.store(true, Ordering::Release); + loop { let peer_mgr = Arc::clone(&peer_manager_connection_handler); tokio::select! { _ = stop_listen.changed() => { - return; + break; } res = listener.accept() => { let tcp_stream = res.unwrap().0; @@ -467,15 +496,18 @@ impl Node { } } } + } + + listening_indicator.store(false, Ordering::Release); }); } - // Regularly reconnect to channel peers. - let connect_cm = Arc::clone(&self.channel_manager); + // Regularly reconnect to persisted peers. + let connect_cm = Arc::clone(&self.connection_manager); let connect_pm = Arc::clone(&self.peer_manager); let connect_logger = Arc::clone(&self.logger); let connect_peer_store = Arc::clone(&self.peer_store); - let mut stop_connect = self.stop_receiver.clone(); + let mut stop_connect = self.stop_sender.subscribe(); runtime.spawn(async move { let mut interval = tokio::time::interval(PEER_RECONNECTION_INTERVAL); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); @@ -486,33 +518,25 @@ impl Node { } _ = interval.tick() => { let pm_peers = connect_pm - .get_peer_node_ids() + .list_peers() .iter() - .map(|(peer, _addr)| *peer) + .map(|peer| peer.counterparty_node_id) .collect::>(); - for node_id in connect_cm - .list_channels() - .iter() - .map(|chan| chan.counterparty.node_id) - .filter(|id| !pm_peers.contains(id)) - { - if let Some(peer_info) = connect_peer_store.get_peer(&node_id) { - let res = do_connect_peer( - peer_info.node_id, - peer_info.address, - Arc::clone(&connect_pm), - Arc::clone(&connect_logger), - ).await; - match res { - Ok(_) => { - log_info!(connect_logger, "Successfully reconnected to peer {}", node_id); - }, - Err(e) => { - log_error!(connect_logger, "Failed to reconnect to peer {}: {}", node_id, e); - } - } - } + + for peer_info in connect_peer_store.list_peers().iter().filter(|info| !pm_peers.contains(&info.node_id)) { + let res = connect_cm.do_connect_peer( + peer_info.node_id, + peer_info.address.clone(), + ).await; + match res { + Ok(_) => { + log_info!(connect_logger, "Successfully reconnected to peer {}", peer_info.node_id); + }, + Err(e) => { + log_error!(connect_logger, "Failed to reconnect to peer {}: {}", peer_info.node_id, e); } + } + } } } } @@ -524,7 +548,8 @@ impl Node { let bcast_config = Arc::clone(&self.config); let bcast_store = Arc::clone(&self.kv_store); let bcast_logger = Arc::clone(&self.logger); - let mut stop_bcast = self.stop_receiver.clone(); + let bcast_ann_timestamp = Arc::clone(&self.latest_node_announcement_broadcast_timestamp); + let mut stop_bcast = self.stop_sender.subscribe(); runtime.spawn(async move { // We check every 30 secs whether our last broadcast is NODE_ANN_BCAST_INTERVAL away. let mut interval = tokio::time::interval(Duration::from_secs(30)); @@ -555,7 +580,7 @@ impl Node { continue; } - if bcast_pm.get_peer_node_ids().is_empty() { + if bcast_pm.list_peers().is_empty() { // Skip if we don't have any connected peers to gossip to. continue; } @@ -569,18 +594,23 @@ impl Node { bcast_pm.broadcast_node_announcement([0; 3], [0; 32], addresses); - let unix_time_secs = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); - io::utils::write_latest_node_ann_bcast_timestamp(unix_time_secs, Arc::clone(&bcast_store), Arc::clone(&bcast_logger)) - .unwrap_or_else(|e| { - log_error!(bcast_logger, "Persistence failed: {}", e); - panic!("Persistence failed"); - }); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + *bcast_ann_timestamp.write().unwrap() = unix_time_secs_opt; + + if let Some(unix_time_secs) = unix_time_secs_opt { + io::utils::write_latest_node_ann_bcast_timestamp(unix_time_secs, Arc::clone(&bcast_store), Arc::clone(&bcast_logger)) + .unwrap_or_else(|e| { + log_error!(bcast_logger, "Persistence failed: {}", e); + panic!("Persistence failed"); + }); + } } } } }); - let mut stop_tx_bcast = self.stop_receiver.clone(); + let mut stop_tx_bcast = self.stop_sender.subscribe(); let tx_bcaster = Arc::clone(&self.tx_broadcaster); runtime.spawn(async move { // Every second we try to clear our broadcasting queue. @@ -598,9 +628,17 @@ impl Node { } }); + let bump_tx_event_handler = Arc::new(BumpTransactionEventHandler::new( + Arc::clone(&self.tx_broadcaster), + Arc::new(LdkWallet::new(Arc::clone(&self.wallet), Arc::clone(&self.logger))), + Arc::clone(&self.keys_manager), + Arc::clone(&self.logger), + )); + let event_handler = Arc::new(EventHandler::new( Arc::clone(&self.event_queue), Arc::clone(&self.wallet), + bump_tx_event_handler, Arc::clone(&self.channel_manager), Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), @@ -621,7 +659,7 @@ impl Node { let background_logger = Arc::clone(&self.logger); let background_error_logger = Arc::clone(&self.logger); let background_scorer = Arc::clone(&self.scorer); - let stop_bp = self.stop_receiver.clone(); + let stop_bp = self.stop_sender.subscribe(); let sleeper = move |d| { let mut stop = stop_bp.clone(); Box::pin(async move { @@ -658,7 +696,7 @@ impl Node { }); if let Some(liquidity_source) = self.liquidity_source.as_ref() { - let mut stop_liquidity_handler = self.stop_receiver.clone(); + let mut stop_liquidity_handler = self.stop_sender.subscribe(); let liquidity_handler = Arc::clone(&liquidity_source); runtime.spawn(async move { loop { @@ -678,11 +716,6 @@ impl Node { Ok(()) } - /// Returns whether the [`Node`] is running. - pub fn is_running(&self) -> bool { - self.runtime.read().unwrap().is_some() - } - /// Disconnects all peers, stops all running background tasks, and shuts down [`Node`]. /// /// After this returns most API methods will return [`Error::NotRunning`]. @@ -713,6 +746,37 @@ impl Node { Ok(()) } + /// Returns the status of the [`Node`]. + pub fn status(&self) -> NodeStatus { + let is_running = self.runtime.read().unwrap().is_some(); + let is_listening = self.is_listening.load(Ordering::Acquire); + let current_best_block = self.channel_manager.current_best_block().into(); + let latest_wallet_sync_timestamp = *self.latest_wallet_sync_timestamp.read().unwrap(); + let latest_onchain_wallet_sync_timestamp = + *self.latest_onchain_wallet_sync_timestamp.read().unwrap(); + let latest_fee_rate_cache_update_timestamp = + *self.latest_fee_rate_cache_update_timestamp.read().unwrap(); + let latest_rgs_snapshot_timestamp = *self.latest_rgs_snapshot_timestamp.read().unwrap(); + let latest_node_announcement_broadcast_timestamp = + *self.latest_node_announcement_broadcast_timestamp.read().unwrap(); + + NodeStatus { + is_running, + is_listening, + current_best_block, + latest_wallet_sync_timestamp, + latest_onchain_wallet_sync_timestamp, + latest_fee_rate_cache_update_timestamp, + latest_rgs_snapshot_timestamp, + latest_node_announcement_broadcast_timestamp, + } + } + + /// Returns the config with which the [`Node`] was initialized. + pub fn config(&self) -> Config { + self.config.as_ref().clone() + } + /// Returns the next event in the event queue, if currently available. /// /// Will return `Some(..)` if an event is available and `None` otherwise. @@ -764,38 +828,90 @@ impl Node { self.config.listening_addresses.clone() } - /// Retrieve a new on-chain/funding address. - pub fn new_onchain_address(&self) -> Result { - let funding_address = self.wallet.get_new_address()?; - log_info!(self.logger, "Generated new funding address: {}", funding_address); - Ok(funding_address) + /// Returns a payment handler allowing to create and pay [BOLT 11] invoices. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + #[cfg(not(feature = "uniffi"))] + pub fn bolt11_payment(&self) -> Bolt11Payment { + Bolt11Payment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + Arc::clone(&self.keys_manager), + self.liquidity_source.clone(), + Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) } - /// Send an on-chain payment to the given address. - pub fn send_to_onchain_address( - &self, address: &bitcoin::Address, amount_sats: u64, - ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } + /// Returns a payment handler allowing to create and pay [BOLT 11] invoices. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + #[cfg(feature = "uniffi")] + pub fn bolt11_payment(&self) -> Arc { + Arc::new(Bolt11Payment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.connection_manager), + Arc::clone(&self.keys_manager), + self.liquidity_source.clone(), + Arc::clone(&self.payment_store), + Arc::clone(&self.peer_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + )) + } - let cur_balance = self.wallet.get_balance()?; - if cur_balance.get_spendable() < amount_sats { - log_error!(self.logger, "Unable to send payment due to insufficient funds."); - return Err(Error::InsufficientFunds); - } - self.wallet.send_to_address(address, Some(amount_sats)) + /// Returns a payment handler allowing to send spontaneous ("keysend") payments. + #[cfg(not(feature = "uniffi"))] + pub fn spontaneous_payment(&self) -> SpontaneousPayment { + SpontaneousPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), + Arc::clone(&self.payment_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) } - /// Send an on-chain payment to the given address, draining all the available funds. - pub fn send_all_to_onchain_address(&self, address: &bitcoin::Address) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } + /// Returns a payment handler allowing to send spontaneous ("keysend") payments. + #[cfg(feature = "uniffi")] + pub fn spontaneous_payment(&self) -> Arc { + Arc::new(SpontaneousPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.channel_manager), + Arc::clone(&self.keys_manager), + Arc::clone(&self.payment_store), + Arc::clone(&self.config), + Arc::clone(&self.logger), + )) + } - self.wallet.send_to_address(address, None) + /// Returns a payment handler allowing to send and receive on-chain payments. + #[cfg(not(feature = "uniffi"))] + pub fn onchain_payment(&self) -> OnchainPayment { + OnchainPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.channel_manager), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) + } + + /// Returns a payment handler allowing to send and receive on-chain payments. + #[cfg(feature = "uniffi")] + pub fn onchain_payment(&self) -> Arc { + Arc::new(OnchainPayment::new( + Arc::clone(&self.runtime), + Arc::clone(&self.wallet), + Arc::clone(&self.channel_manager), + Arc::clone(&self.config), + Arc::clone(&self.logger), + )) } /// Retrieve a list of known channels. @@ -819,14 +935,13 @@ impl Node { let con_node_id = peer_info.node_id; let con_addr = peer_info.address.clone(); - let con_logger = Arc::clone(&self.logger); - let con_pm = Arc::clone(&self.peer_manager); + let con_cm = Arc::clone(&self.connection_manager); // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. tokio::task::block_in_place(move || { runtime.block_on(async move { - connect_peer_if_necessary(con_node_id, con_addr, con_pm, con_logger).await + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await }) })?; @@ -870,6 +985,10 @@ impl Node { /// channel counterparty on channel open. This can be useful to start out with the balance not /// entirely shifted to one side, therefore allowing to receive payments from the getgo. /// + /// If Anchor channels are enabled, this will ensure the configured + /// [`AnchorChannelsConfig::per_channel_reserve_sats`] is available and will be retained before + /// opening the channel. + /// /// Returns a [`UserChannelId`] allowing to locally keep track of the channel. pub fn connect_open_channel( &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, @@ -882,32 +1001,65 @@ impl Node { } let runtime = rt_lock.as_ref().unwrap(); - let cur_balance = self.wallet.get_balance()?; - if cur_balance.get_spendable() < channel_amount_sats { - log_error!(self.logger, "Unable to create channel due to insufficient funds."); - return Err(Error::InsufficientFunds); - } - let peer_info = PeerInfo { node_id, address }; let con_node_id = peer_info.node_id; let con_addr = peer_info.address.clone(); - let con_logger = Arc::clone(&self.logger); - let con_pm = Arc::clone(&self.peer_manager); + let con_cm = Arc::clone(&self.connection_manager); + + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0); + + // Fail early if we have less than the channel value available. + if spendable_amount_sats < channel_amount_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, channel_amount_sats + ); + return Err(Error::InsufficientFunds); + } // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. tokio::task::block_in_place(move || { runtime.block_on(async move { - connect_peer_if_necessary(con_node_id, con_addr, con_pm, con_logger).await + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await }) })?; + // Fail if we have less than the channel value + anchor reserve available (if applicable). + let init_features = self + .peer_manager + .peer_by_node_id(&node_id) + .ok_or(Error::ConnectionFailed)? + .init_features; + 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() + && !c.trusted_peers_no_reserve.contains(&node_id) + { + c.per_channel_reserve_sats + } else { + 0 + } + }); + + if spendable_amount_sats < required_funds_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, required_funds_sats + ); + return Err(Error::InsufficientFunds); + } + let channel_config = (*(channel_config.unwrap_or_default())).clone().into(); let user_config = UserConfig { channel_handshake_limits: Default::default(), channel_handshake_config: ChannelHandshakeConfig { announced_channel: announce_channel, + negotiate_anchors_zero_fee_htlc_tx: self.config.anchor_channels_config.is_some(), ..Default::default() }, channel_config, @@ -1004,27 +1156,66 @@ impl Node { } /// Close a previously opened channel. + /// + /// If `force` is set to `true`, we will force-close the channel, potentially broadcasting our + /// latest state. Note that in contrast to cooperative closure, force-closing will have the + /// channel funds time-locked, i.e., they will only be available after the counterparty had + /// time to contest our claim. Force-closing channels also more costly in terms of on-chain + /// fees. So cooperative closure should always be preferred (and tried first). + /// + /// Broadcasting the closing transactions will be omitted for Anchor channels if we trust the + /// counterparty to broadcast for us (see [`AnchorChannelsConfig::trusted_peers_no_reserve`] + /// for more information). pub fn close_channel( - &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, + &self, user_channel_id: &UserChannelId, counterparty_node_id: PublicKey, force: bool, ) -> Result<(), Error> { let open_channels = self.channel_manager.list_channels_with_counterparty(&counterparty_node_id); if let Some(channel_details) = open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0) { - match self - .channel_manager - .close_channel(&channel_details.channel_id, &counterparty_node_id) - { - Ok(_) => { - // Check if this was the last open channel, if so, forget the peer. - if open_channels.len() == 1 { - self.peer_store.remove_peer(&counterparty_node_id)?; - } - Ok(()) - }, - Err(_) => Err(Error::ChannelClosingFailed), + if force { + if self.config.anchor_channels_config.as_ref().map_or(false, |acc| { + acc.trusted_peers_no_reserve.contains(&counterparty_node_id) + }) { + self.channel_manager + .force_close_without_broadcasting_txn( + &channel_details.channel_id, + &counterparty_node_id, + ) + .map_err(|e| { + log_error!( + self.logger, + "Failed to force-close channel to trusted peer: {:?}", + e + ); + Error::ChannelClosingFailed + })?; + } else { + self.channel_manager + .force_close_broadcasting_latest_txn( + &channel_details.channel_id, + &counterparty_node_id, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to force-close channel: {:?}", e); + Error::ChannelClosingFailed + })?; + } + } else { + self.channel_manager + .close_channel(&channel_details.channel_id, &counterparty_node_id) + .map_err(|e| { + log_error!(self.logger, "Failed to close channel: {:?}", e); + Error::ChannelClosingFailed + })?; } + + // Check if this was the last open channel, if so, forget the peer. + if open_channels.len() == 1 { + self.peer_store.remove_peer(&counterparty_node_id)?; + } + Ok(()) } else { Ok(()) } @@ -1052,617 +1243,33 @@ impl Node { } } - /// Send a payment given an invoice. - pub fn send_payment(&self, invoice: &Bolt11Invoice) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let (payment_hash, recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_payment_using_amount instead."); - Error::InvalidInvoice - })?; - - if let Some(payment) = self.payment_store.get(&payment_hash) { - if payment.status == PaymentStatus::Pending - || payment.status == PaymentStatus::Succeeded - { - log_error!(self.logger, "Payment error: an invoice must not be paid twice."); - return Err(Error::DuplicatePayment); - } - } - - let payment_secret = Some(*invoice.payment_secret()); - let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); - let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - - match self.channel_manager.send_payment( - payment_hash, - recipient_onion, - payment_id, - route_params, - retry_strategy, - ) { - Ok(()) => { - let payee_pubkey = invoice.recover_payee_pub_key(); - let amt_msat = invoice.amount_milli_satoshis().unwrap(); - log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); - - let payment = PaymentDetails { - preimage: None, - hash: payment_hash, - secret: payment_secret, - amount_msat: invoice.amount_milli_satoshis(), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, - lsp_fee_limits: None, - bolt11_invoice: Some(invoice.to_string()), - last_update: 0, - }; - self.payment_store.insert(payment)?; - - Ok(payment_hash) - }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - match e { - channelmanager::RetryableSendFailure::DuplicatePayment => { - Err(Error::DuplicatePayment) - }, - _ => { - let payment = PaymentDetails { - preimage: None, - hash: payment_hash, - secret: payment_secret, - amount_msat: invoice.amount_milli_satoshis(), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, - lsp_fee_limits: None, - bolt11_invoice: Some(invoice.to_string()), - last_update: 0, - }; - - self.payment_store.insert(payment)?; - Err(Error::PaymentSendingFailed) - }, - } - }, - } - } - - /// Send a payment given an invoice and an amount in millisatoshi. - /// - /// This will fail if the amount given is less than the value required by the given invoice. - /// - /// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the - /// amount paid to be determined by the user. - pub fn send_payment_using_amount( - &self, invoice: &Bolt11Invoice, amount_msat: u64, - ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { - if amount_msat < invoice_amount_msat { - log_error!( - self.logger, - "Failed to pay as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); - return Err(Error::InvalidAmount); - } - } - - let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - if let Some(payment) = self.payment_store.get(&payment_hash) { - if payment.status == PaymentStatus::Pending - || payment.status == PaymentStatus::Succeeded - { - log_error!(self.logger, "Payment error: an invoice must not be paid twice."); - return Err(Error::DuplicatePayment); - } - } - - let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); - let payment_secret = invoice.payment_secret(); - let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); - let mut payment_params = PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), - invoice.min_final_cltv_expiry_delta() as u32, - ) - .with_expiry_time(expiry_time.as_secs()) - .with_route_hints(invoice.route_hints()) - .map_err(|_| Error::InvalidInvoice)?; - if let Some(features) = invoice.features() { - payment_params = payment_params - .with_bolt11_features(features.clone()) - .map_err(|_| Error::InvalidInvoice)?; - } - let route_params = - RouteParameters::from_payment_params_and_value(payment_params, amount_msat); - - let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let recipient_fields = RecipientOnionFields::secret_only(*payment_secret); - - match self.channel_manager.send_payment( - payment_hash, - recipient_fields, - payment_id, - route_params, - retry_strategy, - ) { - Ok(_payment_id) => { - let payee_pubkey = invoice.recover_payee_pub_key(); - log_info!( - self.logger, - "Initiated sending {} msat to {}", - amount_msat, - payee_pubkey - ); - - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Pending, - lsp_fee_limits: None, - bolt11_invoice: Some(invoice.to_string()), - last_update: 0, - }; - self.payment_store.insert(payment)?; - - Ok(payment_hash) - }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - - match e { - channelmanager::RetryableSendFailure::DuplicatePayment => { - Err(Error::DuplicatePayment) - }, - _ => { - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(*payment_secret), - amount_msat: Some(amount_msat), - direction: PaymentDirection::Outbound, - status: PaymentStatus::Failed, - lsp_fee_limits: None, - bolt11_invoice: Some(invoice.to_string()), - last_update: 0, - }; - self.payment_store.insert(payment)?; - - Err(Error::PaymentSendingFailed) - }, - } - }, - } - } - - /// Send a spontaneous, aka. "keysend", payment - pub fn send_spontaneous_payment( - &self, amount_msat: u64, node_id: PublicKey, custom_tlvs: Vec, - ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); - let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); - - if let Some(payment) = self.payment_store.get(&payment_hash) { - if payment.status == PaymentStatus::Pending - || payment.status == PaymentStatus::Succeeded - { - log_error!(self.logger, "Payment error: must not send duplicate payments."); - return Err(Error::DuplicatePayment); - } - } - - let route_params = RouteParameters::from_payment_params_and_value( - PaymentParameters::from_node_id(node_id, self.config.default_cltv_expiry_delta), - amount_msat, - ); - let recipient_fields = RecipientOnionFields::spontaneous_empty() - .with_custom_tlvs(custom_tlvs.into_iter().map(|tlv| (tlv.r#type, tlv.value)).collect()) - .map_err(|_| { - log_error!(self.logger, "Payment error: invalid custom TLVs."); - Error::InvalidCustomTlv - })?; - - match self.channel_manager.send_spontaneous_payment_with_retry( - Some(payment_preimage), - recipient_fields, - PaymentId(payment_hash.0), - route_params, - Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT), - ) { - Ok(_payment_id) => { - log_info!(self.logger, "Initiated sending {}msat to {}.", amount_msat, node_id); - - let payment = PaymentDetails { - hash: payment_hash, - preimage: Some(payment_preimage), - secret: None, - status: PaymentStatus::Pending, - direction: PaymentDirection::Outbound, - amount_msat: Some(amount_msat), - lsp_fee_limits: None, - bolt11_invoice: None, - last_update: 0, - }; - self.payment_store.insert(payment)?; - - Ok(payment_hash) - }, - Err(e) => { - log_error!(self.logger, "Failed to send payment: {:?}", e); - - match e { - channelmanager::RetryableSendFailure::DuplicatePayment => { - Err(Error::DuplicatePayment) - }, - _ => { - let payment = PaymentDetails { - hash: payment_hash, - preimage: Some(payment_preimage), - secret: None, - status: PaymentStatus::Failed, - direction: PaymentDirection::Outbound, - amount_msat: Some(amount_msat), - lsp_fee_limits: None, - bolt11_invoice: None, - last_update: 0, - }; - - self.payment_store.insert(payment)?; - Err(Error::PaymentSendingFailed) - }, - } - }, - } - } - - /// Sends payment probes over all paths of a route that would be used to pay the given invoice. - /// - /// This may be used to send "pre-flight" probes, i.e., to train our scorer before conducting - /// the actual payment. Note this is only useful if there likely is sufficient time for the - /// probe to settle before sending out the actual payment, e.g., when waiting for user - /// confirmation in a wallet UI. - /// - /// Otherwise, there is a chance the probe could take up some liquidity needed to complete the - /// actual payment. Users should therefore be cautious and might avoid sending probes if - /// liquidity is scarce and/or they don't expect the probe to return before they send the - /// payment. To mitigate this issue, channels with available liquidity less than the required - /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send - /// pre-flight probes. - pub fn send_payment_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let (_payment_hash, _recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_payment_probes_using_amount instead."); - Error::InvalidInvoice - })?; - - let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); - - self.channel_manager - .send_preflight_probes(route_params, liquidity_limit_multiplier) - .map_err(|e| { - log_error!(self.logger, "Failed to send payment probes: {:?}", e); - Error::ProbeSendingFailed - })?; - - Ok(()) - } - - /// Sends payment probes over all paths of a route that would be used to pay the given - /// amount to the given `node_id`. - /// - /// See [`Self::send_payment_probes`] for more information. - pub fn send_spontaneous_payment_probes( - &self, amount_msat: u64, node_id: PublicKey, - ) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); - let cltv_expiry_delta = self.config.default_cltv_expiry_delta; - - self.channel_manager - .send_spontaneous_preflight_probes( - node_id, - amount_msat, - cltv_expiry_delta, - liquidity_limit_multiplier, - ) - .map_err(|e| { - log_error!(self.logger, "Failed to send payment probes: {:?}", e); - Error::ProbeSendingFailed - })?; - - Ok(()) - } - - /// Sends payment probes over all paths of a route that would be used to pay the given - /// zero-value invoice using the given amount. - /// - /// This can be used to send pre-flight probes for a so-called "zero-amount" invoice, i.e., an - /// invoice that leaves the amount paid to be determined by the user. - /// - /// See [`Self::send_payment_probes`] for more information. - pub fn send_payment_probes_using_amount( - &self, invoice: &Bolt11Invoice, amount_msat: u64, - ) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - return Err(Error::NotRunning); - } - - let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = - invoice.amount_milli_satoshis() - { - if amount_msat < invoice_amount_msat { - log_error!( - self.logger, - "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); - return Err(Error::InvalidAmount); - } - - payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being \"zero-amount\"."); - Error::InvalidInvoice - })? - } else { - payment::payment_parameters_from_zero_amount_invoice(&invoice, amount_msat).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); - Error::InvalidInvoice - })? - }; - - let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); - - self.channel_manager - .send_preflight_probes(route_params, liquidity_limit_multiplier) - .map_err(|e| { - log_error!(self.logger, "Failed to send payment probes: {:?}", e); - Error::ProbeSendingFailed - })?; - - Ok(()) - } - - /// Returns a payable invoice that can be used to request and receive a payment of the amount - /// given. - pub fn receive_payment( - &self, amount_msat: u64, description: &str, expiry_secs: u32, - ) -> Result { - self.receive_payment_inner(Some(amount_msat), description, expiry_secs) - } - - /// Returns a payable invoice that can be used to request and receive a payment for which the - /// amount is to be determined by the user, also known as a "zero-amount" invoice. - pub fn receive_variable_amount_payment( - &self, description: &str, expiry_secs: u32, - ) -> Result { - self.receive_payment_inner(None, description, expiry_secs) - } - - fn receive_payment_inner( - &self, amount_msat: Option, description: &str, expiry_secs: u32, - ) -> Result { - let currency = Currency::from(self.config.network); - let keys_manager = Arc::clone(&self.keys_manager); - let invoice = match lightning_invoice::utils::create_invoice_from_channelmanager( - &self.channel_manager, - keys_manager, - Arc::clone(&self.logger), - currency, - amount_msat, - description.to_string(), - expiry_secs, - None, - ) { - Ok(inv) => { - log_info!(self.logger, "Invoice created: {}", inv); - inv - }, - Err(e) => { - log_error!(self.logger, "Failed to create invoice: {}", e); - return Err(Error::InvoiceCreationFailed); - }, - }; - - let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), - amount_msat, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - lsp_fee_limits: None, - bolt11_invoice: Some(invoice.to_string()), - last_update: 0, - }; - - self.payment_store.insert(payment)?; - - Ok(invoice) - } - - /// Returns a payable invoice that can be used to request a payment of the amount given and - /// receive it via a newly created just-in-time (JIT) channel. - /// - /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel - /// to us, supplying just-in-time inbound liquidity. - /// - /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the - /// channel to us. We'll use its cheapest offer otherwise. - /// - /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - pub fn receive_payment_via_jit_channel( - &self, amount_msat: u64, description: &str, expiry_secs: u32, - max_total_lsp_fee_limit_msat: Option, - ) -> Result { - self.receive_payment_via_jit_channel_inner( - Some(amount_msat), - description, - expiry_secs, - max_total_lsp_fee_limit_msat, - None, - ) - } - - /// Returns a payable invoice that can be used to request a variable amount payment (also known - /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. - /// - /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel - /// to us, supplying just-in-time inbound liquidity. - /// - /// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in - /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. - /// We'll use its cheapest offer otherwise. - /// - /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md - pub fn receive_variable_amount_payment_via_jit_channel( - &self, description: &str, expiry_secs: u32, - max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { - self.receive_payment_via_jit_channel_inner( - None, - description, - expiry_secs, - None, - max_proportional_lsp_fee_limit_ppm_msat, - ) - } - - fn receive_payment_via_jit_channel_inner( - &self, amount_msat: Option, description: &str, expiry_secs: u32, - max_total_lsp_fee_limit_msat: Option, - max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { - let liquidity_source = - self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - - let (node_id, address) = liquidity_source - .get_liquidity_source_details() - .ok_or(Error::LiquiditySourceUnavailable)?; - - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); - - let peer_info = PeerInfo { node_id, address }; - - let con_node_id = peer_info.node_id; - let con_addr = peer_info.address.clone(); - let con_logger = Arc::clone(&self.logger); - let con_pm = Arc::clone(&self.peer_manager); - - // We need to use our main runtime here as a local runtime might not be around to poll - // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - connect_peer_if_necessary(con_node_id, con_addr, con_pm, con_logger).await - }) - })?; - - log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); - - let liquidity_source = Arc::clone(&liquidity_source); - let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = - tokio::task::block_in_place(move || { - runtime.block_on(async move { - if let Some(amount_msat) = amount_msat { - liquidity_source - .lsps2_receive_to_jit_channel( - amount_msat, - description, - expiry_secs, - max_total_lsp_fee_limit_msat, - ) - .await - .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) - } else { - liquidity_source - .lsps2_receive_variable_amount_to_jit_channel( - description, - expiry_secs, - max_proportional_lsp_fee_limit_ppm_msat, - ) - .await - .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) - } - }) - })?; - - // Register payment in payment store. - let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); - let lsp_fee_limits = Some(LSPFeeLimits { - max_total_opening_fee_msat: lsp_total_opening_fee, - max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, - }); - let payment = PaymentDetails { - hash: payment_hash, - preimage: None, - secret: Some(invoice.payment_secret().clone()), - amount_msat, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - lsp_fee_limits, - bolt11_invoice: Some(invoice.to_string()), - last_update: 0, - }; - - self.payment_store.insert(payment)?; - - // Persist LSP peer to make sure we reconnect on restart. - self.peer_store.add_peer(peer_info)?; - - Ok(invoice) - } - - /// Retrieve the details of a specific payment with the given hash. + /// Retrieve the details of a specific payment with the given id. /// /// Returns `Some` if the payment was known and `None` otherwise. - pub fn payment(&self, payment_hash: &PaymentHash) -> Option { - self.payment_store.get(payment_hash) + pub fn payment(&self, payment_id: &PaymentId) -> Option { + self.payment_store.get(payment_id) } - /// Remove the payment with the given hash from the store. - pub fn remove_payment(&self, payment_hash: &PaymentHash) -> Result<(), Error> { - self.payment_store.remove(&payment_hash) + /// Remove the payment with the given id from the store. + pub fn remove_payment(&self, payment_id: &PaymentId) -> Result<(), Error> { + self.payment_store.remove(&payment_id) } /// Retrieves an overview of all known balances. pub fn list_balances(&self) -> BalanceDetails { - let (total_onchain_balance_sats, spendable_onchain_balance_sats) = self - .wallet - .get_balance() - .map(|bal| (bal.get_total(), bal.get_spendable())) - .unwrap_or((0, 0)); + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let (total_onchain_balance_sats, spendable_onchain_balance_sats) = + self.wallet.get_balances(cur_anchor_reserve_sats).unwrap_or((0, 0)); + + let total_anchor_channels_reserve_sats = + std::cmp::min(cur_anchor_reserve_sats, total_onchain_balance_sats); let mut total_lightning_balance_sats = 0; let mut lightning_balances = Vec::new(); - for funding_txo in self.chain_monitor.list_monitors() { + for (funding_txo, channel_id) in self.chain_monitor.list_monitors() { match self.chain_monitor.get_monitor(funding_txo) { Ok(monitor) => { - // TODO: Switch to `channel_id` with LDK 0.0.122: let channel_id = monitor.channel_id(); - let channel_id = funding_txo.to_channel_id(); - // unwrap safety: `get_counterparty_node_id` will always be `Some` after 0.0.110 and - // LDK Node 0.1 depended on 0.0.115 already. let counterparty_node_id = monitor.get_counterparty_node_id().unwrap(); for ldk_balance in monitor.get_claimable_balances() { total_lightning_balance_sats += ldk_balance.claimable_amount_satoshis(); @@ -1689,6 +1296,7 @@ impl Node { BalanceDetails { total_onchain_balance_sats, spendable_onchain_balance_sats, + total_anchor_channels_reserve_sats, total_lightning_balance_sats, lightning_balances, pending_balances_from_channel_closures, @@ -1699,7 +1307,8 @@ impl Node { /// /// For example, you could retrieve all stored outbound payments as follows: /// ``` - /// # use ldk_node::{Builder, Config, PaymentDirection}; + /// # use ldk_node::{Builder, Config}; + /// # use ldk_node::payment::PaymentDirection; /// # use ldk_node::bitcoin::Network; /// # let mut config = Config::default(); /// # config.network = Network::Regtest; @@ -1724,12 +1333,13 @@ impl Node { let mut peers = Vec::new(); // First add all connected peers, preferring to list the connected address if available. - let connected_peers = self.peer_manager.get_peer_node_ids(); + let connected_peers = self.peer_manager.list_peers(); let connected_peers_len = connected_peers.len(); - for (node_id, con_addr_opt) in connected_peers { + for connected_peer in connected_peers { + let node_id = connected_peer.counterparty_node_id; let stored_peer = self.peer_store.get_peer(&node_id); let stored_addr_opt = stored_peer.as_ref().map(|p| p.address.clone()); - let address = match (con_addr_opt, stored_addr_opt) { + let address = match (connected_peer.socket_address, stored_addr_opt) { (Some(con_addr), _) => con_addr, (None, Some(stored_addr)) => stored_addr, (None, None) => continue, @@ -1776,6 +1386,7 @@ impl Node { self.keys_manager.verify_signature(msg, sig, pkey) } + /// Resets router state. pub fn reset_router(&self) -> Result<(), Error> { self.kv_store .remove( @@ -1806,63 +1417,65 @@ impl Node { } } -impl Drop for Node { +impl Drop for Node { fn drop(&mut self) { let _ = self.stop(); } } -async fn connect_peer_if_necessary( - node_id: PublicKey, addr: SocketAddress, peer_manager: Arc>, - logger: Arc, -) -> Result<(), Error> { - for (pman_node_id, _pman_addr) in peer_manager.get_peer_node_ids() { - if node_id == pman_node_id { - return Ok(()); - } - } - - do_connect_peer(node_id, addr, peer_manager, logger).await +/// Represents the status of the [`Node`]. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NodeStatus { + /// Indicates whether the [`Node`] is running. + pub is_running: bool, + /// Indicates whether the [`Node`] is listening for incoming connections on the addresses + /// configured via [`Config::listening_addresses`]. + pub is_listening: bool, + /// The best block to which our Lightning wallet is currently synced. + pub current_best_block: BestBlock, + /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced + /// our Lightning wallet to the chain tip. + /// + /// Will be `None` if the wallet hasn't been synced since the [`Node`] was initialized. + pub latest_wallet_sync_timestamp: Option, + /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced + /// our on-chain wallet to the chain tip. + /// + /// Will be `None` if the wallet hasn't been synced since the [`Node`] was initialized. + pub latest_onchain_wallet_sync_timestamp: Option, + /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully update + /// our fee rate cache. + /// + /// Will be `None` if the cache hasn't been updated since the [`Node`] was initialized. + pub latest_fee_rate_cache_update_timestamp: Option, + /// The timestamp, in seconds since start of the UNIX epoch, when the last rapid gossip sync + /// (RGS) snapshot we successfully applied was generated. + /// + /// Will be `None` if RGS isn't configured or the snapshot hasn't been updated since the [`Node`] was initialized. + pub latest_rgs_snapshot_timestamp: Option, + /// The timestamp, in seconds since start of the UNIX epoch, when we last broadcasted a node + /// announcement. + /// + /// Will be `None` if we have no public channels or we haven't broadcasted since the [`Node`] was initialized. + pub latest_node_announcement_broadcast_timestamp: Option, } -async fn do_connect_peer( - node_id: PublicKey, addr: SocketAddress, peer_manager: Arc>, - logger: Arc, -) -> Result<(), Error> { - log_info!(logger, "Connecting to peer: {}@{}", node_id, addr); - - let socket_addr = addr - .to_socket_addrs() - .map_err(|e| { - log_error!(logger, "Failed to resolve network address: {}", e); - Error::InvalidSocketAddress - })? - .next() - .ok_or(Error::ConnectionFailed)?; - - match lightning_net_tokio::connect_outbound(Arc::clone(&peer_manager), node_id, socket_addr) - .await - { - Some(connection_closed_future) => { - let mut connection_closed_future = Box::pin(connection_closed_future); - loop { - match futures::poll!(&mut connection_closed_future) { - std::task::Poll::Ready(_) => { - log_info!(logger, "Peer connection closed: {}@{}", node_id, addr); - return Err(Error::ConnectionFailed); - }, - std::task::Poll::Pending => {}, - } - // Avoid blocking the tokio context by sleeping a bit - match peer_manager.get_peer_node_ids().iter().find(|(id, _addr)| *id == node_id) { - Some(_) => return Ok(()), - None => tokio::time::sleep(Duration::from_millis(10)).await, - } - } - }, - None => { - log_error!(logger, "Failed to connect to peer: {}@{}", node_id, addr); - Err(Error::ConnectionFailed) - }, - } +pub(crate) fn total_anchor_channels_reserve_sats( + channel_manager: &ChannelManager, config: &Config, +) -> u64 { + config.anchor_channels_config.as_ref().map_or(0, |anchor_channels_config| { + channel_manager + .list_channels() + .into_iter() + .filter(|c| { + !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()) + }) + .count() as u64 + * anchor_channels_config.per_channel_reserve_sats + }) } diff --git a/src/liquidity.rs b/src/liquidity.rs index 0404fe64e..00e9f5717 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -5,7 +5,6 @@ use crate::{Config, Error}; use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; use lightning::ln::msgs::SocketAddress; use lightning::routing::router::{RouteHint, RouteHintHop}; -use lightning::util::persist::KVStore; use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; @@ -33,26 +32,26 @@ struct LSPS2Service { pending_buy_requests: Mutex>>, } -pub(crate) struct LiquiditySource +pub(crate) struct LiquiditySource where L::Target: Logger, { lsps2_service: Option, - channel_manager: Arc>, + channel_manager: Arc, keys_manager: Arc, - liquidity_manager: Arc>, + liquidity_manager: Arc, config: Arc, logger: L, } -impl LiquiditySource +impl LiquiditySource where L::Target: Logger, { pub(crate) fn new_lsps2( address: SocketAddress, node_id: PublicKey, token: Option, - channel_manager: Arc>, keys_manager: Arc, - liquidity_manager: Arc>, config: Arc, logger: L, + channel_manager: Arc, keys_manager: Arc, + liquidity_manager: Arc, config: Arc, logger: L, ) -> Self { let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); @@ -66,12 +65,12 @@ where Self { lsps2_service, channel_manager, keys_manager, liquidity_manager, config, logger } } - pub(crate) fn set_peer_manager(&self, peer_manager: Arc>) { + pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { let process_msgs_callback = move || peer_manager.process_events(); self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); } - pub(crate) fn liquidity_manager(&self) -> &LiquidityManager { + pub(crate) fn liquidity_manager(&self) -> &LiquidityManager { self.liquidity_manager.as_ref() } diff --git a/src/message_handler.rs b/src/message_handler.rs index 852f63cec..89d67d846 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -4,7 +4,6 @@ use lightning::ln::features::{InitFeatures, NodeFeatures}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; use lightning::util::logger::Logger; -use lightning::util::persist::KVStore; use lightning_liquidity::lsps0::ser::RawLSPSMessage; @@ -13,19 +12,19 @@ use bitcoin::secp256k1::PublicKey; use std::ops::Deref; use std::sync::Arc; -pub(crate) enum NodeCustomMessageHandler +pub(crate) enum NodeCustomMessageHandler where L::Target: Logger, { Ignoring, - Liquidity { liquidity_source: Arc> }, + Liquidity { liquidity_source: Arc> }, } -impl NodeCustomMessageHandler +impl NodeCustomMessageHandler where L::Target: Logger, { - pub(crate) fn new_liquidity(liquidity_source: Arc>) -> Self { + pub(crate) fn new_liquidity(liquidity_source: Arc>) -> Self { Self::Liquidity { liquidity_source } } @@ -34,8 +33,7 @@ where } } -impl CustomMessageReader - for NodeCustomMessageHandler +impl CustomMessageReader for NodeCustomMessageHandler where L::Target: Logger, { @@ -53,8 +51,7 @@ where } } -impl CustomMessageHandler - for NodeCustomMessageHandler +impl CustomMessageHandler for NodeCustomMessageHandler where L::Target: Logger, { diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs new file mode 100644 index 000000000..dfa9dc224 --- /dev/null +++ b/src/payment/bolt11.rs @@ -0,0 +1,553 @@ +//! Holds a payment handler allowing to create and pay [BOLT 11] invoices. +//! +//! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::connection::ConnectionManager; +use crate::error::Error; +use crate::liquidity::LiquiditySource; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::payment::payment_store::{ + LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, +}; +use crate::peer_store::{PeerInfo, PeerStore}; +use crate::types::{ChannelManager, KeysManager}; + +use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; +use lightning::ln::PaymentHash; +use lightning::routing::router::{PaymentParameters, RouteParameters}; + +use lightning_invoice::{payment, Bolt11Invoice, Currency}; + +use bitcoin::hashes::Hash; + +use std::sync::{Arc, RwLock}; + +/// A payment handler allowing to create and pay [BOLT 11] invoices. +/// +/// Should be retrieved by calling [`Node::bolt11_payment`]. +/// +/// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +/// [`Node::bolt11_payment`]: crate::Node::bolt11_payment +pub struct Bolt11Payment { + runtime: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, + keys_manager: Arc, + liquidity_source: Option>>>, + payment_store: Arc>>, + peer_store: Arc>>, + config: Arc, + logger: Arc, +} + +impl Bolt11Payment { + pub(crate) fn new( + runtime: Arc>>, + channel_manager: Arc, + connection_manager: Arc>>, + keys_manager: Arc, + liquidity_source: Option>>>, + payment_store: Arc>>, + peer_store: Arc>>, config: Arc, + logger: Arc, + ) -> Self { + Self { + runtime, + channel_manager, + connection_manager, + keys_manager, + liquidity_source, + payment_store, + peer_store, + config, + logger, + } + } + + /// Send a payment given an invoice. + pub fn send(&self, invoice: &Bolt11Invoice) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let (payment_hash, recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead."); + Error::InvalidInvoice + })?; + + let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); + if let Some(payment) = self.payment_store.get(&payment_id) { + if payment.status == PaymentStatus::Pending + || payment.status == PaymentStatus::Succeeded + { + log_error!(self.logger, "Payment error: an invoice must not be paid twice."); + return Err(Error::DuplicatePayment); + } + } + + let payment_secret = Some(*invoice.payment_secret()); + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + + match self.channel_manager.send_payment( + payment_hash, + recipient_onion, + payment_id, + route_params, + retry_strategy, + ) { + Ok(()) => { + let payee_pubkey = invoice.recover_payee_pub_key(); + let amt_msat = invoice.amount_milli_satoshis().unwrap(); + log_info!(self.logger, "Initiated sending {}msat to {}", amt_msat, payee_pubkey); + + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: payment_secret, + bolt11_invoice: Some(invoice.to_string()), + }; + + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: invoice.amount_milli_satoshis(), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Pending, + last_update: 0, + }; + self.payment_store.insert(payment)?; + + Ok(payment_id) + }, + Err(e) => { + log_error!(self.logger, "Failed to send payment: {:?}", e); + match e { + RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + _ => { + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: payment_secret, + bolt11_invoice: Some(invoice.to_string()), + }; + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: invoice.amount_milli_satoshis(), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Failed, + last_update: 0, + }; + + self.payment_store.insert(payment)?; + Err(Error::PaymentSendingFailed) + }, + } + }, + } + } + + /// Send a payment given an invoice and an amount in millisatoshi. + /// + /// This will fail if the amount given is less than the value required by the given invoice. + /// + /// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the + /// amount paid to be determined by the user. + pub fn send_using_amount( + &self, invoice: &Bolt11Invoice, amount_msat: u64, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { + if amount_msat < invoice_amount_msat { + log_error!( + self.logger, + "Failed to pay as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); + return Err(Error::InvalidAmount); + } + } + + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); + if let Some(payment) = self.payment_store.get(&payment_id) { + if payment.status == PaymentStatus::Pending + || payment.status == PaymentStatus::Succeeded + { + log_error!(self.logger, "Payment error: an invoice must not be paid twice."); + return Err(Error::DuplicatePayment); + } + } + + let payment_secret = invoice.payment_secret(); + let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); + let mut payment_params = PaymentParameters::from_node_id( + invoice.recover_payee_pub_key(), + invoice.min_final_cltv_expiry_delta() as u32, + ) + .with_expiry_time(expiry_time.as_secs()) + .with_route_hints(invoice.route_hints()) + .map_err(|_| Error::InvalidInvoice)?; + if let Some(features) = invoice.features() { + payment_params = payment_params + .with_bolt11_features(features.clone()) + .map_err(|_| Error::InvalidInvoice)?; + } + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let recipient_fields = RecipientOnionFields::secret_only(*payment_secret); + + match self.channel_manager.send_payment( + payment_hash, + recipient_fields, + payment_id, + route_params, + retry_strategy, + ) { + Ok(()) => { + let payee_pubkey = invoice.recover_payee_pub_key(); + log_info!( + self.logger, + "Initiated sending {} msat to {}", + amount_msat, + payee_pubkey + ); + + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), + bolt11_invoice: Some(invoice.to_string()), + }; + + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: Some(amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Pending, + last_update: 0, + }; + self.payment_store.insert(payment)?; + + Ok(payment_id) + }, + Err(e) => { + log_error!(self.logger, "Failed to send payment: {:?}", e); + + match e { + RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + _ => { + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(*payment_secret), + bolt11_invoice: Some(invoice.to_string()), + }; + + let payment = PaymentDetails { + id: payment_id, + kind, + amount_msat: Some(amount_msat), + direction: PaymentDirection::Outbound, + status: PaymentStatus::Failed, + last_update: 0, + }; + self.payment_store.insert(payment)?; + + Err(Error::PaymentSendingFailed) + }, + } + }, + } + } + + /// Returns a payable invoice that can be used to request and receive a payment of the amount + /// given. + pub fn receive( + &self, amount_msat: u64, description: &str, expiry_secs: u32, + ) -> Result { + self.receive_inner(Some(amount_msat), description, expiry_secs) + } + + /// Returns a payable invoice that can be used to request and receive a payment for which the + /// amount is to be determined by the user, also known as a "zero-amount" invoice. + pub fn receive_variable_amount( + &self, description: &str, expiry_secs: u32, + ) -> Result { + self.receive_inner(None, description, expiry_secs) + } + + fn receive_inner( + &self, amount_msat: Option, description: &str, expiry_secs: u32, + ) -> Result { + let currency = Currency::from(self.config.network); + let keys_manager = Arc::clone(&self.keys_manager); + let invoice = match lightning_invoice::utils::create_invoice_from_channelmanager( + &self.channel_manager, + keys_manager, + Arc::clone(&self.logger), + currency, + amount_msat, + description.to_string(), + expiry_secs, + None, + ) { + Ok(inv) => { + log_info!(self.logger, "Invoice created: {}", inv); + inv + }, + Err(e) => { + log_error!(self.logger, "Failed to create invoice: {}", e); + return Err(Error::InvoiceCreationFailed); + }, + }; + + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let id = PaymentId(payment_hash.0); + let kind = PaymentKind::Bolt11 { + hash: payment_hash, + preimage: None, + secret: Some(invoice.payment_secret().clone()), + bolt11_invoice: Some(invoice.to_string()), + }; + + let payment = PaymentDetails { + id, + kind, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + last_update: 0, + }; + + self.payment_store.insert(payment)?; + + Ok(invoice) + } + + /// Returns a payable invoice that can be used to request a payment of the amount given and + /// receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the + /// channel to us. We'll use its cheapest offer otherwise. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn receive_via_jit_channel( + &self, amount_msat: u64, description: &str, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, + ) -> Result { + self.receive_via_jit_channel_inner( + Some(amount_msat), + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + ) + } + + /// Returns a payable invoice that can be used to request a variable amount payment (also known + /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in + /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. + /// We'll use its cheapest offer otherwise. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn receive_variable_amount_via_jit_channel( + &self, description: &str, expiry_secs: u32, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + self.receive_via_jit_channel_inner( + None, + description, + expiry_secs, + None, + max_proportional_lsp_fee_limit_ppm_msat, + ) + } + + fn receive_via_jit_channel_inner( + &self, amount_msat: Option, description: &str, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, + ) -> Result { + let liquidity_source = + self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + + let (node_id, address) = liquidity_source + .get_liquidity_source_details() + .ok_or(Error::LiquiditySourceUnavailable)?; + + let rt_lock = self.runtime.read().unwrap(); + let runtime = rt_lock.as_ref().unwrap(); + + let peer_info = PeerInfo { node_id, address }; + + let con_node_id = peer_info.node_id; + let con_addr = peer_info.address.clone(); + let con_cm = Arc::clone(&self.connection_manager); + + // We need to use our main runtime here as a local runtime might not be around to poll + // connection futures going forward. + tokio::task::block_in_place(move || { + runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await + }) + })?; + + log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); + + let liquidity_source = Arc::clone(&liquidity_source); + let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = + tokio::task::block_in_place(move || { + runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_receive_to_jit_channel( + amount_msat, + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + ) + .await + .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) + } else { + liquidity_source + .lsps2_receive_variable_amount_to_jit_channel( + description, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + ) + .await + .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) + } + }) + })?; + + // Register payment in payment store. + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); + let lsp_fee_limits = LSPFeeLimits { + max_total_opening_fee_msat: lsp_total_opening_fee, + max_proportional_opening_fee_ppm_msat: lsp_prop_opening_fee, + }; + let id = PaymentId(payment_hash.0); + let kind = PaymentKind::Bolt11Jit { + hash: payment_hash, + preimage: None, + secret: Some(invoice.payment_secret().clone()), + lsp_fee_limits, + }; + let payment = PaymentDetails { + id, + kind, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + last_update: 0, + }; + + self.payment_store.insert(payment)?; + + // Persist LSP peer to make sure we reconnect on restart. + self.peer_store.add_peer(peer_info)?; + + Ok(invoice) + } + + /// Sends payment probes over all paths of a route that would be used to pay the given invoice. + /// + /// This may be used to send "pre-flight" probes, i.e., to train our scorer before conducting + /// the actual payment. Note this is only useful if there likely is sufficient time for the + /// probe to settle before sending out the actual payment, e.g., when waiting for user + /// confirmation in a wallet UI. + /// + /// Otherwise, there is a chance the probe could take up some liquidity needed to complete the + /// actual payment. Users should therefore be cautious and might avoid sending probes if + /// liquidity is scarce and/or they don't expect the probe to return before they send the + /// payment. To mitigate this issue, channels with available liquidity less than the required + /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send + /// pre-flight probes. + pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let (_payment_hash, _recipient_onion, route_params) = payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_probes_using_amount instead."); + Error::InvalidInvoice + })?; + + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); + + self.channel_manager + .send_preflight_probes(route_params, liquidity_limit_multiplier) + .map_err(|e| { + log_error!(self.logger, "Failed to send payment probes: {:?}", e); + Error::ProbeSendingFailed + })?; + + Ok(()) + } + + /// Sends payment probes over all paths of a route that would be used to pay the given + /// zero-value invoice using the given amount. + /// + /// This can be used to send pre-flight probes for a so-called "zero-amount" invoice, i.e., an + /// invoice that leaves the amount paid to be determined by the user. + /// + /// See [`Self::send_probes`] for more information. + pub fn send_probes_using_amount( + &self, invoice: &Bolt11Invoice, amount_msat: u64, + ) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = + invoice.amount_milli_satoshis() + { + if amount_msat < invoice_amount_msat { + log_error!( + self.logger, + "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); + return Err(Error::InvalidAmount); + } + + payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being \"zero-amount\"."); + Error::InvalidInvoice + })? + } else { + payment::payment_parameters_from_zero_amount_invoice(&invoice, amount_msat).map_err(|_| { + log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); + Error::InvalidInvoice + })? + }; + + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); + + self.channel_manager + .send_preflight_probes(route_params, liquidity_limit_multiplier) + .map_err(|e| { + log_error!(self.logger, "Failed to send payment probes: {:?}", e); + Error::ProbeSendingFailed + })?; + + Ok(()) + } +} diff --git a/src/payment/mod.rs b/src/payment/mod.rs new file mode 100644 index 000000000..be215d4ce --- /dev/null +++ b/src/payment/mod.rs @@ -0,0 +1,13 @@ +//! Objects for different types of payments. + +mod bolt11; +mod onchain; +pub(crate) mod payment_store; +mod spontaneous; + +pub use bolt11::Bolt11Payment; +pub use onchain::OnchainPayment; +pub use payment_store::{ + LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, +}; +pub use spontaneous::SpontaneousPayment; diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs new file mode 100644 index 000000000..41d66861f --- /dev/null +++ b/src/payment/onchain.rs @@ -0,0 +1,85 @@ +//! Holds a payment handler allowing to send and receive on-chain payments. + +use crate::config::Config; +use crate::error::Error; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::types::{ChannelManager, Wallet}; + +use bitcoin::{Address, Txid}; + +use std::sync::{Arc, RwLock}; + +/// A payment handler allowing to send and receive on-chain payments. +/// +/// Should be retrieved by calling [`Node::onchain_payment`]. +/// +/// [`Node::onchain_payment`]: crate::Node::onchain_payment +pub struct OnchainPayment { + runtime: Arc>>, + wallet: Arc, + channel_manager: Arc, + config: Arc, + logger: Arc, +} + +impl OnchainPayment { + pub(crate) fn new( + runtime: Arc>>, wallet: Arc, + channel_manager: Arc, config: Arc, logger: Arc, + ) -> Self { + Self { runtime, wallet, channel_manager, config, logger } + } + + /// Retrieve a new on-chain/funding address. + pub fn new_address(&self) -> Result { + let funding_address = self.wallet.get_new_address()?; + log_info!(self.logger, "Generated new funding address: {}", funding_address); + Ok(funding_address) + } + + /// Send an on-chain payment to the given address. + /// + /// This will respect any on-chain reserve we need to keep, i.e., won't allow to cut into + /// [`BalanceDetails::total_anchor_channels_reserve_sats`]. + /// + /// [`BalanceDetails::total_anchor_channels_reserve_sats`]: crate::BalanceDetails::total_anchor_channels_reserve_sats + pub fn send_to_address( + &self, address: &bitcoin::Address, amount_sats: u64, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let cur_anchor_reserve_sats = + crate::total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_balances(cur_anchor_reserve_sats).map(|(_, s)| s).unwrap_or(0); + + if spendable_amount_sats < amount_sats { + log_error!(self.logger, + "Unable to send payment due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, amount_sats + ); + return Err(Error::InsufficientFunds); + } + self.wallet.send_to_address(address, Some(amount_sats)) + } + + /// Send an on-chain payment to the given address, draining all the available funds. + /// + /// This is useful if you have closed all channels and want to migrate funds to another + /// on-chain wallet. + /// + /// Please note that this will **not** retain any on-chain reserves, which might be potentially + /// dangerous if you have open Anchor channels for which you can't trust the counterparty to + /// spend the Anchor output after channel closure. + pub fn send_all_to_address(&self, address: &bitcoin::Address) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + self.wallet.send_to_address(address, None) + } +} diff --git a/src/payment/payment_store.rs b/src/payment/payment_store.rs new file mode 100644 index 000000000..227d7218d --- /dev/null +++ b/src/payment/payment_store.rs @@ -0,0 +1,598 @@ +use crate::hex_utils; +use crate::io::{ + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use crate::logger::{log_error, Logger}; +use crate::types::DynStore; +use crate::Error; + +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::msgs::DecodeError; +use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning::util::ser::{Readable, Writeable}; +use lightning::{ + _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, + impl_writeable_tlv_based_enum, write_tlv_fields, +}; + +use std::collections::HashMap; +use std::iter::FromIterator; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; +use std::time; + +/// Represents a payment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PaymentDetails { + /// The identifier of this payment. + pub id: PaymentId, + /// The kind of the payment. + pub kind: PaymentKind, + /// The amount transferred. + pub amount_msat: Option, + /// The direction of the payment. + pub direction: PaymentDirection, + /// The status of the payment. + pub status: PaymentStatus, + /// Last update timestamp, as seconds since Unix epoch. + pub last_update: u64, +} + +impl Writeable for PaymentDetails { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + write_tlv_fields!(writer, { + (0, self.id, required), // Used to be `hash` for v0.2.1 and prior + // 1 briefly used to be lsp_fee_limits, could probably be reused at some point in the future. + // 2 used to be `preimage` before it was moved to `kind` in v0.3.0 + (2, None::>, required), + (3, self.kind, required), + // 4 used to be `secret` before it was moved to `kind` in v0.3.0 + (4, None::>, required), + (6, self.amount_msat, required), + (8, self.direction, required), + (10, self.status, required), + (131074, self.last_update, required) + }); + Ok(()) + } +} + +impl Readable for PaymentDetails { + fn read(reader: &mut R) -> Result { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, id, required), // Used to be `hash` + (1, lsp_fee_limits, option), + (2, preimage, required), + (3, kind_opt, option), + (4, secret, required), + (6, amount_msat, required), + (8, direction, required), + (10, status, required), + (131074, last_update, option), + }); + + let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?; + let preimage: Option = preimage.0.ok_or(DecodeError::InvalidValue)?; + let secret: Option = secret.0.ok_or(DecodeError::InvalidValue)?; + let amount_msat: Option = amount_msat.0.ok_or(DecodeError::InvalidValue)?; + let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?; + let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?; + let last_update: u64 = last_update.or(Some(0)).ok_or(DecodeError::InvalidValue)?; + + let kind = if let Some(kind) = kind_opt { + // If we serialized the payment kind, use it. + // This will always be the case for any version after v0.2.1. + kind + } else { + // Otherwise we persisted with v0.2.1 or before, and puzzle together the kind from the + // provided fields. + + // We used to track everything by hash, but switched to track everything by id + // post-v0.2.1. As both are serialized identically, we just switched the `0`-type field above + // from `PaymentHash` to `PaymentId` and serialize a separate `PaymentHash` in + // `PaymentKind` when needed. Here, for backwards compat, we can just re-create the + // `PaymentHash` from the id, as 'back then' `payment_hash == payment_id` was always + // true. + let hash = PaymentHash(id.0); + + if secret.is_some() { + if let Some(lsp_fee_limits) = lsp_fee_limits { + PaymentKind::Bolt11Jit { hash, preimage, secret, lsp_fee_limits } + } else { + PaymentKind::Bolt11 { hash, preimage, secret, bolt11_invoice: None } + } + } else { + PaymentKind::Spontaneous { hash, preimage } + } + }; + + Ok(PaymentDetails { id, kind, amount_msat, direction, status, last_update }) + } +} + +/// Represents the direction of a payment. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum PaymentDirection { + /// The payment is inbound. + Inbound, + /// The payment is outbound. + Outbound, +} + +impl_writeable_tlv_based_enum!(PaymentDirection, + (0, Inbound) => {}, + (1, Outbound) => {}; +); + +/// Represents the current status of a payment. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum PaymentStatus { + /// The payment is still pending. + Pending, + /// The payment succeeded. + Succeeded, + /// The payment failed. + Failed, +} + +impl_writeable_tlv_based_enum!(PaymentStatus, + (0, Pending) => {}, + (2, Succeeded) => {}, + (4, Failed) => {}; +); + +/// Represents the kind of a payment. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum PaymentKind { + /// An on-chain payment. + Onchain, + /// A [BOLT 11] payment. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + // TODO: Bolt11 { invoice: Option }, + Bolt11 { + /// The payment hash, i.e., the hash of the `preimage`. + hash: PaymentHash, + /// The pre-image used by the payment. + preimage: Option, + /// The secret used by the payment. + secret: Option, + /// The invoice that was paid. + bolt11_invoice: Option, + }, + /// A [BOLT 11] payment intended to open an [LSPS 2] just-in-time channel. + /// + /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md + /// [LSPS 2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + // TODO: Bolt11Jit { invoice: Option }, + Bolt11Jit { + /// The payment hash, i.e., the hash of the `preimage`. + hash: PaymentHash, + /// The pre-image used by the payment. + preimage: Option, + /// The secret used by the payment. + secret: Option, + /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. + /// + /// Allowing them to deduct this fee from the first inbound payment will pay for the LSP's + /// channel opening fees. + /// + /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. + /// + /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs + lsp_fee_limits: LSPFeeLimits, + }, + /// A spontaneous ("keysend") payment. + Spontaneous { + /// The payment hash, i.e., the hash of the `preimage`. + hash: PaymentHash, + /// The pre-image used by the payment. + preimage: Option, + }, +} + +impl_writeable_tlv_based_enum!(PaymentKind, + (0, Onchain) => {}, + (2, Bolt11) => { + (0, hash, required), + (2, preimage, option), + (4, secret, option), + (131072, bolt11_invoice, option), + }, + (4, Bolt11Jit) => { + (0, hash, required), + (2, preimage, option), + (4, secret, option), + (6, lsp_fee_limits, required), + }, + (8, Spontaneous) => { + (0, hash, required), + (2, preimage, option), + }; +); + +/// Limits applying to how much fee we allow an LSP to deduct from the payment amount. +/// +/// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. +/// +/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct LSPFeeLimits { + /// The maximal total amount we allow any configured LSP withhold from us when forwarding the + /// payment. + pub max_total_opening_fee_msat: Option, + /// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured + /// LSP withhold from us when forwarding the payment. + pub max_proportional_opening_fee_ppm_msat: Option, +} + +impl_writeable_tlv_based!(LSPFeeLimits, { + (0, max_total_opening_fee_msat, option), + (2, max_proportional_opening_fee_ppm_msat, option), +}); + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PaymentDetailsUpdate { + pub id: PaymentId, + pub preimage: Option>, + pub secret: Option>, + pub amount_msat: Option>, + pub direction: Option, + pub status: Option, +} + +impl PaymentDetailsUpdate { + pub fn new(id: PaymentId) -> Self { + Self { id, preimage: None, secret: None, amount_msat: None, direction: None, status: None } + } +} + +pub(crate) struct PaymentStore +where + L::Target: Logger, +{ + payments: Mutex>, + kv_store: Arc, + logger: L, +} + +impl PaymentStore +where + L::Target: Logger, +{ + pub(crate) fn new(payments: Vec, kv_store: Arc, logger: L) -> Self { + let payments = Mutex::new(HashMap::from_iter( + payments.into_iter().map(|payment| (payment.id, payment)), + )); + Self { payments, kv_store, logger } + } + + pub(crate) fn insert(&self, payment: PaymentDetails) -> Result { + let mut locked_payments = self.payments.lock().unwrap(); + + let id = payment.id.clone(); + let updated = locked_payments.insert(id.clone(), payment.clone()).is_some(); + self.persist_info(&id, &payment)?; + Ok(updated) + } + + pub(crate) fn remove(&self, id: &PaymentId) -> Result<(), Error> { + let store_key = hex_utils::to_string(&id.0); + self.kv_store + .remove( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key, + false, + ) + .map_err(|e| { + log_error!( + self.logger, + "Removing payment data for key {}/{}/{} failed due to: {}", + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + store_key, + e + ); + Error::PersistenceFailed + }) + } + + pub(crate) fn get(&self, id: &PaymentId) -> Option { + self.payments.lock().unwrap().get(id).cloned() + } + + pub(crate) fn update(&self, update: &PaymentDetailsUpdate) -> Result { + let mut updated = false; + let mut locked_payments = self.payments.lock().unwrap(); + + if let Some(payment) = locked_payments.get_mut(&update.id) { + if let Some(preimage_opt) = update.preimage { + match payment.kind { + PaymentKind::Bolt11 { ref mut preimage, .. } => *preimage = preimage_opt, + PaymentKind::Bolt11Jit { ref mut preimage, .. } => *preimage = preimage_opt, + PaymentKind::Spontaneous { ref mut preimage, .. } => *preimage = preimage_opt, + _ => {}, + } + } + + if let Some(secret_opt) = update.secret { + match payment.kind { + PaymentKind::Bolt11 { ref mut secret, .. } => *secret = secret_opt, + PaymentKind::Bolt11Jit { ref mut secret, .. } => *secret = secret_opt, + _ => {}, + } + } + + if let Some(amount_opt) = update.amount_msat { + payment.amount_msat = amount_opt; + } + + if let Some(status) = update.status { + payment.status = status; + } + + payment.last_update = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .unwrap_or(time::Duration::ZERO) + .as_secs(); + + self.persist_info(&update.id, payment)?; + updated = true; + } + + Ok(updated) + } + + pub(crate) fn list_filter bool>( + &self, f: F, + ) -> Vec { + self.payments + .lock() + .unwrap() + .iter() + .map(|(_, p)| p) + .filter(f) + .cloned() + .collect::>() + } + + fn persist_info(&self, id: &PaymentId, payment: &PaymentDetails) -> Result<(), Error> { + let store_key = hex_utils::to_string(&id.0); + let data = payment.encode(); + self.kv_store + .write( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key, + &data, + ) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + store_key, + e + ); + Error::PersistenceFailed + })?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lightning::util::{ + ser::Readable, + test_utils::{TestLogger, TestStore}, + }; + use std::io::Cursor; + use std::sync::Arc; + + /// We refactored `PaymentDetails` to hold a payment id and moved some required fields into + /// `PaymentKind`. Here, we keep the old layout available in order test de/ser compatibility. + #[derive(Clone, Debug, PartialEq, Eq)] + struct OldPaymentDetails { + pub hash: PaymentHash, + pub preimage: Option, + pub secret: Option, + pub amount_msat: Option, + pub direction: PaymentDirection, + pub status: PaymentStatus, + pub lsp_fee_limits: Option, + } + + impl_writeable_tlv_based!(OldPaymentDetails, { + (0, hash, required), + (1, lsp_fee_limits, option), + (2, preimage, required), + (4, secret, required), + (6, amount_msat, required), + (8, direction, required), + (10, status, required), + }); + + #[test] + fn payment_info_is_persisted() { + let store: Arc = Arc::new(TestStore::new(false)); + let logger = Arc::new(TestLogger::new()); + let payment_store = PaymentStore::new(Vec::new(), Arc::clone(&store), logger); + + let hash = PaymentHash([42u8; 32]); + let id = PaymentId([42u8; 32]); + assert!(!payment_store.get(&id).is_some()); + + let store_key = hex_utils::to_string(&hash.0); + assert!(store + .read( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key + ) + .is_err()); + + let kind = PaymentKind::Bolt11 { hash, preimage: None, secret: None, bolt11_invoice: None }; + let payment = PaymentDetails { + id, + kind, + amount_msat: None, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + last_update: 0, + }; + + assert_eq!(Ok(false), payment_store.insert(payment.clone())); + assert!(payment_store.get(&id).is_some()); + assert!(store + .read( + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + &store_key + ) + .is_ok()); + + assert_eq!(Ok(true), payment_store.insert(payment)); + assert!(payment_store.get(&id).is_some()); + + let mut update = PaymentDetailsUpdate::new(id); + update.status = Some(PaymentStatus::Succeeded); + assert_eq!(Ok(true), payment_store.update(&update)); + assert!(payment_store.get(&id).is_some()); + + assert_eq!(PaymentStatus::Succeeded, payment_store.get(&id).unwrap().status); + } + + #[test] + fn old_payment_details_deser_compat() { + // We refactored `PaymentDetails` to hold a payment id and moved some required fields into + // `PaymentKind`. Here, we test compatibility with the old layout. + let hash = PaymentHash([42u8; 32]); + let preimage = Some(PaymentPreimage([43u8; 32])); + let secret = Some(PaymentSecret([44u8; 32])); + let amount_msat = Some(45_000_000); + + // Test `Bolt11` de/ser + { + let old_bolt11_payment = OldPaymentDetails { + hash, + preimage, + secret, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits: None, + }; + + let old_bolt11_encoded = old_bolt11_payment.encode(); + assert_eq!( + old_bolt11_payment, + OldPaymentDetails::read(&mut Cursor::new(old_bolt11_encoded.clone())).unwrap() + ); + + let bolt11_decoded = + PaymentDetails::read(&mut Cursor::new(old_bolt11_encoded)).unwrap(); + let bolt11_reencoded = bolt11_decoded.encode(); + assert_eq!( + bolt11_decoded, + PaymentDetails::read(&mut Cursor::new(bolt11_reencoded)).unwrap() + ); + + match bolt11_decoded.kind { + PaymentKind::Bolt11 { hash: h, preimage: p, secret: s, bolt11_invoice: None } => { + assert_eq!(hash, h); + assert_eq!(preimage, p); + assert_eq!(secret, s); + }, + _ => { + panic!("Unexpected kind!"); + }, + } + } + + // Test `Bolt11Jit` de/ser + { + let lsp_fee_limits = Some(LSPFeeLimits { + max_total_opening_fee_msat: Some(46_000), + max_proportional_opening_fee_ppm_msat: Some(47_000), + }); + + let old_bolt11_jit_payment = OldPaymentDetails { + hash, + preimage, + secret, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits, + }; + + let old_bolt11_jit_encoded = old_bolt11_jit_payment.encode(); + assert_eq!( + old_bolt11_jit_payment, + OldPaymentDetails::read(&mut Cursor::new(old_bolt11_jit_encoded.clone())).unwrap() + ); + + let bolt11_jit_decoded = + PaymentDetails::read(&mut Cursor::new(old_bolt11_jit_encoded)).unwrap(); + let bolt11_jit_reencoded = bolt11_jit_decoded.encode(); + assert_eq!( + bolt11_jit_decoded, + PaymentDetails::read(&mut Cursor::new(bolt11_jit_reencoded)).unwrap() + ); + + match bolt11_jit_decoded.kind { + PaymentKind::Bolt11Jit { hash: h, preimage: p, secret: s, lsp_fee_limits: l } => { + assert_eq!(hash, h); + assert_eq!(preimage, p); + assert_eq!(secret, s); + assert_eq!(lsp_fee_limits, Some(l)); + }, + _ => { + panic!("Unexpected kind!"); + }, + } + } + + // Test `Spontaneous` de/ser + { + let old_spontaneous_payment = OldPaymentDetails { + hash, + preimage, + secret: None, + amount_msat, + direction: PaymentDirection::Inbound, + status: PaymentStatus::Pending, + lsp_fee_limits: None, + }; + + let old_spontaneous_encoded = old_spontaneous_payment.encode(); + assert_eq!( + old_spontaneous_payment, + OldPaymentDetails::read(&mut Cursor::new(old_spontaneous_encoded.clone())).unwrap() + ); + + let spontaneous_decoded = + PaymentDetails::read(&mut Cursor::new(old_spontaneous_encoded)).unwrap(); + let spontaneous_reencoded = spontaneous_decoded.encode(); + assert_eq!( + spontaneous_decoded, + PaymentDetails::read(&mut Cursor::new(spontaneous_reencoded)).unwrap() + ); + + match spontaneous_decoded.kind { + PaymentKind::Spontaneous { hash: h, preimage: p } => { + assert_eq!(hash, h); + assert_eq!(preimage, p); + }, + _ => { + panic!("Unexpected kind!"); + }, + } + } + } +} diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs new file mode 100644 index 000000000..1cd105c38 --- /dev/null +++ b/src/payment/spontaneous.rs @@ -0,0 +1,161 @@ +//! Holds a payment handler allowing to send spontaneous ("keysend") payments. + +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::error::Error; +use crate::logger::{log_error, log_info, FilesystemLogger, Logger}; +use crate::payment::payment_store::{ + PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, +}; +use crate::types::{ChannelManager, KeysManager, TlvEntry}; + +use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; +use lightning::ln::{PaymentHash, PaymentPreimage}; +use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::sign::EntropySource; + +use bitcoin::secp256k1::PublicKey; + +use std::sync::{Arc, RwLock}; + +/// A payment handler allowing to send spontaneous ("keysend") payments. +/// +/// Should be retrieved by calling [`Node::spontaneous_payment`]. +/// +/// [`Node::spontaneous_payment`]: crate::Node::spontaneous_payment +pub struct SpontaneousPayment { + runtime: Arc>>, + channel_manager: Arc, + keys_manager: Arc, + payment_store: Arc>>, + config: Arc, + logger: Arc, +} + +impl SpontaneousPayment { + pub(crate) fn new( + runtime: Arc>>, + channel_manager: Arc, keys_manager: Arc, + payment_store: Arc>>, config: Arc, + logger: Arc, + ) -> Self { + Self { runtime, channel_manager, keys_manager, payment_store, config, logger } + } + + /// Send a spontaneous, aka. "keysend", payment + pub fn send( + &self, amount_msat: u64, node_id: PublicKey, custom_tlvs: Vec, + ) -> Result { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); + let payment_hash = PaymentHash::from(payment_preimage); + let payment_id = PaymentId(payment_hash.0); + + if let Some(payment) = self.payment_store.get(&payment_id) { + if payment.status == PaymentStatus::Pending + || payment.status == PaymentStatus::Succeeded + { + log_error!(self.logger, "Payment error: must not send duplicate payments."); + return Err(Error::DuplicatePayment); + } + } + + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(node_id, self.config.default_cltv_expiry_delta), + amount_msat, + ); + let recipient_fields = RecipientOnionFields::spontaneous_empty() + .with_custom_tlvs(custom_tlvs.into_iter().map(|tlv| (tlv.r#type, tlv.value)).collect()) + .map_err(|_| { + log_error!(self.logger, "Payment error: invalid custom TLVs."); + Error::InvalidCustomTlv + })?; + + match self.channel_manager.send_spontaneous_payment_with_retry( + Some(payment_preimage), + recipient_fields, + PaymentId(payment_hash.0), + route_params, + Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT), + ) { + Ok(_hash) => { + log_info!(self.logger, "Initiated sending {}msat to {}.", amount_msat, node_id); + + let kind = PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(payment_preimage), + }; + + let payment = PaymentDetails { + id: payment_id, + kind, + status: PaymentStatus::Pending, + direction: PaymentDirection::Outbound, + amount_msat: Some(amount_msat), + last_update: 0, + }; + self.payment_store.insert(payment)?; + + Ok(payment_id) + }, + Err(e) => { + log_error!(self.logger, "Failed to send payment: {:?}", e); + + match e { + RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), + _ => { + let kind = PaymentKind::Spontaneous { + hash: payment_hash, + preimage: Some(payment_preimage), + }; + + let payment = PaymentDetails { + id: payment_id, + kind, + status: PaymentStatus::Failed, + direction: PaymentDirection::Outbound, + amount_msat: Some(amount_msat), + last_update: 0, + }; + + self.payment_store.insert(payment)?; + Err(Error::PaymentSendingFailed) + }, + } + }, + } + } + + /// Sends payment probes over all paths of a route that would be used to pay the given + /// amount to the given `node_id`. + /// + /// See [`Bolt11Payment::send_probes`] for more information. + /// + /// [`Bolt11Payment::send_probes`]: crate::payment::Bolt11Payment + pub fn send_probes(&self, amount_msat: u64, node_id: PublicKey) -> Result<(), Error> { + let rt_lock = self.runtime.read().unwrap(); + if rt_lock.is_none() { + return Err(Error::NotRunning); + } + + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); + let cltv_expiry_delta = self.config.default_cltv_expiry_delta; + + self.channel_manager + .send_spontaneous_preflight_probes( + node_id, + amount_msat, + cltv_expiry_delta, + liquidity_limit_multiplier, + ) + .map_err(|e| { + log_error!(self.logger, "Failed to send payment probes: {:?}", e); + Error::ProbeSendingFailed + })?; + + Ok(()) + } +} diff --git a/src/payment_store.rs b/src/payment_store.rs deleted file mode 100644 index 4e627fd40..000000000 --- a/src/payment_store.rs +++ /dev/null @@ -1,333 +0,0 @@ -use crate::hex_utils; -use crate::io::{ - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, -}; -use crate::logger::{log_error, Logger}; -use crate::Error; - -use lightning::ln::{PaymentHash, PaymentPreimage, PaymentSecret}; -use lightning::util::persist::KVStore; -use lightning::util::ser::Writeable; -use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; - -use std::collections::HashMap; -use std::iter::FromIterator; -use std::ops::Deref; -use std::sync::{Arc, Mutex}; -use std::time; - -/// Represents a payment. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PaymentDetails { - /// The payment hash, i.e., the hash of the `preimage`. - pub hash: PaymentHash, - /// The pre-image used by the payment. - pub preimage: Option, - /// The secret used by the payment. - pub secret: Option, - /// The amount transferred. - pub amount_msat: Option, - /// The direction of the payment. - pub direction: PaymentDirection, - /// The status of the payment. - pub status: PaymentStatus, - /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. - /// - /// This is only `Some` for payments received via a JIT-channel, in which case the first - /// inbound payment will pay for the LSP's channel opening fees. - /// - /// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. - /// - /// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs - pub lsp_fee_limits: Option, - /// The invoice that was paid. - pub bolt11_invoice: Option, - /// Last update timestamp, as seconds since Unix epoch. - pub last_update: u64, -} - -impl_writeable_tlv_based!(PaymentDetails, { - (0, hash, required), - (1, lsp_fee_limits, option), - (2, preimage, required), - (4, secret, required), - (6, amount_msat, required), - (8, direction, required), - (10, status, required), - (131072, bolt11_invoice, option), - (131074, last_update, required), -}); - -/// Represents the direction of a payment. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum PaymentDirection { - /// The payment is inbound. - Inbound, - /// The payment is outbound. - Outbound, -} - -impl_writeable_tlv_based_enum!(PaymentDirection, - (0, Inbound) => {}, - (1, Outbound) => {}; -); - -/// Represents the current status of a payment. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum PaymentStatus { - /// The payment is still pending. - Pending, - /// The payment succeeded. - Succeeded, - /// The payment failed. - Failed, -} - -impl_writeable_tlv_based_enum!(PaymentStatus, - (0, Pending) => {}, - (2, Succeeded) => {}, - (4, Failed) => {}; -); - -/// Limits applying to how much fee we allow an LSP to deduct from the payment amount. -/// -/// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information. -/// -/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct LSPFeeLimits { - /// The maximal total amount we allow any configured LSP withhold from us when forwarding the - /// payment. - pub max_total_opening_fee_msat: Option, - /// The maximal proportional fee, in parts-per-million millisatoshi, we allow any configured - /// LSP withhold from us when forwarding the payment. - pub max_proportional_opening_fee_ppm_msat: Option, -} - -impl_writeable_tlv_based!(LSPFeeLimits, { - (0, max_total_opening_fee_msat, option), - (2, max_proportional_opening_fee_ppm_msat, option), -}); - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct PaymentDetailsUpdate { - pub hash: PaymentHash, - pub preimage: Option>, - pub secret: Option>, - pub amount_msat: Option>, - pub direction: Option, - pub status: Option, - pub lsp_fee_limits: Option>, -} - -impl PaymentDetailsUpdate { - pub fn new(hash: PaymentHash) -> Self { - Self { - hash, - preimage: None, - secret: None, - amount_msat: None, - direction: None, - status: None, - lsp_fee_limits: None, - } - } -} - -pub(crate) struct PaymentStore -where - L::Target: Logger, -{ - payments: Mutex>, - kv_store: Arc, - logger: L, -} - -impl PaymentStore -where - L::Target: Logger, -{ - pub(crate) fn new(payments: Vec, kv_store: Arc, logger: L) -> Self { - let payments = Mutex::new(HashMap::from_iter( - payments.into_iter().map(|payment| (payment.hash, payment)), - )); - Self { payments, kv_store, logger } - } - - pub(crate) fn insert(&self, payment: PaymentDetails) -> Result { - let mut locked_payments = self.payments.lock().unwrap(); - - let payment = PaymentDetails { - last_update: time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) - .unwrap_or(time::Duration::ZERO) - .as_secs(), - ..payment - }; - - let hash = payment.hash.clone(); - let updated = locked_payments.insert(hash.clone(), payment.clone()).is_some(); - self.persist_info(&hash, &payment)?; - Ok(updated) - } - - pub(crate) fn remove(&self, hash: &PaymentHash) -> Result<(), Error> { - let store_key = hex_utils::to_string(&hash.0); - self.kv_store - .remove( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key, - false, - ) - .map_err(|e| { - log_error!( - self.logger, - "Removing payment data for key {}/{}/{} failed due to: {}", - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - store_key, - e - ); - Error::PersistenceFailed - }) - } - - pub(crate) fn get(&self, hash: &PaymentHash) -> Option { - self.payments.lock().unwrap().get(hash).cloned() - } - - pub(crate) fn update(&self, update: &PaymentDetailsUpdate) -> Result { - let mut updated = false; - let mut locked_payments = self.payments.lock().unwrap(); - - if let Some(payment) = locked_payments.get_mut(&update.hash) { - if let Some(preimage_opt) = update.preimage { - payment.preimage = preimage_opt; - } - - if let Some(secret_opt) = update.secret { - payment.secret = secret_opt; - } - - if let Some(amount_opt) = update.amount_msat { - payment.amount_msat = amount_opt; - } - - if let Some(status) = update.status { - payment.status = status; - } - - if let Some(lsp_fee_limits) = update.lsp_fee_limits { - payment.lsp_fee_limits = lsp_fee_limits - } - - payment.last_update = time::SystemTime::now() - .duration_since(time::UNIX_EPOCH) - .unwrap_or(time::Duration::ZERO) - .as_secs(); - - self.persist_info(&update.hash, payment)?; - updated = true; - } - - Ok(updated) - } - - pub(crate) fn list_filter bool>( - &self, f: F, - ) -> Vec { - self.payments - .lock() - .unwrap() - .iter() - .map(|(_, p)| p) - .filter(f) - .cloned() - .collect::>() - } - - fn persist_info(&self, hash: &PaymentHash, payment: &PaymentDetails) -> Result<(), Error> { - let store_key = hex_utils::to_string(&hash.0); - let data = payment.encode(); - self.kv_store - .write( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key, - &data, - ) - .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - store_key, - e - ); - Error::PersistenceFailed - })?; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use lightning::util::test_utils::{TestLogger, TestStore}; - use std::sync::Arc; - - #[test] - fn payment_info_is_persisted() { - let store = Arc::new(TestStore::new(false)); - let logger = Arc::new(TestLogger::new()); - let payment_store = PaymentStore::new(Vec::new(), Arc::clone(&store), logger); - - let hash = PaymentHash([42u8; 32]); - assert!(!payment_store.get(&hash).is_some()); - - let store_key = hex_utils::to_string(&hash.0); - assert!(store - .read( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key - ) - .is_err()); - - let payment = PaymentDetails { - hash, - preimage: None, - secret: None, - amount_msat: None, - direction: PaymentDirection::Inbound, - status: PaymentStatus::Pending, - lsp_fee_limits: None, - bolt11_invoice: None, - last_update: 0, - }; - - assert_eq!(Ok(false), payment_store.insert(payment.clone())); - assert!(payment_store.get(&hash).is_some()); - assert!(store - .read( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key - ) - .is_ok()); - - assert_eq!(Ok(true), payment_store.insert(payment)); - assert!(payment_store.get(&hash).is_some()); - assert_ne!(0, payment_store.get(&hash).unwrap().last_update); - - let mut update = PaymentDetailsUpdate::new(hash); - update.status = Some(PaymentStatus::Succeeded); - assert_eq!(Ok(true), payment_store.update(&update)); - assert!(payment_store.get(&hash).is_some()); - - assert_eq!(PaymentStatus::Succeeded, payment_store.get(&hash).unwrap().status); - assert_ne!(0, payment_store.get(&hash).unwrap().last_update); - } -} diff --git a/src/peer_store.rs b/src/peer_store.rs index 46ba1dbe2..21bd50872 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -3,10 +3,10 @@ use crate::io::{ PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, }; use crate::logger::{log_error, Logger}; +use crate::types::DynStore; use crate::{Error, SocketAddress}; use lightning::impl_writeable_tlv_based; -use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use bitcoin::secp256k1::PublicKey; @@ -15,20 +15,20 @@ use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, RwLock}; -pub struct PeerStore +pub struct PeerStore where L::Target: Logger, { peers: RwLock>, - kv_store: Arc, + kv_store: Arc, logger: L, } -impl PeerStore +impl PeerStore where L::Target: Logger, { - pub(crate) fn new(kv_store: Arc, logger: L) -> Self { + pub(crate) fn new(kv_store: Arc, logger: L) -> Self { let peers = RwLock::new(HashMap::new()); Self { peers, kv_store, logger } } @@ -83,13 +83,13 @@ where } } -impl ReadableArgs<(Arc, L)> for PeerStore +impl ReadableArgs<(Arc, L)> for PeerStore where L::Target: Logger, { #[inline] fn read( - reader: &mut R, args: (Arc, L), + reader: &mut R, args: (Arc, L), ) -> Result { let (kv_store, logger) = args; let read_peers: PeerStoreDeserWrapper = Readable::read(reader)?; @@ -150,7 +150,7 @@ mod tests { #[test] fn peer_info_persistence() { - let store = Arc::new(TestStore::new(false)); + let store: Arc = Arc::new(TestStore::new(false)); let logger = Arc::new(TestLogger::new()); let peer_store = PeerStore::new(Arc::clone(&store), Arc::clone(&logger)); diff --git a/src/sweep.rs b/src/sweep.rs index 93dac19fa..0c81c9e31 100644 --- a/src/sweep.rs +++ b/src/sweep.rs @@ -199,7 +199,7 @@ where fn rebroadcast_if_necessary(&self) { let (cur_height, cur_hash) = { let best_block = self.best_block.lock().unwrap(); - (best_block.height(), best_block.block_hash()) + (best_block.height, best_block.block_hash) }; let mut respend_descriptors = Vec::new(); @@ -277,7 +277,7 @@ where } fn prune_confirmed_outputs(&self) { - let cur_height = self.best_block.lock().unwrap().height(); + let cur_height = self.best_block.lock().unwrap().height; let mut locked_outputs = self.outputs.lock().unwrap(); // Prune all outputs that have sufficient depth by now. @@ -370,9 +370,9 @@ where ) { { let best_block = self.best_block.lock().unwrap(); - assert_eq!(best_block.block_hash(), header.prev_blockhash, + assert_eq!(best_block.block_hash, header.prev_blockhash, "Blocks must be connected in chain-order - the connected header must build on the last connected header"); - assert_eq!(best_block.height(), height - 1, + assert_eq!(best_block.height, height - 1, "Blocks must be connected in chain-order - the connected block height must be one greater than the previous height"); } @@ -384,9 +384,9 @@ where let new_height = height - 1; { let mut best_block = self.best_block.lock().unwrap(); - assert_eq!(best_block.block_hash(), header.block_hash(), + assert_eq!(best_block.block_hash, header.block_hash(), "Blocks must be disconnected in chain-order - the disconnected header must be the last connected header"); - assert_eq!(best_block.height(), height, + assert_eq!(best_block.height, height, "Blocks must be disconnected in chain-order - the disconnected block must have the correct height"); *best_block = BestBlock::new(header.prev_blockhash, new_height) } diff --git a/src/types.rs b/src/types.rs index 96b83c11e..e8350dafc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -12,9 +12,10 @@ use lightning::ln::ChannelId; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters}; -use lightning::sign::{EntropySource, InMemorySigner}; +use lightning::sign::InMemorySigner; use lightning::util::config::ChannelConfig as LdkChannelConfig; use lightning::util::config::MaxDustHTLCExposure as LdkMaxDustHTLCExposure; +use lightning::util::persist::KVStore; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning_net_tokio::SocketDescriptor; use lightning_transaction_sync::EsploraSyncClient; @@ -24,35 +25,34 @@ use bitcoin::OutPoint; use std::sync::{Arc, Mutex, RwLock}; -pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< +pub(crate) type DynStore = dyn KVStore + Sync + Send; + +pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< InMemorySigner, Arc, Arc, Arc, Arc, - Arc, + Arc, >; -pub(crate) type PeerManager = lightning::ln::peer_handler::PeerManager< +pub(crate) type PeerManager = lightning::ln::peer_handler::PeerManager< SocketDescriptor, - Arc>, + Arc, Arc, Arc, Arc, - Arc>>, + Arc>>, Arc, >; pub(crate) type ChainSource = EsploraSyncClient>; -pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< - Arc, - Arc>, - Arc, ->; +pub(crate) type LiquidityManager = + lightning_liquidity::LiquidityManager, Arc, Arc>; -pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< - Arc>, +pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< + Arc, Arc, Arc, Arc, @@ -83,6 +83,7 @@ pub(crate) type KeysManager = crate::wallet::WalletKeysManager< pub(crate) type Router = DefaultRouter< Arc, Arc, + Arc, Arc>, ProbabilisticScoringFeeParameters, Scorer, @@ -127,25 +128,29 @@ impl lightning::onion_message::messenger::MessageRouter for FakeMessageRouter { ) -> Result { unimplemented!() } - fn create_blinded_paths< - ES: EntropySource + ?Sized, - T: secp256k1::Signing + secp256k1::Verification, - >( - &self, _recipient: PublicKey, _peers: Vec, _entropy_source: &ES, - _secp_ctx: &Secp256k1, + fn create_blinded_paths( + &self, _recipient: PublicKey, _peers: Vec, _secp_ctx: &Secp256k1, ) -> Result, ()> { unreachable!() } } -pub(crate) type Sweeper = OutputSweeper< +pub(crate) type Sweeper = OutputSweeper< Arc, Arc, Arc, - Arc, + Arc, Arc, >; +pub(crate) type BumpTransactionEventHandler = + lightning::events::bump_transaction::BumpTransactionEventHandler< + Arc, + Arc, Arc>>, + Arc, + Arc, + >; + /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. @@ -166,6 +171,19 @@ impl Readable for UserChannelId { } } +/// The type of a channel, as negotiated during channel opening. +/// +/// See [`BOLT 2`] for more information. +/// +/// [`BOLT 2`]: https://github.com/lightning/bolts/blob/master/02-peer-protocol.md#defined-channel-types +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ChannelType { + /// A channel of type `option_static_remotekey`. + StaticRemoteKey, + /// A channel of type `option_anchors_zero_fee_htlc_tx`. + Anchors, +} + /// Details of a channel as returned by [`Node::list_channels`]. /// /// [`Node::list_channels`]: crate::Node::list_channels @@ -183,6 +201,10 @@ pub struct ChannelDetails { /// The channel's funding transaction output, if we've negotiated the funding transaction with /// our counterparty already. pub funding_txo: Option, + /// The channel type as negotiated during channel opening. + /// + /// Will be `None` until the channel negotiation has been completed. + pub channel_type: Option, /// The value, in satoshis, of this channel as it appears in the funding output. pub channel_value_sats: u64, /// The value, in satoshis, that must always be held as a reserve in the channel for us. This @@ -290,10 +312,19 @@ pub struct ChannelDetails { impl From for ChannelDetails { fn from(value: LdkChannelDetails) -> Self { + let channel_type = value.channel_type.map(|t| { + if t.requires_anchors_zero_fee_htlc_tx() { + ChannelType::Anchors + } else { + ChannelType::StaticRemoteKey + } + }); + ChannelDetails { channel_id: value.channel_id, counterparty_node_id: value.counterparty.node_id, funding_txo: value.funding_txo.and_then(|o| Some(o.into_bitcoin_outpoint())), + channel_type, channel_value_sats: value.channel_value_satoshis, unspendable_punishment_reserve: value.unspendable_punishment_reserve, user_channel_id: UserChannelId(value.user_channel_id), diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 207b40ead..57216c0f9 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -1,9 +1,14 @@ +pub use crate::payment::payment_store::{ + LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, +}; + pub use lightning::events::{ClosureReason, PaymentFailureReason}; -pub use lightning::ln::ChannelId; -pub use lightning::ln::PaymentSecret; +pub use lightning::ln::{ChannelId, PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning::util::string::UntrustedString; -pub use bitcoin::{BlockHash, Network, OutPoint}; +pub use lightning_invoice::Bolt11Invoice; + +pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid}; pub use bip39::Mnemonic; @@ -11,23 +16,17 @@ use crate::UniffiCustomTypeConverter; use crate::error::Error; use crate::hex_utils; -use crate::io::sqlite_store::SqliteStore; -use crate::{Node, SocketAddress, UserChannelId}; +use crate::{SocketAddress, UserChannelId}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; -use bitcoin::{Address, Txid}; -use lightning::ln::{PaymentHash, PaymentPreimage}; -use lightning_invoice::{Bolt11Invoice, SignedRawBolt11Invoice}; +use lightning::ln::channelmanager::PaymentId; +use lightning_invoice::SignedRawBolt11Invoice; use std::convert::TryInto; use std::str::FromStr; -/// This type alias is required as Uniffi doesn't support generics, i.e., we can only expose the -/// concretized types via this aliasing hack. -pub type LDKNode = Node; - impl UniffiCustomTypeConverter for PublicKey { type Builtin = String; @@ -78,6 +77,24 @@ impl UniffiCustomTypeConverter for Bolt11Invoice { } } +impl UniffiCustomTypeConverter for PaymentId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentId(bytes)); + } + } + Err(Error::InvalidPaymentId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + impl UniffiCustomTypeConverter for PaymentHash { type Builtin = String; diff --git a/src/wallet.rs b/src/wallet.rs index aa38eb986..6f6be72c7 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -4,6 +4,7 @@ use crate::Error; use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator}; +use lightning::events::bump_transaction::{Utxo, WalletSource}; use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; use lightning::ln::script::ShutdownScript; use lightning::sign::{ @@ -19,8 +20,14 @@ use bdk::wallet::AddressIndex; use bdk::FeeRate; use bdk::{SignOptions, SyncOptions}; +use bitcoin::address::{Payload, WitnessVersion}; use bitcoin::bech32::u5; +use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; +use bitcoin::hash_types::WPubkeyHash; +use bitcoin::hashes::Hash; +use bitcoin::key::XOnlyPublicKey; +use bitcoin::psbt::PartiallySignedTransaction; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; @@ -162,8 +169,17 @@ where Ok(address_info.address) } - pub(crate) fn get_balance(&self) -> Result { - Ok(self.inner.lock().unwrap().get_balance()?) + pub(crate) fn get_balances( + &self, total_anchor_channels_reserve_sats: u64, + ) -> Result<(u64, u64), Error> { + let wallet_lock = self.inner.lock().unwrap(); + let (total, spendable) = wallet_lock.get_balance().map(|bal| { + ( + bal.get_total(), + bal.get_spendable().saturating_sub(total_anchor_channels_reserve_sats), + ) + })?; + Ok((total, spendable)) } /// Send funds to the given address. @@ -245,6 +261,118 @@ where } } +impl WalletSource for Wallet +where + D: BatchDatabase, + B::Target: BroadcasterInterface, + E::Target: FeeEstimator, + L::Target: Logger, +{ + fn list_confirmed_utxos(&self) -> Result, ()> { + let locked_wallet = self.inner.lock().unwrap(); + let mut utxos = Vec::new(); + let confirmed_txs: Vec = locked_wallet + .list_transactions(false) + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve transactions from wallet: {}", e); + })? + .into_iter() + .filter(|t| t.confirmation_time.is_some()) + .collect(); + let unspent_confirmed_utxos = locked_wallet + .list_unspent() + .map_err(|e| { + log_error!( + self.logger, + "Failed to retrieve unspent transactions from wallet: {}", + e + ); + })? + .into_iter() + .filter(|u| confirmed_txs.iter().find(|t| t.txid == u.outpoint.txid).is_some()); + + for u in unspent_confirmed_utxos { + let payload = Payload::from_script(&u.txout.script_pubkey).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + + match payload { + Payload::WitnessProgram(program) => match program.version() { + WitnessVersion::V0 if program.program().len() == 20 => { + let wpkh = + WPubkeyHash::from_slice(program.program().as_bytes()).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + let utxo = Utxo::new_v0_p2wpkh(u.outpoint, u.txout.value, &wpkh); + utxos.push(utxo); + }, + WitnessVersion::V1 => { + XOnlyPublicKey::from_slice(program.program().as_bytes()).map_err(|e| { + log_error!(self.logger, "Failed to retrieve script payload: {}", e); + })?; + + let utxo = Utxo { + outpoint: u.outpoint, + output: TxOut { + value: u.txout.value, + script_pubkey: ScriptBuf::new_witness_program(&program), + }, + satisfaction_weight: 1 /* empty script_sig */ * WITNESS_SCALE_FACTOR as u64 + + 1 /* witness items */ + 1 /* schnorr sig len */ + 64, /* schnorr sig */ + }; + utxos.push(utxo); + }, + _ => { + log_error!( + self.logger, + "Unexpected witness version or length. Version: {}, Length: {}", + program.version(), + program.program().len() + ); + }, + }, + _ => { + log_error!( + self.logger, + "Tried to use a non-witness script. This must never happen." + ); + panic!("Tried to use a non-witness script. This must never happen."); + }, + } + } + + Ok(utxos) + } + + fn get_change_script(&self) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + let address_info = locked_wallet.get_address(AddressIndex::New).map_err(|e| { + log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); + })?; + + Ok(address_info.address.script_pubkey()) + } + + fn sign_psbt(&self, mut psbt: PartiallySignedTransaction) -> Result { + let locked_wallet = self.inner.lock().unwrap(); + + match locked_wallet.sign(&mut psbt, SignOptions::default()) { + Ok(finalized) => { + if !finalized { + log_error!(self.logger, "Failed to finalize PSBT."); + return Err(()); + } + }, + Err(err) => { + log_error!(self.logger, "Failed to sign transaction: {}", err); + return Err(()); + }, + } + + Ok(psbt.extract_tx()) + } +} + /// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are /// directly spendable by the BDK wallet. pub struct WalletKeysManager @@ -402,11 +530,10 @@ where })?; match address.payload { - bitcoin::address::Payload::WitnessProgram(program) => { - ShutdownScript::new_witness_program(&program).map_err(|e| { + Payload::WitnessProgram(program) => ShutdownScript::new_witness_program(&program) + .map_err(|e| { log_error!(self.logger, "Invalid shutdown script: {:?}", e); - }) - }, + }), _ => { log_error!( self.logger, diff --git a/tests/common.rs b/tests/common/mod.rs similarity index 73% rename from tests/common.rs rename to tests/common/mod.rs index 8b910ce56..110f94295 100644 --- a/tests/common.rs +++ b/tests/common/mod.rs @@ -2,9 +2,8 @@ #![allow(dead_code)] use ldk_node::io::sqlite_store::SqliteStore; -use ldk_node::{ - Builder, Config, Event, LogLevel, Node, NodeError, PaymentDirection, PaymentStatus, TlvEntry, -}; +use ldk_node::payment::{PaymentDirection, PaymentStatus}; +use ldk_node::{Builder, Config, Event, LogLevel, Node, NodeError, TlvEntry}; use lightning::ln::msgs::SocketAddress; use lightning::util::persist::KVStore; @@ -80,6 +79,42 @@ macro_rules! expect_channel_ready_event { pub(crate) use expect_channel_ready_event; +macro_rules! expect_payment_received_event { + ($node: expr, $amount_msat: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => { + println!("{} got event {:?}", $node.node_id(), e); + assert_eq!(amount_msat, $amount_msat); + $node.event_handled(); + payment_id + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); + }, + } + }}; +} + +pub(crate) use expect_payment_received_event; + +macro_rules! expect_payment_successful_event { + ($node: expr, $payment_id: expr, $fee_paid_msat: expr) => {{ + match $node.wait_next_event() { + ref e @ Event::PaymentSuccessful { payment_id, fee_paid_msat, .. } => { + println!("{} got event {:?}", $node.node_id(), e); + assert_eq!(fee_paid_msat, $fee_paid_msat); + assert_eq!(payment_id, $payment_id); + $node.event_handled(); + }, + ref e => { + panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); + }, + } + }}; +} + +pub(crate) use expect_payment_successful_event; + pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let bitcoind_exe = env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect( @@ -126,9 +161,13 @@ pub(crate) fn random_listening_addresses() -> Vec { listening_addresses } -pub(crate) fn random_config() -> Config { +pub(crate) fn random_config(anchor_channels: bool) -> Config { let mut config = Config::default(); + if !anchor_channels { + config.anchor_channels_config = None; + } + config.network = Network::Regtest; println!("Setting network: {}", config.network); @@ -146,9 +185,9 @@ pub(crate) fn random_config() -> Config { } #[cfg(feature = "uniffi")] -type TestNode = Arc>; +type TestNode = Arc; #[cfg(not(feature = "uniffi"))] -type TestNode = Node; +type TestNode = Node; macro_rules! setup_builder { ($builder: ident, $config: expr) => { @@ -162,14 +201,14 @@ macro_rules! setup_builder { pub(crate) use setup_builder; pub(crate) fn setup_two_nodes( - electrsd: &ElectrsD, allow_0conf: bool, -) -> (TestNode, TestNode) { + electrsd: &ElectrsD, allow_0conf: bool, anchor_channels: bool, +) -> (TestNode, TestNode) { println!("== Node A =="); - let config_a = random_config(); + let config_a = random_config(anchor_channels); let node_a = setup_node(electrsd, config_a); println!("\n== Node B =="); - let mut config_b = random_config(); + let mut config_b = random_config(anchor_channels); if allow_0conf { config_b.trusted_peers_0conf.push(node_a.node_id()); } @@ -177,13 +216,15 @@ pub(crate) fn setup_two_nodes( (node_a, node_b) } -pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { +pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); setup_builder!(builder, config); builder.set_esplora_server(esplora_url.clone()); let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); let node = builder.build_with_store(test_sync_store).unwrap(); node.start().unwrap(); + assert!(node.status().is_running); + assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some()); node } @@ -294,8 +335,8 @@ pub(crate) fn premine_and_distribute_funds( generate_blocks_and_wait(bitcoind, electrs, 1); } -pub fn open_channel( - node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, announce: bool, +pub fn open_channel( + node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, announce: bool, electrsd: &ElectrsD, ) { node_a @@ -316,14 +357,14 @@ pub fn open_channel( wait_for_tx(&electrsd.client, funding_txo_a.txid); } -pub(crate) fn do_channel_full_cycle( - node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, - allow_0conf: bool, +pub(crate) fn do_channel_full_cycle( + node_a: TestNode, node_b: TestNode, bitcoind: &BitcoindClient, electrsd: &E, allow_0conf: bool, + expect_anchor_channel: bool, ) { - let addr_a = node_a.new_onchain_address().unwrap(); - let addr_b = node_b.new_onchain_address().unwrap(); + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); - let premine_amount_sat = 100_000; + let premine_amount_sat = if expect_anchor_channel { 125_000 } else { 100_000 }; premine_and_distribute_funds( &bitcoind, @@ -355,6 +396,7 @@ pub(crate) fn do_channel_full_cycle( .unwrap(); assert_eq!(node_a.list_peers().first().unwrap().node_id, node_b.node_id()); + assert!(node_a.list_peers().first().unwrap().is_persisted); let funding_txo_a = expect_channel_pending_event!(node_a, node_b.node_id()); let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id()); assert_eq!(funding_txo_a, funding_txo_b); @@ -369,25 +411,30 @@ pub(crate) fn do_channel_full_cycle( node_b.sync_wallets().unwrap(); let onchain_fee_buffer_sat = 1500; - let node_a_upper_bound_sat = premine_amount_sat - funding_amount_sat; - let node_a_lower_bound_sat = premine_amount_sat - funding_amount_sat - onchain_fee_buffer_sat; + let anchor_reserve_sat = if expect_anchor_channel { 25_000 } else { 0 }; + let node_a_upper_bound_sat = premine_amount_sat - anchor_reserve_sat - funding_amount_sat; + let node_a_lower_bound_sat = + premine_amount_sat - anchor_reserve_sat - funding_amount_sat - onchain_fee_buffer_sat; assert!(node_a.list_balances().spendable_onchain_balance_sats < node_a_upper_bound_sat); assert!(node_a.list_balances().spendable_onchain_balance_sats > node_a_lower_bound_sat); - assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!( + node_b.list_balances().spendable_onchain_balance_sats, + premine_amount_sat - anchor_reserve_sat + ); expect_channel_ready_event!(node_a, node_b.node_id()); let user_channel_id = expect_channel_ready_event!(node_b, node_a.node_id()); - println!("\nB receive_payment"); + println!("\nB receive"); let invoice_amount_1_msat = 2500_000; - let invoice = node_b.receive_payment(invoice_amount_1_msat, &"asdf", 9217).unwrap(); + let invoice = node_b.bolt11_payment().receive(invoice_amount_1_msat, &"asdf", 9217).unwrap(); - println!("\nA send_payment"); - let payment_hash = node_a.send_payment(&invoice).unwrap(); - assert_eq!(node_a.send_payment(&invoice), Err(NodeError::DuplicatePayment)); + println!("\nA send"); + let payment_id = node_a.bolt11_payment().send(&invoice).unwrap(); + assert_eq!(node_a.bolt11_payment().send(&invoice), Err(NodeError::DuplicatePayment)); - assert_eq!(node_a.list_payments().first().unwrap().hash, payment_hash); + assert_eq!(node_a.list_payments().first().unwrap().id, payment_id); let outbound_payments_a = node_a.list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound); @@ -407,38 +454,39 @@ pub(crate) fn do_channel_full_cycle( expect_event!(node_a, PaymentSuccessful); expect_event!(node_b, PaymentReceived); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); // Assert we fail duplicate outbound payments and check the status hasn't changed. - assert_eq!(Err(NodeError::DuplicatePayment), node_a.send_payment(&invoice)); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(Err(NodeError::DuplicatePayment), node_a.bolt11_payment().send(&invoice)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(invoice_amount_1_msat)); // Test under-/overpayment let invoice_amount_2_msat = 2500_000; - let invoice = node_b.receive_payment(invoice_amount_2_msat, &"asdf", 9217).unwrap(); + let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &"asdf", 9217).unwrap(); let underpaid_amount = invoice_amount_2_msat - 1; assert_eq!( Err(NodeError::InvalidAmount), - node_a.send_payment_using_amount(&invoice, underpaid_amount) + node_a.bolt11_payment().send_using_amount(&invoice, underpaid_amount) ); - println!("\nB overpaid receive_payment"); - let invoice = node_b.receive_payment(invoice_amount_2_msat, &"asdf", 9217).unwrap(); + println!("\nB overpaid receive"); + let invoice = node_b.bolt11_payment().receive(invoice_amount_2_msat, &"asdf", 9217).unwrap(); let overpaid_amount_msat = invoice_amount_2_msat + 100; - println!("\nA overpaid send_payment"); - let payment_hash = node_a.send_payment_using_amount(&invoice, overpaid_amount_msat).unwrap(); + println!("\nA overpaid send"); + let payment_id = + node_a.bolt11_payment().send_using_amount(&invoice, overpaid_amount_msat).unwrap(); expect_event!(node_a, PaymentSuccessful); let received_amount = match node_b.wait_next_event() { ref e @ Event::PaymentReceived { amount_msat, .. } => { @@ -451,21 +499,27 @@ pub(crate) fn do_channel_full_cycle( }, }; assert_eq!(received_amount, overpaid_amount_msat); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(overpaid_amount_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(overpaid_amount_msat)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(overpaid_amount_msat)); // Test "zero-amount" invoice payment println!("\nB receive_variable_amount_payment"); - let variable_amount_invoice = node_b.receive_variable_amount_payment(&"asdf", 9217).unwrap(); + let variable_amount_invoice = + node_b.bolt11_payment().receive_variable_amount(&"asdf", 9217).unwrap(); let determined_amount_msat = 2345_678; - assert_eq!(Err(NodeError::InvalidInvoice), node_a.send_payment(&variable_amount_invoice)); - println!("\nA send_payment_using_amount"); - let payment_hash = - node_a.send_payment_using_amount(&variable_amount_invoice, determined_amount_msat).unwrap(); + assert_eq!( + Err(NodeError::InvalidInvoice), + node_a.bolt11_payment().send(&variable_amount_invoice) + ); + println!("\nA send_using_amount"); + let payment_id = node_a + .bolt11_payment() + .send_using_amount(&variable_amount_invoice, determined_amount_msat) + .unwrap(); expect_event!(node_a, PaymentSuccessful); let received_amount = match node_b.wait_next_event() { @@ -479,20 +533,21 @@ pub(crate) fn do_channel_full_cycle( }, }; assert_eq!(received_amount, determined_amount_msat); - assert_eq!(node_a.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_a.payment(&payment_hash).unwrap().direction, PaymentDirection::Outbound); - assert_eq!(node_a.payment(&payment_hash).unwrap().amount_msat, Some(determined_amount_msat)); - assert_eq!(node_b.payment(&payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!(node_b.payment(&payment_hash).unwrap().amount_msat, Some(determined_amount_msat)); + assert_eq!(node_a.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); + assert_eq!(node_b.payment(&payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&payment_id).unwrap().amount_msat, Some(determined_amount_msat)); // Test spontaneous/keysend payments println!("\nA send_spontaneous_payment"); let keysend_amount_msat = 2500_000; let tlv1 = TlvEntry { r#type: 131073, value: vec![0x00, 0x11, 0x22, 0x33] }; let tlv2 = TlvEntry { r#type: 131075, value: vec![0xaa, 0xbb] }; - let keysend_payment_hash = node_a - .send_spontaneous_payment(keysend_amount_msat, node_b.node_id(), vec![tlv1, tlv2]) + let keysend_payment_id = node_a + .spontaneous_payment() + .send(keysend_amount_msat, node_b.node_id(), vec![tlv1, tlv2]) .unwrap(); expect_event!(node_a, PaymentSuccessful); let received_keysend_amount = match node_b.wait_next_event() { @@ -506,24 +561,15 @@ pub(crate) fn do_channel_full_cycle( }, }; assert_eq!(received_keysend_amount, keysend_amount_msat); - assert_eq!(node_a.payment(&keysend_payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!( - node_a.payment(&keysend_payment_hash).unwrap().direction, - PaymentDirection::Outbound - ); - assert_eq!( - node_a.payment(&keysend_payment_hash).unwrap().amount_msat, - Some(keysend_amount_msat) - ); - assert_eq!(node_b.payment(&keysend_payment_hash).unwrap().status, PaymentStatus::Succeeded); - assert_eq!(node_b.payment(&keysend_payment_hash).unwrap().direction, PaymentDirection::Inbound); - assert_eq!( - node_b.payment(&keysend_payment_hash).unwrap().amount_msat, - Some(keysend_amount_msat) - ); + assert_eq!(node_a.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_a.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Outbound); + assert_eq!(node_a.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); + assert_eq!(node_b.payment(&keysend_payment_id).unwrap().status, PaymentStatus::Succeeded); + assert_eq!(node_b.payment(&keysend_payment_id).unwrap().direction, PaymentDirection::Inbound); + assert_eq!(node_b.payment(&keysend_payment_id).unwrap().amount_msat, Some(keysend_amount_msat)); println!("\nB close_channel"); - node_b.close_channel(&user_channel_id, node_a.node_id()).unwrap(); + node_b.close_channel(&user_channel_id, node_a.node_id(), false).unwrap(); expect_event!(node_a, ChannelClosed); expect_event!(node_b, ChannelClosed); diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index a5c11ad1b..d84eb6e86 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -5,7 +5,7 @@ mod common; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Amount; use ldk_node::lightning::ln::msgs::SocketAddress; -use ldk_node::{Builder, Event}; +use ldk_node::{Builder, ChannelType, Event}; use clightningrpc::lightningrpc::LightningRPC; use clightningrpc::responses::NetworkAddress; @@ -36,7 +36,7 @@ fn test_cln() { common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1); // Setup LDK Node - let config = common::random_config(); + let config = common::random_config(true); let mut builder = Builder::from_config(config); builder.set_esplora_server("http://127.0.0.1:3002".to_string()); @@ -44,7 +44,7 @@ fn test_cln() { node.start().unwrap(); // Premine some funds and distribute - let address = node.new_onchain_address().unwrap(); + let address = node.onchain_payment().new_address().unwrap(); let premine_amount = Amount::from_sat(5_000_000); common::premine_and_distribute_funds( &bitcoind_client, @@ -89,6 +89,7 @@ fn test_cln() { common::wait_for_tx(&electrs_client, funding_txo.txid); common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 6); let user_channel_id = common::expect_channel_ready_event!(node, cln_node_id); + assert_eq!(node.list_channels().first().unwrap().channel_type, Some(ChannelType::Anchors)); // Send a payment to CLN let mut rng = thread_rng(); @@ -97,7 +98,7 @@ fn test_cln() { cln_client.invoice(Some(2_500_000), &rand_label, &rand_label, None, None, None).unwrap(); let parsed_invoice = Bolt11Invoice::from_str(&cln_invoice.bolt11).unwrap(); - node.send_payment(&parsed_invoice).unwrap(); + node.bolt11_payment().send(&parsed_invoice).unwrap(); common::expect_event!(node, PaymentSuccessful); let cln_listed_invoices = cln_client.listinvoices(Some(&rand_label), None, None, None).unwrap().invoices; @@ -106,11 +107,11 @@ fn test_cln() { // Send a payment to LDK let rand_label: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let ldk_invoice = node.receive_payment(2_500_000, &rand_label, 3600).unwrap(); + let ldk_invoice = node.bolt11_payment().receive(2_500_000, &rand_label, 3600).unwrap(); cln_client.pay(&ldk_invoice.to_string(), Default::default()).unwrap(); common::expect_event!(node, PaymentReceived); - node.close_channel(&user_channel_id, cln_node_id).unwrap(); + node.close_channel(&user_channel_id, cln_node_id, false).unwrap(); common::expect_event!(node, ChannelClosed); node.stop().unwrap(); } diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 55e3dc553..7f3995750 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1,13 +1,16 @@ mod common; use common::{ - do_channel_full_cycle, expect_event, generate_blocks_and_wait, open_channel, + do_channel_full_cycle, expect_event, expect_payment_received_event, + expect_payment_successful_event, generate_blocks_and_wait, open_channel, premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestSyncStore, }; use ldk_node::{Builder, Event, NodeError}; +use lightning::util::persist::KVStore; + use bitcoin::{Amount, Network}; use std::sync::Arc; @@ -15,24 +18,31 @@ use std::sync::Arc; #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true); } #[test] fn channel_full_cycle_0conf() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, true); - do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true) + let (node_a, node_b) = setup_two_nodes(&electrsd, true, true); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, true, true) +} + +#[test] +fn channel_full_cycle_legacy_staticremotekey() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, false); } #[test] fn channel_open_fails_when_funds_insufficient() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); - let addr_a = node_a.new_onchain_address().unwrap(); - let addr_b = node_b.new_onchain_address().unwrap(); + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); let premine_amount_sat = 100_000; @@ -69,7 +79,7 @@ fn multi_hop_sending() { // Setup and fund 5 nodes let mut nodes = Vec::new(); for _ in 0..5 { - let config = random_config(); + let config = random_config(true); setup_builder!(builder, config); builder.set_esplora_server(esplora_url.clone()); let node = builder.build().unwrap(); @@ -77,7 +87,7 @@ fn multi_hop_sending() { nodes.push(node); } - let addresses = nodes.iter().map(|n| n.new_onchain_address().unwrap()).collect(); + let addresses = nodes.iter().map(|n| n.onchain_payment().new_address().unwrap()).collect(); let premine_amount_sat = 5_000_000; premine_and_distribute_funds( &bitcoind.client, @@ -130,16 +140,17 @@ fn multi_hop_sending() { // Sleep a bit for gossip to propagate. std::thread::sleep(std::time::Duration::from_secs(1)); - let invoice = nodes[4].receive_payment(2_500_000, &"asdf", 9217).unwrap(); - nodes[0].send_payment(&invoice).unwrap(); + let invoice = nodes[4].bolt11_payment().receive(2_500_000, &"asdf", 9217).unwrap(); + nodes[0].bolt11_payment().send(&invoice).unwrap(); - expect_event!(nodes[4], PaymentReceived); - expect_event!(nodes[0], PaymentSuccessful); + let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); + let fee_paid_msat = Some(2000); + expect_payment_successful_event!(nodes[0], payment_id, fee_paid_msat); } #[test] fn connect_to_public_testnet_esplora() { - let mut config = random_config(); + let mut config = random_config(true); config.network = Network::Testnet; setup_builder!(builder, config); builder.set_esplora_server("https://blockstream.info/testnet/api".to_string()); @@ -151,11 +162,12 @@ fn connect_to_public_testnet_esplora() { #[test] fn start_stop_reinit() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(); + let config = random_config(true); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.clone().into())); + let test_sync_store: Arc = + Arc::new(TestSyncStore::new(config.storage_dir_path.clone().into())); setup_builder!(builder, config); builder.set_esplora_server(esplora_url.clone()); @@ -166,7 +178,7 @@ fn start_stop_reinit() { let expected_node_id = node.node_id(); assert_eq!(node.start(), Err(NodeError::AlreadyRunning)); - let funding_address = node.new_onchain_address().unwrap(); + let funding_address = node.onchain_payment().new_address().unwrap(); assert_eq!(node.list_balances().total_onchain_balance_sats, 0); @@ -218,10 +230,10 @@ fn start_stop_reinit() { #[test] fn onchain_spend_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let (node_a, node_b) = setup_two_nodes(&electrsd, false); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); - let addr_a = node_a.new_onchain_address().unwrap(); - let addr_b = node_b.new_onchain_address().unwrap(); + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); premine_and_distribute_funds( &bitcoind.client, @@ -234,9 +246,12 @@ fn onchain_spend_receive() { node_b.sync_wallets().unwrap(); assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, 100000); - assert_eq!(Err(NodeError::InsufficientFunds), node_a.send_to_onchain_address(&addr_b, 1000)); + assert_eq!( + Err(NodeError::InsufficientFunds), + node_a.onchain_payment().send_to_address(&addr_b, 1000) + ); - let txid = node_b.send_to_onchain_address(&addr_a, 1000).unwrap(); + let txid = node_b.onchain_payment().send_to_address(&addr_a, 1000).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); wait_for_tx(&electrsd.client, txid); @@ -247,8 +262,8 @@ fn onchain_spend_receive() { assert!(node_b.list_balances().spendable_onchain_balance_sats > 98000); assert!(node_b.list_balances().spendable_onchain_balance_sats < 100000); - let addr_b = node_b.new_onchain_address().unwrap(); - let txid = node_a.send_all_to_onchain_address(&addr_b).unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + let txid = node_a.onchain_payment().send_all_to_address(&addr_b).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); wait_for_tx(&electrsd.client, txid); @@ -263,7 +278,7 @@ fn onchain_spend_receive() { #[test] fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let config = random_config(); + let config = random_config(true); let node = setup_node(&electrsd, config); // Tests arbitrary message signing and later verification @@ -272,3 +287,89 @@ fn sign_verify_msg() { let pkey = node.node_id(); assert!(node.verify_signature(msg, sig.as_str(), &pkey)); } + +#[test] +fn connection_restart_behavior() { + do_connection_restart_behavior(true); + do_connection_restart_behavior(false); +} + +fn do_connection_restart_behavior(persist: bool) { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, false); + + let node_id_a = node_a.node_id(); + let node_id_b = node_b.node_id(); + + let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); + + while !node_b.status().is_listening { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + node_a.connect(node_id_b, node_addr_b, persist).unwrap(); + + let peer_details_a = node_a.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_a.node_id, node_id_b); + assert_eq!(peer_details_a.is_persisted, persist); + assert!(peer_details_a.is_connected); + + let peer_details_b = node_b.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_b.node_id, node_id_a); + assert_eq!(peer_details_b.is_persisted, false); + assert!(peer_details_a.is_connected); + + // Restart nodes. + node_a.stop().unwrap(); + node_b.stop().unwrap(); + node_b.start().unwrap(); + node_a.start().unwrap(); + + // Sleep a bit to allow for the reconnect to happen. + std::thread::sleep(std::time::Duration::from_secs(5)); + + if persist { + let peer_details_a = node_a.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_a.node_id, node_id_b); + assert_eq!(peer_details_a.is_persisted, persist); + assert!(peer_details_a.is_connected); + + let peer_details_b = node_b.list_peers().first().unwrap().clone(); + assert_eq!(peer_details_b.node_id, node_id_a); + assert_eq!(peer_details_b.is_persisted, false); + assert!(peer_details_a.is_connected); + } else { + assert!(node_a.list_peers().is_empty()); + assert!(node_b.list_peers().is_empty()); + } +} + +#[test] +fn concurrent_connections_succeed() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let (node_a, node_b) = setup_two_nodes(&electrsd, false, true); + + let node_a = Arc::new(node_a); + let node_b = Arc::new(node_b); + + let node_id_b = node_b.node_id(); + let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); + + while !node_b.status().is_listening { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let mut handles = Vec::new(); + for _ in 0..10 { + let thread_node = Arc::clone(&node_a); + let thread_addr = node_addr_b.clone(); + let handle = std::thread::spawn(move || { + thread_node.connect(node_id_b, thread_addr, false).unwrap(); + }); + handles.push(handle); + } + + for h in handles { + h.join().unwrap(); + } +} diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 26d0456d4..47bae6b94 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -9,7 +9,7 @@ fn channel_full_cycle_with_vss_store() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); println!("== Node A =="); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let config_a = common::random_config(); + let config_a = common::random_config(true); let mut builder_a = Builder::from_config(config_a); builder_a.set_esplora_server(esplora_url.clone()); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); @@ -18,11 +18,11 @@ fn channel_full_cycle_with_vss_store() { node_a.start().unwrap(); println!("\n== Node B =="); - let config_b = common::random_config(); + let config_b = common::random_config(true); let mut builder_b = Builder::from_config(config_b); builder_b.set_esplora_server(esplora_url); let node_b = builder_b.build_with_vss_store(vss_base_url, "node_2_store".to_string()).unwrap(); node_b.start().unwrap(); - common::do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false); + common::do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true); }