diff --git a/.envrc b/.envrc new file mode 100644 index 00000000000..3550a30f2de --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6db02669e3c..431bfb0240c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust stable toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable + rustup default stable - name: Run externalized tests run: | cd ext-functional-test-demo @@ -45,7 +45,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust ${{ matrix.toolchain }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} - name: Use rust-lld linker on Windows if: matrix.platform == 'windows-latest' shell: bash @@ -78,7 +78,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust ${{ matrix.toolchain }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ matrix.toolchain }} + rustup default ${{ matrix.toolchain }} - name: Set RUSTFLAGS to deny warnings if: "matrix.toolchain == '1.75.0'" run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" @@ -120,7 +120,7 @@ jobs: fetch-depth: 0 - name: Install Rust stable toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal + rustup default stable - name: Run tests with coverage generation run: | cargo install cargo-llvm-cov @@ -150,7 +150,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + rustup default ${{ env.TOOLCHAIN }} - name: Cache routing graph snapshot id: cache-graph uses: actions/cache@v4 @@ -216,7 +216,7 @@ jobs: fetch-depth: 0 - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + rustup default ${{ env.TOOLCHAIN }} - name: Run cargo check for release build. run: | cargo check --release @@ -247,7 +247,7 @@ jobs: fetch-depth: 0 - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + rustup default ${{ env.TOOLCHAIN }} - name: Simulate docs.rs build run: ci/check-docsrs.sh @@ -260,11 +260,21 @@ jobs: uses: actions/checkout@v4 - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + rustup default ${{ env.TOOLCHAIN }} - name: Pin the syn and regex dependencies run: | - cd fuzz && cargo update -p regex --precise "1.9.6" && cargo update -p syn --precise "2.0.106" && cargo update -p quote --precise "1.0.41" - cd write-seeds && cargo update -p regex --precise "1.9.6" && cargo update -p syn --precise "2.0.106" && cargo update -p quote --precise "1.0.41" + cd fuzz && cargo update -p regex --precise "1.9.6" && cargo update -p serde --precise "1.0.228" && cargo update -p syn --precise "2.0.106" && cargo update -p quote --precise "1.0.41" + cargo update -p proc-macro2 --precise "1.0.103" --verbose && cargo update -p serde_json --precise "1.0.145" --verbose + cargo update -p itoa --precise "1.0.15" --verbose && cargo update -p ryu --precise "1.0.20" --verbose + cargo update -p libc --precise 0.2.183 + cargo update -p unicode-ident --precise 1.0.22 + cargo update -p honggfuzz --precise 0.5.59 + cargo update -p semver --precise 1.0.27 + cd write-seeds && cargo update -p regex --precise "1.9.6" && cargo update -p serde --precise "1.0.228" && cargo update -p syn --precise "2.0.106" && cargo update -p quote --precise "1.0.41" + cargo update -p proc-macro2 --precise "1.0.103" --verbose && cargo update -p serde_json --precise "1.0.145" --verbose + cargo update -p itoa --precise "1.0.15" --verbose && cargo update -p ryu --precise "1.0.20" --verbose + cargo update -p libc --precise 0.2.183 + cargo update -p unicode-ident --precise 1.0.22 - name: Sanity check fuzz targets on Rust ${{ env.TOOLCHAIN }} run: | cd fuzz @@ -282,7 +292,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + rustup default ${{ env.TOOLCHAIN }} - name: Install clippy run: | rustup component add clippy @@ -299,7 +309,7 @@ jobs: uses: actions/checkout@v4 - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + rustup default ${{ env.TOOLCHAIN }} - name: Install rustfmt run: | rustup component add rustfmt diff --git a/.github/workflows/check_commits.yml b/.github/workflows/check_commits.yml index 2920318858f..cd1fba023ec 100644 --- a/.github/workflows/check_commits.yml +++ b/.github/workflows/check_commits.yml @@ -21,7 +21,7 @@ jobs: fetch-depth: 0 - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + rustup default ${{ env.TOOLCHAIN }} - name: Fetch full tree and rebase on upstream run: | git remote add upstream https://github.com/lightningdevkit/rust-lightning diff --git a/.github/workflows/check_unicode.yml b/.github/workflows/check_unicode.yml new file mode 100644 index 00000000000..a01add3f814 --- /dev/null +++ b/.github/workflows/check_unicode.yml @@ -0,0 +1,26 @@ +name: Unicode listing up to date +on: + workflow_dispatch: + schedule: + - cron: '42 3 * * *' + +jobs: + check-unicode: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Check unicode file state + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/UnicodeData.txt https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt + contrib/gen_unicode_general_category.py /tmp/UnicodeData.txt -o /tmp/unicode.rs + if ! diff -u lightning-types/src/unicode.rs /tmp/unicode.rs; then + TITLE="Unicode listing out of date: ${{ github.workflow }}" + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + BODY="The unicode character listing is out of date, see $RUN_URL" + gh issue create --title "$TITLE" --body "$BODY" + fi diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index 03017e19320..e253be3cce3 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -9,48 +9,24 @@ on: jobs: semver-checks: - runs-on: ubuntu-latest + runs-on: debian-trixie steps: - name: Checkout source code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Rust stable toolchain run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable + rustup default stable rustup override set stable - - name: Check SemVer with default features - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - feature-group: default-features - - name: Check SemVer *without* default features - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - feature-group: only-explicit-features - - name: Check lightning-background-processor SemVer - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - package: lightning-background-processor - feature-group: only-explicit-features - - name: Check lightning-block-sync SemVer - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - package: lightning-block-sync - feature-group: only-explicit-features - features: rpc-client,rest-client - - name: Check lightning-transaction-sync electrum SemVer - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - manifest-path: lightning-transaction-sync/Cargo.toml - feature-group: only-explicit-features - features: electrum - - name: Check lightning-transaction-sync esplora-blocking SemVer - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - manifest-path: lightning-transaction-sync/Cargo.toml - feature-group: only-explicit-features - features: esplora-blocking - - name: Check lightning-transaction-sync esplora-async SemVer - uses: obi1kenobi/cargo-semver-checks-action@v2 - with: - manifest-path: lightning-transaction-sync/Cargo.toml - feature-group: only-explicit-features - features: esplora-async + - name: Install SemVer Checker + run: cargo install cargo-semver-checks --locked + - name: Check SemVer with all features + run: cargo semver-checks + - name: Check SemVer without any non-default features + run: cargo semver-checks --only-explicit-features + - name: Check lightning-transaction-sync SemVer + run: | + cd lightning-transaction-sync + cargo semver-checks + cargo semver-checks --only-explicit-features diff --git a/CHANGELOG.md b/CHANGELOG.md index e14753538be..8be785f004a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,188 @@ +# 0.2.5 - Aug 4, 2026 - "The MegaScan Project" + +## Bug Fixes + * LDK will no longer generate compact blinded paths with introduction nodes + defined by channel references for channels which are closed. This ensures + compact blinded paths built by LDK are reachable (#4828). + +## Security +0.2.5 fixes an on-chain funds-theft vulnerability for nodes forwarding HTLCs +which accept channels from untrusted nodes and several denial of service +vulnerabilities when forwarding HTLCs, accepting HTLCs which merit a response, +or in rare cases after a reorg when claiming HTLCs on chain. + * When processing an un-revoked counterparty's HTLC claim, ChannelMonitor will + no longer confuse two HTLCs with equivalent `payment_hash` and amounts, + leading to incorrect HTLC resolution (#4854). + * LDK will no longer panic when an onion message contains an invalid reply path + with the local node as the listed introduction point (#4850). + * LDK will no longer panic attempting to send messages which are too large to + fit in the lightning protocol's framing, including in cases where an HTLC + failure contains an oversized failure onion (in which case it will instead + decline to include attribution data) or when splicing with a transaction + which is too large (#4845, #4852). + * LDK's limit of 50 peers with unfunded channels is now properly enforced when + peers flood us with `open_channel` messages (#4851). + * LDK will no longer panic in rare cases when multiple pending HTLCs which + expire at different heights are being claimed on-chain and a reorg occurs + which changes whether we wish to batch our claim (#4849). + * `UntrustedString` and `PrintableString` now filter line- and paragraph-break + characters as well as control characters, providing additional robustness + especially for log parsing (#4848). + +Thanks to Project Loupe and Kyle W. Santiago for reporting security issues fixed +in this release. + + +# 0.2.4 - Jun 25, 2026 - "Release the CI" + +## Bug Fixes + * 0.2.3 broke the MSRV of the `lightning` crate, which now again compiles on + `rustc` 1.63 (#4737). + + +# 0.2.3 - Jun 18, 2026 - "Through the Loupe" + +## API Updates + * `DefaultMessageRouter` will now always generate blinded message paths that + provide no privacy (where our node is the introduction node) for nodes with + public channels. This works around an issue which will appear for any nodes + with LND peers that enable onion messaging - such peers will refuse to + forward BOLT 12 messages from unknown third parties, which most BOLT 12 + payers rely on today (#4647). + * Explicit `amount_msats` of 0 is rejected in BOLT 12 `Offer`s; `OfferBuilder` + now maps 0-amounts to an amount of `None` (#4324). + +## Bug Fixes + * `Features::supports_zero_conf` no longer clears the `ZeroConf` features and + `Features::requires_zero_conf` now correctly reports required, rather than + supported, status (#4517). + * If an MPP payment is claimed but `ChannelMonitorUpdate`s for some parts are + still being completed asynchronously, further channel updates (e.g. + forwarding another payment) are pending and the node restarts, the channel + could have become stuck (#4520). + * The presence of unconfirmed transactions actually no longer causes + `ElectrumSyncClient` to spuriously fail to sync (#4590). + * LSPS1, LSPS2, and LSPS5 persistence will no longer get stuck and refuse to + persist again after a single failure from the KVStore (#4597, #4282). + * Dropping the future returned by + `OutputSweeper::regenerate_and_broadcast_spend_if_necessary` no longer + results in future calls to the same method being spuriously ignored (#4598). + * Used async-receive offers are no longer refreshed on every timer tick once + their refresh time is reached (#4672). + * `FilesystemStore::list_all_keys` will no longer fail if there are stale + intermediate files lying around from a previous unclean shutdown (#4618). + * When forwarding an HTLC while in a blinded path with proportional fees over + 200%, LDK will no longer spuriously allow a forward that pays us 1 msat too + little in fees (#4697). + * Fixed a rare case where a channel could get stuck on reconnect when using + both async `ChannelMonitorUpdate` persistence and async signing (#4684). + * If we had exactly zero balance in a zero-fee-commitment channel, the + counterparty was able to splice all of their balance out, violating the + reserve requirements they'd otherwise be forced to keep (#4580). + * Providing an `Event::HTLCIntercepted` to the `LSPS2ServiceHandler` twice no + longer results in spuriously opening a channel early (#4656). + * `Event::PaymentSent::fee_paid_msat` is no longer `None` in cases where + `ChannelManager::abandon_payment` was called before the payment ultimately + completes anyway (#4651). + * `AnchorDescriptor::previous_utxo` now provides the correct `script_pubkey` + for non-zero-commitment-fee anchor channels (#4669). + * Syncing a `ChainMonitor` using the `Confirm` trait will no longer write some + full `ChannelMonitor`s to disk several times per block (#4544). + * `OMDomainResolver` now correctly accounts for failed queries when rate + limiting, ensuring we continue to respond to queries after failures (#4591). + * Calling `ChannelManager::send_payment_with_route` without a `route_params` + and with an invalid `Route` will no longer panic (#4707). + * `LSPS2ServiceHandler::channel_open_failed` now correctly fails intercepted + HTLCs rather than allowing them to fail just before expiry (#4677). + * `StaticInvoice::is_offer_expired` was corrected to check offer, rather than + static invoice, expiry (#4594). + * `lightning-custom-message`'s handling of `peer_connected` events now ensures + that sub-handlers will see a `peer_disconnected` event if a different + sub-handler refused the connection by `Err`ing `peer_connected` (#4595). + * Replay protection for LSPS5 signatures now detects replays which are only + different in the encoded signature's case (#4701). + * When `lightning-liquidity` is configured in the background processor, there + is no longer a stream of `Persisting LiquidityManager...` log spam (#4246). + * Incomplete MPP keysend payments will no longer see their HTLCs held until + expiry (#4558). + * `InvoiceRequestBuilder` will no longer accept a `quantity` of `0` for a + BOLT 12 `Offer`, allowing any quantity up to a bound (#4667). + * `lightning-custom-message` handlers that return `Ok(None)` when asked to + deserialize a message in their defined range no longer cause panics (#4709). + * Several spurious debug assertions were fixed (#4537, #4618, #4026) + +## Security +0.2.3 fixes several underestimates of the anchor reserves required to ensure we +can reliably close channels, several denial-of-service vulnerabilities and a +sanitization issue. + * `Bolt11Invoice::recover_payee_pub_key` no longer panics if called on an + invoice which set an explicit public key, rather than relying on public key + recovery. Note that this method is called from + `PaymentParameters::from_bolt11_invoice` (#4717). + * Maliciously-crafted unpayable invoices which have overflowing feerates will + no longer cause an `unwrap` failure panic (#4716). + * Parsing an `LSPSDateTime` which is before 1970 no longer panics. This is + reachable when parsing messages from counterparties (#4715). + * `possiblyrandom` did not properly generate random data except when it was + explicitly configured to. By default this means LDK is vulnerable to various + HashDoS attacks (#4719). + * `OMNameResolver` will no longer panic when looking up payment instructions + which include unicode characters at the start of a TXT record (#4718). + * When using the `anchor_channel_reserves` module to calculate reserves + required to pay for fees when closing anchor channels, zero-fee-commitment + channels were not considered. This could allow a counterparty to open many + channels, leaving us unable to properly force-close (#4592). + * The `anchor_channel_reserves` module overestimated the value of `Utxo`s in + the wallet by ignoring the `TxIn` cost to spend them (#4670). + * `PrintableString` did not properly sanitize unicode format characters, + allowing an attacker to corrupt the rendering of logs or UI (#4593, #4605). + * RGS data is now limited in how large of a graph it is able to cause a client + to store in memory. Note that RGS data is still considered a DoS vector in + general and you should only use semi-trusted RGS data (#4713). + * Counterparty-provided strings in failure messages are no longer logged in + full, reducing the ability of such a counterparty to spam our logs (#4714). + * Reading a corrupted `ChannelManager` or `ProbabilisticScorer` can no longer + cause us to allocate large amounts of memory (#4712). + +Thanks to Project Loupe for reporting most of the issues fixed in this release. + + +# 0.2.2 - Feb 6, 2025 - "An Async Splicing Production" + +## API Updates + * The `SplicePrototype` feature flag has been updated to refer to feature bit + 63 - the same as `SpliceProduction`. This resolves a compatibility issue with + eclair nodes due to the use of the same splicing feature flag (155) they were + using for a pre-standardization version of splicing (#4387). + +## Bug Fixes + * Async `ChannelMonitorUpdate` persistence operations which complete, but are + not marked as complete in a persisted `ChannelManager` prior to restart, + followed immediately by a block connection and then another restart could + result in some channel operations hanging leading for force-closures (#4377). + * A debug assertion failure reachable when receiving an invalid splicing + message from a peer was fixed (#4383). + + +# 0.2.1 - Jan 29, 2025 - "Electrum Confirmations Logged" + +## API Updates + * The `AttributionData` struct is now public, correcting an issue where it was + accidentally sealed preventing construction of some messages (#4268). + * The async background processor now exits even if work remains to be done as + soon as the sleeper returns the exit flag (#4259). + +## Bug Fixes + * The presence of unconfirmed transactions no longer causes + `ElectrumSyncClient` to spuriously fail to sync (#4341). + * `ChannelManager::splice_channel` now properly fails immediately if the + peer does not support splicing (#4262, #4274). + * A spurious debug assertion was removed which could fail in cases where an + HTLC fails to be forwarded after being accepted (#4312). + * Many log calls related to outbound payments were corrected to include a + `payment_hash` field (#4342). + + # 0.2 - Dec 2, 2025 - "Natively Asynchronous Splicing" ## API Updates @@ -40,11 +225,13 @@ pre-signed transactions, relying on anchor bumps instead. They also utilize the new TRUC + ephemeral dust policy in Bitcoin Core 29 to substantially improve the lightning security model. This requires having a path of Bitcoin - Core 29+ nodes between you and a miner for transactions to be mined. This - only works with LDK peers, and feature signaling may change in a future - version of LDK, breaking compatibility. This is negotiated automatically for - manually-accepted inbound channels and negotiated for outbound channels based - on `ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments`. + Core 29+ nodes between you and a miner for transactions to be mined. Bitcoin + Knots blocks these transactions by default, and is not recommended for use + with a lightning node. 0FC channels currently only work with LDK peers, and + feature signaling may change in a future version of LDK, breaking + compatibility. This is negotiated automatically for manually-accepted inbound + channels and negotiated for outbound channels based on + `ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments`. * `Event::BumpTransaction` is now always generated even if the transaction has sufficient fee. This allows you to manage transaction broadcasting more granularly for anchor channels (#4001). @@ -144,7 +331,9 @@ `ListProtocols` message (#3785). * A rare race which might lead `PeerManager` (and `lightning-net-tokio`) to stop reading from a peer until a new message is sent to that peer has been - fixed (#4168). + fixed. Note that this changed the semantics of the + `SocketDescriptor::send_data` method without changing its signature, check + that your implementation matches the new documentation (#4168). * The fields in `SocketAddress::OnionV3` are now correctly parsed, and the `Display` for such addresses is now lowercase (#4090). * `PeerManager` is now more conservative about disconnecting peers which aren't diff --git a/ci/check-lint.sh b/ci/check-lint.sh index bd4df3b85b8..11c6f083dd0 100755 --- a/ci/check-lint.sh +++ b/ci/check-lint.sh @@ -13,6 +13,7 @@ CLIPPY() { -A clippy::unwrap-or-default \ -A clippy::upper_case_acronyms \ -A clippy::swap-with-temporary \ + -A clippy::assertions-on-constants \ `# Things where we do odd stuff on purpose ` \ -A clippy::unusual_byte_groupings \ -A clippy::unit_arg \ diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index f3ecc72806a..5647267886e 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -11,12 +11,33 @@ function PIN_RELEASE_DEPS { # Starting with version 1.39.0, the `tokio` crate has an MSRV of rustc 1.70.0 [ "$RUSTC_MINOR_VERSION" -lt 70 ] && cargo update -p tokio --precise "1.38.1" --verbose + # Starting with version 1.0.229, the `serde_derive` crate has an MSRV of rustc 1.71. + [ "$RUSTC_MINOR_VERSION" -lt 71 ] && cargo update -p serde --precise "1.0.228" --verbose + # syn 2.0.107 requires rustc 1.68.0 [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p syn --precise "2.0.106" --verbose # quote 1.0.42 requires rustc 1.68.0 [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p quote --precise "1.0.41" --verbose + # Starting with version 1.0.104, the `proc-macro2` crate has an MSRV of rustc 1.68 + [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p proc-macro2 --precise "1.0.103" --verbose + + # Starting with version 1.0.146, the `serde_json` crate has an MSRV of rustc 1.68 + [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p serde_json --precise "1.0.145" --verbose + + # Starting with version 1.0.16, the `itoa` crate has an MSRV of rustc 1.68 + [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p itoa --precise "1.0.15" --verbose + + # Starting with version 1.0.21, the `ryu` crate has an MSRV of rustc 1.68 + [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p ryu --precise "1.0.20" --verbose + + # Starting with version 1.0.23, the `unicode-ident` crate has an MSRV of rustc 1.71 + [ "$RUSTC_MINOR_VERSION" -lt 71 ] && cargo update -p unicode-ident --precise 1.0.22 + + # Starting with version 0.2.184, the `libc` crate has an MSRV of rustc 1.65 + [ "$RUSTC_MINOR_VERSION" -lt 65 ] && cargo update -p libc --precise 0.2.183 --verbose + return 0 # Don't fail the script if our rustc is higher than the last check } @@ -43,6 +64,13 @@ PIN_RELEASE_DEPS # pin the release dependencies in our main workspace # lock_api 0.4.13 requires rustc 1.64.0 [ "$RUSTC_MINOR_VERSION" -lt 64 ] && cargo update -p lock_api --precise "0.4.12" --verbose +# inventory 0.3.22 requires rustc 1.68.0 +[ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p inventory --precise "0.3.21" --verbose + +# Starting with version 0.4.0, the `getrandom` crate has an MSRV of rustc 1.85 +GETRANDOM_VERSION="$(cargo tree 2>&1 | grep -o 'getrandom v0.4.*' | tr -d ' `' | tr 'v' '@' || echo -n)" +[ "$RUSTC_MINOR_VERSION" -lt 85 ] && cargo update -p "$GETRANDOM_VERSION" --precise 0.3.4 --verbose + export RUST_BACKTRACE=1 echo -e "\n\nChecking the workspace, except lightning-transaction-sync." @@ -56,8 +84,12 @@ cargo test --verbose --color always echo -e "\n\nTesting upgrade from prior versions of LDK" pushd lightning-tests [ "$RUSTC_MINOR_VERSION" -lt 65 ] && cargo update -p regex --precise "1.9.6" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 71 ] && cargo update -p serde --precise "1.0.228" --verbose [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p syn --precise "2.0.106" --verbose [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p quote --precise "1.0.41" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p proc-macro2 --precise "1.0.103" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 71 ] && cargo update -p unicode-ident --precise 1.0.22 +[ "$RUSTC_MINOR_VERSION" -lt 65 ] && cargo update -p libc --precise 0.2.183 --verbose cargo test popd @@ -128,8 +160,15 @@ cargo test -p lightning-invoice --verbose --color always --no-default-features - echo -e "\n\nTesting no_std build on a downstream no-std crate" # check no-std compatibility across dependencies pushd no-std-check +[ "$RUSTC_MINOR_VERSION" -lt 71 ] && cargo update -p serde --precise "1.0.228" --verbose [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p syn --precise "2.0.106" --verbose [ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p quote --precise "1.0.41" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p proc-macro2 --precise "1.0.103" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p serde_json --precise "1.0.145" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p itoa --precise "1.0.15" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 68 ] && cargo update -p ryu --precise "1.0.20" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 71 ] && cargo update -p unicode-ident --precise "1.0.22" --verbose +[ "$RUSTC_MINOR_VERSION" -lt 65 ] && cargo update -p libc --precise "0.2.183" --verbose cargo check --verbose --color always [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean popd @@ -143,6 +182,7 @@ popd if [ -f "$(which arm-none-eabi-gcc)" ]; then pushd no-std-check + [ "$RUSTC_MINOR_VERSION" -lt 71 ] && cargo update -p unicode-ident --precise "1.0.22" --verbose cargo build --target=thumbv7m-none-eabi [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean popd diff --git a/ci/ci-tx-sync-tests.sh b/ci/ci-tx-sync-tests.sh index 5f926a8e37b..f5199194d64 100755 --- a/ci/ci-tx-sync-tests.sh +++ b/ci/ci-tx-sync-tests.sh @@ -20,6 +20,22 @@ PIN_RELEASE_DEPS # pin the release dependencies # Starting with version 1.2.0, the `idna_adapter` crate has an MSRV of rustc 1.81.0. [ "$RUSTC_MINOR_VERSION" -lt 81 ] && cargo update -p idna_adapter --precise "1.1.0" --verbose +# Starting with version 1.9.0, the `zeroize` crate has an MSRV of rustc 1.85. +[ "$RUSTC_MINOR_VERSION" -lt 85 ] && cargo update -p zeroize --precise "1.8.2" --verbose + +# Starting with version 0.27.8, the `hyper-rustls` crate has an MSRV of rustc 1.85. +[ "$RUSTC_MINOR_VERSION" -lt 85 ] && cargo update -p hyper-rustls --precise "0.27.7" --verbose + +# Starting with version 0.1.35, the `jobserver` crate has an MSRV of rustc 1.85. +[ "$RUSTC_MINOR_VERSION" -lt 85 ] && cargo update -p jobserver --precise "0.1.34" --verbose + +# Starting with version 0.11.16, the `quinn-proto` crate depends on `rand` 0.10, which pulls in +# `chacha20` 0.10, `cpufeatures` 0.3, `rand_core` 0.10, `rand_pcg` 0.10 and `getrandom` 0.4, all of +# which are edition 2024 and thus not even parsable by our MSRV's cargo. `quinn` only reaches us as +# an unused optional dependency of `reqwest`, so it is never built, but cargo still insists on +# reading the manifests. +[ "$RUSTC_MINOR_VERSION" -lt 85 ] && cargo update -p quinn-proto --precise "0.11.15" --verbose + export RUST_BACKTRACE=1 echo -e "\n\nChecking Transaction Sync Clients with features." diff --git a/contrib/gen_unicode_general_category.py b/contrib/gen_unicode_general_category.py new file mode 100755 index 00000000000..28922e2acb6 --- /dev/null +++ b/contrib/gen_unicode_general_category.py @@ -0,0 +1,329 @@ +#!/usr/bin/env python3 +# This file is Copyright its original authors, visible in version control +# history. +# +# This file is licensed under the Apache License, Version 2.0 or the MIT license +# , at your option. +# You may not use this file except in accordance with one or both of these +# licenses. + +"""Generate Unicode general-category predicates from `UnicodeData.txt`. + +Emits three `pub(crate)` functions taking a `char`, split into disjoint +buckets across the Unicode top-level `C` ("Other") and `Z` ("Separator") +categories so callers can compose them: + + is_unicode_general_category_other — Cc / Cf / Cs / Co (assigned) + is_unicode_general_category_separator — Zl / Zp (line and paragraph + separators; `Zs` is + excluded as it contains + U+0020 SPACE) + is_unicode_general_category_unassigned — Cn (plus codepoints above + U+10FFFF, which aren't + valid codepoints at all) + +`UnicodeData.txt` is the canonical machine-readable listing of every assigned +codepoint in the Unicode Character Database. Each line is `;`-separated; field +0 is the codepoint (hex), field 1 is the name, and field 2 is the two-letter +general category (e.g. `Lu`, `Cf`, `Mn`). Codepoints absent from the file have +category `Cn` (Unassigned) by convention. + +Two encoding details to preserve: + * Large blocks of contiguous same-category codepoints are written as two + consecutive entries whose names end in `, First>` and `, Last>`. Every + codepoint between First and Last (inclusive) shares the listed category. + * The codepoint range is U+0000..=U+10FFFF. + +Each `matches!` arm in the assigned-Other table carries an end-of-line comment +derived from the `UnicodeData.txt` name field — typically the longest common +word prefix or suffix across the names in the range, falling back to the set +of categories when the names share nothing meaningful. The unassigned table +omits per-arm comments since every range there has the same meaning by +construction. + +Usage: + contrib/gen_unicode_general_category.py UnicodeData.txt > out.rs +""" + +import argparse +import sys +from pathlib import Path + +MAX_CODEPOINT = 0x10FFFF + +LICENSE_HEADER = """\ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. +""" + +GENERATED_NOTICE = """\ +// Auto-generated from the Unicode Character Database (UnicodeData.txt) by +// contrib/gen_unicode_general_category.py. Do not edit by hand; rerun the +// generator with an updated UnicodeData.txt to refresh the table. +""" + + +def _normalize_name(name): + """Strip the `<...>` wrapping and `, First` / `, Last` range markers so + that, e.g., `` becomes + `Non Private Use High Surrogate` and `` becomes `control`. + """ + if name.startswith("<") and name.endswith(">"): + inner = name[1:-1] + for suffix in (", First", ", Last"): + if inner.endswith(suffix): + inner = inner[: -len(suffix)] + return inner + return name + + +def parse_categories(path): + """Return `(cats, names)` mapping every codepoint listed in `path` to its + general category and to its (normalised) name. Codepoints absent from the + returned dicts have category `Cn` (Unassigned) and no name. + """ + cats = {} + names = {} + pending_first = None # (first_cp, first_cat, normalised_name) once a range opens. + with path.open() as f: + for lineno, raw in enumerate(f, 1): + line = raw.rstrip("\n") + if not line: + continue + fields = line.split(";") + if len(fields) < 3: + raise ValueError(f"{path}:{lineno}: expected at least 3 fields, got {len(fields)}") + cp = int(fields[0], 16) + name = fields[1] + cat = fields[2] + if pending_first is not None: + first_cp, first_cat, first_name = pending_first + if not name.endswith(", Last>"): + raise ValueError( + f"{path}:{lineno}: expected `, Last>` to close range " + f"opened at U+{first_cp:04X}, got name {name!r}" + ) + if cat != first_cat: + raise ValueError( + f"{path}:{lineno}: range U+{first_cp:04X}..=U+{cp:04X} " + f"has mismatched categories {first_cat!r} / {cat!r}" + ) + for x in range(first_cp, cp + 1): + cats[x] = cat + names[x] = first_name + pending_first = None + elif name.endswith(", First>"): + pending_first = (cp, cat, _normalize_name(name)) + else: + cats[cp] = cat + names[cp] = _normalize_name(name) + if pending_first is not None: + raise ValueError(f"{path}: dangling `, First>` entry at U+{pending_first[0]:04X}") + return cats, names + + +ASSIGNED_OTHER_CATS = frozenset({"Cc", "Cf", "Cs", "Co"}) +SEPARATOR_CATS = frozenset({"Zl", "Zp"}) + + +def coalesce_ranges(cats, names, target_cats, *, label): + """Walk U+0000..=U+10FFFF and return a list of `(start, end, label)` for + every contiguous run of codepoints whose general category is in + `target_cats`. Codepoints absent from `cats` are treated as `Cn`. + + If `label` is `True`, attach a comment summarising the codepoint names in + each range; otherwise every range gets an empty label. + """ + ranges = [] + start = None + for cp in range(MAX_CODEPOINT + 1): + in_target = cats.get(cp, "Cn") in target_cats + if in_target and start is None: + start = cp + elif not in_target and start is not None: + ranges.append((start, cp - 1)) + start = None + if start is not None: + ranges.append((start, MAX_CODEPOINT)) + + if not label: + return [(s, e, "") for s, e in ranges] + + labelled = [] + for s, e in ranges: + range_names = [] + range_cats = set() + for cp in range(s, e + 1): + range_cats.add(cats.get(cp, "Cn")) + n = names.get(cp) + if n is not None: + range_names.append(n) + labelled.append((s, e, _make_label(range_names, range_cats))) + return labelled + + +def _common_word_run(names, *, from_end): + """Return the longest sequence of words shared by every name, taken from + either the start (`from_end=False`) or the end (`from_end=True`) of each + name's whitespace-split tokens. + """ + if not names: + return "" + tokenised = [n.split() for n in names] + if from_end: + tokenised = [list(reversed(t)) for t in tokenised] + limit = min(len(t) for t in tokenised) + common = [] + for i in range(limit): + token = tokenised[0][i] + if all(t[i] == token for t in tokenised): + common.append(token) + else: + break + if from_end: + common.reverse() + return " ".join(common) + + +def _make_label(names, cats_in_range): + """Build a short human-readable label for a coalesced range. Applied to + the assigned-Other buckets only; each range there is `Cc`, `Cf`, `Cs`, + `Co`, or some contiguous union thereof. + + Rules, in order: + 1. All names identical → that name (e.g. `control`). + 2. Common leading or trailing words → the longer of the two. + 3. Otherwise, list the categories present (e.g. `Co / Cs`). + """ + unique = list(dict.fromkeys(names)) + if len(unique) == 1: + return unique[0] + + prefix = _common_word_run(names, from_end=False) + suffix = _common_word_run(names, from_end=True) + # Pick whichever is more informative; when both are non-empty, prefer the + # longer one. A multi-word prefix beats a single-word suffix. + label = prefix if len(prefix) >= len(suffix) else suffix + if label: + return label + return " / ".join(sorted(cats_in_range)) + + +def fmt_codepoint(cp): + # `UnicodeData.txt` uses 4-digit hex for the BMP and wider for higher + # planes; mirror that so the output stays readable next to the source data. + return f"0x{cp:04X}" if cp <= 0xFFFF else f"0x{cp:X}" + + +def _pattern(start, end): + if start == end: + return fmt_codepoint(start) + return f"{fmt_codepoint(start)}..={fmt_codepoint(end)}" + + +def _emit_matches_body(lines, arms): + """Append a `matches!(c as u32, ...)` body to `lines`, with one + `(pattern, label)` tuple per arm. The first arm sits at the `matches!` + argument indent and continuation `| ...` arms indent one level deeper, + matching the rustfmt convention used elsewhere in the tree. + """ + lines.append("\tmatches!(") + lines.append("\t\tc as u32,") + for i, (pattern, label) in enumerate(arms): + prefix = "\t\t" if i == 0 else "\t\t\t| " + comment = f" // {label}" if label else "" + lines.append(f"{prefix}{pattern}{comment}") + lines.append("\t)") + + +def render_rust(other_ranges, separator_ranges, unassigned_ranges): + """Render the final Rust source defining the `char`-taking predicates. + + `other_ranges`, `separator_ranges`, and `unassigned_ranges` are lists of + `(start, end, label)`. + The unassigned function additionally gets a synthetic final arm catching + `u32` values above U+10FFFF — these aren't valid Unicode codepoints, so + by definition they have no general category and the unassigned bucket is + the closest match. + """ + lines = [LICENSE_HEADER, GENERATED_NOTICE] + + lines.append("/// Returns `true` if `c` is in Unicode general category `Cc` (Control), `Cf`") + lines.append("/// (Format), `Cs` (Surrogate), or `Co` (Private Use) — the assigned codepoints") + lines.append("/// in the top-level `C` (\"Other\") category. The `Cs` portion of the table is") + lines.append("/// unreachable for `char` input (a `char` cannot hold a surrogate) but is kept") + lines.append("/// so the table mirrors the source UCD data verbatim. The disjoint `Cn`") + lines.append("/// (Unassigned) bucket is `is_unicode_general_category_unassigned`.") + lines.append("#[allow(dead_code)]") + lines.append("pub(crate) fn is_unicode_general_category_other(c: char) -> bool {") + other_arms = [(_pattern(s, e), label) for s, e, label in other_ranges] + _emit_matches_body(lines, other_arms) + lines.append("}") + lines.append("") + + lines.append("/// Returns `true` if `c` is in Unicode general category `Zl` (Line") + lines.append("/// Separator) or `Zp` (Paragraph Separator). Terminals and log viewers") + lines.append("/// commonly render these as hard line breaks, so untrusted strings filter") + lines.append("/// them alongside the `C` buckets. `Zs` (Space Separator) is deliberately") + lines.append("/// excluded: it contains U+0020 SPACE and other ordinary spacing") + lines.append("/// characters.") + lines.append("#[allow(dead_code)]") + lines.append("pub(crate) fn is_unicode_general_category_separator(c: char) -> bool {") + separator_arms = [(_pattern(s, e), label) for s, e, label in separator_ranges] + _emit_matches_body(lines, separator_arms) + lines.append("}") + lines.append("") + + lines.append("/// Returns `true` if `c` is in Unicode general category `Cn` (Unassigned), or") + lines.append("/// strictly above U+10FFFF. The trailing `0x110000..=u32::MAX` arm is") + lines.append("/// unreachable for `char` input (a `char` is bounded to U+10FFFF) but is kept") + lines.append("/// for defensive coverage of the underlying `u32`. The disjoint Cc / Cf / Cs /") + lines.append("/// Co bucket is `is_unicode_general_category_other`.") + lines.append("#[allow(dead_code)]") + lines.append("pub(crate) fn is_unicode_general_category_unassigned(c: char) -> bool {") + unassigned_arms = [(_pattern(s, e), label) for s, e, label in unassigned_ranges] + unassigned_arms.append(("0x110000..=u32::MAX", "above U+10FFFF — unreachable for `char`")) + _emit_matches_body(lines, unassigned_arms) + lines.append("}") + lines.append("") + + return "\n".join(lines) + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("unicode_data", type=Path, help="Path to UnicodeData.txt") + ap.add_argument( + "-o", "--output", type=Path, default=None, + help="Output Rust file (default: stdout)", + ) + args = ap.parse_args(argv) + + cats, names = parse_categories(args.unicode_data) + other = coalesce_ranges(cats, names, ASSIGNED_OTHER_CATS, label=True) + separator = coalesce_ranges(cats, names, SEPARATOR_CATS, label=True) + unassigned = coalesce_ranges(cats, names, frozenset({"Cn"}), label=False) + rust = render_rust(other, separator, unassigned) + + if args.output is None: + sys.stdout.write(rust) + else: + args.output.write_text(rust) + print( + f"Wrote {args.output} " + f"({len(other)} assigned-Other ranges, " + f"{len(separator)} separator ranges, " + f"{len(unassigned)} unassigned ranges).", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000000..fec6727ceba --- /dev/null +++ b/flake.lock @@ -0,0 +1,100 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1786348189, + "narHash": "sha256-tnTpFky6/GeXVMBO+hLJK2jWS4J8TYloL+rqRME9gAk=", + "owner": "nix-community", + "repo": "fenix", + "rev": "a0b1aef63d7e9c5568958e0d90f6fe48f0f4a7a2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1786313170, + "narHash": "sha256-9BG7OgUWdu0ONDO5X2q6+K4bsuBITkX/3W4nNJu1Ito=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "fcb8fcd6bf2d0adecae5bd491afaaaf8311b758d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-26.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1786285255, + "narHash": "sha256-zJukKxGKLxcO7E6+P49Qe6XaSbbyGG44kFvQYLWaD3M=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "57411b3a0fb0181ed5965f6541d5ed39a9d79d4c", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000000..f2b3835a1af --- /dev/null +++ b/flake.nix @@ -0,0 +1,85 @@ +{ + description = "Rust Lightning Development Environment"; + + inputs = { + # Nixpkgs channel. New channels are released every 6 months. + # See: https://github.com/NixOS/nixpkgs/tags + nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; + + # This makes it easy for the flake to be multi-platform. + # See: https://github.com/numtide/flake-utils + flake-utils.url = "github:numtide/flake-utils"; + + # Provides pinned Rust toolchains. + # See: https://github.com/nix-community/fenix + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + fenix, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = import nixpkgs { inherit system; }; + + # Pinned stable toolchain. The version is deliberately bounded: + # * clippy must be >= 1.87 so `./ci/check-lint.sh` resolves the lints it + # `-A`s (e.g. `clippy::manual_is_multiple_of`); older clippy E0602s on the + # unknown lint name. + # * but NOT 1.92+, whose clippy grows `assertions_on_constants` (fires on + # `assert!(cfg!(fuzzing), ..)` in lightning/src/chain/channelmonitor.rs) + # and would break `-D warnings` on the pre-existing tree. + # 1.90.0 sits in that window and matches what CI's `stable` resolves to. + rust-toolchain = + (fenix.packages.${system}.toolchainOf { + channel = "1.90.0"; + sha256 = "sha256-SJwZ8g0zF2WrKDVmHrVG3pD2RGoQeo24MEXnNx5FyuI="; + }).withComponents + [ + "rustc" + "cargo" + "clippy" + "rustfmt" + "rust-src" # Needed for the rust-analyzer extension to work. + ]; + in + { + # The default development shell. Use `nix develop` or direnv to enter it. + devShells.default = pkgs.mkShell { + name = "rust-lightning-dev-shell"; + + packages = [ + rust-toolchain + ] + ++ (with pkgs; [ + just # Command runner + nodejs # JavaScript runtime, required for MCP tools + mold # Fast linker for Rust/C/C++ + pnpm # Package manager for JavaScript, required for MCP tools + stdenv.cc.cc.lib # C++ standard library for runtime + ]); + + env = { + # OpenSSL configuration for Nix + PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; + # C++ standard library path for runtime + LD_LIBRARY_PATH = "${pkgs.stdenv.cc.cc.lib}/lib"; + }; + + shellHook = '' + echo "Rust Lightning dev shell" + rustc --version + cargo --version + ''; + }; + } + ); +} diff --git a/fuzz/ci-fuzz.sh b/fuzz/ci-fuzz.sh index d1274d751a8..92d689f2175 100755 --- a/fuzz/ci-fuzz.sh +++ b/fuzz/ci-fuzz.sh @@ -21,7 +21,7 @@ RUSTFLAGS="$RUSTFLAGS --cfg=fuzzing" cargo run ../hfuzz_workspace/full_stack_tar cargo clean popd -cargo install --color always --force honggfuzz --no-default-features +cargo install --color always --force honggfuzz --no-default-features --version "=0.5.59" --locked # Because we're fuzzing relatively few iterations, the maximum possible # compiler optimizations aren't necessary, so we turn off LTO diff --git a/fuzz/src/peer_crypt.rs b/fuzz/src/peer_crypt.rs index b01aa02400b..afd6ccef307 100644 --- a/fuzz/src/peer_crypt.rs +++ b/fuzz/src/peer_crypt.rs @@ -79,9 +79,8 @@ pub fn do_test(data: &[u8]) { let mut buf = [0; 65536 + 16]; loop { if get_slice!(1)[0] == 0 { - crypter.encrypt_buffer(MessageBuf::from_encoded(&get_slice!(slice_to_be16( - get_slice!(2) - )))); + let msg = MessageBuf::from_encoded(&get_slice!(slice_to_be16(get_slice!(2)))).unwrap(); + crypter.encrypt_buffer(msg); } else { let len = match crypter.decrypt_length_header(get_slice!(16 + 2)) { Ok(len) => len, diff --git a/fuzz/src/process_onion_failure.rs b/fuzz/src/process_onion_failure.rs index 1bc9900718a..ac70562c006 100644 --- a/fuzz/src/process_onion_failure.rs +++ b/fuzz/src/process_onion_failure.rs @@ -9,10 +9,12 @@ use lightning::{ ln::{ channelmanager::{HTLCSource, PaymentId}, msgs::OnionErrorPacket, + onion_utils, }, routing::router::{BlindedTail, Path, RouteHop, TrampolineHop}, types::features::{ChannelFeatures, NodeFeatures}, util::logger::Logger, + util::ser::Readable, }; // Imports that need to be added manually @@ -126,19 +128,18 @@ fn do_test(data: &[u8], out: Out) { let failure_data = get_slice!(failure_len); let attribution_data = if get_bool!() { - Some(lightning::ln::AttributionData { - hold_times: get_slice!(80).try_into().unwrap(), - hmacs: get_slice!(840).try_into().unwrap(), - }) + let mut bytes = get_slice!(80 + 840); + let data: onion_utils::AttributionData = Readable::read(&mut bytes).unwrap(); + Some(data) } else { None }; let encrypted_packet = OnionErrorPacket { data: failure_data.into(), attribution_data: attribution_data.clone() }; - lightning::ln::process_onion_failure(&secp_ctx, &logger, &htlc_source, encrypted_packet); + onion_utils::process_onion_failure(&secp_ctx, &logger, &htlc_source, encrypted_packet); if let Some(attribution_data) = attribution_data { - lightning::ln::decode_fulfill_attribution_data( + onion_utils::decode_fulfill_attribution_data( &secp_ctx, &logger, &path, diff --git a/justfile b/justfile new file mode 100644 index 00000000000..ad4ad0626f5 --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +default: + @just --list --unsorted + +# Run all code quality checks. TODO: Add format and linting when branch is clean +check: + RUSTFLAGS="--cfg=lsps1_service" cargo test --workspace + +# Run just the tests +test: + RUSTFLAGS="--cfg=lsps1_service" cargo test --workspace + +# Format all sources in place. +fmt: + cargo fmt --all diff --git a/lightning-background-processor/Cargo.toml b/lightning-background-processor/Cargo.toml index 828a8017574..ffe0b4ce743 100644 --- a/lightning-background-processor/Cargo.toml +++ b/lightning-background-processor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-background-processor" -version = "0.2.0" +version = "0.2.3" authors = ["Valentine Wallace "] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning" diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index 19333c5823a..3a3fbe499a9 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -416,6 +416,11 @@ pub const NO_ONION_MESSENGER: Option< >, > = None; +/// Supertrait combining [`KVStore`] and [`KVStoreSync`] so that a single `dyn` trait object can +/// satisfy the bounds required by [`ALiquidityManager`]. +pub trait KVStoreFull: KVStore + KVStoreSync {} +impl KVStoreFull for T {} + /// When initializing a background processor without a liquidity manager, this can be used to avoid /// specifying a concrete `LiquidityManager` type. #[cfg(not(c_bindings))] @@ -430,14 +435,16 @@ pub const NO_LIQUIDITY_MANAGER: Option< CM = &DynChannelManager, Filter = dyn chain::Filter + Send + Sync, C = &(dyn chain::Filter + Send + Sync), - KVStore = dyn lightning::util::persist::KVStore + Send + Sync, - K = &(dyn lightning::util::persist::KVStore + Send + Sync), + KVStore = dyn KVStoreFull + Send + Sync, + K = &(dyn KVStoreFull + Send + Sync), TimeProvider = dyn lightning_liquidity::utils::time::TimeProvider + Send + Sync, TP = &(dyn lightning_liquidity::utils::time::TimeProvider + Send + Sync), BroadcasterInterface = dyn lightning::chain::chaininterface::BroadcasterInterface + Send + Sync, T = &(dyn BroadcasterInterface + Send + Sync), + Logger = dyn lightning::util::logger::Logger + Send + Sync, + L = &(dyn lightning::util::logger::Logger + Send + Sync), > + Send + Sync, >, @@ -465,6 +472,8 @@ pub const NO_LIQUIDITY_MANAGER_SYNC: Option< + Send + Sync, T = &(dyn BroadcasterInterface + Send + Sync), + Logger = dyn lightning::util::logger::Logger + Send + Sync, + L = &(dyn lightning::util::logger::Logger + Send + Sync), > + Send + Sync, >, @@ -476,11 +485,11 @@ pub(crate) mod futures_util { use core::pin::Pin; use core::task::{Poll, RawWaker, RawWakerVTable, Waker}; pub(crate) struct Selector< - A: Future + Unpin, + A: Future + Unpin, B: Future + Unpin, C: Future + Unpin, D: Future + Unpin, - E: Future + Unpin, + E: Future + Unpin, > { pub a: A, pub b: B, @@ -490,28 +499,30 @@ pub(crate) mod futures_util { } pub(crate) enum SelectorOutput { - A, + A(bool), B, C, D, - E(bool), + E, } impl< - A: Future + Unpin, + A: Future + Unpin, B: Future + Unpin, C: Future + Unpin, D: Future + Unpin, - E: Future + Unpin, + E: Future + Unpin, > Future for Selector { type Output = SelectorOutput; fn poll( mut self: Pin<&mut Self>, ctx: &mut core::task::Context<'_>, ) -> Poll { + // Bias the selector so it first polls the sleeper future, allowing to exit immediately + // if the flag is set. match Pin::new(&mut self.a).poll(ctx) { - Poll::Ready(()) => { - return Poll::Ready(SelectorOutput::A); + Poll::Ready(res) => { + return Poll::Ready(SelectorOutput::A(res)); }, Poll::Pending => {}, } @@ -534,8 +545,8 @@ pub(crate) mod futures_util { Poll::Pending => {}, } match Pin::new(&mut self.e).poll(ctx) { - Poll::Ready(res) => { - return Poll::Ready(SelectorOutput::E(res)); + Poll::Ready(()) => { + return Poll::Ready(SelectorOutput::E); }, Poll::Pending => {}, } @@ -758,6 +769,12 @@ use futures_util::{dummy_waker, Joiner, OptionalSelector, Selector, SelectorOutp /// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> Pin> + 'static + Send>> { todo!() } /// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> Pin, io::Error>> + 'static + Send>> { todo!() } /// # } +/// # impl lightning::util::persist::KVStoreSync for Store { +/// # fn read(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> io::Result> { Ok(Vec::new()) } +/// # fn write(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec) -> io::Result<()> { Ok(()) } +/// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> io::Result<()> { Ok(()) } +/// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { Ok(Vec::new()) } +/// # } /// # use core::time::Duration; /// # struct DefaultTimeProvider; /// # @@ -782,7 +799,7 @@ use futures_util::{dummy_waker, Joiner, OptionalSelector, Selector, SelectorOutp /// # type P2PGossipSync
    = lightning::routing::gossip::P2PGossipSync, Arc
      , Arc>; /// # type ChannelManager = lightning::ln::channelmanager::SimpleArcChannelManager, B, FE, Logger>; /// # type OnionMessenger = lightning::onion_message::messenger::OnionMessenger, Arc, Arc, Arc>, Arc, Arc, Arc>>, Arc>, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler>; -/// # type LiquidityManager = lightning_liquidity::LiquidityManager, Arc, Arc>, Arc, Arc, Arc, Arc>; +/// # type LiquidityManager = lightning_liquidity::LiquidityManager, Arc, Arc>, Arc, Arc, Arc, Arc, Arc>; /// # type Scorer = RwLock, Arc>>; /// # type PeerManager = lightning::ln::peer_handler::SimpleArcPeerManager, B, FE, Arc
        , Logger, F, StoreSync>; /// # type OutputSweeper = lightning::util::sweep::OutputSweeper, Arc, Arc, Arc, Arc, Arc, Arc>; @@ -1037,15 +1054,15 @@ where (false, false) => FASTEST_TIMER, }; let fut = Selector { - a: channel_manager.get_cm().get_event_or_persistence_needed_future(), - b: chain_monitor.get_update_future(), - c: om_fut, - d: lm_fut, - e: sleeper(sleep_delay), + a: sleeper(sleep_delay), + b: channel_manager.get_cm().get_event_or_persistence_needed_future(), + c: chain_monitor.get_update_future(), + d: om_fut, + e: lm_fut, }; match fut.await { - SelectorOutput::A | SelectorOutput::B | SelectorOutput::C | SelectorOutput::D => {}, - SelectorOutput::E(exit) => { + SelectorOutput::B | SelectorOutput::C | SelectorOutput::D | SelectorOutput::E => {}, + SelectorOutput::A(exit) => { if exit { break; } @@ -1228,12 +1245,20 @@ where } if let Some(liquidity_manager) = liquidity_manager.as_ref() { - log_trace!(logger, "Persisting LiquidityManager..."); let fut = async { - liquidity_manager.get_lm().persist().await.map_err(|e| { - log_error!(logger, "Persisting LiquidityManager failed: {}", e); - e - }) + liquidity_manager + .get_lm() + .persist() + .await + .map(|did_persist| { + if did_persist { + log_trace!(logger, "Persisted LiquidityManager."); + } + }) + .map_err(|e| { + log_error!(logger, "Persisting LiquidityManager failed: {}", e); + e + }) }; futures.set_e(Box::pin(fut)); } @@ -1963,6 +1988,7 @@ mod tests { Arc, DefaultTimeProvider, Arc, + Arc, >; struct Node { @@ -2422,6 +2448,7 @@ mod tests { Arc::clone(&tx_broadcaster), None, None, + Arc::clone(&logger), ) .unwrap(), ); @@ -2794,10 +2821,10 @@ mod tests { let kv_store = KVStoreSyncWrapper(kv_store_sync); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) @@ -3313,10 +3340,10 @@ mod tests { let kv_store = KVStoreSyncWrapper(kv_store_sync); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) @@ -3540,10 +3567,10 @@ mod tests { let (exit_sender, exit_receiver) = tokio::sync::watch::channel(()); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) diff --git a/lightning-custom-message/Cargo.toml b/lightning-custom-message/Cargo.toml index 96632f24bc1..8c1bcced08c 100644 --- a/lightning-custom-message/Cargo.toml +++ b/lightning-custom-message/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-custom-message" -version = "0.2.0" +version = "0.2.3" authors = ["Jeffrey Czyz"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning" diff --git a/lightning-custom-message/src/lib.rs b/lightning-custom-message/src/lib.rs index 32d5a9e4389..06e57b47b84 100644 --- a/lightning-custom-message/src/lib.rs +++ b/lightning-custom-message/src/lib.rs @@ -312,13 +312,25 @@ macro_rules! composite_custom_message_handler { } fn peer_connected(&self, their_node_id: $crate::bitcoin::secp256k1::PublicKey, msg: &$crate::lightning::ln::msgs::Init, inbound: bool) -> Result<(), ()> { - let mut result = Ok(()); + // Per the `CustomMessageHandler::peer_connected` contract, `peer_disconnected` + // will not be called by `PeerManager` if we return `Err`. To avoid leaking + // per-peer state in sub-handlers that already returned `Ok` when a later one + // errors, record each sub-handler's result and roll back the successful ones + // ourselves before propagating the failure. $( - if let Err(e) = self.$field.peer_connected(their_node_id, msg, inbound) { - result = Err(e); - } + let $field = self.$field.peer_connected(their_node_id, msg, inbound); )* - result + let any_err = false $( || $field.is_err() )*; + if any_err { + $( + if $field.is_ok() { + self.$field.peer_disconnected(their_node_id); + } + )* + Err(()) + } else { + Ok(()) + } } fn provided_node_features(&self) -> $crate::lightning::types::features::NodeFeatures { @@ -346,7 +358,12 @@ macro_rules! composite_custom_message_handler { match message_type { $( $pattern => match <$type>::read(&self.$field, message_type, buffer)? { - None => unreachable!(), + // A sub-handler returns `None` for a `message_type` it doesn't + // recognize. The composite's pattern can be broader than the types + // the sub-handler decodes (e.g. a range), and `message_type` is + // peer-provided, so report the message as unknown rather than + // treating this as unreachable and panicking. + None => Ok(None), Some(message) => Ok(Some($message::$variant(message))), }, )* @@ -376,3 +393,208 @@ macro_rules! composite_custom_message_handler { } } } + +#[cfg(test)] +mod tests { + use bitcoin::secp256k1::PublicKey; + use core::sync::atomic::{AtomicUsize, Ordering}; + use lightning::io; + use lightning::ln::msgs::{DecodeError, Init, LightningError}; + use lightning::ln::peer_handler::CustomMessageHandler; + use lightning::ln::wire::{CustomMessageReader, Type}; + use lightning::types::features::{InitFeatures, NodeFeatures}; + use lightning::util::ser::{LengthLimitedRead, Writeable, Writer}; + + #[derive(Debug)] + pub struct Foo; + impl Type for Foo { + fn type_id(&self) -> u16 { + 32768 + } + } + impl Writeable for Foo { + fn write(&self, _: &mut W) -> Result<(), io::Error> { + Ok(()) + } + } + + pub struct CountingHandler { + pub connect_count: AtomicUsize, + } + impl CustomMessageReader for CountingHandler { + type CustomMessage = Foo; + fn read( + &self, _t: u16, _b: &mut R, + ) -> Result, DecodeError> { + Ok(None) + } + } + impl CustomMessageHandler for CountingHandler { + fn handle_custom_message(&self, _msg: Foo, _: PublicKey) -> Result<(), LightningError> { + Ok(()) + } + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Foo)> { + vec![] + } + fn peer_disconnected(&self, _: PublicKey) { + self.connect_count.fetch_sub(1, Ordering::SeqCst); + } + fn peer_connected(&self, _: PublicKey, _: &Init, _: bool) -> Result<(), ()> { + self.connect_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + fn provided_node_features(&self) -> NodeFeatures { + NodeFeatures::empty() + } + fn provided_init_features(&self, _: PublicKey) -> InitFeatures { + InitFeatures::empty() + } + } + + #[derive(Debug)] + pub struct Bar; + impl Type for Bar { + fn type_id(&self) -> u16 { + 32769 + } + } + impl Writeable for Bar { + fn write(&self, _: &mut W) -> Result<(), io::Error> { + Ok(()) + } + } + + pub struct ErroringHandler; + impl CustomMessageReader for ErroringHandler { + type CustomMessage = Bar; + fn read( + &self, _t: u16, _b: &mut R, + ) -> Result, DecodeError> { + Ok(None) + } + } + impl CustomMessageHandler for ErroringHandler { + fn handle_custom_message(&self, _msg: Bar, _: PublicKey) -> Result<(), LightningError> { + Ok(()) + } + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Bar)> { + vec![] + } + fn peer_disconnected(&self, _: PublicKey) { + debug_assert!(false); + } + fn peer_connected(&self, _: PublicKey, _: &Init, _: bool) -> Result<(), ()> { + Err(()) + } + fn provided_node_features(&self) -> NodeFeatures { + NodeFeatures::empty() + } + fn provided_init_features(&self, _: PublicKey) -> InitFeatures { + InitFeatures::empty() + } + } + + composite_custom_message_handler!( + pub struct CompositeHandler { + counting: CountingHandler, + erroring: ErroringHandler, + } + + pub enum CompositeMessage { + Foo(32768), + Bar(32769), + } + ); + + struct ReservedBlockHandler; + impl CustomMessageReader for ReservedBlockHandler { + type CustomMessage = Foo; + fn read( + &self, message_type: u16, _b: &mut R, + ) -> Result, DecodeError> { + // This build defines only the message at 32768; the rest of the block its + // protocol reserved (32768..=32777) is for types future versions may add. + // A not-yet-defined type is unknown to this build, so per the + // `CustomMessageReader` contract it returns `Ok(None)` -- a newer peer can + // send one and this older node will treat it as an unknown message. + match message_type { + 32768 => Ok(Some(Foo)), + _ => Ok(None), + } + } + } + impl CustomMessageHandler for ReservedBlockHandler { + fn handle_custom_message(&self, _msg: Foo, _: PublicKey) -> Result<(), LightningError> { + Ok(()) + } + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Foo)> { + vec![] + } + fn peer_disconnected(&self, _: PublicKey) {} + fn peer_connected(&self, _: PublicKey, _: &Init, _: bool) -> Result<(), ()> { + Ok(()) + } + fn provided_node_features(&self) -> NodeFeatures { + NodeFeatures::empty() + } + fn provided_init_features(&self, _: PublicKey) -> InitFeatures { + InitFeatures::empty() + } + } + + composite_custom_message_handler!( + struct ReservedBlockComposite { + proto: ReservedBlockHandler, + } + + enum ReservedBlockMessage { + Proto(32768..=32777), + } + ); + + #[test] + fn read_treats_a_reserved_in_range_type_as_unknown() { + // A sub-handler may own a block of type ids (declared here as a range) yet only + // decode the subset its build defines, returning `Ok(None)` for reserved or + // not-yet-defined types in the block -- exactly what a node does on receiving a + // newer peer's message. `read` must surface that as an unknown message, not + // panic. + let composite = ReservedBlockComposite { proto: ReservedBlockHandler }; + let mut buffer: &[u8] = &[]; + // The message this build defines decodes to its variant. + assert!(matches!( + composite.read(32768, &mut buffer), + Ok(Some(ReservedBlockMessage::Proto(_))) + )); + // A reserved type from the same block is reported unknown, not panicked + // (pre-fix the matched arm hit `unreachable!()`). + assert!(matches!(composite.read(32770, &mut buffer), Ok(None))); + } + + #[test] + fn peer_connected_failure_does_not_leak_subhandler_state() { + let composite = CompositeHandler { + counting: CountingHandler { connect_count: AtomicUsize::new(0) }, + erroring: ErroringHandler, + }; + let pk_bytes = [ + 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, + 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, + 0x5B, 0x16, 0xF8, 0x17, 0x98, + ]; + let pk = PublicKey::from_slice(&pk_bytes).unwrap(); + let init = + Init { features: InitFeatures::empty(), networks: None, remote_network_address: None }; + + let result = composite.peer_connected(pk, &init, true); + assert!(result.is_err(), "Composite must propagate the inner Err"); + + let leaked = composite.counting.connect_count.load(Ordering::SeqCst); + assert_eq!( + leaked, 0, + "CountingHandler tracked {leaked} connected peer(s) after the composite \ + returned Err; this state will never be cleaned up because per the trait \ + contract peer_disconnected won't be called when peer_connected returns Err.", + ); + } +} diff --git a/lightning-dns-resolver/Cargo.toml b/lightning-dns-resolver/Cargo.toml index 248cb73025a..bdc13f5e41b 100644 --- a/lightning-dns-resolver/Cargo.toml +++ b/lightning-dns-resolver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-dns-resolver" -version = "0.3.0" +version = "0.3.1" authors = ["Matt Corallo"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning/" diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index f5b1d53fc8a..fbafeb76860 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -135,8 +135,8 @@ where let contents = DNSResolverMessage::DNSSECProof(DNSSECProof { name: q.0, proof }); let instructions = responder.respond().into_instructions(); us.pending_replies.lock().unwrap().push((contents, instructions)); - us.pending_query_count.fetch_sub(1, Ordering::Relaxed); } + us.pending_query_count.fetch_sub(1, Ordering::Relaxed); }); None } @@ -518,4 +518,94 @@ mod test { ) .await; } + + #[tokio::test] + async fn failed_query_does_not_leak_pending_counter() { + use std::sync::atomic::Ordering; + + let secp_ctx = Secp256k1::new(); + + // Resolver points at a port that should refuse TCP, so build_txt_proof_async + // returns Err quickly. + let resolver_keys = Arc::new(KeysManager::new(&[99; 32], 42, 43, true)); + let resolver_logger = TestLogger { node: "resolver" }; + let resolver = + Arc::new(OMDomainResolver::::ignoring_incoming_proofs( + "127.0.0.1:1".parse().unwrap(), + )); + let resolver_state = Arc::clone(&resolver.state); + let resolver_messenger = OnionMessenger::new( + Arc::clone(&resolver_keys), + Arc::clone(&resolver_keys), + resolver_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&resolver), + IgnoringMessageHandler {}, + ); + let resolver_id = resolver_keys.get_node_id(Recipient::Node).unwrap(); + + let resolver_dest = Destination::Node(resolver_id); + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + + let payment_id = PaymentId([42; 32]); + let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); + + let payer_keys = Arc::new(KeysManager::new(&[2; 32], 42, 43, true)); + let payer_logger = TestLogger { node: "payer" }; + let payer_id = payer_keys.get_node_id(Recipient::Node).unwrap(); + let payer = Arc::new(URIResolver { + resolved_uri: Mutex::new(None), + resolver: OMNameResolver::new(now as u32, 1), + pending_messages: Mutex::new(Vec::new()), + }); + let payer_messenger = Arc::new(OnionMessenger::new( + Arc::clone(&payer_keys), + Arc::clone(&payer_keys), + payer_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&payer), + IgnoringMessageHandler {}, + )); + + let init_msg = get_om_init(); + payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); + resolver_messenger.peer_connected(payer_id, &init_msg, false).unwrap(); + + let (msg, context) = + payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap(); + let query_context = MessageContext::DNSResolver(context); + let receive_key = payer_keys.get_receive_auth_key(); + let reply_path = BlindedMessagePath::one_hop( + payer_id, + receive_key, + query_context, + &*payer_keys, + &secp_ctx, + ); + payer.pending_messages.lock().unwrap().push(( + DNSResolverMessage::DNSSECQuery(msg), + MessageSendInstructions::WithSpecifiedReplyPath { + destination: resolver_dest, + reply_path, + }, + )); + + let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap(); + resolver_messenger.handle_onion_message(payer_id, &query); + + let start = Instant::now(); + while resolver_state.pending_query_count.load(Ordering::Relaxed) != 0 { + tokio::time::sleep(Duration::from_millis(50)).await; + assert!( + start.elapsed() < Duration::from_secs(10), + "pending_query_count not decremented after failed proof: counter leaks" + ); + } + } } diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml index 30bbfa9a3be..ecd1552cfbc 100644 --- a/lightning-invoice/Cargo.toml +++ b/lightning-invoice/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "lightning-invoice" description = "Data structures to parse and serialize BOLT11 lightning invoices" -version = "0.34.0" +version = "0.34.1" authors = ["Sebastian Geisler "] documentation = "https://docs.rs/lightning-invoice/" license = "MIT OR Apache-2.0" diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 47f929377de..34014594543 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -1498,17 +1498,22 @@ impl Bolt11Invoice { self.signed_invoice.features() } - /// Recover the payee's public key (only to be used if none was included in the invoice) + /// Get the invoice's payee public key. + /// + /// This uses the explicitly included payee public key, if present, otherwise it recovers the + /// payee public key from the signature. Prefer [`Self::get_payee_pub_key`] for clarity. pub fn recover_payee_pub_key(&self) -> PublicKey { - self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0 + self.get_payee_pub_key() } - /// Recover the payee's public key if one was included in the invoice, otherwise return the - /// recovered public key from the signature + /// Get the invoice's payee public key, preferring an explicitly included payee public key and + /// falling back to recovering the key from the signature. pub fn get_payee_pub_key(&self) -> PublicKey { match self.payee_pub_key() { Some(pk) => *pk, - None => self.recover_payee_pub_key(), + None => { + self.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0 + }, } } @@ -2044,6 +2049,44 @@ mod test { assert!(new_signed.check_signature()); } + #[test] + fn recover_payee_pub_key_uses_included_payee_pub_key() { + use crate::*; + use bitcoin::secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use core::time::Duration; + + let secp_ctx = Secp256k1::new(); + let private_key = SecretKey::from_slice(&[42; 32]).unwrap(); + let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); + + let invoice = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".to_string()) + .payment_hash(sha256::Hash::from_slice(&[0; 32][..]).unwrap()) + .payment_secret(PaymentSecret([21; 32])) + .payee_pub_key(public_key) + .min_final_cltv_expiry_delta(144) + .duration_since_epoch(Duration::from_secs(1234567)) + .build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key)) + .unwrap(); + + let signed_raw = invoice.into_signed_raw(); + let (raw_invoice, hash, signature) = signed_raw.into_parts(); + let (_orig_rid, sig_bytes) = signature.0.serialize_compact(); + let bad_rid = RecoveryId::from_i32(2).unwrap(); + let bad_sig = RecoverableSignature::from_compact(&sig_bytes, bad_rid).unwrap(); + let bad_signed_raw = SignedRawBolt11Invoice { + raw_invoice, + hash, + signature: Bolt11InvoiceSignature(bad_sig), + }; + let bad_invoice = Bolt11Invoice::from_signed(bad_signed_raw).unwrap(); + + assert_eq!(bad_invoice.payee_pub_key(), Some(&public_key)); + assert_eq!(bad_invoice.recover_payee_pub_key(), public_key); + assert_eq!(bad_invoice.get_payee_pub_key(), public_key); + } + #[test] fn test_check_feature_bits() { use crate::TaggedField::*; diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml index a4855957f7a..4d1f8616e6d 100644 --- a/lightning-liquidity/Cargo.toml +++ b/lightning-liquidity/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-liquidity" -version = "0.2.0" +version = "0.2.3" authors = ["John Cantrell ", "Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" diff --git a/lightning-liquidity/src/events/event_queue.rs b/lightning-liquidity/src/events/event_queue.rs index cd1162cee31..0d6e3a0ec54 100644 --- a/lightning-liquidity/src/events/event_queue.rs +++ b/lightning-liquidity/src/events/event_queue.rs @@ -129,12 +129,12 @@ where EventQueueNotifierGuard(self) } - pub async fn persist(&self) -> Result<(), lightning::io::Error> { + pub async fn persist(&self) -> Result { let fut = { let mut state_lock = self.state.lock().unwrap(); if !state_lock.needs_persist { - return Ok(()); + return Ok(false); } state_lock.needs_persist = false; @@ -153,7 +153,7 @@ where e })?; - Ok(()) + Ok(true) } } diff --git a/lightning-liquidity/src/events/mod.rs b/lightning-liquidity/src/events/mod.rs index c39b8b9fd59..fb3d8cefc4d 100644 --- a/lightning-liquidity/src/events/mod.rs +++ b/lightning-liquidity/src/events/mod.rs @@ -23,6 +23,7 @@ pub(crate) use event_queue::{EventQueue, EventQueueDeserWrapper}; use crate::lsps0; use crate::lsps1; use crate::lsps2; +use crate::lsps4; use crate::lsps5; /// An event which you should probably take some action in response to. @@ -39,6 +40,10 @@ pub enum LiquidityEvent { LSPS2Client(lsps2::event::LSPS2ClientEvent), /// An LSPS2 (JIT Channel) server event. LSPS2Service(lsps2::event::LSPS2ServiceEvent), + /// An LSPS4 (Liquidity) client event. + LSPS4Client(lsps4::event::LSPS4ClientEvent), + /// An LSPS4 (Liquidity) server event. + LSPS4Service(lsps4::event::LSPS4ServiceEvent), /// An LSPS5 (Webhook) client event. LSPS5Client(lsps5::event::LSPS5ClientEvent), /// An LSPS5 (Webhook) server event. @@ -76,6 +81,18 @@ impl From for LiquidityEvent { } } +impl From for LiquidityEvent { + fn from(event: lsps4::event::LSPS4ClientEvent) -> Self { + Self::LSPS4Client(event) + } +} + +impl From for LiquidityEvent { + fn from(event: lsps4::event::LSPS4ServiceEvent) -> Self { + Self::LSPS4Service(event) + } +} + impl From for LiquidityEvent { fn from(event: lsps5::event::LSPS5ClientEvent) -> Self { Self::LSPS5Client(event) diff --git a/lightning-liquidity/src/lib.rs b/lightning-liquidity/src/lib.rs index 2f1d5bd01e7..b8aa448977b 100644 --- a/lightning-liquidity/src/lib.rs +++ b/lightning-liquidity/src/lib.rs @@ -62,6 +62,7 @@ pub mod events; pub mod lsps0; pub mod lsps1; pub mod lsps2; +pub mod lsps4; pub mod lsps5; mod manager; pub mod message_queue; diff --git a/lightning-liquidity/src/lsps0/msgs.rs b/lightning-liquidity/src/lsps0/msgs.rs index 6fb885659b5..a3e028f0fce 100644 --- a/lightning-liquidity/src/lsps0/msgs.rs +++ b/lightning-liquidity/src/lsps0/msgs.rs @@ -83,6 +83,7 @@ impl TryFrom for LSPS0Message { LSPSMessage::LSPS0(message) => Ok(message), LSPSMessage::LSPS1(_) => Err(()), LSPSMessage::LSPS2(_) => Err(()), + LSPSMessage::LSPS4(_) => Err(()), LSPSMessage::LSPS5(_) => Err(()), } } diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs index 70649fe0f50..f3ea8f2ee4f 100644 --- a/lightning-liquidity/src/lsps0/ser.rs +++ b/lightning-liquidity/src/lsps0/ser.rs @@ -21,6 +21,9 @@ use crate::lsps1::msgs::{ use crate::lsps2::msgs::{ LSPS2Message, LSPS2Request, LSPS2Response, LSPS2_BUY_METHOD_NAME, LSPS2_GET_INFO_METHOD_NAME, }; +use crate::lsps4::msgs::{ + LSPS4Message, LSPS4Request, LSPS4Response, LSPS4_REGISTER_NODE_METHOD_NAME, +}; use crate::lsps5::msgs::{ LSPS5Message, LSPS5Request, LSPS5Response, LSPS5_LIST_WEBHOOKS_METHOD_NAME, LSPS5_REMOVE_WEBHOOK_METHOD_NAME, LSPS5_SET_WEBHOOK_METHOD_NAME, @@ -66,6 +69,7 @@ pub(crate) enum LSPSMethod { LSPS1CreateOrder, LSPS2GetInfo, LSPS2Buy, + LSPS4RegisterNode, LSPS5SetWebhook, LSPS5ListWebhooks, LSPS5RemoveWebhook, @@ -80,6 +84,7 @@ impl LSPSMethod { Self::LSPS1GetOrder => LSPS1_GET_ORDER_METHOD_NAME, Self::LSPS2GetInfo => LSPS2_GET_INFO_METHOD_NAME, Self::LSPS2Buy => LSPS2_BUY_METHOD_NAME, + Self::LSPS4RegisterNode => LSPS4_REGISTER_NODE_METHOD_NAME, Self::LSPS5SetWebhook => LSPS5_SET_WEBHOOK_METHOD_NAME, Self::LSPS5ListWebhooks => LSPS5_LIST_WEBHOOKS_METHOD_NAME, Self::LSPS5RemoveWebhook => LSPS5_REMOVE_WEBHOOK_METHOD_NAME, @@ -97,6 +102,7 @@ impl FromStr for LSPSMethod { LSPS1_GET_ORDER_METHOD_NAME => Ok(Self::LSPS1GetOrder), LSPS2_GET_INFO_METHOD_NAME => Ok(Self::LSPS2GetInfo), LSPS2_BUY_METHOD_NAME => Ok(Self::LSPS2Buy), + LSPS4_REGISTER_NODE_METHOD_NAME => Ok(Self::LSPS4RegisterNode), LSPS5_SET_WEBHOOK_METHOD_NAME => Ok(Self::LSPS5SetWebhook), LSPS5_LIST_WEBHOOKS_METHOD_NAME => Ok(Self::LSPS5ListWebhooks), LSPS5_REMOVE_WEBHOOK_METHOD_NAME => Ok(Self::LSPS5RemoveWebhook), @@ -132,6 +138,14 @@ impl From<&LSPS2Request> for LSPSMethod { } } +impl From<&LSPS4Request> for LSPSMethod { + fn from(value: &LSPS4Request) -> Self { + match value { + LSPS4Request::RegisterNode(_) => Self::LSPS4RegisterNode, + } + } +} + impl From<&LSPS5Request> for LSPSMethod { fn from(value: &LSPS5Request) -> Self { match value { @@ -234,7 +248,7 @@ impl Readable for LSPSRequestId { } /// An object representing datetimes as described in bLIP-50 / LSPS0. -#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Serialize)] #[serde(transparent)] pub struct LSPSDateTime(pub chrono::DateTime); @@ -271,8 +285,23 @@ impl LSPSDateTime { impl FromStr for LSPSDateTime { type Err = (); fn from_str(s: &str) -> Result { - let datetime = chrono::DateTime::parse_from_rfc3339(s).map_err(|_| ())?; - Ok(Self(datetime.into())) + let datetime: chrono::DateTime = + chrono::DateTime::parse_from_rfc3339(s).map_err(|_| ())?.into(); + // Reject pre-epoch datetimes here so peer-controlled `valid_until` / + // `expires_at` fields can never produce an `LSPSDateTime` with a negative + // UNIX timestamp, which would otherwise panic the `i64 -> u64` cast in + // `is_past`. + if datetime.timestamp() < 0 { + return Err(()); + } + Ok(Self(datetime)) + } +} + +impl<'de> Deserialize<'de> for LSPSDateTime { + fn deserialize>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(|()| de::Error::custom("invalid LSPSDateTime")) } } @@ -324,6 +353,8 @@ pub enum LSPSMessage { LSPS1(LSPS1Message), /// An LSPS2 message. LSPS2(LSPS2Message), + /// An LSPS4 message. + LSPS4(LSPS4Message), /// An LSPS5 message. LSPS5(LSPS5Message), } @@ -353,6 +384,9 @@ impl LSPSMessage { LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => { Some((LSPSRequestId(request_id.0.clone()), request.into())) }, + LSPSMessage::LSPS4(LSPS4Message::Request(request_id, request)) => { + Some((LSPSRequestId(request_id.0.clone()), request.into())) + }, LSPSMessage::LSPS5(LSPS5Message::Request(request_id, request)) => { Some((LSPSRequestId(request_id.0.clone()), request.into())) }, @@ -468,6 +502,26 @@ impl Serialize for LSPSMessage { }, } }, + LSPSMessage::LSPS4(LSPS4Message::Request(request_id, request)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + jsonrpc_object + .serialize_field(JSONRPC_METHOD_FIELD_KEY, &LSPSMethod::from(request))?; + + match request { + LSPS4Request::RegisterNode(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + }, + } + }, + LSPSMessage::LSPS4(LSPS4Message::Response(request_id, response)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + + match response { + LSPS4Response::RegisterNode(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + }, + } + }, LSPSMessage::Invalid(error) => { jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &serde_json::Value::Null)?; jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, &error)?; @@ -623,6 +677,11 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { .map_err(de::Error::custom)?; Ok(LSPSMessage::LSPS2(LSPS2Message::Request(id, LSPS2Request::Buy(request)))) }, + LSPSMethod::LSPS4RegisterNode => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS4(LSPS4Message::Request(id, LSPS4Request::RegisterNode(request)))) + }, LSPSMethod::LSPS5SetWebhook => { let request = serde_json::from_value(params.unwrap_or(json!({}))) .map_err(de::Error::custom)?; @@ -752,6 +811,18 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) } }, + LSPSMethod::LSPS4RegisterNode => { + if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS4(LSPS4Message::Response( + id, + LSPS4Response::RegisterNode(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + }, LSPSMethod::LSPS5SetWebhook => { if let Some(error) = error { Ok(LSPSMessage::LSPS5(LSPS5Message::Response( @@ -981,4 +1052,15 @@ mod tests { let decoded_datetime: LSPSDateTime = Readable::read(&mut Cursor::new(buf)).unwrap(); assert_eq!(expected_datetime, decoded_datetime); } + + #[test] + fn is_past_handles_pre_epoch_datetime() { + // A peer-controlled RFC3339 datetime before 1970 must be rejected at parse + // time, so it can never reach `is_past` (or any other consumer) and panic. + assert!(LSPSDateTime::from_str("1900-01-01T00:00:00Z").is_err()); + + // JSON deserialization (the path peer messages take) must reject it too. + let json = "\"1900-01-01T00:00:00Z\""; + assert!(serde_json::from_str::(json).is_err()); + } } diff --git a/lightning-liquidity/src/lsps2/payment_queue.rs b/lightning-liquidity/src/lsps2/payment_queue.rs index 003939d699d..81b5ce151d0 100644 --- a/lightning-liquidity/src/lsps2/payment_queue.rs +++ b/lightning-liquidity/src/lsps2/payment_queue.rs @@ -26,21 +26,29 @@ impl PaymentQueue { PaymentQueue { payments: Vec::new() } } + fn payment_status(entry: &PaymentQueueEntry) -> (u64, usize) { + let total_expected_outbound_amount_msat = + entry.htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum(); + (total_expected_outbound_amount_msat, entry.htlcs.len()) + } + pub(crate) fn add_htlc(&mut self, new_htlc: InterceptedHTLC) -> (u64, usize) { + if let Some(entry) = self + .payments + .iter() + .find(|entry| entry.htlcs.iter().any(|htlc| htlc.intercept_id == new_htlc.intercept_id)) + { + debug_assert_eq!(entry.payment_hash, new_htlc.payment_hash); + return Self::payment_status(entry); + } + let payment = self.payments.iter_mut().find(|entry| entry.payment_hash == new_htlc.payment_hash); if let Some(entry) = payment { // HTLCs within a payment should have the same payment hash. debug_assert!(entry.htlcs.iter().all(|htlc| htlc.payment_hash == entry.payment_hash)); - // The given HTLC should not already be present. - debug_assert!(entry - .htlcs - .iter() - .all(|htlc| htlc.intercept_id != new_htlc.intercept_id)); entry.htlcs.push(new_htlc); - let total_expected_outbound_amount_msat = - entry.htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum(); - (total_expected_outbound_amount_msat, entry.htlcs.len()) + Self::payment_status(entry) } else { let expected_outbound_amount_msat = new_htlc.expected_outbound_amount_msat; let entry = @@ -127,6 +135,15 @@ mod tests { (500_000_000, 2), ); + assert_eq!( + payment_queue.add_htlc(InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([100; 32]), + }), + (500_000_000, 2), + ); + let expected_entry = PaymentQueueEntry { payment_hash: PaymentHash([100; 32]), htlcs: vec![ diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index dda9922686d..70d1536e5be 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -42,7 +42,7 @@ use crate::utils::async_poll::dummy_waker; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::events::HTLCHandlingFailureType; -use lightning::ln::channelmanager::{AChannelManager, FailureCode, InterceptId}; +use lightning::ln::channelmanager::{AChannelManager, InterceptId}; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::ln::types::ChannelId; use lightning::util::errors::APIError; @@ -1257,6 +1257,8 @@ where /// This removes the intercept SCID, any outbound channel state, and associated /// channel‐ID mappings for the specified `user_channel_id`, but only while no payment /// has been forwarded yet and no channel has been opened on-chain. + /// Any held HTLCs for the pending flow are failed backwards before the local state + /// is removed. /// /// Returns an error if: /// - there is no channel matching `user_channel_id`, or @@ -1292,25 +1294,27 @@ where let jit_channel = peer_state .outbound_channels_by_intercept_scid - .get(&intercept_scid) + .get_mut(&intercept_scid) .ok_or_else(|| APIError::APIMisuseError { - err: format!( - "Failed to map intercept_scid {} for user_channel_id {} to a channel.", - intercept_scid, user_channel_id, - ), - })?; + err: format!( + "Failed to map intercept_scid {} for user_channel_id {} to a channel.", + intercept_scid, user_channel_id, + ), + })?; - let is_pending = matches!( - jit_channel.state, - OutboundJITChannelState::PendingInitialPayment { .. } - | OutboundJITChannelState::PendingChannelOpen { .. } - ); + let intercepted_htlcs = match &mut jit_channel.state { + OutboundJITChannelState::PendingInitialPayment { payment_queue } + | OutboundJITChannelState::PendingChannelOpen { payment_queue, .. } => payment_queue.clear(), + _ => { + return Err(APIError::APIMisuseError { + err: "Cannot abandon channel open after channel creation or payment forwarding" + .to_string(), + }); + }, + }; - if !is_pending { - return Err(APIError::APIMisuseError { - err: "Cannot abandon channel open after channel creation or payment forwarding" - .to_string(), - }); + for htlc in intercepted_htlcs { + let _ = self.channel_manager.get_cm().fail_intercepted_htlc(htlc.intercept_id); } peer_state.intercept_scid_by_user_channel_id.remove(&user_channel_id); @@ -1375,10 +1379,8 @@ where { let intercepted_htlcs = payment_queue.clear(); for htlc in intercepted_htlcs { - self.channel_manager.get_cm().fail_htlc_backwards_with_reason( - &htlc.payment_hash, - FailureCode::TemporaryNodeFailure, - ); + // A missing intercept has already been released; still reset this LSPS2 state. + let _ = self.channel_manager.get_cm().fail_intercepted_htlc(htlc.intercept_id); } jit_channel.state = OutboundJITChannelState::PendingInitialPayment { @@ -1782,7 +1784,7 @@ where }) } - pub(crate) async fn persist(&self) -> Result<(), lightning::io::Error> { + pub(crate) async fn persist(&self) -> Result { // TODO: We should eventually persist in parallel, however, when we do, we probably want to // introduce some batching to upper-bound the number of requests inflight at any given // time. @@ -1790,9 +1792,18 @@ where if self.persistence_in_flight.fetch_add(1, Ordering::AcqRel) > 0 { // If we're not the first event processor to get here, just return early, the increment // we just did will be treated as "go around again" at the end. - return Ok(()); + return Ok(false); } + let res = self.do_persist().await; + debug_assert!(res.is_err() || self.persistence_in_flight.load(Ordering::Acquire) == 0); + self.persistence_in_flight.store(0, Ordering::Release); + res + } + + async fn do_persist(&self) -> Result { + let mut did_persist = false; + loop { let mut need_remove = Vec::new(); let mut need_persist = Vec::new(); @@ -1816,6 +1827,7 @@ where for counterparty_node_id in need_persist.into_iter() { debug_assert!(!need_remove.contains(&counterparty_node_id)); self.persist_peer_state(counterparty_node_id).await?; + did_persist = true; } for counterparty_node_id in need_remove { @@ -1850,8 +1862,10 @@ where } if let Some(future) = future_opt { future.await?; + did_persist = true; } else { self.persist_peer_state(counterparty_node_id).await?; + did_persist = true; } } @@ -1864,7 +1878,7 @@ where break; } - Ok(()) + Ok(did_persist) } pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { @@ -2352,6 +2366,8 @@ mod tests { use bitcoin::{absolute::LockTime, transaction::Version}; use core::str::FromStr; + use lightning::io::Cursor; + use lightning::util::ser::{Readable, Writeable}; const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000; @@ -2759,6 +2775,52 @@ mod tests { } } + #[test] + fn replayed_intercepted_htlc_after_persist_is_idempotent() { + let payment_size_msat = Some(500_000_000); + let opening_fee_params = LSPS2OpeningFeeParams { + min_fee_msat: 10_000_000, + proportional: 10_000, + valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(), + min_lifetime: 4032, + max_client_to_self_delay: 2016, + min_payment_size_msat: 10_000_000, + max_payment_size_msat: 1_000_000_000, + promise: "ignore".to_string(), + }; + let intercept_scid = 42; + let user_channel_id = 43; + let htlc = InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([2; 32]), + }; + + let mut jit_channel = + OutboundJITChannel::new(payment_size_msat, opening_fee_params, user_channel_id, false); + assert!(matches!( + jit_channel.htlc_intercepted(htlc).unwrap(), + Some(HTLCInterceptedAction::OpenChannel(_)) + )); + + let mut peer_state = PeerState::new(); + peer_state.intercept_scid_by_user_channel_id.insert(user_channel_id, intercept_scid); + peer_state.insert_outbound_channel(intercept_scid, jit_channel); + + let encoded_peer_state = peer_state.encode(); + let mut decoded_peer_state = PeerState::read(&mut Cursor::new(encoded_peer_state)).unwrap(); + let decoded_jit_channel = decoded_peer_state + .outbound_channels_by_intercept_scid + .get_mut(&intercept_scid) + .unwrap(); + + assert!(decoded_jit_channel.htlc_intercepted(htlc).unwrap().is_none()); + + let ForwardPaymentAction(_, fee_payment) = + decoded_jit_channel.channel_ready(ChannelId([3; 32])).unwrap(); + assert_eq!(fee_payment.htlcs, vec![htlc]); + } + #[test] fn broadcast_not_allowed_after_non_paying_fee_payment_claimed() { let min_fee_msat: u64 = 12345; diff --git a/lightning-liquidity/src/lsps4/claim.rs b/lightning-liquidity/src/lsps4/claim.rs new file mode 100644 index 00000000000..609b635e410 --- /dev/null +++ b/lightning-liquidity/src/lsps4/claim.rs @@ -0,0 +1,313 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +//! Signed fee-policy claims presented by a registering node. +//! +//! A node may carry a claim that grants it a non-standard [`FeePolicy`] (for example a zero-fee +//! grant for funding we do not skim). The claim is minted off-band by an issuer the LSP trusts and +//! presented verbatim in `register_node`; the LSP verifies it locally against its configured issuer +//! keys, with no network I/O. With no issuer key configured every claim is rejected, so every peer +//! falls back to `Flat(Standard)` and behaviour is identical to a node that never sees a claim. +//! +//! The wire format is versioned and TLV-encoded rather than a fixed concatenation, so later schemes +//! (more policy arms, an issuer key-id, per-key scope) are additive: a new tag, not a re-issue. +//! The signed bytes are the *opaque* encoded [`ClaimPayload`], carried as a byte string inside +//! [`SignedFeeClaim`]. The verifier hashes exactly those bytes, so it never has to reproduce the +//! payload's TLV layout to check the signature — it only re-reads the payload after the signature +//! has already covered it. + +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::hex::FromHex; +use bitcoin::secp256k1::{schnorr, Message, PublicKey, Secp256k1, Verification, XOnlyPublicKey}; + +use lightning::impl_writeable_tlv_based; +use lightning::util::ser::{Readable, Writeable}; + +use std::vec::Vec; + +use crate::lsps4::fee_policy::FeePolicy; + +/// The only signature scheme this version understands: a BIP340 Schnorr signature over secp256k1. +const CLAIM_SCHEME_V1: u8 = 1; + +/// The signed half of a claim: the issuer commits to *this node* getting *this policy*. +/// +/// Encoded and hashed as an opaque byte string by [`SignedFeeClaim`]; the verifier re-reads it only +/// after the signature has been checked over those exact bytes. +#[derive(Clone, Debug, PartialEq, Eq)] +struct ClaimPayload { + /// Signature scheme tag; only [`CLAIM_SCHEME_V1`] is accepted. + scheme: u8, + /// The node the grant is bound to. Must equal the registering peer's id. + node_id: PublicKey, + /// The policy the issuer is granting. + policy: FeePolicy, +} + +impl_writeable_tlv_based!(ClaimPayload, { + (0, scheme, required), + (2, node_id, required), + (4, policy, required), +}); + +/// The wire container: an opaque [`ClaimPayload`] blob plus the issuer's detached signature over it. +#[derive(Clone, Debug, PartialEq, Eq)] +struct SignedFeeClaim { + /// The encoded [`ClaimPayload`]. Kept as bytes so the signature is verified over exactly what + /// was signed, independent of how this verifier would re-serialize the payload. + payload: Vec, + /// BIP340 Schnorr signature over `SHA256(payload)`. + sig: schnorr::Signature, +} + +impl_writeable_tlv_based!(SignedFeeClaim, { + (0, payload, required), + (2, sig, required), +}); + +/// Why a presented claim was not honoured. Every variant resolves the peer to `Flat(Standard)`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum ClaimError { + /// The hex, the outer container, or the inner payload failed to decode. + Malformed, + /// The payload's scheme tag is not one this version understands. + UnknownScheme(u8), + /// No configured issuer key verified the signature (an empty issuer set always lands here). + BadSignature, + /// The claim is valid but bound to a different node than the one presenting it. + NodeIdMismatch, +} + +/// Verify a hex-encoded [`SignedFeeClaim`] and return the granted [`FeePolicy`]. +/// +/// A claim is accepted when it decodes, carries [`CLAIM_SCHEME_V1`], is signed by *any* of +/// `issuer_pubkeys`, and is bound to `counterparty`. An empty `issuer_pubkeys` rejects every claim +/// ([`ClaimError::BadSignature`]), which is how the feature stays inert until a key is configured. +/// +/// `scheme` is read and matched *before* the signature because it selects the verification rules: +/// a future scheme could sign a different digest, so there is no single "verify first" step that +/// works across schemes. Only `scheme == 1` (BIP340 over `SHA256(payload)`) exists today. +/// +/// Every configured key is trusted equally — any one of them may grant any [`FeePolicy`]. That is +/// fine for a single issuer. Once a second, less-trusted issuer exists (e.g. a promo key that must +/// not be able to grant a permanent zero-fee), the trust set has to carry per-key scope checked +/// against the granted policy. That gap is deliberate, not forgotten; a keyed `&[(key, scope)]` +/// shape plus a payload key-id are additive when it lands. +pub(crate) fn verify_claim( + secp_ctx: &Secp256k1, fee_claim: &str, issuer_pubkeys: &[XOnlyPublicKey], + counterparty: &PublicKey, +) -> Result { + let bytes = >::from_hex(fee_claim).map_err(|_| ClaimError::Malformed)?; + let signed = SignedFeeClaim::read(&mut &bytes[..]).map_err(|_| ClaimError::Malformed)?; + let payload = + ClaimPayload::read(&mut &signed.payload[..]).map_err(|_| ClaimError::Malformed)?; + + if payload.scheme != CLAIM_SCHEME_V1 { + return Err(ClaimError::UnknownScheme(payload.scheme)); + } + + let digest = Message::from_digest(sha256::Hash::hash(&signed.payload).to_byte_array()); + let verified = + issuer_pubkeys.iter().any(|pk| secp_ctx.verify_schnorr(&signed.sig, &digest, pk).is_ok()); + if !verified { + return Err(ClaimError::BadSignature); + } + + if payload.node_id != *counterparty { + return Err(ClaimError::NodeIdMismatch); + } + + Ok(payload.policy) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lsps4::fee_policy::FeeTier; + use crate::lsps4::utils; + use bitcoin::secp256k1::{Keypair, SecretKey, VerifyOnly}; + + /// A throwaway verification context for the test call sites. Production threads the service's + /// long-lived context instead. + fn verify_ctx() -> Secp256k1 { + Secp256k1::verification_only() + } + + /// Fixed issuer secret used to mint the in-tree test vector. Test-only; never a real key. + const ISSUER_SECRET: [u8; 32] = [0x42; 32]; + /// Fixed node secret whose public key the test vector binds the grant to. + const NODE_SECRET: [u8; 32] = [0x11; 32]; + + fn issuer_keypair() -> Keypair { + let secp = Secp256k1::new(); + Keypair::from_secret_key(&secp, &SecretKey::from_slice(&ISSUER_SECRET).unwrap()) + } + + fn issuer_xonly() -> XOnlyPublicKey { + issuer_keypair().x_only_public_key().0 + } + + fn node_id() -> PublicKey { + let secp = Secp256k1::new(); + PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&NODE_SECRET).unwrap()) + } + + /// Mint a claim the way the off-band issuer is expected to: sign `SHA256(payload)` with a + /// deterministic (no-aux-rand) BIP340 signature so the resulting hex is a stable vector. + fn mint_claim(node_id: PublicKey, policy: FeePolicy, sk: &[u8; 32]) -> String { + let secp = Secp256k1::new(); + let keypair = Keypair::from_secret_key(&secp, &SecretKey::from_slice(sk).unwrap()); + let payload = ClaimPayload { scheme: CLAIM_SCHEME_V1, node_id, policy }.encode(); + let digest = Message::from_digest(sha256::Hash::hash(&payload).to_byte_array()); + let sig = secp.sign_schnorr_no_aux_rand(&digest, &keypair); + utils::to_string(&SignedFeeClaim { payload, sig }.encode()) + } + + #[test] + fn claim_payload_round_trips() { + let payload = ClaimPayload { + scheme: CLAIM_SCHEME_V1, + node_id: node_id(), + policy: FeePolicy::Flat(FeeTier::ZeroFee), + }; + let bytes = payload.encode(); + assert_eq!(payload, ClaimPayload::read(&mut &bytes[..]).unwrap()); + } + + #[test] + fn valid_claim_yields_granted_policy() { + let claim = mint_claim(node_id(), FeePolicy::Flat(FeeTier::ZeroFee), &ISSUER_SECRET); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[issuer_xonly()], &node_id()), + Ok(FeePolicy::Flat(FeeTier::ZeroFee)) + ); + } + + #[test] + fn empty_issuer_set_rejects() { + let claim = mint_claim(node_id(), FeePolicy::Flat(FeeTier::ZeroFee), &ISSUER_SECRET); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[], &node_id()), + Err(ClaimError::BadSignature) + ); + } + + #[test] + fn wrong_issuer_key_rejects() { + let claim = mint_claim(node_id(), FeePolicy::Flat(FeeTier::ZeroFee), &ISSUER_SECRET); + // An issuer key that did not sign this claim. + let secp = Secp256k1::new(); + let other = Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[0x07; 32]).unwrap()) + .x_only_public_key() + .0; + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[other], &node_id()), + Err(ClaimError::BadSignature) + ); + } + + #[test] + fn forged_signature_rejects() { + // Mint with the wrong secret, then check against the real issuer's key. + let claim = mint_claim(node_id(), FeePolicy::Flat(FeeTier::ZeroFee), &[0x07; 32]); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[issuer_xonly()], &node_id()), + Err(ClaimError::BadSignature) + ); + } + + #[test] + fn node_id_mismatch_rejects() { + let claim = mint_claim(node_id(), FeePolicy::Flat(FeeTier::ZeroFee), &ISSUER_SECRET); + // A different counterparty than the one the claim is bound to. + let secp = Secp256k1::new(); + let other = + PublicKey::from_secret_key(&secp, &SecretKey::from_slice(&[0x22; 32]).unwrap()); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[issuer_xonly()], &other), + Err(ClaimError::NodeIdMismatch) + ); + } + + #[test] + fn unknown_scheme_rejects() { + let secp = Secp256k1::new(); + let keypair = issuer_keypair(); + let payload = ClaimPayload { scheme: 2, node_id: node_id(), policy: FeePolicy::Flat(FeeTier::ZeroFee) } + .encode(); + let digest = Message::from_digest(sha256::Hash::hash(&payload).to_byte_array()); + let sig = secp.sign_schnorr_no_aux_rand(&digest, &keypair); + let claim = utils::to_string(&SignedFeeClaim { payload, sig }.encode()); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[issuer_xonly()], &node_id()), + Err(ClaimError::UnknownScheme(2)) + ); + } + + #[test] + fn malformed_hex_rejects() { + assert_eq!( + verify_claim(&verify_ctx(), "zzzz", &[issuer_xonly()], &node_id()), + Err(ClaimError::Malformed) + ); + } + + #[test] + fn malformed_tlv_rejects() { + // Valid hex, but not a decodable SignedFeeClaim. + let claim = utils::to_string(&[0xff, 0xff, 0xff]); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[issuer_xonly()], &node_id()), + Err(ClaimError::Malformed) + ); + } + + #[test] + fn multi_issuer_second_key_verifies() { + // The signing key sits second in the trust set, so this exercises `.any()` past index 0 — + // the multi-issuer / key-rotation path the config shape is built for. + let claim = mint_claim(node_id(), FeePolicy::Flat(FeeTier::ZeroFee), &ISSUER_SECRET); + let secp = Secp256k1::new(); + let other = Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[0x07; 32]).unwrap()) + .x_only_public_key() + .0; + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[other, issuer_xonly()], &node_id()), + Ok(FeePolicy::Flat(FeeTier::ZeroFee)) + ); + } + + #[test] + fn custom_policy_survives_verify() { + // Any signed FeePolicy comes back intact, not just ZeroFee. + let policy = FeePolicy::Flat(FeeTier::Custom { ppm: 1_234, base_msat: 56 }); + let claim = mint_claim(node_id(), policy.clone(), &ISSUER_SECRET); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[issuer_xonly()], &node_id()), + Ok(policy) + ); + } + + /// The cross-repo contract the client and the issuer must reproduce byte-for-byte. The issuer + /// secret, the bound node id, and a `ZeroFee` grant mint exactly this hex; pinning it here + /// catches any drift in the TLV byte layout or the signing input. + #[test] + fn in_tree_test_vector() { + const EXPECTED_CLAIM_HEX: &str = "73002f002d2c0001010221034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa04040002020002408f3868ca716c39d580d8b54c8d852c22f0f1ea4a174ba13ad571e37fd182aa60e82a88c00225a3f61112804cf1e7c41bd39dbdc7fcb78e779b78423fff47d964"; + const EXPECTED_ISSUER_XONLY: &str = + "24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c"; + + assert_eq!(utils::to_string(&issuer_xonly().serialize()), EXPECTED_ISSUER_XONLY); + + let claim = mint_claim(node_id(), FeePolicy::Flat(FeeTier::ZeroFee), &ISSUER_SECRET); + assert_eq!(claim, EXPECTED_CLAIM_HEX); + assert_eq!( + verify_claim(&verify_ctx(), &claim, &[issuer_xonly()], &node_id()), + Ok(FeePolicy::Flat(FeeTier::ZeroFee)) + ); + } +} diff --git a/lightning-liquidity/src/lsps4/client.rs b/lightning-liquidity/src/lsps4/client.rs new file mode 100644 index 00000000000..0f5b96f2ea8 --- /dev/null +++ b/lightning-liquidity/src/lsps4/client.rs @@ -0,0 +1,274 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS4 client object, [`LSPS4ClientHandler`]. + +use crate::events::{LiquidityEvent, EventQueue}; +use crate::lsps0::ser::{LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError}; +use crate::lsps4::event::LSPS4ClientEvent; +use crate::message_queue::MessageQueue; +use crate::prelude::{new_hash_map, new_hash_set, HashMap, HashSet}; +use crate::sync::{Arc, Mutex, RwLock}; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::errors::APIError; +use lightning::util::logger::Level; +use lightning::util::persist::KVStore; + +use bitcoin::secp256k1::PublicKey; + +use std::string::String; + + +use core::default::Default; +use core::ops::Deref; + +use crate::lsps4::msgs::{ + RegisterNodeRequest, RegisterNodeResponse, LSPS4Message, LSPS4Request, + LSPS4Response, +}; + +/// Client-side configuration options for JIT channels. +#[derive(Clone, Debug, Copy, Default)] +pub struct LSPS4ClientConfig {} + +struct PeerState { + pending_register_node_requests: HashSet, +} + +impl PeerState { + fn new() -> Self { + let pending_register_node_requests = new_hash_set(); + Self { pending_register_node_requests } + } +} + +/// The main object allowing to send and receive LSPS4 messages. +pub struct LSPS4ClientHandler +where + ES::Target: EntropySource, + K::Target: KVStore, +{ + entropy_source: ES, + pending_messages: Arc, + pending_events: Arc>, + per_peer_state: RwLock>>, + _config: LSPS4ClientConfig, +} + +impl LSPS4ClientHandler +where + ES::Target: EntropySource, + K::Target: KVStore, +{ + /// Constructs an `LSPS4ClientHandler`. + pub(crate) fn new( + entropy_source: ES, pending_messages: Arc, pending_events: Arc>, + _config: LSPS4ClientConfig, + ) -> Self { + Self { + entropy_source, + pending_messages, + pending_events, + per_peer_state: RwLock::new(new_hash_map()), + _config, + } + } + + /// Requests the LSP to register the node. + /// + /// `fee_claim` is an optional lowercase-hex signed grant for a non-standard fee policy; the LSP + /// verifies it against its configured issuer keys. `None` (or an unverifiable claim) leaves the + /// node on the standard policy. + /// + /// The user will receive the LSP's response via an [`InvoiceParametersReady`] event. + /// + /// [`InvoiceParametersReady`]: crate::lsps4::event::LSPS4ClientEvent::InvoiceParametersReady + pub fn register_node( + &self, counterparty_node_id: PublicKey, fee_claim: Option, + ) -> Result { + let request_id = crate::utils::generate_request_id(&self.entropy_source); + + { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(counterparty_node_id) + .or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if !peer_state_lock + .pending_register_node_requests + .insert(request_id.clone()) + { + return Err(APIError::APIMisuseError { + err: "Failed due to duplicate request_id. This should never happen!" + .to_string(), + }); + } + } + + let request = LSPS4Request::RegisterNode(RegisterNodeRequest { fee_claim }); + let msg = LSPS4Message::Request(request_id.clone(), request).into(); + let mut message_queue_notifier = self.pending_messages.notifier(); + message_queue_notifier.enqueue(&counterparty_node_id, msg); + + Ok(request_id) + } + + + fn handle_register_node_response( + &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, result: RegisterNodeResponse, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + if !peer_state.pending_register_node_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received register_node response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + if let Ok(intercept_scid) = result.jit_channel_scid.to_scid() { + let mut event_queue_notifier = self.pending_events.notifier(); + event_queue_notifier.enqueue(LiquidityEvent::LSPS4Client( + LSPS4ClientEvent::InvoiceParametersReady { + request_id, + counterparty_node_id: *counterparty_node_id, + intercept_scid, + cltv_expiry_delta: result.lsp_cltv_expiry_delta, + }, + )); + } else { + return Err(LightningError { + err: format!( + "Received register_node response with an invalid intercept scid {:?}", + result.jit_channel_scid + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + }, + None => { + return Err(LightningError { + err: format!( + "Received register_node response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + }, + } + Ok(()) + } +} + +impl LSPSProtocolMessageHandler for LSPS4ClientHandler +where + ES::Target: EntropySource, + K::Target: KVStore, +{ + type ProtocolMessage = LSPS4Message; + const PROTOCOL_NUMBER: Option = Some(4); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS4Message::Response(request_id, response) => match response { + LSPS4Response::RegisterNode(result) => { + self.handle_register_node_response(request_id, counterparty_node_id, result) + }, + }, + _ => { + debug_assert!( + false, + "Client handler received LSPS4 request message. This should never happen." + ); + Err(LightningError { err: format!("Client handler received LSPS4 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lsps0::ser::LSPSMessage; + use bitcoin::secp256k1::{Secp256k1, SecretKey}; + use core::sync::atomic::{AtomicU64, Ordering}; + use lightning::util::persist::KVStoreSyncWrapper; + use lightning::util::test_utils::TestStore; + use lightning::util::wakers::Notifier; + use std::collections::VecDeque; + + struct CountingEntropy { + counter: AtomicU64, + } + + impl EntropySource for CountingEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + let counter = self.counter.fetch_add(1, Ordering::SeqCst); + let mut bytes = [0u8; 32]; + bytes[0..8].copy_from_slice(&counter.to_be_bytes()); + bytes + } + } + + type TestKVStore = Arc>>; + + fn setup() -> (LSPS4ClientHandler, TestKVStore>, Arc, PublicKey) + { + let entropy = Arc::new(CountingEntropy { counter: AtomicU64::new(1) }); + let message_queue = Arc::new(MessageQueue::new(Arc::new(Notifier::new()))); + let kv_store = Arc::new(KVStoreSyncWrapper(Arc::new(TestStore::new(false)))); + let event_queue = + Arc::new(EventQueue::new(VecDeque::new(), kv_store, Arc::new(Notifier::new()))); + let client = LSPS4ClientHandler::new( + entropy, + Arc::clone(&message_queue), + event_queue, + LSPS4ClientConfig::default(), + ); + let peer = PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42u8; 32]).unwrap()); + (client, message_queue, peer) + } + + /// The single enqueued message must be a `register_node` request; returns its `fee_claim`. + fn sole_request_fee_claim(message_queue: &MessageQueue) -> Option { + let mut pending = message_queue.get_and_clear_pending_msgs(); + assert_eq!(pending.len(), 1); + match pending.pop().unwrap().1 { + LSPSMessage::LSPS4(LSPS4Message::Request(_, LSPS4Request::RegisterNode(req))) => { + req.fee_claim + }, + other => panic!("expected a register_node request, got {:?}", other), + } + } + + #[test] + fn register_node_carries_the_claim() { + let (client, message_queue, peer) = setup(); + let claim = "deadbeef".to_string(); + client.register_node(peer, Some(claim.clone())).unwrap(); + assert_eq!(sole_request_fee_claim(&message_queue), Some(claim)); + } + + #[test] + fn register_node_without_a_claim_omits_it() { + let (client, message_queue, peer) = setup(); + client.register_node(peer, None).unwrap(); + assert_eq!(sole_request_fee_claim(&message_queue), None); + } +} diff --git a/lightning-liquidity/src/lsps4/event.rs b/lightning-liquidity/src/lsps4/event.rs new file mode 100644 index 00000000000..b6f40feeb2e --- /dev/null +++ b/lightning-liquidity/src/lsps4/event.rs @@ -0,0 +1,83 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains LSPS4 event types + +use crate::lsps0::ser::LSPSRequestId; +use std::string::String; +use std::vec::Vec; +use lightning::ln::types::ChannelId; +use lightning_types::payment::PaymentHash; + +use bitcoin::secp256k1::PublicKey; + +/// An event which an LSPS4 client should take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS4ClientEvent { + /// Provides the necessary information to generate a payable invoice that then may be given to + /// the payer. + /// The LSP will ensure that invoices with this intercept scid are always payable by opening + /// channels as necessary. + InvoiceParametersReady { + /// The identifier of the issued LSPS4 `buy` request, as returned by + /// [`LSPS4ClientHandler::select_opening_params`]. + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS4ClientHandler::select_opening_params`]: crate::lsps4::client::LSPS4ClientHandler::select_opening_params + request_id: LSPSRequestId, + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The intercept short channel id to use in the route hint. + intercept_scid: u64, + /// The `cltv_expiry_delta` to use in the route hint. + cltv_expiry_delta: u32, + }, +} + +/// An event which an LSPS4 server should take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS4ServiceEvent { + /// Send a webhook to the client to notify them they have pending payments. + SendWebhook { + /// The node id of the counterparty. + counterparty_node_id: PublicKey, + /// The payment hash + payment_hash: PaymentHash, + }, + /// You should open a channel using [`ChannelManager::create_channel`]. + /// + /// [`ChannelManager::create_channel`]: lightning::ln::channelmanager::ChannelManager::create_channel + OpenChannel { + /// The node to open channel with. + their_network_key: PublicKey, + /// The amount we need to forward to the client after opening. + amt_to_forward_msat: u64, + /// The number of channels currently open with the peer when the request is made. + channel_count: usize, + }, + /// You should splice into an existing channel to add capacity. + /// + /// If the splice fails, fall back to opening a new channel using the same sizing policy + /// as [`OpenChannel`]. + /// + /// [`OpenChannel`]: Self::OpenChannel + SpliceChannel { + /// The node whose channel to splice. + their_network_key: PublicKey, + /// The channel to splice into (the largest usable channel with this peer). + channel_id: ChannelId, + /// The user channel id, for diagnostics. + user_channel_id: u128, + /// Additional capacity needed (msat). The splice-in amount should be at least this. + amt_to_forward_msat: u64, + /// The number of channels currently open with the peer. + channel_count: usize, + }, +} diff --git a/lightning-liquidity/src/lsps4/fee_policy.rs b/lightning-liquidity/src/lsps4/fee_policy.rs new file mode 100644 index 00000000000..0d5aad32576 --- /dev/null +++ b/lightning-liquidity/src/lsps4/fee_policy.rs @@ -0,0 +1,201 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +//! Fee policy for the LSPS4 forwarding skim. +//! +//! The LSP skims a forwarding fee from every JIT-channel HTLC. This module carries the policy +//! describing *what* to skim for a given peer and resolves it to a concrete msat amount via the +//! single function [`resolve_skim`]. +//! +//! The initial version only ever constructs [`FeePolicy::Flat`], and the only tier the service +//! resolves is [`FeeTier::Standard`], so for any realistically-sized HTLC the skim matches the +//! previous hard-coded 2%. The richer arms exist so later milestones (per-peer policies, zero-fee +//! grants) are purely additive. + +use lightning::impl_writeable_tlv_based_enum; + +/// The rate at which a peer's forwarded HTLCs are skimmed. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FeeTier { + /// Skim at the LSP's configured proportional rate (`forwarding_fee_proportional_millionths`). + Standard, + /// Never skim. Used for grant recipients whose funding we do not take a cut of. + ZeroFee, + /// Skim at an explicit rate: `base_msat` plus `ppm` proportional millionths. + Custom { + /// Proportional rate in millionths applied to the HTLC amount. + ppm: u64, + /// Flat fee in millisatoshis added on top of the proportional component. + base_msat: u64, + }, +} + +/// The discount applied to a peer. v1 only constructs [`FeePolicy::Flat`]; richer arms +/// (time-limited, volume-capped, ...) are reserved as future tags so the wire format stays +/// additive. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FeePolicy { + /// A flat policy that applies the same [`FeeTier`] to every HTLC. + Flat(FeeTier), +} + +impl_writeable_tlv_based_enum!(FeeTier, + (0, Standard) => {}, + (2, ZeroFee) => {}, + (4, Custom) => { + (0, ppm, required), + (2, base_msat, required), + }, +); + +impl_writeable_tlv_based_enum!(FeePolicy, + {0, Flat} => (), +); + +/// Resolve a [`FeePolicy`] to the msat amount to skim from a single HTLC. +/// +/// `standard_ppm` is the LSP's configured proportional rate, used only by [`FeeTier::Standard`]. +/// +/// The skim is zero in two distinct cases. [`FeeTier::ZeroFee`] never skims, by design. Any other +/// tier is additionally forced to zero when its fee would consume the entire HTLC: a zero-value +/// forward is rejected by the channel (`channel.rs` force-closes on a 0-msat `update_add_htlc`), so +/// skimming the whole amount would break the forward; dropping to zero forwards it intact instead. +/// The proportional component is computed in 128-bit precision, so a very large HTLC is skimmed +/// correctly rather than (as the previous `u64` arithmetic did) overflowing and forwarding the +/// whole amount for free. +pub(crate) fn resolve_skim(policy: &FeePolicy, htlc_amount_msat: u64, standard_ppm: u64) -> u64 { + let fee_msat = match policy { + FeePolicy::Flat(FeeTier::ZeroFee) => 0, + FeePolicy::Flat(FeeTier::Standard) => proportional_fee_msat(htlc_amount_msat, standard_ppm), + FeePolicy::Flat(FeeTier::Custom { ppm, base_msat }) => { + base_msat.saturating_add(proportional_fee_msat(htlc_amount_msat, *ppm)) + }, + }; + + if fee_msat >= htlc_amount_msat { + 0 + } else { + fee_msat + } +} + +/// `amount_msat * ppm / 1_000_000`, rounded up, computed in 128-bit so it can't overflow for any +/// `u64` inputs. Saturates to `u64::MAX`, which the caller reads as "skims the whole HTLC". +fn proportional_fee_msat(amount_msat: u64, ppm: u64) -> u64 { + // `+ 999_999` before the integer divide is ceiling division: adding `denominator - 1` rounds + // the result up, so a sub-msat fee skims 1 rather than truncating to 0 (never under-skim). + let scaled = (amount_msat as u128) * (ppm as u128) + 999_999; + u64::try_from(scaled / 1_000_000).unwrap_or(u64::MAX) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lsps4::utils::compute_forward_fee; + use lightning::util::ser::{Readable, Writeable}; + + /// The legacy inline computation the service used before `resolve_skim` existed, kept here as + /// the oracle the `Standard` tier must match for any HTLC small enough not to overflow its + /// `u64` arithmetic. + fn legacy_standard_skim(amount: u64, ppm: u64) -> u64 { + match compute_forward_fee(amount, ppm) { + Some(fee) => { + let fee = core::cmp::min(fee, amount); + if amount.saturating_sub(fee) == 0 && fee > 0 { + 0 + } else { + fee + } + }, + None => 0, + } + } + + #[test] + fn standard_matches_legacy_across_sizes() { + let ppm = 20_000; // 2% + // All sizes below the u64 overflow threshold (~9.2e14 msat at 2%), where the new 128-bit + // math and the legacy u64 math agree exactly. + for amount in [0u64, 1, 999, 1_000, 50_000, 1_000_000, 100_000_000, 1_000_000_000_000] { + let policy = FeePolicy::Flat(FeeTier::Standard); + assert_eq!( + resolve_skim(&policy, amount, ppm), + legacy_standard_skim(amount, ppm), + "mismatch at amount={amount}" + ); + } + } + + #[test] + fn large_htlc_skims_instead_of_forwarding_free() { + // 1e18 msat * 20_000 ppm overflows u64, so the legacy code skimmed nothing and forwarded + // the whole HTLC for free. The 128-bit math skims the correct 2% instead. + let amount = 1_000_000_000_000_000_000u64; + assert_eq!(legacy_standard_skim(amount, 20_000), 0); + let policy = FeePolicy::Flat(FeeTier::Standard); + assert_eq!(resolve_skim(&policy, amount, 20_000), 20_000_000_000_000_000); + } + + #[test] + fn zero_fee_never_skims() { + let policy = FeePolicy::Flat(FeeTier::ZeroFee); + for amount in [0u64, 1, 1_000, u64::MAX] { + assert_eq!(resolve_skim(&policy, amount, 1_000_000), 0); + } + } + + #[test] + fn custom_adds_base_and_proportional() { + // 1% proportional plus a flat 100 msat base. + let policy = FeePolicy::Flat(FeeTier::Custom { ppm: 10_000, base_msat: 100 }); + // 10_000 ppm of 1_000_000 = 10_000, plus 100 base = 10_100. + assert_eq!(resolve_skim(&policy, 1_000_000, 0), 10_100); + } + + #[test] + fn custom_base_only_with_zero_ppm() { + let policy = FeePolicy::Flat(FeeTier::Custom { ppm: 0, base_msat: 100 }); + assert_eq!(resolve_skim(&policy, 1_000_000, 0), 100); + } + + #[test] + fn fee_at_or_above_amount_never_skims_whole_htlc() { + // fee == amount: 1_000_000 ppm of 1_000 = 1_000 == amount -> 0. + let policy = FeePolicy::Flat(FeeTier::Standard); + assert_eq!(resolve_skim(&policy, 1_000, 1_000_000), 0); + + // fee > amount: 2_000_000 ppm of 1_000 = 2_000 > amount -> 0. + assert_eq!(resolve_skim(&policy, 1_000, 2_000_000), 0); + + // Proportional component saturates to u64::MAX, which is >= the amount -> 0. + assert_eq!(resolve_skim(&policy, u64::MAX, u64::MAX), 0); + + // A Custom base on its own large enough to swallow the HTLC -> 0. + let policy = FeePolicy::Flat(FeeTier::Custom { ppm: 0, base_msat: u64::MAX }); + assert_eq!(resolve_skim(&policy, 1_000, 0), 0); + } + + fn round_trip(value: &T) { + let bytes = value.encode(); + let decoded: T = Readable::read(&mut &bytes[..]).unwrap(); + assert_eq!(*value, decoded); + } + + #[test] + fn fee_tier_round_trips() { + round_trip(&FeeTier::Standard); + round_trip(&FeeTier::ZeroFee); + round_trip(&FeeTier::Custom { ppm: 12_345, base_msat: 678 }); + } + + #[test] + fn fee_policy_round_trips() { + round_trip(&FeePolicy::Flat(FeeTier::Standard)); + round_trip(&FeePolicy::Flat(FeeTier::ZeroFee)); + round_trip(&FeePolicy::Flat(FeeTier::Custom { ppm: 1, base_msat: 2 })); + } +} diff --git a/lightning-liquidity/src/lsps4/htlc_store.rs b/lightning-liquidity/src/lsps4/htlc_store.rs new file mode 100644 index 00000000000..2f7963832b0 --- /dev/null +++ b/lightning-liquidity/src/lsps4/htlc_store.rs @@ -0,0 +1,233 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use lightning::util::ser::{Readable, Writeable}; +use lightning::{impl_writeable_tlv_based, log_error}; +use lightning::ln::channelmanager::InterceptId; +use lightning::util::logger::Logger; +use lightning::util::persist::KVStoreSync; +use lightning_types::payment::PaymentHash; + +use bitcoin::secp256k1::PublicKey; + +use lightning::io::{self, Cursor}; + +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::lsps4::utils; + +/// The Intercepted HTLC store information will be persisted under this key. +pub(crate) const INTERCEPTED_HTLC_STORE_PERSISTENCE_PRIMARY_NAMESPACE: &str = "intercepted_htlcs"; +pub(crate) const INTERCEPTED_HTLC_STORE_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; + + +/// Represents an intercepted HTLC that is stored in the data store +#[derive(Clone, Debug, PartialEq)] +pub struct InterceptedHtlc { + /// Unique identifier for this intercepted HTLC + id: InterceptId, + /// The short channel ID of the next hop that the HTLC was originally intended for + requested_next_hop_scid: u64, + /// The amount in millisatoshis that should be forwarded to the next hop + expected_outbound_amount_msat: u64, + /// The payment hash of the HTLC + payment_hash: PaymentHash, + /// Timestamp (seconds since UNIX_EPOCH) when the HTLC was intercepted + intercepted_at: u64, + /// The next node ID that this HTLC should be forwarded to + next_node_id: PublicKey, +} + +impl InterceptedHtlc { + /// Create a new intercepted HTLC + pub fn new( + intercept_id: InterceptId, requested_next_hop_scid: u64, + expected_outbound_amount_msat: u64, payment_hash: PaymentHash, next_node_id: PublicKey, + ) -> Self { + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_secs(); + Self { + id: intercept_id, + requested_next_hop_scid, + expected_outbound_amount_msat, + payment_hash, + intercepted_at: now, + next_node_id, + } + } + + pub fn id(&self) -> InterceptId { + self.id + } + + pub fn requested_next_hop_scid(&self) -> u64 { + self.requested_next_hop_scid + } + + pub fn expected_outbound_amount_msat(&self) -> u64 { + self.expected_outbound_amount_msat + } + + pub fn payment_hash(&self) -> PaymentHash { + self.payment_hash + } + + pub fn intercepted_at(&self) -> u64 { + self.intercepted_at + } + + pub fn next_node_id(&self) -> PublicKey { + self.next_node_id + } +} + +impl_writeable_tlv_based!(InterceptedHtlc, { + (0, id, required), + (2, requested_next_hop_scid, required), + (4, expected_outbound_amount_msat, required), + (6, payment_hash, required), + (10, intercepted_at, required), + (12, next_node_id, required), +}); + +pub struct HTLCStore +where L::Target: Logger, KV::Target: KVStoreSync { + htlcs: Mutex>, + kv_store: KV, + logger: L +} + +impl HTLCStore +where L::Target: Logger, KV::Target: KVStoreSync { + pub(crate) fn new( + kv_store: KV, logger: L, + ) -> Result { + let mut htlcs = Vec::new(); + + let stored_keys = kv_store.list( + INTERCEPTED_HTLC_STORE_PERSISTENCE_PRIMARY_NAMESPACE, + INTERCEPTED_HTLC_STORE_PERSISTENCE_SECONDARY_NAMESPACE, + )?; + + for stored_key in stored_keys { + let data = kv_store.read( + INTERCEPTED_HTLC_STORE_PERSISTENCE_PRIMARY_NAMESPACE, + INTERCEPTED_HTLC_STORE_PERSISTENCE_SECONDARY_NAMESPACE, + &stored_key, + )?; + let mut reader = Cursor::new(data); + let htlc = InterceptedHtlc::read(&mut reader).map_err(|e| { + log_error!(logger, "Failed to deserialize InterceptedHtlc: {}", e); + io::Error::new( + io::ErrorKind::InvalidData, + "Failed to deserialize InterceptedHtlc", + ) + })?; + htlcs.push(htlc); + } + + let htlcs = + Mutex::new(HashMap::from_iter(htlcs.into_iter().map(|obj| (obj.id(), obj)))); + + Ok(Self { htlcs, kv_store, logger }) + } + + pub(crate) fn insert(&self, htlc: InterceptedHtlc) -> Result { + // Insert into the in-memory map first for immediate visibility to other threads + // (e.g., peer_connected checking for pending HTLCs). Then persist to S3 for durability. + { + let mut locked_htlcs = self.htlcs.lock().unwrap(); + if locked_htlcs.contains_key(&htlc.id()) { + return Ok(false); + } + locked_htlcs.insert(htlc.id(), htlc.clone()); + } + + // Persist to durable storage. If this fails, remove from the map to stay consistent. + if let Err(e) = self.persist(&htlc) { + self.htlcs.lock().unwrap().remove(&htlc.id()); + return Err(e); + } + + Ok(true) + } + + pub(crate) fn remove(&self, id: &InterceptId) -> Result<(), io::Error> { + let removed = self.htlcs.lock().unwrap().remove(id).is_some(); + if removed { + let store_key = utils::to_string(&id.0); + self.kv_store + .remove(INTERCEPTED_HTLC_STORE_PERSISTENCE_PRIMARY_NAMESPACE, INTERCEPTED_HTLC_STORE_PERSISTENCE_SECONDARY_NAMESPACE, &store_key, false) + .map_err(|e| { + log_error!( + self.logger, + "Removing htlc with intercept id {} failed due to: {}", + store_key, + e + ); + e + })?; + } + Ok(()) + } + + pub(crate) fn get(&self, id: &InterceptId) -> Option { + self.htlcs.lock().unwrap().get(id).cloned() + } + + pub(crate) fn list_filter bool>(&self, f: F) -> Vec { + self.htlcs.lock().unwrap().values().filter(f).cloned().collect::>() + } + + /// Get all intercepted HTLCs for a specific node + pub fn get_htlcs_by_node_id(&self, node_id: &PublicKey) -> Vec { + let mut htlcs = self.list_filter(|htlc| htlc.next_node_id() == *node_id); + htlcs.sort_by_key(|htlc| htlc.expected_outbound_amount_msat()); + htlcs + } + + /// Get all intercepted HTLCs that are older than the specified threshold in seconds. + pub fn get_expired_htlcs(&self, now: u64, threshold_seconds: u64) -> Vec { + self.list_filter(|htlc| { + // Only include HTLCs that are older than the threshold + now.saturating_sub(htlc.intercepted_at()) > threshold_seconds + }) + } + + fn persist(&self, htlc: &InterceptedHtlc) -> Result<(), io::Error> { + let store_key = utils::to_string(&htlc.id().0); + let data = htlc.encode(); + self.kv_store + .write(INTERCEPTED_HTLC_STORE_PERSISTENCE_PRIMARY_NAMESPACE, INTERCEPTED_HTLC_STORE_PERSISTENCE_SECONDARY_NAMESPACE, &store_key, data) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {} failed due to: {}", + store_key, + e + ); + e + })?; + Ok(()) + } + + pub fn add_intercepted_htlc( + &self, intercept_id: InterceptId, requested_next_hop_scid: u64, + expected_outbound_amount_msat: u64, payment_hash: PaymentHash, next_node_id: PublicKey, + ) -> Result { + let htlc = InterceptedHtlc::new( + intercept_id, + requested_next_hop_scid, + expected_outbound_amount_msat, + payment_hash, + next_node_id, + ); + self.insert(htlc) + } +} \ No newline at end of file diff --git a/lightning-liquidity/src/lsps4/mod.rs b/lightning-liquidity/src/lsps4/mod.rs new file mode 100644 index 00000000000..5d16b18fba0 --- /dev/null +++ b/lightning-liquidity/src/lsps4/mod.rs @@ -0,0 +1,20 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Implementation of LSPS4: JIT Channel Negotiation specification. + +pub(crate) mod claim; +pub mod client; +pub mod event; +pub mod fee_policy; +pub(crate) mod htlc_store; +pub mod msgs; +pub(crate) mod scid_store; +pub mod service; +pub mod utils; diff --git a/lightning-liquidity/src/lsps4/msgs.rs b/lightning-liquidity/src/lsps4/msgs.rs new file mode 100644 index 00000000000..04a0f0ad91e --- /dev/null +++ b/lightning-liquidity/src/lsps4/msgs.rs @@ -0,0 +1,133 @@ +//! Message, request, and other primitive types used to implement LSPS4. + +use core::convert::TryFrom; + +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::{Hash, HashEngine}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; + +use lightning::util::scid_utils; + +use crate::lsps0::ser::{ + string_amount, string_amount_option, LSPSMessage, LSPSRequestId, LSPSResponseError, +}; + +use std::string::String; +use std::vec::Vec; +use crate::utils; + +pub(crate) const LSPS4_REGISTER_NODE_METHOD_NAME: &str = "lsps4.register_node"; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +/// A request made to an LSP to register a node. +pub struct RegisterNodeRequest { + /// An optional signed claim, lowercase-hex encoded, granting this node a + /// non-standard fee policy. Absent or unverifiable claims leave the node on + /// the standard policy. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fee_claim: Option, +} + +/// A newtype that holds a `short_channel_id` in human readable format of BBBxTTTx000. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct InterceptScid(String); + +impl From for InterceptScid { + fn from(scid: u64) -> Self { + let block = scid_utils::block_from_scid(scid); + let tx_index = scid_utils::tx_index_from_scid(scid); + let vout = scid_utils::vout_from_scid(scid); + + Self(format!("{}x{}x{}", block, tx_index, vout)) + } +} + +impl InterceptScid { + /// Try to convert a [`InterceptScid`] into a u64 used by LDK. + pub fn to_scid(&self) -> Result { + utils::scid_from_human_readable_string(&self.0) + } +} + +/// A response to a [`RegisterNodeRequest`]. +/// +/// Includes information needed to construct an invoice. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct RegisterNodeResponse { + /// The intercept short channel id used by LSP to identify need to open channel. + pub jit_channel_scid: InterceptScid, + /// The locktime expiry delta the lsp requires. + pub lsp_cltv_expiry_delta: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all the valid JSON-RPC requests in the LSPS4 protocol. +pub enum LSPS4Request { + /// A request to register a node with an LSP. + RegisterNode(RegisterNodeRequest), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all the valid JSON-RPC responses in the LSPS4 protocol. +pub enum LSPS4Response { + /// A successful response to a [`LSPS4Request::RegisterNode`] request. + RegisterNode(RegisterNodeResponse), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all valid JSON-RPC messages in the LSPS4 protocol. +pub enum LSPS4Message { + /// An LSPS4 JSON-RPC request. + Request(LSPSRequestId, LSPS4Request), + /// An LSPS4 JSON-RPC response. + Response(LSPSRequestId, LSPS4Response), +} + +impl TryFrom for LSPS4Message { + type Error = (); + + fn try_from(message: LSPSMessage) -> Result { + if let LSPSMessage::LSPS4(message) = message { + return Ok(message); + } + + Err(()) + } +} + +impl From for LSPSMessage { + fn from(message: LSPS4Message) -> Self { + LSPSMessage::LSPS4(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::alloc::string::ToString; + + #[test] + fn register_node_request_with_claim_round_trips() { + let request = RegisterNodeRequest { fee_claim: Some("deadbeef".to_string()) }; + let json_str = r#"{"fee_claim":"deadbeef"}"#; + + assert_eq!(json_str, serde_json::json!(request).to_string()); + assert_eq!(request, serde_json::from_str(json_str).unwrap()); + } + + #[test] + fn register_node_request_without_claim_omits_field() { + let request = RegisterNodeRequest { fee_claim: None }; + + assert_eq!("{}", serde_json::json!(request).to_string()); + } + + #[test] + fn legacy_empty_object_decodes_to_no_claim() { + let request: RegisterNodeRequest = serde_json::from_str("{}").unwrap(); + + assert_eq!(request, RegisterNodeRequest { fee_claim: None }); + } +} diff --git a/lightning-liquidity/src/lsps4/scid_store.rs b/lightning-liquidity/src/lsps4/scid_store.rs new file mode 100644 index 00000000000..e3561c30927 --- /dev/null +++ b/lightning-liquidity/src/lsps4/scid_store.rs @@ -0,0 +1,307 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use lightning::util::ser::{Readable, Writeable}; +use lightning::{impl_writeable_tlv_based, log_error}; +use lightning::util::logger::Logger; +use lightning::util::persist::KVStoreSync; + +use bitcoin::secp256k1::PublicKey; + +use lightning::io::{self, Cursor}; + +use std::collections::HashMap; +use std::ops::Deref; +use crate::sync::RwLock; + + +use crate::lsps4::fee_policy::{FeePolicy, FeeTier}; +use crate::lsps4::utils; + +/// The Intercepted HTLC store information will be persisted under this key. +pub(crate) const INTERCEPT_SCID_STORE_PERSISTENCE_PRIMARY_NAMESPACE: &str = "intercept_scids"; +pub(crate) const INTERCEPT_SCID_STORE_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; + + +/// Represents an intercepted HTLC that is stored in the data store +#[derive(Clone, Debug, PartialEq)] +pub struct ScidWithPeer { + scid: u64, + peer_id: PublicKey, + policy: FeePolicy, +} + +impl ScidWithPeer { + pub fn new( + scid: u64, peer_id: PublicKey, policy: FeePolicy, + ) -> Self { + Self { scid, peer_id, policy } + } + + pub fn store_key(&self) -> String { + utils::to_string(&self.scid.to_be_bytes()) + } + + pub fn scid(&self) -> u64 { + self.scid + } + + pub fn peer_id(&self) -> PublicKey { + self.peer_id + } + + pub fn policy(&self) -> &FeePolicy { + &self.policy + } +} + +impl_writeable_tlv_based!(ScidWithPeer, { + (0, scid, required), + (2, peer_id, required), + (4, policy, (default_value, FeePolicy::Flat(FeeTier::Standard))), +}); + +pub struct ScidStore +where L::Target: Logger, KV::Target: KVStoreSync { + peer_by_scid: RwLock>, + scid_by_peer: RwLock>, + policy_by_peer: RwLock>, + kv_store: KV, + logger: L +} + +impl ScidStore +where L::Target: Logger, KV::Target: KVStoreSync { + pub(crate) fn new( + kv_store: KV, logger: L, + ) -> Result { + let mut scids = Vec::new(); + + let stored_keys = kv_store.list( + INTERCEPT_SCID_STORE_PERSISTENCE_PRIMARY_NAMESPACE, + INTERCEPT_SCID_STORE_PERSISTENCE_SECONDARY_NAMESPACE, + )?; + + for stored_key in stored_keys { + let data = kv_store.read( + INTERCEPT_SCID_STORE_PERSISTENCE_PRIMARY_NAMESPACE, + INTERCEPT_SCID_STORE_PERSISTENCE_SECONDARY_NAMESPACE, + &stored_key, + )?; + let mut reader = Cursor::new(data); + let scid = ScidWithPeer::read(&mut reader).map_err(|e| { + log_error!(logger, "Failed to deserialize InterceptScid: {}", e); + io::Error::new( + io::ErrorKind::InvalidData, + "Failed to deserialize InterceptScid", + ) + })?; + scids.push(scid); + } + + let peer_by_scid = + RwLock::new(HashMap::from_iter(scids.iter().map(|obj| (obj.scid(), obj.peer_id())))); + + let scid_by_peer = + RwLock::new(HashMap::from_iter(scids.iter().map(|obj| (obj.peer_id(), obj.scid())))); + + let policy_by_peer = RwLock::new(HashMap::from_iter( + scids.iter().map(|obj| (obj.peer_id(), obj.policy().clone())), + )); + + Ok(Self { peer_by_scid, scid_by_peer, policy_by_peer, kv_store, logger }) + } + + pub(crate) fn insert(&self, scid: ScidWithPeer) -> Result { + use lightning::log_info; + log_info!(self.logger, "[LSPS4 ScidStore] Inserting SCID {} for peer {}", scid.scid(), scid.peer_id()); + + // Persist first + self.persist(&scid)?; + + // Then insert into the maps + let mut locked_peer_by_scid = self.peer_by_scid.write().unwrap(); + let mut locked_scid_by_peer = self.scid_by_peer.write().unwrap(); + let mut locked_policy_by_peer = self.policy_by_peer.write().unwrap(); + let updated = locked_peer_by_scid.insert(scid.scid(), scid.peer_id().clone()).is_some(); + locked_scid_by_peer.insert(scid.peer_id().clone(), scid.scid()); + locked_policy_by_peer.insert(scid.peer_id().clone(), scid.policy().clone()); + + log_info!( + self.logger, + "[LSPS4 ScidStore] Successfully inserted SCID {} for peer {} (was_update: {})", + scid.scid(), + scid.peer_id(), + updated + ); + + Ok(updated) + } + + pub(crate) fn remove(&self, scid: u64) -> Result<(), io::Error> { + let mut locked_peer_by_scid = self.peer_by_scid.write().unwrap(); + let mut locked_scid_by_peer = self.scid_by_peer.write().unwrap(); + let mut locked_policy_by_peer = self.policy_by_peer.write().unwrap(); + + let removed = locked_peer_by_scid.remove(&scid); + if let Some(peer_id) = removed { + locked_scid_by_peer.remove(&peer_id); + locked_policy_by_peer.remove(&peer_id); + let store_key = utils::to_string(&scid.to_be_bytes()); + self.kv_store + .remove(INTERCEPT_SCID_STORE_PERSISTENCE_PRIMARY_NAMESPACE, INTERCEPT_SCID_STORE_PERSISTENCE_SECONDARY_NAMESPACE, &store_key, false) + .map_err(|e| { + log_error!( + self.logger, + "Removing htlc with intercept id {} failed due to: {}", + store_key, + e + ); + e + })?; + } + Ok(()) + } + + fn persist(&self, scid: &ScidWithPeer) -> Result<(), io::Error> { + let store_key = scid.store_key(); + let data = scid.encode(); + self.kv_store + .write(INTERCEPT_SCID_STORE_PERSISTENCE_PRIMARY_NAMESPACE, INTERCEPT_SCID_STORE_PERSISTENCE_SECONDARY_NAMESPACE, &store_key, data) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {} failed due to: {}", + store_key, + e + ); + e + })?; + Ok(()) + } + + pub fn get_peer(&self, scid: u64) -> Option { + use lightning::log_debug; + let result = self.peer_by_scid.read().unwrap().get(&scid).cloned(); + log_debug!( + self.logger, + "[LSPS4 ScidStore] get_peer({}) = {:?}", + scid, + result + ); + result + } + + pub fn get_scid(&self, peer_id: &PublicKey) -> Option { + use lightning::log_debug; + let result = self.scid_by_peer.read().unwrap().get(peer_id).cloned(); + log_debug!( + self.logger, + "[LSPS4 ScidStore] get_scid({}) = {:?}", + peer_id, + result + ); + result + } + + pub fn get_policy(&self, peer_id: &PublicKey) -> Option { + self.policy_by_peer.read().unwrap().get(peer_id).cloned() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lightning::impl_writeable_tlv_based; + + /// A copy of the pre-policy `ScidWithPeer` layout (tlv 0/2 only) used to prove that records + /// persisted before the `policy` field existed still decode, defaulting to `Flat(Standard)`. + struct LegacyScidWithPeer { + scid: u64, + peer_id: PublicKey, + } + + impl_writeable_tlv_based!(LegacyScidWithPeer, { + (0, scid, required), + (2, peer_id, required), + }); + + fn test_peer() -> PublicKey { + // The secp256k1 generator point: a valid compressed public key. + PublicKey::from_slice(&[ + 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, + 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, + 0x5B, 0x16, 0xF8, 0x17, 0x98, + ]) + .unwrap() + } + + #[test] + fn round_trips_with_policy() { + let record = ScidWithPeer::new(42, test_peer(), FeePolicy::Flat(FeeTier::ZeroFee)); + let bytes = record.encode(); + let decoded = ScidWithPeer::read(&mut &bytes[..]).unwrap(); + assert_eq!(record, decoded); + assert_eq!(decoded.policy(), &FeePolicy::Flat(FeeTier::ZeroFee)); + } + + #[test] + fn legacy_record_defaults_to_standard_policy() { + let legacy = LegacyScidWithPeer { scid: 42, peer_id: test_peer() }; + let bytes = legacy.encode(); + let decoded = ScidWithPeer::read(&mut &bytes[..]).unwrap(); + assert_eq!(decoded.scid(), 42); + assert_eq!(decoded.peer_id(), test_peer()); + assert_eq!(decoded.policy(), &FeePolicy::Flat(FeeTier::Standard)); + } + + use bitcoin::secp256k1::{Secp256k1, SecretKey}; + use lightning::util::test_utils::{TestLogger, TestStore}; + use std::sync::Arc; + + fn other_peer() -> PublicKey { + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[0x24; 32]).unwrap()) + } + + fn test_store() -> ScidStore, Arc> { + ScidStore::new(Arc::new(TestStore::new(false)), Arc::new(TestLogger::new())).unwrap() + } + + #[test] + fn insert_with_policy_then_get_policy_returns_it() { + let store = test_store(); + store + .insert(ScidWithPeer::new(42, test_peer(), FeePolicy::Flat(FeeTier::ZeroFee))) + .unwrap(); + + assert_eq!(store.get_policy(&test_peer()), Some(FeePolicy::Flat(FeeTier::ZeroFee))); + assert_eq!(store.get_policy(&other_peer()), None); + } + + #[test] + fn load_rebuilds_policy_map() { + let kv_store = Arc::new(TestStore::new(false)); + { + let store = + ScidStore::new(kv_store.clone(), Arc::new(TestLogger::new())).unwrap(); + store + .insert(ScidWithPeer::new(42, test_peer(), FeePolicy::Flat(FeeTier::ZeroFee))) + .unwrap(); + } + + let reloaded = ScidStore::new(kv_store, Arc::new(TestLogger::new())).unwrap(); + assert_eq!(reloaded.get_policy(&test_peer()), Some(FeePolicy::Flat(FeeTier::ZeroFee))); + } + + #[test] + fn default_record_resolves_to_standard_policy() { + let store = test_store(); + store.insert(ScidWithPeer::new(42, test_peer(), FeePolicy::Flat(FeeTier::Standard))).unwrap(); + + assert_eq!(store.get_policy(&test_peer()), Some(FeePolicy::Flat(FeeTier::Standard))); + } +} \ No newline at end of file diff --git a/lightning-liquidity/src/lsps4/service.rs b/lightning-liquidity/src/lsps4/service.rs new file mode 100644 index 00000000000..bd074df2351 --- /dev/null +++ b/lightning-liquidity/src/lsps4/service.rs @@ -0,0 +1,1204 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS4 server-side object, [`LSPS4ServiceHandler`]. + +use crate::events::{LiquidityEvent, EventQueue}; +use crate::lsps0::ser::{ + LSPSMessage, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError, + JSONRPC_INTERNAL_ERROR_ERROR_CODE, JSONRPC_INTERNAL_ERROR_ERROR_MESSAGE, + LSPS0_CLIENT_REJECTED_ERROR_CODE, +}; +use crate::lsps4::claim::verify_claim; +use crate::lsps4::event::LSPS4ServiceEvent; +use crate::lsps4::htlc_store::{HTLCStore, InterceptedHtlc}; +use crate::lsps4::fee_policy::{resolve_skim, FeePolicy, FeeTier}; +use crate::lsps4::scid_store::{ScidStore, ScidWithPeer}; +use crate::message_queue::MessageQueue; +use crate::prelude::hash_map::Entry; +use crate::prelude::{new_hash_map, HashMap}; +use crate::sync::{Arc, Mutex, MutexGuard, RwLock}; + +use lightning::ln::channelmanager::{AChannelManager, InterceptId}; +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::ln::types::ChannelId; +use lightning::{log_debug, log_error, log_info}; +use lightning::util::errors::APIError; +use lightning::util::logger::{Level, Logger}; + +use lightning::util::persist::{KVStore, KVStoreSync}; +use lightning_types::payment::PaymentHash; + +use bitcoin::secp256k1::{PublicKey, Secp256k1, VerifyOnly, XOnlyPublicKey}; + +use core::ops::Deref; +use core::sync::atomic::{AtomicUsize, Ordering}; +use std::collections::HashSet; + +use crate::lsps4::msgs::{ + RegisterNodeRequest, RegisterNodeResponse, LSPS4Message, LSPS4Request, + LSPS4Response, +}; + +use std::string::String; +use std::time::Instant; +use std::vec::Vec; + +const HTLC_EXPIRY_THRESHOLD_SECS: u64 = 45; + +/// Cooldown duration for liquidity actions. After a splice or channel open is emitted, +/// suppress further liquidity actions for this peer until the cooldown expires. +/// Set below HTLC_EXPIRY_THRESHOLD_SECS so at most 1 wasted retry occurs before expiry. +const LIQUIDITY_COOLDOWN_SECS: u64 = 30; + +/// Action to forward a specific HTLC through a channel +#[derive(Debug)] +pub(crate) struct HtlcForwardAction { + pub htlc: InterceptedHtlc, + pub channel_id: ChannelId, + pub amount_to_forward_msat: u64, + pub skimmed_fee_msat: u64, +} + +/// Action to splice into an existing channel to add capacity. +#[derive(Debug)] +pub(crate) struct SpliceAction { + pub channel_id: ChannelId, + pub user_channel_id: u128, + pub amt_to_forward_msat: u64, +} + +/// Actions to take for processing HTLCs for a peer +#[derive(Debug)] +pub(crate) struct HtlcProcessingActions { + pub forwards: Vec, + pub new_channel_needed_msat: Option, + pub splice_needed: Option, + pub channel_count: usize, +} + +impl HtlcProcessingActions { + pub fn is_empty(&self) -> bool { + self.forwards.is_empty() + && self.new_channel_needed_msat.is_none() + && self.splice_needed.is_none() + } + + pub fn needs_liquidity_action(&self) -> bool { + self.new_channel_needed_msat.is_some() || self.splice_needed.is_some() + } + + pub fn total_forward_amount(&self) -> u64 { + self.forwards.iter().map(|f| f.amount_to_forward_msat).sum() + } +} + +/// Server-side configuration options for JIT channels. +#[derive(Clone, Debug)] +pub struct LSPS4ServiceConfig { + /// The CLTV expiry delta to use for the JIT channels. + pub cltv_expiry_delta: u32, + /// The proportional fee, in millionths, to skim from forwarded payments. + pub forwarding_fee_proportional_millionths: u64, + /// Issuer keys trusted to sign fee-policy grants. Empty disables the feature: every claim is + /// rejected and every peer resolves to the standard policy. A claim signed by any one of these + /// keys is honoured. + pub issuer_pubkeys: Vec, +} + +/// The main object allowing to send and receive LSPS4 messages. +pub struct LSPS4ServiceHandler +where + CM::Target: AChannelManager, + K::Target: KVStore + KVStoreSync, + L::Target: Logger, +{ + channel_manager: CM, + logger: L, + pending_messages: Arc, + pending_events: Arc>, + scid_store: ScidStore, + htlc_store: HTLCStore, + connected_peers: RwLock>, + config: LSPS4ServiceConfig, + /// Long-lived context for verifying fee-claim signatures during registration. + secp_ctx: Secp256k1, + /// Per-peer timestamped cooldown for liquidity actions (splice or channel open). + /// Prevents 1Hz retry loops from process_pending_htlcs when actions fail. + /// Auto-expires after LIQUIDITY_COOLDOWN_SECS. + liquidity_cooldown: RwLock>, +} + +impl LSPS4ServiceHandler +where + CM::Target: AChannelManager, + K::Target: KVStore + KVStoreSync, + L::Target: Logger, +{ + /// Constructs a `LSPS4ServiceHandler`. + pub(crate) async fn new( + pending_messages: Arc, + pending_events: Arc>, + channel_manager: CM, + config: LSPS4ServiceConfig, + kv_store: K, + logger: L, + ) -> Result { + Ok(Self { + pending_messages, + pending_events, + scid_store: ScidStore::new(kv_store.clone(), logger.clone())?, + htlc_store: HTLCStore::new(kv_store.clone(), logger.clone())?, + channel_manager, + config, + logger, + connected_peers: RwLock::new(HashSet::new()), + secp_ctx: Secp256k1::verification_only(), + liquidity_cooldown: RwLock::new(new_hash_map()), + }) + } + + /// Forward [`Event::HTLCIntercepted`] event parameters into this function. + /// + /// Will generate a [`LSPS4ServiceEvent::OpenChannel`] event if the intercept scid + /// matches and the user needs more liquidity. + /// + /// Will do nothing if the intercept scid does not match any of the ones we gave out. + /// + /// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted + /// [`LSPS4ServiceEvent::OpenChannel`]: crate::lsps4::event::LSPS4ServiceEvent::OpenChannel + pub fn htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) -> Result<(), APIError> { + log_info!( + self.logger, + "[LSPS4] HTLC intercepted - SCID: {}, intercept_id: {:?}, amount: {} msat, payment_hash: {}", + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash + ); + if let Some(counterparty_node_id) = self.scid_store.get_peer(intercept_scid) { + log_info!( + self.logger, + "[LSPS4] SCID {} matched to peer {}, processing HTLC", + intercept_scid, + counterparty_node_id + ); + let htlc = InterceptedHtlc::new( + intercept_id, + intercept_scid, + expected_outbound_amount_msat, + payment_hash, + counterparty_node_id.clone(), + ); + + let is_connected = self.is_peer_connected(&counterparty_node_id); + let connected_count = self.connected_peers.read().unwrap().len(); + + if !is_connected { + log_info!( + self.logger, + "[LSPS4] htlc_intercepted: peer {} NOT in connected_peers (set size: {}), storing HTLC and sending webhook. payment_hash: {}", + counterparty_node_id, + connected_count, + payment_hash + ); + self.htlc_store.insert(htlc).unwrap(); // TODO: handle persistence failures + // Send webhook to wake the peer. When the peer reconnects, + // peer_connected() will find this HTLC in the store and process it. + // The process_pending_htlcs timer also serves as a fallback. + let mut event_queue_notifier = self.pending_events.notifier(); + event_queue_notifier.enqueue(crate::events::LiquidityEvent::LSPS4Service(LSPS4ServiceEvent::SendWebhook { + counterparty_node_id: counterparty_node_id.clone(), + payment_hash, + })); + } else { + let channels = self + .channel_manager + .get_cm() + .list_channels_with_counterparty(&counterparty_node_id); + let any_usable = channels.iter().any(|ch| ch.is_usable); + + log_info!( + self.logger, + "[LSPS4] htlc_intercepted: peer {} IS in connected_peers (set size: {}), channels: {}, any_usable: {}, payment_hash: {}", + counterparty_node_id, + connected_count, + channels.len(), + any_usable, + payment_hash + ); + for ch in &channels { + log_info!( + self.logger, + "[LSPS4] htlc_intercepted: channel {} - is_usable: {}, is_channel_ready: {}, outbound_capacity: {}msat", + ch.channel_id, + ch.is_usable, + ch.is_channel_ready, + ch.outbound_capacity_msat + ); + } + + // Always persist before calculating actions. execute_htlc_actions + // removes the HTLC on successful forward. For the liquidity path + // (splice/open) the HTLC must survive in the store so the timer + // can forward it once the new channel is ready. + let persisted = match self.htlc_store.insert(htlc.clone()) { + Ok(_) => true, + Err(e) => { + log_error!( + self.logger, + "[LSPS4] htlc_intercepted: failed to persist HTLC {:?}, \ + payment_hash: {}, error: {}", + intercept_id, + payment_hash, + e + ); + false + } + }; + + let actions = self.calculate_htlc_actions_for_peer( + counterparty_node_id, + vec![htlc], + ); + + log_debug!( + self.logger, + "[LSPS4] htlc_intercepted: calculated actions for peer {}: {:?}", + counterparty_node_id, + actions + ); + + // Liquidity actions (splice/open) are async — the timer forwards + // the HTLC once the channel is ready. Without persistence the + // splice/open fires but the HTLC is never forwarded, wasting + // on-chain fees for a payment that times out anyway. + if !persisted && actions.needs_liquidity_action() { + log_error!( + self.logger, + "[LSPS4] htlc_intercepted: liquidity action needed but HTLC {:?} \ + not persisted, failing back to sender. payment_hash: {}", + intercept_id, + payment_hash + ); + let _ = self.channel_manager.get_cm().fail_intercepted_htlc(intercept_id); + return Ok(()); + } + + // Forward-only path is fine without persistence — the HTLC is + // consumed immediately by forward_intercepted_htlc. When persisted, + // the store entry also covers the TOCTOU race where the channel + // becomes unusable between calculate and execute; the timer retries + // from the store. + self.execute_htlc_actions(actions, counterparty_node_id.clone()); + } + } else { + log_error!( + self.logger, + "[LSPS4] SCID {} NOT FOUND in scid_store! Cannot route HTLC {:?}. Payment will fail.", + intercept_scid, + intercept_id + ); + } + log_debug!( + self.logger, + "[htlc_intercepted] Finalizing htlc_intercepted for intercept_scid {}", + intercept_scid + ); + + Ok(()) + } + + /// Forward [`Event::ChannelReady`] event parameters into this function. + /// + /// Will attempt to forward pending htlcs for this counterparty if there are any. + /// + /// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady + pub fn channel_ready( + &self, counterparty_node_id: &PublicKey, + ) -> Result<(), APIError> { + // Clear liquidity cooldown on channel_ready. This fires for both new channels + // and splice completions (splice_locked), allowing immediate retry if needed. + self.liquidity_cooldown.write().unwrap().remove(counterparty_node_id); + + let is_connected = self.is_peer_connected(counterparty_node_id); + + log_info!( + self.logger, + "[LSPS4] channel_ready: peer {} (is_peer_connected: {})", + counterparty_node_id, + is_connected + ); + + if is_connected { + let htlcs = self.htlc_store.get_htlcs_by_node_id(counterparty_node_id); + log_info!( + self.logger, + "[LSPS4] channel_ready: peer {} has {} HTLCs in store to process", + counterparty_node_id, + htlcs.len() + ); + + // Log channel states at channel_ready time - key diagnostic for race condition + let channels = self + .channel_manager + .get_cm() + .list_channels_with_counterparty(counterparty_node_id); + for ch in &channels { + log_info!( + self.logger, + "[LSPS4] channel_ready: channel {} - is_usable: {}, is_channel_ready: {}, outbound_capacity: {}msat", + ch.channel_id, + ch.is_usable, + ch.is_channel_ready, + ch.outbound_capacity_msat + ); + } + + self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs); + } + + Ok(()) + } + + /// Will attempt to forward any pending intercepted htlcs to this counterparty, + /// but only if a usable channel exists. After reconnect, channels are not usable + /// until channel_reestablish completes. Call [`process_pending_htlcs`] on a timer + /// to retry peers whose channels were not yet usable at connect time. + /// + /// [`process_pending_htlcs`]: Self::process_pending_htlcs + pub fn peer_connected(&self, counterparty_node_id: PublicKey) { + let fn_start = Instant::now(); + let connected_count = { + let mut peers = self.connected_peers.write().unwrap(); + peers.insert(counterparty_node_id); + peers.len() + }; + + log_info!( + self.logger, + "[LSPS4] peer_connected: {} (total connected peers: {})", + counterparty_node_id, + connected_count + ); + + // Log channel states BEFORE attempting to forward - key diagnostic for race condition. + // If is_usable is false but is_channel_ready is true, the channel hasn't completed + // ChannelReestablish yet and forward_intercepted_htlc will silently fail later. + let channels = self + .channel_manager + .get_cm() + .list_channels_with_counterparty(&counterparty_node_id); + for ch in &channels { + log_info!( + self.logger, + "[LSPS4] peer_connected: channel {} with {} - is_usable: {}, is_channel_ready: {}, outbound_capacity: {}msat", + ch.channel_id, + counterparty_node_id, + ch.is_usable, + ch.is_channel_ready, + ch.outbound_capacity_msat + ); + } + if channels.is_empty() { + log_info!( + self.logger, + "[LSPS4] peer_connected: {} has NO channels", + counterparty_node_id + ); + } + + let htlcs = self.htlc_store.get_htlcs_by_node_id(&counterparty_node_id); + let htlc_count = htlcs.len(); + + log_info!( + self.logger, + "[LSPS4] peer_connected: {} has {} HTLCs waiting to be forwarded", + counterparty_node_id, + htlc_count + ); + + if htlc_count > 0 { + for htlc in &htlcs { + log_info!( + self.logger, + "[LSPS4] peer_connected: pending HTLC {:?} - payment_hash: {}, amount: {}msat, scid: {}", + htlc.id(), + htlc.payment_hash(), + htlc.expected_outbound_amount_msat(), + htlc.requested_next_hop_scid() + ); + } + } + + self.process_htlcs_for_peer(counterparty_node_id.clone(), htlcs); + + log_info!( + self.logger, + "[LSPS4] peer_connected: TOTAL took {}ms for {}", + fn_start.elapsed().as_millis(), + counterparty_node_id + ); + + + } + + /// Handle expired HTLCs. + /// + /// Will fail the HTLCs and remove them from the store. + /// Needs to be called regularly to cleanup old htlcs. + pub async fn handle_expired_htlcs(&self, now: u64) { + let expired = self.htlc_store.get_expired_htlcs(now, HTLC_EXPIRY_THRESHOLD_SECS); + if !expired.is_empty() { + log_info!( + self.logger, + "[LSPS4] handle_expired_htlcs: found {} expired HTLCs (threshold: {}s, now: {})", + expired.len(), + HTLC_EXPIRY_THRESHOLD_SECS, + now + ); + } + + for htlc in expired { + log_info!( + self.logger, + "[LSPS4] handle_expired_htlcs: failing HTLC {:?} - payment_hash: {}, amount: {}msat, peer: {}", + htlc.id(), + htlc.payment_hash(), + htlc.expected_outbound_amount_msat(), + htlc.next_node_id() + ); + + if let Err(e) = self.channel_manager.get_cm().fail_intercepted_htlc(htlc.id()) { + log_error!( + self.logger, + "[LSPS4] handle_expired_htlcs: HTLC {:?} was already failed: {:?}", + htlc.id(), + e + ); + } + + if let Err(e) = self.htlc_store.remove(&htlc.id()) { + log_error!( + self.logger, + "[LSPS4] handle_expired_htlcs: failed to remove HTLC {:?} from store: {}", + htlc.id(), + e + ); + } + } + } + + fn is_peer_connected(&self, counterparty_node_id: &PublicKey) -> bool { + self.connected_peers.read().unwrap().contains(counterparty_node_id) + } + + fn has_usable_channel(&self, counterparty_node_id: &PublicKey) -> bool { + self.channel_manager + .get_cm() + .list_channels_with_counterparty(counterparty_node_id) + .iter() + .any(|c| c.is_usable) + } + + fn is_liquidity_cooling_down(&self, peer: &PublicKey) -> bool { + self.liquidity_cooldown + .read() + .unwrap() + .get(peer) + .map(|t| t.elapsed().as_secs() < LIQUIDITY_COOLDOWN_SECS) + .unwrap_or(false) + } + + /// Called when a liquidity action (splice or channel open) completes successfully. + /// Clears the cooldown so new actions can be emitted immediately. + pub fn liquidity_action_completed(&self, peer: &PublicKey) { + self.liquidity_cooldown.write().unwrap().remove(peer); + } + + /// Called when a liquidity action fails asynchronously (e.g., ChannelClosed before ready). + /// Clears the cooldown so the timer or next HTLC can retry immediately. + pub fn liquidity_action_failed(&self, peer: &PublicKey) { + self.liquidity_cooldown.write().unwrap().remove(peer); + } + + /// Called when a splice fails asynchronously (SpliceFailed event). + /// Resets the cooldown timestamp instead of clearing it entirely. + /// This allows retry after LIQUIDITY_COOLDOWN_SECS, not immediately, + /// preventing 1Hz loops on persistent async splice failures. + pub fn liquidity_action_reset_cooldown(&self, peer: &PublicKey) { + self.liquidity_cooldown.write().unwrap().insert(*peer, Instant::now()); + } + + /// Will update the set of connected peers + pub fn peer_disconnected(&self, counterparty_node_id: &PublicKey) { + let (was_present, remaining_count) = { + let mut peers = self.connected_peers.write().unwrap(); + let was = peers.remove(counterparty_node_id); + (was, peers.len()) + }; + + let pending_htlcs = self.htlc_store.get_htlcs_by_node_id(counterparty_node_id); + + log_info!( + self.logger, + "[LSPS4] peer_disconnected: {} (was_in_set: {}, remaining connected: {}, pending_htlcs_in_store: {})", + counterparty_node_id, + was_present, + remaining_count, + pending_htlcs.len() + ); + + if !pending_htlcs.is_empty() { + for htlc in &pending_htlcs { + log_info!( + self.logger, + "[LSPS4] peer_disconnected: ORPHANED HTLC {:?} left in store - payment_hash: {}, amount: {}msat", + htlc.id(), + htlc.payment_hash(), + htlc.expected_outbound_amount_msat() + ); + } + } + } + + /// Attempt to forward pending HTLCs for all connected peers that have usable channels. + /// + /// After reconnect, `peer_connected` fires before `channel_reestablish` completes, + /// so channels are not yet usable. This method should be called on a regular timer + /// (e.g. every few seconds) to pick up HTLCs that were deferred at connect time. + pub fn process_pending_htlcs(&self) { + let connected_peers: Vec = + self.connected_peers.read().unwrap().iter().copied().collect(); + + for node_id in connected_peers { + // Skip peers with a liquidity action cooling down. + // Prevents 1Hz retry loops. execute_htlc_actions also checks this + // as a safety net for the htlc_intercepted path. + if self.is_liquidity_cooling_down(&node_id) { + continue; + } + + let htlcs = self.htlc_store.get_htlcs_by_node_id(&node_id); + if htlcs.is_empty() { + continue; + } + + if self.has_usable_channel(&node_id) { + // Channel reestablish completed — attempt to forward the deferred HTLCs. + log_info!( + self.logger, + "[LSPS4] process_pending_htlcs: forwarding {} HTLCs for peer {} (channel now usable)", + htlcs.len(), + node_id + ); + self.process_htlcs_for_peer(node_id, htlcs); + } + } + } + + /// Resolve any fee-policy grant the registering node presented. + /// + /// Returns `None` when the feature is off (no issuer keys configured), when no claim was + /// presented, or when the claim does not verify. A `None` must never downgrade a live grant, so + /// the caller only upserts on `Some`; a new record falls back to the standard policy. + fn resolve_claim_policy( + &self, counterparty: &PublicKey, fee_claim: &Option, + ) -> Option { + if self.config.issuer_pubkeys.is_empty() { + return None; + } + let fee_claim = fee_claim.as_ref()?; + match verify_claim(&self.secp_ctx, fee_claim, &self.config.issuer_pubkeys, counterparty) { + Ok(policy) => Some(policy), + Err(e) => { + log_error!( + self.logger, + "[LSPS4] Rejected fee claim from {}: {:?}", + counterparty, + e + ); + None + }, + } + } + + /// Persist (upsert) a SCID record carrying the resolved fee policy for a peer. + fn persist_scid_policy( + &self, intercept_scid: u64, peer: &PublicKey, policy: FeePolicy, + ) -> Result<(), LightningError> { + self.scid_store + .insert(ScidWithPeer::new(intercept_scid, peer.clone(), policy)) + .map(|_| ()) + .map_err(|e| { + log_error!( + self.logger, + "[LSPS4] Failed to persist intercept SCID {} for peer {}: {}", + intercept_scid, + peer, + e + ); + LightningError { + err: format!("Failed to add intercepted SCID: {}", e), + action: ErrorAction::IgnoreAndLog(Level::Error), + } + }) + } + + fn handle_register_node_request( + &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, params: RegisterNodeRequest, + ) -> Result<(), LightningError> { + log_info!( + self.logger, + "[LSPS4] Received RegisterNodeRequest from {} with request_id {:?}", + counterparty_node_id, + request_id + ); + + let granted_policy = self.resolve_claim_policy(counterparty_node_id, ¶ms.fee_claim); + + let intercept_scid = match self.scid_store.get_scid(counterparty_node_id) { + Some(intercept_scid) => { + log_info!( + self.logger, + "[LSPS4] Found existing intercept SCID {} for peer {}", + intercept_scid, + counterparty_node_id + ); + // Upsert a verified grant onto the existing record. An absent or invalid claim + // leaves the live policy untouched so a transient miss can't wipe a grant. + if let Some(policy) = granted_policy { + self.persist_scid_policy(intercept_scid, counterparty_node_id, policy)?; + } + intercept_scid + }, + None => { + let intercept_scid = self.channel_manager.get_cm().get_intercept_scid(); + log_info!( + self.logger, + "[LSPS4] Generated NEW intercept SCID {} for peer {}", + intercept_scid, + counterparty_node_id + ); + let policy = granted_policy.unwrap_or(FeePolicy::Flat(FeeTier::Standard)); + self.persist_scid_policy(intercept_scid, counterparty_node_id, policy)?; + log_info!( + self.logger, + "[LSPS4] Successfully stored intercept SCID {} for peer {}", + intercept_scid, + counterparty_node_id + ); + intercept_scid + } + }; + + log_info!( + self.logger, + "[LSPS4] Sending RegisterNodeResponse to {} with SCID {} and CLTV delta {}", + counterparty_node_id, + intercept_scid, + self.config.cltv_expiry_delta + ); + + let mut message_queue_notifier = self.pending_messages.notifier(); + message_queue_notifier.enqueue(counterparty_node_id, LSPS4Message::Response(request_id, LSPS4Response::RegisterNode(RegisterNodeResponse { + jit_channel_scid: intercept_scid.into(), + lsp_cltv_expiry_delta: self.config.cltv_expiry_delta, + })).into()); + + log_info!( + self.logger, + "[LSPS4] RegisterNodeResponse enqueued successfully for peer {}", + counterparty_node_id + ); + + Ok(()) + } + + /// Calculate what actions to take for a list of HTLCs for a specific peer + /// This is a pure function that doesn't perform any side effects + pub(crate) fn calculate_htlc_actions_for_peer( + &self, their_node_id: PublicKey, mut htlcs: Vec, + ) -> HtlcProcessingActions { + let channels = + self.channel_manager.get_cm().list_channels_with_counterparty(&their_node_id); + let channel_count = channels.len(); + + let mut channel_capacity_map: HashMap = new_hash_map(); + for channel in &channels { + if !channel.is_usable { + continue; + } + channel_capacity_map.insert(channel.channel_id, channel.outbound_capacity_msat); + log_info!( + self.logger, + "[LSPS4] calculate_htlc_actions: channel {} - is_usable: {}, is_channel_ready: {}, outbound_capacity: {}msat", + channel.channel_id, + channel.is_usable, + channel.is_channel_ready, + channel.outbound_capacity_msat + ); + } + + log_info!( + self.logger, + "[LSPS4] calculate_htlc_actions: {} has {} channels, {} HTLCs to process", + their_node_id, + channels.len(), + htlcs.len() + ); + + // Channels exist but none are usable (reestablish in progress). + // Return empty actions. We can't forward and we must not decide to splice or + // open a new channel based on stale capacity. The timer retries once usable. + if !channels.is_empty() && channel_capacity_map.is_empty() { + log_info!( + self.logger, + "[LSPS4] calculate_htlc_actions: {} has {} channels but none usable yet \ + - deferring decision", + their_node_id, + channels.len() + ); + return HtlcProcessingActions { + forwards: vec![], + new_channel_needed_msat: None, + splice_needed: None, + channel_count, + }; + } + + struct ComputedHtlc { + htlc: InterceptedHtlc, + amount_to_forward_msat: u64, + skimmed_fee_msat: u64, + } + + // The peer's granted policy, looked up once. Peers with no grant resolve to Standard, so + // the skim matches the historical 2% for everyone the issuer set hasn't waived. + let policy = self + .scid_store + .get_policy(&their_node_id) + .unwrap_or(FeePolicy::Flat(FeeTier::Standard)); + let is_zero_fee = matches!(policy, FeePolicy::Flat(FeeTier::ZeroFee)); + + let mut computed_htlcs: Vec = htlcs + .drain(..) + .map(|htlc| { + let expected_outbound_msat = htlc.expected_outbound_amount_msat(); + if expected_outbound_msat == 0 { + return ComputedHtlc { htlc, amount_to_forward_msat: 0, skimmed_fee_msat: 0 }; + } + + let htlc_id = htlc.id(); + let skimmed_fee_msat = resolve_skim( + &policy, + expected_outbound_msat, + self.config.forwarding_fee_proportional_millionths, + ); + if skimmed_fee_msat == 0 { + if is_zero_fee { + log_info!( + self.logger, + "Zero-fee policy for HTLC {:?}; forwarding the full amount.", + htlc_id + ); + } else { + // A non-zero-fee tier skimmed nothing only because the fee would have + // consumed the entire HTLC; forward it intact rather than break it. + log_error!( + self.logger, + "Skim would have consumed the entire HTLC {:?}; forwarding the full amount.", + htlc_id + ); + } + } + + let amount_to_forward_msat = expected_outbound_msat.saturating_sub(skimmed_fee_msat); + + ComputedHtlc { htlc, amount_to_forward_msat, skimmed_fee_msat } + }) + .collect(); + + let mut forwards = Vec::new(); + + while let Some(computed) = computed_htlcs.pop() { + let required_amount = computed.amount_to_forward_msat; + let mut can_forward = false; + + // Check if any existing channel has sufficient remaining capacity + for (channel_id, remaining_capacity) in channel_capacity_map.iter_mut() { + if *remaining_capacity >= required_amount { + // Plan to forward this HTLC through this channel + forwards.push(HtlcForwardAction { + htlc: computed.htlc, + channel_id: *channel_id, + amount_to_forward_msat: computed.amount_to_forward_msat, + skimmed_fee_msat: computed.skimmed_fee_msat, + }); + + // Update remaining capacity after planning the forward + *remaining_capacity -= required_amount; + can_forward = true; + break; + } + } + + if !can_forward { + // No existing channel has sufficient capacity. + // Calculate total amount needed for remaining HTLCs (including current one) + let total_remaining_amount = computed_htlcs + .iter() + .fold(required_amount, |acc, h| acc.saturating_add(h.amount_to_forward_msat)); + + // Prefer splicing into the largest usable channel over opening a new one. + // Only splice into usable channels. A mid-reestablish channel may + // already have sufficient capacity that just isn't visible yet; + // splice_channel() would also reject if the channel does become + // usable in time. + let splice_candidate = channels + .iter() + .filter(|c| c.is_usable) + .max_by_key(|c| c.channel_value_satoshis); + + if let Some(candidate) = splice_candidate { + return HtlcProcessingActions { + forwards, + new_channel_needed_msat: None, + splice_needed: Some(SpliceAction { + channel_id: candidate.channel_id, + user_channel_id: candidate.user_channel_id, + amt_to_forward_msat: total_remaining_amount, + }), + channel_count, + }; + } + + // No usable channels to splice into - need a new channel. + return HtlcProcessingActions { + forwards, + new_channel_needed_msat: Some(total_remaining_amount), + splice_needed: None, + channel_count, + }; + } + } + + HtlcProcessingActions { + forwards, + new_channel_needed_msat: None, + splice_needed: None, + channel_count, + } + } + + /// Execute the actions calculated by calculate_htlc_actions_for_peer + pub(crate) fn execute_htlc_actions( + &self, actions: HtlcProcessingActions, their_node_id: PublicKey, + ) { + let HtlcProcessingActions { + forwards, + new_channel_needed_msat, + splice_needed, + channel_count, + } = actions; + + // Execute forwards + for forward_action in forwards { + // Re-check channel usability right before forwarding to narrow the + // TOCTOU window. Covers both peer disconnect and disconnect+reconnect + // (where the peer is connected but the channel is still reestablishing). + if !self.has_usable_channel(&their_node_id) { + log_info!( + self.logger, + "[LSPS4] execute_htlc_actions: no usable channel for peer {}, \ + skipping HTLC {:?} (will retry on next timer tick). payment_hash: {}", + their_node_id, + forward_action.htlc.id(), + forward_action.htlc.payment_hash() + ); + continue; + } + + log_info!( + self.logger, + "[LSPS4] execute_htlc_actions: forwarding HTLC {:?} through channel {} to peer {}, amount: {}msat, payment_hash: {}", + forward_action.htlc.id(), + forward_action.channel_id, + their_node_id, + forward_action.amount_to_forward_msat, + forward_action.htlc.payment_hash() + ); + + let fwd_start = Instant::now(); + match self.channel_manager.get_cm().forward_intercepted_htlc( + forward_action.htlc.id(), + &forward_action.channel_id, + forward_action.htlc.next_node_id(), + forward_action.amount_to_forward_msat, + ) { + Ok(()) => { + log_info!( + self.logger, + "[LSPS4] execute_htlc_actions: forward_intercepted_htlc returned Ok for HTLC {:?} \ + (took {}ms). payment_hash: {}. NOTE: Ok does NOT guarantee delivery - channel may still be reestablishing.", + forward_action.htlc.id(), + fwd_start.elapsed().as_millis(), + forward_action.htlc.payment_hash() + ); + }, + Err(ref e) => { + log_info!( + self.logger, + "[LSPS4] execute_htlc_actions: forward returned Err for HTLC {:?}: {:?} \ + (took {}ms). payment_hash: {}", + forward_action.htlc.id(), + e, + fwd_start.elapsed().as_millis(), + forward_action.htlc.payment_hash() + ); + }, + } + + // Always remove from store after forward attempt. On Ok the InterceptId is + // consumed and the HTLC is in-flight — we cannot defer removal because the + // in-flight HTLC reduces outbound capacity, causing the next timer tick to + // see insufficient capacity and emit a spurious OpenChannel. If send_htlc + // later fails internally, LDK re-emits HTLCIntercepted with a fresh + // InterceptId. On Err the InterceptId was already consumed or stale. + match self.htlc_store.remove(&forward_action.htlc.id()) { + Ok(()) => log_info!( + self.logger, + "[LSPS4] execute_htlc_actions: removed HTLC {:?} from store. payment_hash: {}", + forward_action.htlc.id(), + forward_action.htlc.payment_hash() + ), + Err(e) => log_info!( + self.logger, + "[LSPS4] execute_htlc_actions: HTLC {:?} not in store: {}. payment_hash: {}", + forward_action.htlc.id(), + e, + forward_action.htlc.payment_hash() + ), + } + } + + // Handle liquidity actions (splice or new channel) + let needs_liquidity = new_channel_needed_msat.is_some() || splice_needed.is_some(); + if needs_liquidity { + if self.is_liquidity_cooling_down(&their_node_id) { + log_info!( + self.logger, + "[LSPS4] Liquidity action suppressed for peer {} (cooling down)", + their_node_id + ); + return; + } + + // Set cooldown BEFORE emitting to prevent re-entrant emit on next tick. + self.liquidity_cooldown.write().unwrap().insert(their_node_id, Instant::now()); + + let mut event_queue_notifier = self.pending_events.notifier(); + + if let Some(splice_action) = splice_needed { + log_info!( + self.logger, + "[LSPS4] Splicing into channel {} with peer {} for {}msat", + splice_action.channel_id, + their_node_id, + splice_action.amt_to_forward_msat + ); + event_queue_notifier.enqueue(crate::events::LiquidityEvent::LSPS4Service( + LSPS4ServiceEvent::SpliceChannel { + their_network_key: their_node_id, + channel_id: splice_action.channel_id, + user_channel_id: splice_action.user_channel_id, + amt_to_forward_msat: splice_action.amt_to_forward_msat, + channel_count, + }, + )); + } else if let Some(channel_size_msat) = new_channel_needed_msat { + log_info!( + self.logger, + "[LSPS4] Need a new channel with peer {} for {}msat to forward HTLCs", + their_node_id, + channel_size_msat + ); + event_queue_notifier.enqueue(crate::events::LiquidityEvent::LSPS4Service( + LSPS4ServiceEvent::OpenChannel { + their_network_key: their_node_id, + amt_to_forward_msat: channel_size_msat, + channel_count, + }, + )); + } + } + } + + /// Convenience function that calculates and executes HTLC actions in one call + pub(crate) fn process_htlcs_for_peer( + &self, their_node_id: PublicKey, htlcs: Vec, + ) { + let actions = self.calculate_htlc_actions_for_peer(their_node_id, htlcs); + + log_info!(self.logger, "Calculated actions for peer {}: {:?}", their_node_id, actions); + + if actions.is_empty() { + log_debug!(self.logger, "No HTLCs to process for peer {}", their_node_id); + return; + } + + if actions.total_forward_amount() > 0 { + log_debug!( + self.logger, + "Processing {} HTLCs for peer {} totaling {}msat", + actions.forwards.len(), + their_node_id, + actions.total_forward_amount() + ); + } + + self.execute_htlc_actions(actions, their_node_id); + } + + /// Persists the state of the service handler towards the given [`KVStore`] implementation. + /// + /// This will be regularly called by LDK's background processor if necessary and only needs to + /// be called manually if it's not utilized. + /// + /// Note: LSPS4 stores (ScidStore and HTLCStore) persist immediately when operations occur, + /// so this method is primarily for consistency with the interface. It may be used for + /// future cleanup or pruning operations. + pub(crate) fn persist(&self) -> Result<(), lightning::io::Error> { + // LSPS4 stores persist immediately when operations occur, so there's no batched + // persistence needed. This method exists for interface consistency and potential + // future use cases (e.g., cleanup, pruning, etc.). + Ok(()) + } + +} + +impl LSPSProtocolMessageHandler for LSPS4ServiceHandler +where + CM::Target: AChannelManager, + K::Target: KVStore + KVStoreSync, + L::Target: Logger, +{ + type ProtocolMessage = LSPS4Message; + const PROTOCOL_NUMBER: Option = Some(4); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS4Message::Request(request_id, request) => { + let res = match request { + LSPS4Request::RegisterNode(params) => { + self.handle_register_node_request(request_id, counterparty_node_id, params) + }, + }; + res + }, + _ => { + debug_assert!( + false, + "Service handler received LSPS4 response message. This should never happen." + ); + Err(LightningError { err: format!("Service handler received LSPS4 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_htlc_processing_actions_is_empty() { + let empty = HtlcProcessingActions { + forwards: vec![], + new_channel_needed_msat: None, + splice_needed: None, + channel_count: 0, + }; + assert!(empty.is_empty()); + + let with_channel = HtlcProcessingActions { + forwards: vec![], + new_channel_needed_msat: Some(100_000), + splice_needed: None, + channel_count: 0, + }; + assert!(!with_channel.is_empty()); + + let with_splice = HtlcProcessingActions { + forwards: vec![], + new_channel_needed_msat: None, + splice_needed: Some(SpliceAction { + channel_id: ChannelId::from_bytes([0; 32]), + user_channel_id: 0, + amt_to_forward_msat: 50_000, + }), + channel_count: 1, + }; + assert!(!with_splice.is_empty()); + } + + #[test] + fn test_needs_liquidity_action() { + let no_action = HtlcProcessingActions { + forwards: vec![], + new_channel_needed_msat: None, + splice_needed: None, + channel_count: 0, + }; + assert!(!no_action.needs_liquidity_action()); + + let needs_channel = HtlcProcessingActions { + forwards: vec![], + new_channel_needed_msat: Some(100_000), + splice_needed: None, + channel_count: 0, + }; + assert!(needs_channel.needs_liquidity_action()); + + let needs_splice = HtlcProcessingActions { + forwards: vec![], + new_channel_needed_msat: None, + splice_needed: Some(SpliceAction { + channel_id: ChannelId::from_bytes([0; 32]), + user_channel_id: 0, + amt_to_forward_msat: 50_000, + }), + channel_count: 1, + }; + assert!(needs_splice.needs_liquidity_action()); + } + + #[test] + fn test_cooldown_auto_expires() { + // Verify the cooldown constant is less than HTLC expiry + assert!(LIQUIDITY_COOLDOWN_SECS < HTLC_EXPIRY_THRESHOLD_SECS); + } + + #[test] + fn test_splice_action_fields() { + let action = SpliceAction { + channel_id: ChannelId::from_bytes([1; 32]), + user_channel_id: 42, + amt_to_forward_msat: 1_000_000, + }; + assert_eq!(action.channel_id, ChannelId::from_bytes([1; 32])); + assert_eq!(action.user_channel_id, 42); + assert_eq!(action.amt_to_forward_msat, 1_000_000); + } +} \ No newline at end of file diff --git a/lightning-liquidity/src/lsps4/utils.rs b/lightning-liquidity/src/lsps4/utils.rs new file mode 100644 index 00000000000..25b3cf24508 --- /dev/null +++ b/lightning-liquidity/src/lsps4/utils.rs @@ -0,0 +1,47 @@ +//! Utilities for implementing the LSPS4 standard. +use std::fmt::Write; + +/// Computes the forward fee given a payment size and the fee parameters. +/// +/// Returns [`Option::None`] when the computation overflows. +pub fn compute_forward_fee(payment_size_msat: u64, fee_proportional: u64) -> Option { + payment_size_msat + .checked_mul(fee_proportional) + .and_then(|f| f.checked_add(999999)) + .and_then(|f| f.checked_div(1000000)) +} + +/// Convert a byte slice to a hex string. +#[inline] +pub fn to_string(value: &[u8]) -> String { + let mut res = String::with_capacity(2 * value.len()); + for v in value { + write!(&mut res, "{:02x}", v).expect("Unable to write"); + } + res +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000; + + fn arb_forward_fee_params() -> impl Strategy { + (0u64..MAX_VALUE_MSAT, 0u64..MAX_VALUE_MSAT) + } + + proptest! { + #[test] + fn test_compute_forward_fee((payment_size_msat, fee_proportional) in arb_forward_fee_params()) { + if let Some(res) = compute_forward_fee(payment_size_msat, fee_proportional) { + assert_eq!(res as f32, (payment_size_msat as f32 * fee_proportional as f32)); + } else { + // Check we actually overflowed. + let max_value = u64::MAX as u128; + assert!((payment_size_msat as u128 * fee_proportional as u128) > max_value); + } + } + } +} diff --git a/lightning-liquidity/src/lsps5/service.rs b/lightning-liquidity/src/lsps5/service.rs index 8b1f0ec70cb..85e308fea80 100644 --- a/lightning-liquidity/src/lsps5/service.rs +++ b/lightning-liquidity/src/lsps5/service.rs @@ -244,7 +244,7 @@ where }) } - pub(crate) async fn persist(&self) -> Result<(), lightning::io::Error> { + pub(crate) async fn persist(&self) -> Result { // TODO: We should eventually persist in parallel, however, when we do, we probably want to // introduce some batching to upper-bound the number of requests inflight at any given // time. @@ -252,80 +252,98 @@ where if self.persistence_in_flight.fetch_add(1, Ordering::AcqRel) > 0 { // If we're not the first event processor to get here, just return early, the increment // we just did will be treated as "go around again" at the end. - return Ok(()); + return Ok(false); } + let mut did_persist = false; + loop { - let mut need_remove = Vec::new(); - let mut need_persist = Vec::new(); + match self.do_persist().await { + Ok(pass_did_persist) => did_persist |= pass_did_persist, + Err(e) => { + self.persistence_in_flight.store(0, Ordering::Release); + return Err(e); + }, + } - self.check_prune_stale_webhooks(&mut self.per_peer_state.write().unwrap()); - { - let outer_state_lock = self.per_peer_state.read().unwrap(); - - for (client_id, peer_state) in outer_state_lock.iter() { - let is_prunable = peer_state.is_prunable(); - let has_open_channel = self.client_has_open_channel(client_id); - if is_prunable && !has_open_channel { - need_remove.push(*client_id); - } else if peer_state.needs_persist { - need_persist.push(*client_id); - } - } + if self.persistence_in_flight.fetch_sub(1, Ordering::AcqRel) != 1 { + // If another thread incremented the state while we were running we should go + // around again, but only once. + self.persistence_in_flight.store(1, Ordering::Release); + continue; } + break; + } - for client_id in need_persist.into_iter() { - debug_assert!(!need_remove.contains(&client_id)); - self.persist_peer_state(client_id).await?; + Ok(did_persist) + } + + async fn do_persist(&self) -> Result { + let mut did_persist = false; + let mut need_remove = Vec::new(); + let mut need_persist = Vec::new(); + + self.check_prune_stale_webhooks(&mut self.per_peer_state.write().unwrap()); + { + let outer_state_lock = self.per_peer_state.read().unwrap(); + + for (client_id, peer_state) in outer_state_lock.iter() { + let is_prunable = peer_state.is_prunable(); + let has_open_channel = self.client_has_open_channel(client_id); + if is_prunable && !has_open_channel { + need_remove.push(*client_id); + } else if peer_state.needs_persist { + need_persist.push(*client_id); + } } + } + + for client_id in need_persist.into_iter() { + debug_assert!(!need_remove.contains(&client_id)); + self.persist_peer_state(client_id).await?; + did_persist = true; + } - for client_id in need_remove { - let mut future_opt = None; - { - // We need to take the `per_peer_state` write lock to remove an entry, but also - // have to hold it until after the `remove` call returns (but not through - // future completion) to ensure that writes for the peer's state are - // well-ordered with other `persist_peer_state` calls even across the removal - // itself. - let mut per_peer_state = self.per_peer_state.write().unwrap(); - if let Entry::Occupied(mut entry) = per_peer_state.entry(client_id) { - let state = entry.get_mut(); - if state.is_prunable() && !self.client_has_open_channel(&client_id) { - entry.remove(); - let key = client_id.to_string(); - future_opt = Some(self.kv_store.remove( - LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, - LSPS5_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, - &key, - true, - )); - } else { - // If the peer was re-added, force a re-persist of the current state. - state.needs_persist = true; - } + for client_id in need_remove { + let mut future_opt = None; + { + // We need to take the `per_peer_state` write lock to remove an entry, but also + // have to hold it until after the `remove` call returns (but not through + // future completion) to ensure that writes for the peer's state are + // well-ordered with other `persist_peer_state` calls even across the removal + // itself. + let mut per_peer_state = self.per_peer_state.write().unwrap(); + if let Entry::Occupied(mut entry) = per_peer_state.entry(client_id) { + let state = entry.get_mut(); + if state.is_prunable() && !self.client_has_open_channel(&client_id) { + entry.remove(); + let key = client_id.to_string(); + future_opt = Some(self.kv_store.remove( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS5_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &key, + true, + )); } else { - // This should never happen, we can only have one `persist` call - // in-progress at once and map entries are only removed by it. - debug_assert!(false); + // If the peer was re-added, force a re-persist of the current state. + state.needs_persist = true; } - } - if let Some(future) = future_opt { - future.await?; } else { - self.persist_peer_state(client_id).await?; + // This should never happen, we can only have one `persist` call + // in-progress at once and map entries are only removed by it. + debug_assert!(false); } } - - if self.persistence_in_flight.fetch_sub(1, Ordering::AcqRel) != 1 { - // If another thread incremented the state while we were running we should go - // around again, but only once. - self.persistence_in_flight.store(1, Ordering::Release); - continue; + if let Some(future) = future_opt { + future.await?; + did_persist = true; + } else { + self.persist_peer_state(client_id).await?; + did_persist = true; } - break; } - Ok(()) + Ok(did_persist) } fn check_prune_stale_webhooks<'a>( diff --git a/lightning-liquidity/src/lsps5/validator.rs b/lightning-liquidity/src/lsps5/validator.rs index 8063ea743b7..50a36ea1d2f 100644 --- a/lightning-liquidity/src/lsps5/validator.rs +++ b/lightning-liquidity/src/lsps5/validator.rs @@ -11,7 +11,6 @@ use super::msgs::LSPS5ClientError; -use crate::alloc::string::ToString; use crate::lsps0::ser::LSPSDateTime; use crate::lsps5::msgs::WebhookNotification; use crate::sync::Mutex; @@ -91,14 +90,17 @@ impl LSPS5Validator { } fn check_for_replay_attack(&self, signature: &str) -> Result<(), LSPS5ClientError> { + // zbase32 decoding accepts case aliases, so canonicalize the cache key + // to match verification semantics without decoding the signature again. + let signature = signature.to_ascii_lowercase(); let mut signatures = self.recent_signatures.lock().unwrap(); - if signatures.contains(&signature.to_string()) { + if signatures.contains(&signature) { return Err(LSPS5ClientError::ReplayAttack); } if signatures.len() == MAX_RECENT_SIGNATURES { signatures.pop_back(); } - signatures.push_front(signature.to_string()); + signatures.push_front(signature); Ok(()) } } diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 5d95d32d540..23c7d772a06 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -36,6 +36,9 @@ use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler}; use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler}; use crate::lsps2::msgs::LSPS2Message; use crate::lsps2::service::{LSPS2ServiceConfig, LSPS2ServiceHandler, LSPS2ServiceHandlerSync}; +use crate::lsps4::client::{LSPS4ClientConfig, LSPS4ClientHandler}; +use crate::lsps4::msgs::LSPS4Message; +use crate::lsps4::service::{LSPS4ServiceConfig, LSPS4ServiceHandler}; use crate::prelude::{new_hash_map, new_hash_set, HashMap, HashSet}; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils::async_poll::dummy_waker; @@ -50,7 +53,7 @@ use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; use lightning::sign::{EntropySource, NodeSigner}; -use lightning::util::logger::Level; +use lightning::util::logger::{Level, Logger}; use lightning::util::persist::{KVStore, KVStoreSync, KVStoreSyncWrapper}; use lightning::util::ser::{LengthLimitedRead, LengthReadable}; use lightning::util::wakers::{Future, Notifier}; @@ -77,6 +80,8 @@ pub struct LiquidityServiceConfig { /// Optional server-side configuration for JIT channels /// should you want to support them. pub lsps2_service_config: Option, + /// Optional server-side configuration for LSPS4 channels. + pub lsps4_service_config: Option, /// Optional server-side configuration for LSPS5 webhook service. pub lsps5_service_config: Option, /// Controls whether the liquidity service should be advertised via setting the feature bit in @@ -94,6 +99,8 @@ pub struct LiquidityClientConfig { pub lsps1_client_config: Option, /// Optional client-side configuration for JIT channels. pub lsps2_client_config: Option, + /// Optional client-side configuration for LSPS4 channels. + pub lsps4_client_config: Option, /// Optional client-side configuration for LSPS5 webhook service. pub lsps5_client_config: Option, } @@ -120,7 +127,7 @@ pub trait ALiquidityManager { /// A type that may be dereferenced to [`Self::Filter`]. type C: Deref + Clone; /// A type implementing [`KVStore`]. - type KVStore: KVStore + ?Sized; + type KVStore: KVStore + KVStoreSync + ?Sized; /// A type that may be dereferenced to [`Self::KVStore`]. type K: Deref + Clone; /// A type implementing [`TimeProvider`]. @@ -131,10 +138,14 @@ pub trait ALiquidityManager { type BroadcasterInterface: BroadcasterInterface + ?Sized; /// A type that may be dereferenced to [`Self::BroadcasterInterface`]. type T: Deref + Clone; + /// A type implementing [`Logger`]. + type Logger: Logger + ?Sized; + /// A type that may be dereferenced to [`Self::Logger`]. + type L: Deref + Clone; /// Returns a reference to the actual [`LiquidityManager`] object. fn get_lm( &self, - ) -> &LiquidityManager; + ) -> &LiquidityManager; } impl< @@ -145,15 +156,17 @@ impl< K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > ALiquidityManager for LiquidityManager + L: Deref + Clone, + > ALiquidityManager for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { type EntropySource = ES::Target; type ES = ES; @@ -169,7 +182,9 @@ where type TP = TP; type BroadcasterInterface = T::Target; type T = T; - fn get_lm(&self) -> &LiquidityManager { + type Logger = L::Target; + type L = L; + fn get_lm(&self) -> &LiquidityManager { self } } @@ -207,6 +222,10 @@ pub trait ALiquidityManagerSync { type BroadcasterInterface: BroadcasterInterface + ?Sized; /// A type that may be dereferenced to [`Self::BroadcasterInterface`]. type T: Deref + Clone; + /// A type implementing [`Logger`]. + type Logger: Logger + ?Sized; + /// A type that may be dereferenced to [`Self::Logger`]. + type L: Deref + Clone; /// Returns the inner async [`LiquidityManager`] for testing purposes. #[cfg(any(test, feature = "_test_utils"))] fn get_lm_async( @@ -219,11 +238,12 @@ pub trait ALiquidityManagerSync { KVStoreSyncWrapper, Self::TP, Self::T, + Self::L, >; /// Returns a reference to the actual [`LiquidityManager`] object. fn get_lm( &self, - ) -> &LiquidityManagerSync; + ) -> &LiquidityManagerSync; } impl< @@ -234,7 +254,8 @@ impl< KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > ALiquidityManagerSync for LiquidityManagerSync + L: Deref + Clone, + > ALiquidityManagerSync for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -243,6 +264,7 @@ where KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { type EntropySource = ES::Target; type ES = ES; @@ -258,6 +280,8 @@ where type TP = TP; type BroadcasterInterface = T::Target; type T = T; + type Logger = L::Target; + type L = L; /// Returns the inner async [`LiquidityManager`] for testing purposes. #[cfg(any(test, feature = "_test_utils"))] fn get_lm_async( @@ -270,10 +294,11 @@ where KVStoreSyncWrapper, Self::TP, Self::T, + Self::L, > { &self.inner } - fn get_lm(&self) -> &LiquidityManagerSync { + fn get_lm(&self) -> &LiquidityManagerSync { self } } @@ -305,14 +330,16 @@ pub struct LiquidityManager< K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, + L: Deref + Clone > where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { pending_messages: Arc, pending_events: Arc>, @@ -326,6 +353,8 @@ pub struct LiquidityManager< lsps1_client_handler: Option>, lsps2_service_handler: Option>, lsps2_client_handler: Option>, + lsps4_client_handler: Option>, + lsps4_service_handler: Option>, lsps5_service_handler: Option>, lsps5_client_handler: Option>, service_config: Option, @@ -343,23 +372,28 @@ impl< C: Deref + Clone, K: Deref + Clone, T: Deref + Clone, - > LiquidityManager + L: Deref + Clone, + > LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, T::Target: BroadcasterInterface, + L::Target: Logger, { /// Constructor for the [`LiquidityManager`] using the default system clock /// /// Will read persisted service states from the given [`KVStore`]. + /// + /// `logger` is required when LSPS4 service is configured, otherwise it can be `None`. pub async fn new( entropy_source: ES, node_signer: NS, channel_manager: CM, chain_source: Option, chain_params: Option, kv_store: K, transaction_broadcaster: T, service_config: Option, client_config: Option, + logger: L, ) -> Result { Self::new_with_custom_time_provider( entropy_source, @@ -372,6 +406,7 @@ where service_config, client_config, DefaultTimeProvider, + logger, ) .await } @@ -385,15 +420,17 @@ impl< K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > LiquidityManager + L: Deref + Clone, + > LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { /// Constructor for the [`LiquidityManager`] with a custom time provider. /// @@ -403,11 +440,14 @@ where /// available. /// Sets up the required protocol message handlers based on the given /// [`LiquidityClientConfig`] and [`LiquidityServiceConfig`]. + /// + /// `logger` is required when LSPS4 service is configured, otherwise it can be `None`. pub async fn new_with_custom_time_provider( entropy_source: ES, node_signer: NS, channel_manager: CM, transaction_broadcaster: T, chain_source: Option, chain_params: Option, kv_store: K, service_config: Option, client_config: Option, time_provider: TP, + logger: L, ) -> Result { let pending_msgs_or_needs_persist_notifier = Arc::new(Notifier::new()); let pending_messages = @@ -458,6 +498,17 @@ where None }; + let lsps4_client_handler = client_config.as_ref().and_then(|config| { + config.lsps4_client_config.map(|config| { + LSPS4ClientHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + config.clone(), + ) + }) + }); + let lsps5_client_handler = client_config.as_ref().and_then(|config| { config.lsps5_client_config.as_ref().map(|config| { LSPS5ClientHandler::new( @@ -495,6 +546,29 @@ where None }; + let lsps4_service_handler = if let Some(service_config) = service_config.as_ref() { + if let Some(lsps4_service_config) = service_config.lsps4_service_config.as_ref() { + if let Some(number) = + as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER + { + supported_protocols.push(number); + } + + Some(LSPS4ServiceHandler::new( + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + lsps4_service_config.clone(), + kv_store.clone(), + logger.clone(), + ).await?) + } else { + None + } + } else { + None + }; + let lsps1_client_handler = client_config.as_ref().and_then(|config| { config.lsps1_client_config.as_ref().map(|config| { LSPS1ClientHandler::new( @@ -549,6 +623,8 @@ where lsps1_service_handler, lsps2_client_handler, lsps2_service_handler, + lsps4_client_handler, + lsps4_service_handler, lsps5_client_handler, lsps5_service_handler, service_config, @@ -599,6 +675,23 @@ where self.lsps2_service_handler.as_ref() } + /// Returns a reference to the LSPS4 client-side handler. + /// + /// The returned hendler allows to initiate the LSPS4 client-side flow. That is, it allows to + /// retrieve all necessary data to create 'just-in-time' invoices that, when paid, will have + /// the configured LSP open a 'just-in-time' channel. + pub fn lsps4_client_handler(&self) -> Option<&LSPS4ClientHandler> { + self.lsps4_client_handler.as_ref() + } + + /// Returns a reference to the LSPS4 server-side handler. + /// + /// The returned handler allows to initiate the LSPS4 service-side flow. + pub fn lsps4_service_handler(&self) -> Option<&LSPS4ServiceHandler> { + self.lsps4_service_handler.as_ref() + } + + /// Returns a reference to the LSPS5 client-side handler. /// /// The returned handler allows to initiate the LSPS5 client-side flow. That is, it allows to @@ -670,23 +763,31 @@ where self.pending_events.get_and_clear_pending_events() } - /// Persists the state of the service handlers towards the given [`KVStore`] implementation. + /// Persists the state of the service handlers towards the given [`KVStore`] implementation if + /// needed. + /// + /// Returns `true` if it persisted service handler data. /// /// This will be regularly called by LDK's background processor if necessary and only needs to /// be called manually if it's not utilized. - pub async fn persist(&self) -> Result<(), lightning::io::Error> { + pub async fn persist(&self) -> Result { // TODO: We should eventually persist in parallel. - self.pending_events.persist().await?; + let mut did_persist = false; + did_persist |= self.pending_events.persist().await?; if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() { - lsps2_service_handler.persist().await?; + did_persist |= lsps2_service_handler.persist().await?; + } + + if let Some(lsps4_service_handler) = self.lsps4_service_handler.as_ref() { + lsps4_service_handler.persist()?; } if let Some(lsps5_service_handler) = self.lsps5_service_handler.as_ref() { - lsps5_service_handler.persist().await?; + did_persist |= lsps5_service_handler.persist().await?; } - Ok(()) + Ok(did_persist) } fn handle_lsps_message( @@ -752,6 +853,26 @@ where }, } }, + LSPSMessage::LSPS4(msg @ LSPS4Message::Response(..)) => { + match &self.lsps4_client_handler { + Some(lsps4_client_handler) => { + lsps4_client_handler.handle_message(msg, sender_node_id)?; + }, + None => { + return Err(LightningError { err: format!("Received LSPS4 response message without LSPS4 client handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + }, + LSPSMessage::LSPS4(msg @ LSPS4Message::Request(..)) => { + match &self.lsps4_service_handler { + Some(lsps4_service_handler) => { + lsps4_service_handler.handle_message(msg, sender_node_id)?; + }, + None => { + return Err(LightningError { err: format!("Received LSPS4 request message without LSPS4 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + }, LSPSMessage::LSPS5(msg @ LSPS5Message::Response(..)) => { match &self.lsps5_client_handler { Some(lsps5_client_handler) => { @@ -808,15 +929,17 @@ impl< K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageReader for LiquidityManager + L: Deref + Clone + > CustomMessageReader for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { type CustomMessage = RawLSPSMessage; @@ -840,15 +963,17 @@ impl< K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageHandler for LiquidityManager + L: Deref + Clone + > CustomMessageHandler for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { fn handle_custom_message( &self, msg: Self::CustomMessage, sender_node_id: PublicKey, @@ -950,6 +1075,10 @@ where lsps2_service_handler.peer_disconnected(counterparty_node_id); } + if let Some(lsps4_service_handler) = self.lsps4_service_handler.as_ref() { + lsps4_service_handler.peer_disconnected(&counterparty_node_id); + } + if let Some(lsps5_service_handler) = self.lsps5_service_handler.as_ref() { lsps5_service_handler.peer_disconnected(&counterparty_node_id); } @@ -958,6 +1087,10 @@ where &self, counterparty_node_id: bitcoin::secp256k1::PublicKey, _: &lightning::ln::msgs::Init, _: bool, ) -> Result<(), ()> { + if let Some(lsps4_service_handler) = self.lsps4_service_handler.as_ref() { + lsps4_service_handler.peer_connected(counterparty_node_id); + } + if let Some(lsps5_service_handler) = self.lsps5_service_handler.as_ref() { lsps5_service_handler.peer_connected(&counterparty_node_id); } @@ -974,15 +1107,17 @@ impl< K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > Listen for LiquidityManager + L: Deref + Clone + > Listen for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { fn filtered_block_connected( &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, @@ -1020,15 +1155,17 @@ impl< K: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > Confirm for LiquidityManager + L: Deref + Clone + > Confirm for LiquidityManager where ES::Target: EntropySource, NS::Target: NodeSigner, CM::Target: AChannelManager, C::Target: Filter, - K::Target: KVStore, + K::Target: KVStore + KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { fn transactions_confirmed( &self, _header: &bitcoin::block::Header, _txdata: &chain::transaction::TransactionData, @@ -1066,6 +1203,7 @@ pub struct LiquidityManagerSync< KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, + L: Deref + Clone, > where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1074,8 +1212,9 @@ pub struct LiquidityManagerSync< KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { - inner: LiquidityManager, TP, T>, + inner: LiquidityManager, TP, T, L>, } #[cfg(feature = "time")] @@ -1086,7 +1225,8 @@ impl< C: Deref + Clone, KS: Deref + Clone, T: Deref + Clone, - > LiquidityManagerSync + L: Deref + Clone + > LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1094,6 +1234,7 @@ where KS::Target: KVStoreSync, C::Target: Filter, T::Target: BroadcasterInterface, + L::Target: Logger, { /// Constructor for the [`LiquidityManagerSync`] using the default system clock /// @@ -1103,19 +1244,21 @@ where chain_params: Option, kv_store_sync: KS, transaction_broadcaster: T, service_config: Option, client_config: Option, + logger: L, ) -> Result { - let kv_store = KVStoreSyncWrapper(kv_store_sync); + let kv_store = KVStoreSyncWrapper(kv_store_sync.clone()); let mut fut = Box::pin(LiquidityManager::new( - entropy_source, - node_signer, - channel_manager, + entropy_source.clone(), + node_signer.clone(), + channel_manager.clone(), chain_source, chain_params, kv_store, transaction_broadcaster, - service_config, + service_config.clone(), client_config, + logger, )); let mut waker = dummy_waker(); @@ -1127,6 +1270,8 @@ where unreachable!("LiquidityManager::new should not be pending in a sync context"); }, }?; + + Ok(Self { inner }) } } @@ -1139,7 +1284,8 @@ impl< KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > LiquidityManagerSync + L: Deref + Clone + > LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1148,6 +1294,7 @@ where KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { /// Constructor for the [`LiquidityManagerSync`] with a custom time provider. /// @@ -1157,19 +1304,21 @@ where chain_params: Option, kv_store_sync: KS, transaction_broadcaster: T, service_config: Option, client_config: Option, time_provider: TP, + logger: L, ) -> Result { - let kv_store = KVStoreSyncWrapper(kv_store_sync); + let kv_store = KVStoreSyncWrapper(kv_store_sync.clone()); let mut fut = Box::pin(LiquidityManager::new_with_custom_time_provider( - entropy_source, - node_signer, - channel_manager, + entropy_source.clone(), + node_signer.clone(), + channel_manager.clone(), transaction_broadcaster, chain_source, chain_params, kv_store, - service_config, + service_config.clone(), client_config, time_provider, + logger, )); let mut waker = dummy_waker(); @@ -1181,6 +1330,8 @@ where unreachable!("LiquidityManager::new should not be pending in a sync context"); }, }?; + + Ok(Self { inner }) } @@ -1231,6 +1382,22 @@ where self.inner.lsps2_service_handler.as_ref().map(|r| LSPS2ServiceHandlerSync::from_inner(r)) } + /// Returns a reference to the LSPS4 client-side handler. + /// + /// The returned hendler allows to initiate the LSPS4 client-side flow. That is, it allows to + /// retrieve all necessary data to create 'just-in-time' invoices that, when paid, will have + /// the configured LSP open a 'just-in-time' channel. + pub fn lsps4_client_handler(&self) -> Option<&LSPS4ClientHandler>> { + self.inner.lsps4_client_handler() + } + + /// Returns a reference to the LSPS4 server-side handler. + /// + /// Wraps [`LiquidityManager::lsps4_service_handler`]. + pub fn lsps4_service_handler(&self) -> Option<&LSPS4ServiceHandler, L>> { + self.inner.lsps4_service_handler() + } + /// Returns a reference to the LSPS5 client-side handler. /// /// Wraps [`LiquidityManager::lsps5_client_handler`]. @@ -1285,8 +1452,10 @@ where /// Persists the state of the service handlers towards the given [`KVStoreSync`] implementation. /// + /// Returns `true` if it persisted service handler data. + /// /// Wraps [`LiquidityManager::persist`]. - pub fn persist(&self) -> Result<(), lightning::io::Error> { + pub fn persist(&self) -> Result { let mut waker = dummy_waker(); let mut ctx = task::Context::from_waker(&mut waker); match Box::pin(self.inner.persist()).as_mut().poll(&mut ctx) { @@ -1307,7 +1476,8 @@ impl< KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageReader for LiquidityManagerSync + L: Deref + Clone, + > CustomMessageReader for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1316,6 +1486,7 @@ where KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { type CustomMessage = RawLSPSMessage; @@ -1334,7 +1505,8 @@ impl< KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > CustomMessageHandler for LiquidityManagerSync + L: Deref + Clone, + > CustomMessageHandler for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1343,6 +1515,7 @@ where KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { fn handle_custom_message( &self, msg: Self::CustomMessage, sender_node_id: PublicKey, @@ -1381,7 +1554,8 @@ impl< KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > Listen for LiquidityManagerSync + L: Deref + Clone, + > Listen for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1390,6 +1564,7 @@ where KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { fn filtered_block_connected( &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, @@ -1411,7 +1586,8 @@ impl< KS: Deref + Clone, TP: Deref + Clone, T: Deref + Clone, - > Confirm for LiquidityManagerSync + L: Deref + Clone, + > Confirm for LiquidityManagerSync where ES::Target: EntropySource, NS::Target: NodeSigner, @@ -1420,6 +1596,7 @@ where KS::Target: KVStoreSync, TP::Target: TimeProvider, T::Target: BroadcasterInterface, + L::Target: Logger, { fn transactions_confirmed( &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index dea987527ad..08305d236a2 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -6,7 +6,7 @@ use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, Liquidity use lightning::chain::{BestBlock, Filter}; use lightning::ln::channelmanager::ChainParameters; use lightning::ln::functional_test_utils::{Node, TestChannelManager}; -use lightning::util::test_utils::{TestBroadcaster, TestKeysInterface, TestStore}; +use lightning::util::test_utils::{TestBroadcaster, TestKeysInterface, TestLogger, TestStore}; use bitcoin::Network; @@ -47,6 +47,7 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( Some(service_config), None, Arc::clone(&time_provider), + service_inner.logger, ) .unwrap(); @@ -61,6 +62,7 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( None, Some(client_config), time_provider, + client_inner.logger, ) .unwrap(); @@ -141,6 +143,7 @@ pub(crate) struct LiquidityNode<'a, 'b, 'c> { Arc, Arc, &'c TestBroadcaster, + &'c TestLogger, >, } @@ -155,6 +158,7 @@ impl<'a, 'b, 'c> LiquidityNode<'a, 'b, 'c> { Arc, Arc, &'c TestBroadcaster, + &'c TestLogger, >, ) -> Self { Self { inner: node, liquidity_manager } diff --git a/lightning-liquidity/tests/lsps0_integration_tests.rs b/lightning-liquidity/tests/lsps0_integration_tests.rs index 423d49785f2..ff41a140902 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -40,6 +40,7 @@ fn list_protocols_integration_test() { #[cfg(lsps1_service)] lsps1_service_config: Some(lsps1_service_config), lsps2_service_config: Some(lsps2_service_config), + lsps4_service_config: None, lsps5_service_config: Some(lsps5_service_config), advertise_service: true, }; @@ -54,6 +55,7 @@ fn list_protocols_integration_test() { #[cfg(not(lsps1_service))] lsps1_client_config: None, lsps2_client_config: Some(lsps2_client_config), + lsps4_client_config: None, lsps5_client_config: Some(lsps5_client_config), }; diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 82f93b5990c..da288330fb9 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -8,7 +8,7 @@ use common::{ }; use lightning::check_added_monitors; -use lightning::events::{ClosureReason, Event}; +use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType}; use lightning::get_event_msg; use lightning::ln::channelmanager::PaymentId; use lightning::ln::channelmanager::Retry; @@ -73,6 +73,7 @@ fn build_lsps2_configs() -> ([u8; 32], LiquidityServiceConfig, LiquidityClientCo #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(lsps2_service_config), + lsps4_service_config: None, lsps5_service_config: None, advertise_service: true, }; @@ -81,6 +82,7 @@ fn build_lsps2_configs() -> ([u8; 32], LiquidityServiceConfig, LiquidityClientCo let client_config = LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config: Some(lsps2_client_config), + lsps4_client_config: None, lsps5_client_config: None, }; @@ -466,6 +468,126 @@ fn channel_open_failed() { }; } +#[test] +fn channel_open_failed_releases_intercepted_htlcs() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut service_node_config = test_default_channel_config(); + service_node_config.accept_intercept_htlcs = true; + + let mut client_node_config = test_default_channel_config(); + client_node_config.channel_config.accept_underpaying_htlcs = true; + + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(service_node_config), Some(client_node_config), None], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let (lsps_nodes, promise_secret) = setup_test_lsps2_nodes_with_payer(nodes); + let LSPSNodesWithPayer { ref service_node, ref client_node, ref payer_node } = lsps_nodes; + + let payer_node_id = payer_node.node.get_our_node_id(); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + create_chan_between_nodes_with_value(&payer_node, &service_node.inner, 2_000_000, 100_000); + + let intercept_scid = service_node.node.get_intercept_scid(); + let user_channel_id = 42u128; + let cltv_expiry_delta: u32 = 144; + let payment_size_msat = Some(1_000_000); + let fee_base_msat: u64 = 1_000; + + execute_lsps2_dance( + &lsps_nodes, + intercept_scid, + user_channel_id, + cltv_expiry_delta, + promise_secret, + payment_size_msat, + fee_base_msat, + ); + + let invoice = create_jit_invoice( + &client_node, + service_node_id, + intercept_scid, + cltv_expiry_delta, + payment_size_msat, + "channel-open-failed-cleanup", + 3600, + ) + .unwrap(); + + payer_node + .node + .pay_for_bolt11_invoice( + &invoice, + PaymentId(invoice.payment_hash().to_byte_array()), + None, + Default::default(), + Retry::Attempts(0), + ) + .unwrap(); + + check_added_monitors!(payer_node, 1); + let events = payer_node.node.get_and_clear_pending_msg_events(); + let ev = SendEvent::from_event(events[0].clone()); + service_node.inner.node.handle_update_add_htlc(payer_node_id, &ev.msgs[0]); + do_commitment_signed_dance(&service_node.inner, &payer_node, &ev.commitment_msg, false, true); + service_node.inner.node.process_pending_htlc_forwards(); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let intercept_id = match &events[0] { + Event::HTLCIntercepted { + intercept_id, + requested_next_hop_scid, + payment_hash, + expected_outbound_amount_msat, + .. + } => { + assert_eq!(*requested_next_hop_scid, intercept_scid); + service_handler + .htlc_intercepted( + *requested_next_hop_scid, + *intercept_id, + *expected_outbound_amount_msat, + *payment_hash, + ) + .unwrap(); + *intercept_id + }, + other => panic!("Expected HTLCIntercepted, got {:?}", other), + }; + + match service_node.liquidity_manager.next_event().unwrap() { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { .. }) => {}, + other => panic!("Unexpected event: {:?}", other), + }; + + service_handler.channel_open_failed(&client_node_id, user_channel_id).unwrap(); + + let res = service_node.inner.node.fail_intercepted_htlc(intercept_id); + assert!( + res.is_err(), + "channel_open_failed must release the intercepted HTLC via fail_intercepted_htlc, but the entry is still pending: {:?}", + res, + ); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::HTLCHandlingFailed { + failure_type: HTLCHandlingFailureType::InvalidForward { requested_forward_scid }, + .. + } => assert_eq!(*requested_forward_scid, intercept_scid), + other => panic!("Expected HTLCHandlingFailed, got {:?}", other), + } +} + #[test] fn channel_open_failed_nonexistent_channel() { let chanmon_cfgs = create_chanmon_cfgs(2); @@ -576,6 +698,126 @@ fn channel_open_abandoned() { assert!(result.is_err()); } +#[test] +fn channel_open_abandoned_releases_intercepted_htlcs() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut service_node_config = test_default_channel_config(); + service_node_config.accept_intercept_htlcs = true; + + let mut client_node_config = test_default_channel_config(); + client_node_config.channel_config.accept_underpaying_htlcs = true; + + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(service_node_config), Some(client_node_config), None], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let (lsps_nodes, promise_secret) = setup_test_lsps2_nodes_with_payer(nodes); + let LSPSNodesWithPayer { ref service_node, ref client_node, ref payer_node } = lsps_nodes; + + let payer_node_id = payer_node.node.get_our_node_id(); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + create_chan_between_nodes_with_value(&payer_node, &service_node.inner, 2_000_000, 100_000); + + let intercept_scid = service_node.node.get_intercept_scid(); + let user_channel_id = 42u128; + let cltv_expiry_delta: u32 = 144; + let payment_size_msat = Some(1_000_000); + let fee_base_msat: u64 = 1_000; + + execute_lsps2_dance( + &lsps_nodes, + intercept_scid, + user_channel_id, + cltv_expiry_delta, + promise_secret, + payment_size_msat, + fee_base_msat, + ); + + let invoice = create_jit_invoice( + &client_node, + service_node_id, + intercept_scid, + cltv_expiry_delta, + payment_size_msat, + "channel-open-abandoned-cleanup", + 3600, + ) + .unwrap(); + + payer_node + .node + .pay_for_bolt11_invoice( + &invoice, + PaymentId(invoice.payment_hash().to_byte_array()), + None, + Default::default(), + Retry::Attempts(0), + ) + .unwrap(); + + check_added_monitors!(&payer_node, 1); + let events = payer_node.node.get_and_clear_pending_msg_events(); + let ev = SendEvent::from_event(events[0].clone()); + service_node.inner.node.handle_update_add_htlc(payer_node_id, &ev.msgs[0]); + do_commitment_signed_dance(&service_node.inner, &payer_node, &ev.commitment_msg, false, true); + service_node.inner.node.process_pending_htlc_forwards(); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let intercept_id = match &events[0] { + Event::HTLCIntercepted { + intercept_id, + requested_next_hop_scid, + payment_hash, + expected_outbound_amount_msat, + .. + } => { + assert_eq!(*requested_next_hop_scid, intercept_scid); + service_handler + .htlc_intercepted( + *requested_next_hop_scid, + *intercept_id, + *expected_outbound_amount_msat, + *payment_hash, + ) + .unwrap(); + *intercept_id + }, + other => panic!("Expected HTLCIntercepted, got {:?}", other), + }; + + match service_node.liquidity_manager.next_event().unwrap() { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { .. }) => {}, + other => panic!("Unexpected event: {:?}", other), + }; + + service_handler.channel_open_abandoned(&client_node_id, user_channel_id).unwrap(); + + let res = service_node.inner.node.fail_intercepted_htlc(intercept_id); + assert!( + res.is_err(), + "channel_open_abandoned must release the intercepted HTLC via fail_intercepted_htlc, but the entry is still pending: {:?}", + res, + ); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::HTLCHandlingFailed { + failure_type: HTLCHandlingFailureType::InvalidForward { requested_forward_scid }, + .. + } => assert_eq!(*requested_forward_scid, intercept_scid), + other => panic!("Expected HTLCHandlingFailed, got {:?}", other), + } +} + #[test] fn channel_open_abandoned_nonexistent_channel() { let chanmon_cfgs = create_chanmon_cfgs(2); @@ -958,6 +1200,7 @@ fn lsps2_service_handler_persistence_across_restarts() { #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(LSPS2ServiceConfig { promise_secret }), + lsps4_service_config: None, lsps5_service_config: None, advertise_service: true, }; @@ -1102,6 +1345,7 @@ fn lsps2_service_handler_persistence_across_restarts() { Some(service_config), None, time_provider, + nodes_restart[0].logger, ) .unwrap(); diff --git a/lightning-liquidity/tests/lsps5_integration_tests.rs b/lightning-liquidity/tests/lsps5_integration_tests.rs index 80707a60774..22b63cd0ee7 100644 --- a/lightning-liquidity/tests/lsps5_integration_tests.rs +++ b/lightning-liquidity/tests/lsps5_integration_tests.rs @@ -59,6 +59,7 @@ pub(crate) fn lsps5_test_setup_with_kv_stores<'a, 'b, 'c>( #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: None, + lsps4_service_config: None, lsps5_service_config: Some(lsps5_service_config), advertise_service: true, }; @@ -68,6 +69,7 @@ pub(crate) fn lsps5_test_setup_with_kv_stores<'a, 'b, 'c>( let client_config = LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config: None, + lsps4_client_config: None, lsps5_client_config: Some(lsps5_client_config), }; @@ -243,6 +245,7 @@ pub(crate) fn lsps5_lsps2_test_setup<'a, 'b, 'c>( #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(lsps2_service_config), + lsps4_service_config: None, lsps5_service_config: Some(lsps5_service_config), advertise_service: true, }; @@ -252,6 +255,7 @@ pub(crate) fn lsps5_lsps2_test_setup<'a, 'b, 'c>( let client_config = LiquidityClientConfig { lsps1_client_config: None, lsps2_client_config: Some(lsps2_client_config), + lsps4_client_config: None, lsps5_client_config: Some(lsps5_client_config), }; @@ -994,6 +998,16 @@ fn replay_prevention_test() { assert!(replay_result.is_err(), "Immediate replay attack should be detected"); assert_eq!(replay_result.unwrap_err(), LSPS5ClientError::ReplayAttack); + let case_modified_signature = signature.to_ascii_uppercase(); + assert_ne!(case_modified_signature, signature); + let case_modified_replay_result = + validator.validate(service_node_id, ×tamp, &case_modified_signature, &body); + assert!( + case_modified_replay_result.is_err(), + "Immediate replay attack should be detected when the signature case changes" + ); + assert_eq!(case_modified_replay_result.unwrap_err(), LSPS5ClientError::ReplayAttack); + // Fill up the validator's signature cache to push out the original signature. for i in 0..MAX_RECENT_SIGNATURES { // Advance time, allowing for another notification @@ -1519,6 +1533,7 @@ fn lsps5_service_handler_persistence_across_restarts() { #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: None, + lsps4_service_config: None, lsps5_service_config: Some(LSPS5ServiceConfig::default()), advertise_service: true, }; @@ -1619,6 +1634,7 @@ fn lsps5_service_handler_persistence_across_restarts() { Some(service_config), None, Arc::clone(&time_provider), + nodes_restart[0].logger, ) .unwrap(); @@ -1648,3 +1664,187 @@ fn lsps5_service_handler_persistence_across_restarts() { } } } + +struct FailableKVStore { + inner: TestStore, + fail_lsps5: std::sync::atomic::AtomicBool, +} + +impl FailableKVStore { + fn new() -> Self { + Self { inner: TestStore::new(false), fail_lsps5: std::sync::atomic::AtomicBool::new(false) } + } + + fn set_fail_lsps5(&self, fail: bool) { + self.fail_lsps5.store(fail, std::sync::atomic::Ordering::SeqCst); + } +} + +impl lightning::util::persist::KVStoreSync for FailableKVStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> lightning::io::Result> { + ::read( + &self.inner, + primary_namespace, + secondary_namespace, + key, + ) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> lightning::io::Result<()> { + if secondary_namespace == "lsps5_service" + && self.fail_lsps5.load(std::sync::atomic::Ordering::SeqCst) + { + return Err(lightning::io::Error::new( + lightning::io::ErrorKind::Other, + "intentional failure for lsps5 namespace", + )); + } + ::write( + &self.inner, + primary_namespace, + secondary_namespace, + key, + buf, + ) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> lightning::io::Result<()> { + if secondary_namespace == "lsps5_service" + && self.fail_lsps5.load(std::sync::atomic::Ordering::SeqCst) + { + return Err(lightning::io::Error::new( + lightning::io::ErrorKind::Other, + "intentional failure for lsps5 namespace", + )); + } + ::remove( + &self.inner, + primary_namespace, + secondary_namespace, + key, + lazy, + ) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> lightning::io::Result> { + ::list( + &self.inner, + primary_namespace, + secondary_namespace, + ) + } +} + +#[test] +fn lsps5_service_persist_resets_in_flight_counter_on_io_error() { + use lightning::ln::peer_handler::CustomMessageHandler; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let service_kv_store = Arc::new(FailableKVStore::new()); + let client_kv_store = Arc::new(TestStore::new(false)); + + let service_config = LiquidityServiceConfig { + #[cfg(lsps1_service)] + lsps1_service_config: None, + lsps2_service_config: None, + lsps4_service_config: None, + lsps5_service_config: Some(LSPS5ServiceConfig::default()), + advertise_service: true, + }; + let client_config = LiquidityClientConfig { + lsps1_client_config: None, + lsps2_client_config: None, + lsps4_client_config: None, + lsps5_client_config: Some(LSPS5ClientConfig::default()), + }; + let time_provider: Arc = Arc::new(DefaultTimeProvider); + + let chain_params = ChainParameters { + network: Network::Testnet, + best_block: BestBlock::from_network(Network::Testnet), + }; + + let service_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes[0].keys_manager, + nodes[0].keys_manager, + nodes[0].node, + None::>, + Some(chain_params), + Arc::clone(&service_kv_store), + nodes[0].tx_broadcaster, + Some(service_config), + None, + Arc::clone(&time_provider), + nodes[0].logger, + ) + .unwrap(); + + let client_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes[1].keys_manager, + nodes[1].keys_manager, + nodes[1].node, + None::>, + Some(chain_params), + client_kv_store, + nodes[1].tx_broadcaster, + None, + Some(client_config), + Arc::clone(&time_provider), + nodes[1].logger, + ) + .unwrap(); + + let service_node_id = nodes[0].node.get_our_node_id(); + let client_node_id = nodes[1].node.get_our_node_id(); + + create_chan_between_nodes(&nodes[0], &nodes[1]); + + let client_handler = client_lm.lsps5_client_handler().unwrap(); + client_handler + .set_webhook(service_node_id, "App".to_string(), "https://example.org/hook".to_string()) + .unwrap(); + + let req_msgs = client_lm.get_and_clear_pending_msg(); + assert_eq!(req_msgs.len(), 1); + let (_, request) = req_msgs.into_iter().next().unwrap(); + service_lm.handle_custom_message(request, client_node_id).unwrap(); + + // Consume the SendWebhookNotification event so pending events queue is drained. + let _ = service_lm.next_event(); + let _ = service_lm.get_and_clear_pending_msg(); + + // Initial persist should succeed and clear all needs_persist flags. + service_lm.persist().expect("initial persist should succeed"); + + // Now arrange for lsps5 writes to fail and dirty lsps5 state without dirtying + // pending_events (which lives in a different namespace). + service_kv_store.set_fail_lsps5(true); + service_lm.peer_disconnected(client_node_id); + + // First persist attempt should error out due to the failing kv_store. + let res1 = service_lm.persist(); + assert!(res1.is_err(), "persist should fail when lsps5 kv_store write fails"); + + // Second persist attempt must still attempt the write (and fail again). With the + // bug, the LSPS5 service handler's `persistence_in_flight` counter is left above + // zero on error so this returns Ok(false) immediately, silently dropping the + // pending state and breaking persistence forever. + let res2 = service_lm.persist(); + assert!( + res2.is_err(), + "after a failed persist, subsequent persist calls must still attempt to persist; got {:?}", + res2, + ); +} diff --git a/lightning-macros/Cargo.toml b/lightning-macros/Cargo.toml index 546c4de5129..a7a5eabf589 100644 --- a/lightning-macros/Cargo.toml +++ b/lightning-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-macros" -version = "0.2.0" +version = "0.2.1" authors = ["Elias Rohrer"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning/" diff --git a/lightning-persister/Cargo.toml b/lightning-persister/Cargo.toml index cdd4b3a5086..dfea46d0d91 100644 --- a/lightning-persister/Cargo.toml +++ b/lightning-persister/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-persister" -version = "0.2.0" +version = "0.2.3" authors = ["Valentine Wallace", "Matt Corallo"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning" diff --git a/lightning-persister/src/fs_store.rs b/lightning-persister/src/fs_store.rs index 9b15398d4d1..05530b8fcf0 100644 --- a/lightning-persister/src/fs_store.rs +++ b/lightning-persister/src/fs_store.rs @@ -554,20 +554,25 @@ impl KVStore for FilesystemStore { } } +fn dir_entry_is_store_artifact(path: &Path) -> bool { + match path.extension().and_then(|ext| ext.to_str()) { + Some("tmp") => true, + Some("trash") => { + #[cfg(target_os = "windows")] + { + // Clean up any trash files lying around. + fs::remove_file(path).ok(); + } + true + }, + _ => false, + } +} + fn dir_entry_is_key(dir_entry: &fs::DirEntry) -> Result { let p = dir_entry.path(); - if let Some(ext) = p.extension() { - #[cfg(target_os = "windows")] - { - // Clean up any trash files lying around. - if ext == "trash" { - fs::remove_file(p).ok(); - return Ok(false); - } - } - if ext == "tmp" { - return Ok(false); - } + if dir_entry_is_store_artifact(&p) { + return Ok(false); } let metadata = dir_entry.metadata()?; @@ -654,6 +659,9 @@ impl MigratableKVStore for FilesystemStore { 'primary_loop: for primary_entry in fs::read_dir(prefixed_dest)? { let primary_entry = primary_entry?; let primary_path = primary_entry.path(); + if dir_entry_is_store_artifact(&primary_path) { + continue 'primary_loop; + } if dir_entry_is_key(&primary_entry)? { let primary_namespace = String::new(); @@ -667,6 +675,9 @@ impl MigratableKVStore for FilesystemStore { 'secondary_loop: for secondary_entry in fs::read_dir(&primary_path)? { let secondary_entry = secondary_entry?; let secondary_path = secondary_entry.path(); + if dir_entry_is_store_artifact(&secondary_path) { + continue 'secondary_loop; + } if dir_entry_is_key(&secondary_entry)? { let primary_namespace = @@ -681,6 +692,9 @@ impl MigratableKVStore for FilesystemStore { for tertiary_entry in fs::read_dir(&secondary_path)? { let tertiary_entry = tertiary_entry?; let tertiary_path = tertiary_entry.path(); + if dir_entry_is_store_artifact(&tertiary_path) { + continue; + } if dir_entry_is_key(&tertiary_entry)? { let primary_namespace = @@ -806,6 +820,28 @@ mod tests { assert_eq!(listed_keys.len(), 0); } + #[test] + fn list_all_keys_skips_leftover_store_artifacts() { + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_list_all_keys_skips_leftover_store_artifacts"); + let fs_store = FilesystemStore::new(temp_path.clone()); + KVStoreSync::write(&fs_store, "primary", "secondary", "key", vec![1]).unwrap(); + + fs::write(temp_path.join("top_level.0.tmp"), b"stale").unwrap(); + fs::write(temp_path.join("top_level.0.trash"), b"stale").unwrap(); + + let primary_path = temp_path.join("primary"); + fs::write(primary_path.join("primary_level.0.tmp"), b"stale").unwrap(); + fs::write(primary_path.join("primary_level.0.trash"), b"stale").unwrap(); + + let secondary_path = primary_path.join("secondary"); + fs::write(secondary_path.join("secondary_level.0.tmp"), b"stale").unwrap(); + fs::write(secondary_path.join("secondary_level.0.trash"), b"stale").unwrap(); + + let keys = fs_store.list_all_keys().unwrap(); + assert_eq!(keys, vec![("primary".to_string(), "secondary".to_string(), "key".to_string())]); + } + #[test] fn test_data_migration() { let mut source_temp_path = std::env::temp_dir(); diff --git a/lightning-rapid-gossip-sync/Cargo.toml b/lightning-rapid-gossip-sync/Cargo.toml index 695e41a3662..54a73a00f78 100644 --- a/lightning-rapid-gossip-sync/Cargo.toml +++ b/lightning-rapid-gossip-sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-rapid-gossip-sync" -version = "0.2.0" +version = "0.2.3" authors = ["Arik Sosman "] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning" diff --git a/lightning-rapid-gossip-sync/src/lib.rs b/lightning-rapid-gossip-sync/src/lib.rs index 429a3560be0..374cffd8330 100644 --- a/lightning-rapid-gossip-sync/src/lib.rs +++ b/lightning-rapid-gossip-sync/src/lib.rs @@ -153,6 +153,10 @@ where /// Sync gossip data from a file. /// Returns the last sync timestamp to be used the next time rapid sync data is queried. /// + /// You should consider the gossip data source as semi-trusted. It is generally the case that it + /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating + /// the graph such that it leads to eventual OOM on the client. + /// /// `network_graph`: The network graph to apply the updates to /// /// `sync_path`: Path to the file where the gossip update data is located @@ -172,6 +176,10 @@ where /// Update network graph from binary data. /// Returns the last sync timestamp to be used the next time rapid sync data is queried. /// + /// You should consider the gossip data source as semi-trusted. It is generally the case that it + /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating + /// the graph such that it leads to eventual OOM on the client. + /// /// `update_data`: `&[u8]` binary stream that comprises the update data #[cfg(feature = "std")] pub fn update_network_graph(&self, update_data: &[u8]) -> Result { @@ -182,6 +190,10 @@ where /// Update network graph from binary data. /// Returns the last sync timestamp to be used the next time rapid sync data is queried. /// + /// You should consider the gossip data source as semi-trusted. It is generally the case that it + /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating + /// the graph such that it leads to eventual OOM on the client. + /// /// `update_data`: `&[u8]` binary stream that comprises the update data /// `current_time_unix`: `Option` optional current timestamp to verify data age pub fn update_network_graph_no_std( diff --git a/lightning-rapid-gossip-sync/src/processing.rs b/lightning-rapid-gossip-sync/src/processing.rs index 8319506b574..ad58c74eb54 100644 --- a/lightning-rapid-gossip-sync/src/processing.rs +++ b/lightning-rapid-gossip-sync/src/processing.rs @@ -9,7 +9,9 @@ use lightning::ln::msgs::{ DecodeError, ErrorAction, LightningError, SocketAddress, UnsignedChannelUpdate, UnsignedNodeAnnouncement, }; -use lightning::routing::gossip::{NetworkGraph, NodeAlias, NodeId}; +use lightning::routing::gossip::{ + NetworkGraph, NodeAlias, NodeId, CHAN_COUNT_ESTIMATE, NODE_COUNT_ESTIMATE, +}; use lightning::util::logger::Logger; use lightning::util::ser::{BigSize, FixedLengthReader, Readable}; use lightning::{log_debug, log_given_level, log_gossip, log_trace, log_warn}; @@ -115,17 +117,27 @@ where } }; + const MAX_NODE_COUNT: u32 = (NODE_COUNT_ESTIMATE as u32) * 10; + const MAX_CHANNEL_COUNT: u64 = (CHAN_COUNT_ESTIMATE as u64) * 10; + let node_id_count: u32 = Readable::read(read_cursor)?; + if node_id_count > MAX_NODE_COUNT { + return Err(LightningError { + err: "RGS data contained nonsense number of nodes to update".to_owned(), + action: ErrorAction::IgnoreError, + } + .into()); + } let mut node_ids: Vec = Vec::with_capacity(core::cmp::min( node_id_count, MAX_INITIAL_NODE_ID_VECTOR_CAPACITY, ) as usize); - let network_graph = &self.network_graph; let mut node_modifications: Vec = Vec::new(); + let read_only_network_graph = network_graph.read_only(); + if parse_node_details { - let read_only_network_graph = network_graph.read_only(); for _ in 0..node_id_count { let mut pubkey_bytes = [0u8; 33]; read_cursor.read_exact(&mut pubkey_bytes)?; @@ -237,9 +249,12 @@ where } } + let original_graph_channel_count = read_only_network_graph.channels().len() as u32; + core::mem::drop(read_only_network_graph); + let mut previous_scid: u64 = 0; let announcement_count: u32 = Readable::read(read_cursor)?; - for _ in 0..announcement_count { + for i in 0..announcement_count { let features = Readable::read(read_cursor)?; // handle SCID @@ -284,6 +299,10 @@ where } } + if (original_graph_channel_count as u64) + (i as u64) > MAX_CHANNEL_COUNT { + continue; + } + let announcement_result = network_graph.add_channel_from_partial_announcement( short_channel_id, funding_sats, @@ -329,6 +348,13 @@ where previous_scid = 0; let update_count: u32 = Readable::read(read_cursor)?; + if update_count as u64 > MAX_CHANNEL_COUNT { + return Err(LightningError { + err: "RGS data contained nonsense number of channels to update".to_owned(), + action: ErrorAction::IgnoreError, + } + .into()); + } log_debug!(self.logger, "Processing RGS update from {} with {} nodes, {} channel announcements and {} channel updates.", latest_seen_timestamp, node_id_count, announcement_count, update_count); if update_count == 0 { diff --git a/lightning-transaction-sync/Cargo.toml b/lightning-transaction-sync/Cargo.toml index 2e6a99810ff..30908cabdb7 100644 --- a/lightning-transaction-sync/Cargo.toml +++ b/lightning-transaction-sync/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-transaction-sync" -version = "0.2.0" +version = "0.2.1" authors = ["Elias Rohrer"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning" diff --git a/lightning-transaction-sync/src/electrum.rs b/lightning-transaction-sync/src/electrum.rs index 47489df69bb..70e4f79a595 100644 --- a/lightning-transaction-sync/src/electrum.rs +++ b/lightning-transaction-sync/src/electrum.rs @@ -335,11 +335,22 @@ where let mut filtered_history = script_history.iter().filter(|h| h.tx_hash == **txid); if let Some(history) = filtered_history.next() { + if history.height <= 0 { + // Skip if it's a an unconfirmed entry. + continue; + } let prob_conf_height = history.height as u32; let confirmed_tx = self.get_confirmed_tx(tx, prob_conf_height)?; confirmed_txs.push(confirmed_tx); } - debug_assert!(filtered_history.next().is_none()); + if filtered_history.next().is_some() { + log_error!( + self.logger, + "Failed due to server returning multiple history entries for Tx {}.", + txid + ); + return Err(InternalError::Failed); + } } for (watched_output, script_history) in @@ -347,6 +358,7 @@ where { for possible_output_spend in script_history { if possible_output_spend.height <= 0 { + // Skip if it's a an unconfirmed entry. continue; } diff --git a/lightning-transaction-sync/src/esplora.rs b/lightning-transaction-sync/src/esplora.rs index a191260bc01..e96110cc164 100644 --- a/lightning-transaction-sync/src/esplora.rs +++ b/lightning-transaction-sync/src/esplora.rs @@ -367,8 +367,13 @@ where let mut matches = Vec::new(); let mut indexes = Vec::new(); - let _ = merkle_block.txn.extract_matches(&mut matches, &mut indexes); - if indexes.len() != 1 || matches.len() != 1 || matches[0] != txid { + let computed_merkle_root = + merkle_block.txn.extract_matches(&mut matches, &mut indexes).ok(); + if computed_merkle_root != Some(block_header.merkle_root) + || indexes.len() != 1 + || matches.len() != 1 + || matches[0] != txid + { log_error!(self.logger, "Retrieved Merkle block for txid {} doesn't match expectations. This should not happen. Please verify server integrity.", txid); return Err(InternalError::Failed); } diff --git a/lightning-types/Cargo.toml b/lightning-types/Cargo.toml index d492698eb4c..0f3aab0f356 100644 --- a/lightning-types/Cargo.toml +++ b/lightning-types/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning-types" -version = "0.3.0" +version = "0.3.5" authors = ["Matt Corallo"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning/" diff --git a/lightning-types/src/features.rs b/lightning-types/src/features.rs index 54f1d9e44f0..8cc0782ffc2 100644 --- a/lightning-types/src/features.rs +++ b/lightning-types/src/features.rs @@ -166,7 +166,7 @@ mod sealed { // Byte 6 ZeroConf, // Byte 7 - Trampoline | SimpleClose | SpliceProduction, + Trampoline | SimpleClose | SpliceProduction | SplicePrototype, // Byte 8 - 16 ,,,,,,,,, // Byte 17 @@ -174,7 +174,7 @@ mod sealed { // Byte 18 , // Byte 19 - HtlcHold | SplicePrototype, + HtlcHold | SplicePrototypeLegacy, ] ); define_context!( @@ -195,7 +195,7 @@ mod sealed { // Byte 6 ZeroConf | Keysend, // Byte 7 - Trampoline | SimpleClose | SpliceProduction, + Trampoline | SimpleClose | SpliceProduction | SplicePrototype, // Byte 8 - 16 ,,,,,,,,, // Byte 17 @@ -203,7 +203,7 @@ mod sealed { // Byte 18 , // Byte 19 - HtlcHold | SplicePrototype, + HtlcHold | SplicePrototypeLegacy, // Byte 20 - 31 ,,,,,,,,,,,, // Byte 32 @@ -649,9 +649,17 @@ mod sealed { supports_payment_metadata, requires_payment_metadata ); - define_feature!(51, ZeroConf, [InitContext, NodeContext, ChannelTypeContext], + define_feature!( + 51, + ZeroConf, + [InitContext, NodeContext, ChannelTypeContext], "Feature flags for accepting channels with zero confirmations. Called `option_zeroconf` in the BOLTs", - set_zero_conf_optional, set_zero_conf_required, supports_zero_conf, requires_zero_conf); + set_zero_conf_optional, + set_zero_conf_required, + clear_zero_conf, + supports_zero_conf, + requires_zero_conf + ); define_feature!( 55, Keysend, @@ -722,7 +730,7 @@ mod sealed { requires_htlc_hold ); define_feature!( - 155, // Splice prototype feature bit as listed in https://github.com/lightning/bolts/issues/605#issuecomment-877237519. + 63, // Actually the SpliceProduction feature SplicePrototype, [InitContext, NodeContext], "Feature flags for channel splicing.", @@ -732,6 +740,17 @@ mod sealed { supports_splicing, requires_splicing ); + define_feature!( + 155, // The splicing bit LDK used before 0.2.2 moved it to 63 + SplicePrototypeLegacy, + [InitContext, NodeContext], + "Feature flags for channel splicing with peers on the pre-0.2.2 prototype bit.", + set_splicing_legacy_optional, + set_splicing_legacy_required, + clear_splicing_legacy, + supports_splicing_legacy, + requires_splicing_legacy + ); define_feature!( 259, DnsResolver, @@ -1441,8 +1460,8 @@ mod tests { // - onion_messages // - option_channel_type | option_scid_alias // - option_zeroconf - // - option_simple_close | option_splice - assert_eq!(node_features.flags.len(), 20); + // - option_simple_close + assert_eq!(node_features.flags.len(), 8); assert_eq!(node_features.flags[0], 0b00000001); assert_eq!(node_features.flags[1], 0b01010001); assert_eq!(node_features.flags[2], 0b10001010); @@ -1450,19 +1469,7 @@ mod tests { assert_eq!(node_features.flags[4], 0b10001000); assert_eq!(node_features.flags[5], 0b10100000); assert_eq!(node_features.flags[6], 0b00001000); - assert_eq!(node_features.flags[7], 0b00100000); - assert_eq!(node_features.flags[8], 0b00000000); - assert_eq!(node_features.flags[9], 0b00000000); - assert_eq!(node_features.flags[10], 0b00000000); - assert_eq!(node_features.flags[11], 0b00000000); - assert_eq!(node_features.flags[12], 0b00000000); - assert_eq!(node_features.flags[13], 0b00000000); - assert_eq!(node_features.flags[14], 0b00000000); - assert_eq!(node_features.flags[15], 0b00000000); - assert_eq!(node_features.flags[16], 0b00000000); - assert_eq!(node_features.flags[17], 0b00000000); - assert_eq!(node_features.flags[18], 0b00000000); - assert_eq!(node_features.flags[19], 0b00001000); + assert_eq!(node_features.flags[7], 0b10100000); } // Check that cleared flags are kept blank when converting back: diff --git a/lightning-types/src/lib.rs b/lightning-types/src/lib.rs index 7f72d6d2671..6a526adaed2 100644 --- a/lightning-types/src/lib.rs +++ b/lightning-types/src/lib.rs @@ -27,3 +27,4 @@ pub mod features; pub mod payment; pub mod routing; pub mod string; +mod unicode; diff --git a/lightning-types/src/string.rs b/lightning-types/src/string.rs index ae5395a5289..a92c5c3e93a 100644 --- a/lightning-types/src/string.rs +++ b/lightning-types/src/string.rs @@ -12,6 +12,8 @@ use alloc::string::String; use core::fmt; +use crate::unicode::*; + /// Struct to `Display` fields in a safe way using `PrintableString` #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub struct UntrustedString(pub String); @@ -31,7 +33,20 @@ impl<'a> fmt::Display for PrintableString<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { use core::fmt::Write; for c in self.0.chars() { - let c = if c.is_control() { core::char::REPLACEMENT_CHARACTER } else { c }; + let is_other = is_unicode_general_category_other(c); + let is_unassigned = is_unicode_general_category_unassigned(c); + // U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR (general + // categories `Zl`/`Zp`) are covered by neither `char::is_control` + // (`Cc` only) nor the top-level `C` tables above, but terminals and + // log viewers commonly render them as hard line breaks, allowing an + // attacker-controlled string to inject forged lines into operator + // logs — so the generated separator table filters them as well. + let is_line_separator = is_unicode_general_category_separator(c); + let c = if c.is_control() || is_other || is_unassigned || is_line_separator { + core::char::REPLACEMENT_CHARACTER + } else { + c + }; f.write_char(c)?; } @@ -50,4 +65,37 @@ mod tests { "I \u{1F496} LDK!\u{FFFD}\u{26A1}", ); } + + #[test] + fn sanitizes_unicode_bidi_override_characters() { + // U+202E RIGHT-TO-LEFT OVERRIDE and friends are Unicode general category + // `Cf` (Format), not `Cc` (Control). They enable "Trojan Source" / + // bidi-spoofing attacks where an attacker-supplied string (e.g. a node + // alias gossiped from a peer) renders to a human reader as something + // other than its byte content. `PrintableString` is the sanitiser used + // for exactly these untrusted strings, so it must replace them. + let rendered = format!("{}", PrintableString("safe\u{202E}cipsxe.exe")); + assert!( + !rendered.contains('\u{202E}'), + "PrintableString left a U+202E RLO override in its output: {:?}", + rendered + ); + + // U+13440 is in the Egyptian Hieroglyph Format Controls block, but its + // general category is `Mn`, not `Cf`, so the `Cf` range ends at U+1343F. + assert_eq!(format!("{}", PrintableString("x\u{1343F}y\u{13440}z")), "x\u{FFFD}y\u{13440}z"); + } + + #[test] + fn sanitizes_line_and_paragraph_separators() { + // U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are general + // categories `Zl`/`Zp`, not `Cc`, so `char::is_control` does not catch + // them, yet terminals and log viewers commonly render them as hard line + // breaks. As with the bidi overrides above, an attacker-controlled string + // must not be able to use them to inject forged log lines. + assert_eq!( + format!("{}", PrintableString("ok\u{2028}forged\u{2029}more")), + "ok\u{FFFD}forged\u{FFFD}more" + ); + } } diff --git a/lightning-types/src/unicode.rs b/lightning-types/src/unicode.rs new file mode 100644 index 00000000000..569424cec75 --- /dev/null +++ b/lightning-types/src/unicode.rs @@ -0,0 +1,813 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// Auto-generated from the Unicode Character Database (UnicodeData.txt) by +// contrib/gen_unicode_general_category.py. Do not edit by hand; rerun the +// generator with an updated UnicodeData.txt to refresh the table. + +/// Returns `true` if `c` is in Unicode general category `Cc` (Control), `Cf` +/// (Format), `Cs` (Surrogate), or `Co` (Private Use) — the assigned codepoints +/// in the top-level `C` ("Other") category. The `Cs` portion of the table is +/// unreachable for `char` input (a `char` cannot hold a surrogate) but is kept +/// so the table mirrors the source UCD data verbatim. The disjoint `Cn` +/// (Unassigned) bucket is `is_unicode_general_category_unassigned`. +#[allow(dead_code)] +pub(crate) fn is_unicode_general_category_other(c: char) -> bool { + matches!( + c as u32, + 0x0000..=0x001F // control + | 0x007F..=0x009F // control + | 0x00AD // SOFT HYPHEN + | 0x0600..=0x0605 // ARABIC + | 0x061C // ARABIC LETTER MARK + | 0x06DD // ARABIC END OF AYAH + | 0x070F // SYRIAC ABBREVIATION MARK + | 0x0890..=0x0891 // MARK ABOVE + | 0x08E2 // ARABIC DISPUTED END OF AYAH + | 0x180E // MONGOLIAN VOWEL SEPARATOR + | 0x200B..=0x200F // Cf + | 0x202A..=0x202E // Cf + | 0x2060..=0x2064 // Cf + | 0x2066..=0x206F // Cf + | 0xD800..=0xF8FF // Co / Cs + | 0xFEFF // ZERO WIDTH NO-BREAK SPACE + | 0xFFF9..=0xFFFB // INTERLINEAR ANNOTATION + | 0x110BD // KAITHI NUMBER SIGN + | 0x110CD // KAITHI NUMBER SIGN ABOVE + | 0x13430..=0x1343F // EGYPTIAN HIEROGLYPH + | 0x1BCA0..=0x1BCA3 // SHORTHAND FORMAT + | 0x1D173..=0x1D17A // MUSICAL SYMBOL + | 0xE0001 // LANGUAGE TAG + | 0xE0020..=0xE007F // Cf + | 0xF0000..=0xFFFFD // Plane 15 Private Use + | 0x100000..=0x10FFFD // Plane 16 Private Use + ) +} + +/// Returns `true` if `c` is in Unicode general category `Zl` (Line +/// Separator) or `Zp` (Paragraph Separator). Terminals and log viewers +/// commonly render these as hard line breaks, so untrusted strings filter +/// them alongside the `C` buckets. `Zs` (Space Separator) is deliberately +/// excluded: it contains U+0020 SPACE and other ordinary spacing +/// characters. +#[allow(dead_code)] +pub(crate) fn is_unicode_general_category_separator(c: char) -> bool { + matches!( + c as u32, + 0x2028..=0x2029 // SEPARATOR + ) +} + +/// Returns `true` if `c` is in Unicode general category `Cn` (Unassigned), or +/// strictly above U+10FFFF. The trailing `0x110000..=u32::MAX` arm is +/// unreachable for `char` input (a `char` is bounded to U+10FFFF) but is kept +/// for defensive coverage of the underlying `u32`. The disjoint Cc / Cf / Cs / +/// Co bucket is `is_unicode_general_category_other`. +#[allow(dead_code)] +pub(crate) fn is_unicode_general_category_unassigned(c: char) -> bool { + matches!( + c as u32, + 0x0378..=0x0379 + | 0x0380..=0x0383 + | 0x038B + | 0x038D + | 0x03A2 + | 0x0530 + | 0x0557..=0x0558 + | 0x058B..=0x058C + | 0x0590 + | 0x05C8..=0x05CF + | 0x05EB..=0x05EE + | 0x05F5..=0x05FF + | 0x070E + | 0x074B..=0x074C + | 0x07B2..=0x07BF + | 0x07FB..=0x07FC + | 0x082E..=0x082F + | 0x083F + | 0x085C..=0x085D + | 0x085F + | 0x086B..=0x086F + | 0x0892..=0x0896 + | 0x0984 + | 0x098D..=0x098E + | 0x0991..=0x0992 + | 0x09A9 + | 0x09B1 + | 0x09B3..=0x09B5 + | 0x09BA..=0x09BB + | 0x09C5..=0x09C6 + | 0x09C9..=0x09CA + | 0x09CF..=0x09D6 + | 0x09D8..=0x09DB + | 0x09DE + | 0x09E4..=0x09E5 + | 0x09FF..=0x0A00 + | 0x0A04 + | 0x0A0B..=0x0A0E + | 0x0A11..=0x0A12 + | 0x0A29 + | 0x0A31 + | 0x0A34 + | 0x0A37 + | 0x0A3A..=0x0A3B + | 0x0A3D + | 0x0A43..=0x0A46 + | 0x0A49..=0x0A4A + | 0x0A4E..=0x0A50 + | 0x0A52..=0x0A58 + | 0x0A5D + | 0x0A5F..=0x0A65 + | 0x0A77..=0x0A80 + | 0x0A84 + | 0x0A8E + | 0x0A92 + | 0x0AA9 + | 0x0AB1 + | 0x0AB4 + | 0x0ABA..=0x0ABB + | 0x0AC6 + | 0x0ACA + | 0x0ACE..=0x0ACF + | 0x0AD1..=0x0ADF + | 0x0AE4..=0x0AE5 + | 0x0AF2..=0x0AF8 + | 0x0B00 + | 0x0B04 + | 0x0B0D..=0x0B0E + | 0x0B11..=0x0B12 + | 0x0B29 + | 0x0B31 + | 0x0B34 + | 0x0B3A..=0x0B3B + | 0x0B45..=0x0B46 + | 0x0B49..=0x0B4A + | 0x0B4E..=0x0B54 + | 0x0B58..=0x0B5B + | 0x0B5E + | 0x0B64..=0x0B65 + | 0x0B78..=0x0B81 + | 0x0B84 + | 0x0B8B..=0x0B8D + | 0x0B91 + | 0x0B96..=0x0B98 + | 0x0B9B + | 0x0B9D + | 0x0BA0..=0x0BA2 + | 0x0BA5..=0x0BA7 + | 0x0BAB..=0x0BAD + | 0x0BBA..=0x0BBD + | 0x0BC3..=0x0BC5 + | 0x0BC9 + | 0x0BCE..=0x0BCF + | 0x0BD1..=0x0BD6 + | 0x0BD8..=0x0BE5 + | 0x0BFB..=0x0BFF + | 0x0C0D + | 0x0C11 + | 0x0C29 + | 0x0C3A..=0x0C3B + | 0x0C45 + | 0x0C49 + | 0x0C4E..=0x0C54 + | 0x0C57 + | 0x0C5B + | 0x0C5E..=0x0C5F + | 0x0C64..=0x0C65 + | 0x0C70..=0x0C76 + | 0x0C8D + | 0x0C91 + | 0x0CA9 + | 0x0CB4 + | 0x0CBA..=0x0CBB + | 0x0CC5 + | 0x0CC9 + | 0x0CCE..=0x0CD4 + | 0x0CD7..=0x0CDB + | 0x0CDF + | 0x0CE4..=0x0CE5 + | 0x0CF0 + | 0x0CF4..=0x0CFF + | 0x0D0D + | 0x0D11 + | 0x0D45 + | 0x0D49 + | 0x0D50..=0x0D53 + | 0x0D64..=0x0D65 + | 0x0D80 + | 0x0D84 + | 0x0D97..=0x0D99 + | 0x0DB2 + | 0x0DBC + | 0x0DBE..=0x0DBF + | 0x0DC7..=0x0DC9 + | 0x0DCB..=0x0DCE + | 0x0DD5 + | 0x0DD7 + | 0x0DE0..=0x0DE5 + | 0x0DF0..=0x0DF1 + | 0x0DF5..=0x0E00 + | 0x0E3B..=0x0E3E + | 0x0E5C..=0x0E80 + | 0x0E83 + | 0x0E85 + | 0x0E8B + | 0x0EA4 + | 0x0EA6 + | 0x0EBE..=0x0EBF + | 0x0EC5 + | 0x0EC7 + | 0x0ECF + | 0x0EDA..=0x0EDB + | 0x0EE0..=0x0EFF + | 0x0F48 + | 0x0F6D..=0x0F70 + | 0x0F98 + | 0x0FBD + | 0x0FCD + | 0x0FDB..=0x0FFF + | 0x10C6 + | 0x10C8..=0x10CC + | 0x10CE..=0x10CF + | 0x1249 + | 0x124E..=0x124F + | 0x1257 + | 0x1259 + | 0x125E..=0x125F + | 0x1289 + | 0x128E..=0x128F + | 0x12B1 + | 0x12B6..=0x12B7 + | 0x12BF + | 0x12C1 + | 0x12C6..=0x12C7 + | 0x12D7 + | 0x1311 + | 0x1316..=0x1317 + | 0x135B..=0x135C + | 0x137D..=0x137F + | 0x139A..=0x139F + | 0x13F6..=0x13F7 + | 0x13FE..=0x13FF + | 0x169D..=0x169F + | 0x16F9..=0x16FF + | 0x1716..=0x171E + | 0x1737..=0x173F + | 0x1754..=0x175F + | 0x176D + | 0x1771 + | 0x1774..=0x177F + | 0x17DE..=0x17DF + | 0x17EA..=0x17EF + | 0x17FA..=0x17FF + | 0x181A..=0x181F + | 0x1879..=0x187F + | 0x18AB..=0x18AF + | 0x18F6..=0x18FF + | 0x191F + | 0x192C..=0x192F + | 0x193C..=0x193F + | 0x1941..=0x1943 + | 0x196E..=0x196F + | 0x1975..=0x197F + | 0x19AC..=0x19AF + | 0x19CA..=0x19CF + | 0x19DB..=0x19DD + | 0x1A1C..=0x1A1D + | 0x1A5F + | 0x1A7D..=0x1A7E + | 0x1A8A..=0x1A8F + | 0x1A9A..=0x1A9F + | 0x1AAE..=0x1AAF + | 0x1ADE..=0x1ADF + | 0x1AEC..=0x1AFF + | 0x1B4D + | 0x1BF4..=0x1BFB + | 0x1C38..=0x1C3A + | 0x1C4A..=0x1C4C + | 0x1C8B..=0x1C8F + | 0x1CBB..=0x1CBC + | 0x1CC8..=0x1CCF + | 0x1CFB..=0x1CFF + | 0x1F16..=0x1F17 + | 0x1F1E..=0x1F1F + | 0x1F46..=0x1F47 + | 0x1F4E..=0x1F4F + | 0x1F58 + | 0x1F5A + | 0x1F5C + | 0x1F5E + | 0x1F7E..=0x1F7F + | 0x1FB5 + | 0x1FC5 + | 0x1FD4..=0x1FD5 + | 0x1FDC + | 0x1FF0..=0x1FF1 + | 0x1FF5 + | 0x1FFF + | 0x2065 + | 0x2072..=0x2073 + | 0x208F + | 0x209D..=0x209F + | 0x20C2..=0x20CF + | 0x20F1..=0x20FF + | 0x218C..=0x218F + | 0x242A..=0x243F + | 0x244B..=0x245F + | 0x2B74..=0x2B75 + | 0x2CF4..=0x2CF8 + | 0x2D26 + | 0x2D28..=0x2D2C + | 0x2D2E..=0x2D2F + | 0x2D68..=0x2D6E + | 0x2D71..=0x2D7E + | 0x2D97..=0x2D9F + | 0x2DA7 + | 0x2DAF + | 0x2DB7 + | 0x2DBF + | 0x2DC7 + | 0x2DCF + | 0x2DD7 + | 0x2DDF + | 0x2E5E..=0x2E7F + | 0x2E9A + | 0x2EF4..=0x2EFF + | 0x2FD6..=0x2FEF + | 0x3040 + | 0x3097..=0x3098 + | 0x3100..=0x3104 + | 0x3130 + | 0x318F + | 0x31E6..=0x31EE + | 0x321F + | 0xA48D..=0xA48F + | 0xA4C7..=0xA4CF + | 0xA62C..=0xA63F + | 0xA6F8..=0xA6FF + | 0xA7DD..=0xA7F0 + | 0xA82D..=0xA82F + | 0xA83A..=0xA83F + | 0xA878..=0xA87F + | 0xA8C6..=0xA8CD + | 0xA8DA..=0xA8DF + | 0xA954..=0xA95E + | 0xA97D..=0xA97F + | 0xA9CE + | 0xA9DA..=0xA9DD + | 0xA9FF + | 0xAA37..=0xAA3F + | 0xAA4E..=0xAA4F + | 0xAA5A..=0xAA5B + | 0xAAC3..=0xAADA + | 0xAAF7..=0xAB00 + | 0xAB07..=0xAB08 + | 0xAB0F..=0xAB10 + | 0xAB17..=0xAB1F + | 0xAB27 + | 0xAB2F + | 0xAB6C..=0xAB6F + | 0xABEE..=0xABEF + | 0xABFA..=0xABFF + | 0xD7A4..=0xD7AF + | 0xD7C7..=0xD7CA + | 0xD7FC..=0xD7FF + | 0xFA6E..=0xFA6F + | 0xFADA..=0xFAFF + | 0xFB07..=0xFB12 + | 0xFB18..=0xFB1C + | 0xFB37 + | 0xFB3D + | 0xFB3F + | 0xFB42 + | 0xFB45 + | 0xFDD0..=0xFDEF + | 0xFE1A..=0xFE1F + | 0xFE53 + | 0xFE67 + | 0xFE6C..=0xFE6F + | 0xFE75 + | 0xFEFD..=0xFEFE + | 0xFF00 + | 0xFFBF..=0xFFC1 + | 0xFFC8..=0xFFC9 + | 0xFFD0..=0xFFD1 + | 0xFFD8..=0xFFD9 + | 0xFFDD..=0xFFDF + | 0xFFE7 + | 0xFFEF..=0xFFF8 + | 0xFFFE..=0xFFFF + | 0x1000C + | 0x10027 + | 0x1003B + | 0x1003E + | 0x1004E..=0x1004F + | 0x1005E..=0x1007F + | 0x100FB..=0x100FF + | 0x10103..=0x10106 + | 0x10134..=0x10136 + | 0x1018F + | 0x1019D..=0x1019F + | 0x101A1..=0x101CF + | 0x101FE..=0x1027F + | 0x1029D..=0x1029F + | 0x102D1..=0x102DF + | 0x102FC..=0x102FF + | 0x10324..=0x1032C + | 0x1034B..=0x1034F + | 0x1037B..=0x1037F + | 0x1039E + | 0x103C4..=0x103C7 + | 0x103D6..=0x103FF + | 0x1049E..=0x1049F + | 0x104AA..=0x104AF + | 0x104D4..=0x104D7 + | 0x104FC..=0x104FF + | 0x10528..=0x1052F + | 0x10564..=0x1056E + | 0x1057B + | 0x1058B + | 0x10593 + | 0x10596 + | 0x105A2 + | 0x105B2 + | 0x105BA + | 0x105BD..=0x105BF + | 0x105F4..=0x105FF + | 0x10737..=0x1073F + | 0x10756..=0x1075F + | 0x10768..=0x1077F + | 0x10786 + | 0x107B1 + | 0x107BB..=0x107FF + | 0x10806..=0x10807 + | 0x10809 + | 0x10836 + | 0x10839..=0x1083B + | 0x1083D..=0x1083E + | 0x10856 + | 0x1089F..=0x108A6 + | 0x108B0..=0x108DF + | 0x108F3 + | 0x108F6..=0x108FA + | 0x1091C..=0x1091E + | 0x1093A..=0x1093E + | 0x1095A..=0x1097F + | 0x109B8..=0x109BB + | 0x109D0..=0x109D1 + | 0x10A04 + | 0x10A07..=0x10A0B + | 0x10A14 + | 0x10A18 + | 0x10A36..=0x10A37 + | 0x10A3B..=0x10A3E + | 0x10A49..=0x10A4F + | 0x10A59..=0x10A5F + | 0x10AA0..=0x10ABF + | 0x10AE7..=0x10AEA + | 0x10AF7..=0x10AFF + | 0x10B36..=0x10B38 + | 0x10B56..=0x10B57 + | 0x10B73..=0x10B77 + | 0x10B92..=0x10B98 + | 0x10B9D..=0x10BA8 + | 0x10BB0..=0x10BFF + | 0x10C49..=0x10C7F + | 0x10CB3..=0x10CBF + | 0x10CF3..=0x10CF9 + | 0x10D28..=0x10D2F + | 0x10D3A..=0x10D3F + | 0x10D66..=0x10D68 + | 0x10D86..=0x10D8D + | 0x10D90..=0x10E5F + | 0x10E7F + | 0x10EAA + | 0x10EAE..=0x10EAF + | 0x10EB2..=0x10EC1 + | 0x10EC8..=0x10ECF + | 0x10ED9..=0x10EF9 + | 0x10F28..=0x10F2F + | 0x10F5A..=0x10F6F + | 0x10F8A..=0x10FAF + | 0x10FCC..=0x10FDF + | 0x10FF7..=0x10FFF + | 0x1104E..=0x11051 + | 0x11076..=0x1107E + | 0x110C3..=0x110CC + | 0x110CE..=0x110CF + | 0x110E9..=0x110EF + | 0x110FA..=0x110FF + | 0x11135 + | 0x11148..=0x1114F + | 0x11177..=0x1117F + | 0x111E0 + | 0x111F5..=0x111FF + | 0x11212 + | 0x11242..=0x1127F + | 0x11287 + | 0x11289 + | 0x1128E + | 0x1129E + | 0x112AA..=0x112AF + | 0x112EB..=0x112EF + | 0x112FA..=0x112FF + | 0x11304 + | 0x1130D..=0x1130E + | 0x11311..=0x11312 + | 0x11329 + | 0x11331 + | 0x11334 + | 0x1133A + | 0x11345..=0x11346 + | 0x11349..=0x1134A + | 0x1134E..=0x1134F + | 0x11351..=0x11356 + | 0x11358..=0x1135C + | 0x11364..=0x11365 + | 0x1136D..=0x1136F + | 0x11375..=0x1137F + | 0x1138A + | 0x1138C..=0x1138D + | 0x1138F + | 0x113B6 + | 0x113C1 + | 0x113C3..=0x113C4 + | 0x113C6 + | 0x113CB + | 0x113D6 + | 0x113D9..=0x113E0 + | 0x113E3..=0x113FF + | 0x1145C + | 0x11462..=0x1147F + | 0x114C8..=0x114CF + | 0x114DA..=0x1157F + | 0x115B6..=0x115B7 + | 0x115DE..=0x115FF + | 0x11645..=0x1164F + | 0x1165A..=0x1165F + | 0x1166D..=0x1167F + | 0x116BA..=0x116BF + | 0x116CA..=0x116CF + | 0x116E4..=0x116FF + | 0x1171B..=0x1171C + | 0x1172C..=0x1172F + | 0x11747..=0x117FF + | 0x1183C..=0x1189F + | 0x118F3..=0x118FE + | 0x11907..=0x11908 + | 0x1190A..=0x1190B + | 0x11914 + | 0x11917 + | 0x11936 + | 0x11939..=0x1193A + | 0x11947..=0x1194F + | 0x1195A..=0x1199F + | 0x119A8..=0x119A9 + | 0x119D8..=0x119D9 + | 0x119E5..=0x119FF + | 0x11A48..=0x11A4F + | 0x11AA3..=0x11AAF + | 0x11AF9..=0x11AFF + | 0x11B0A..=0x11B5F + | 0x11B68..=0x11BBF + | 0x11BE2..=0x11BEF + | 0x11BFA..=0x11BFF + | 0x11C09 + | 0x11C37 + | 0x11C46..=0x11C4F + | 0x11C6D..=0x11C6F + | 0x11C90..=0x11C91 + | 0x11CA8 + | 0x11CB7..=0x11CFF + | 0x11D07 + | 0x11D0A + | 0x11D37..=0x11D39 + | 0x11D3B + | 0x11D3E + | 0x11D48..=0x11D4F + | 0x11D5A..=0x11D5F + | 0x11D66 + | 0x11D69 + | 0x11D8F + | 0x11D92 + | 0x11D99..=0x11D9F + | 0x11DAA..=0x11DAF + | 0x11DDC..=0x11DDF + | 0x11DEA..=0x11EDF + | 0x11EF9..=0x11EFF + | 0x11F11 + | 0x11F3B..=0x11F3D + | 0x11F5B..=0x11FAF + | 0x11FB1..=0x11FBF + | 0x11FF2..=0x11FFE + | 0x1239A..=0x123FF + | 0x1246F + | 0x12475..=0x1247F + | 0x12544..=0x12F8F + | 0x12FF3..=0x12FFF + | 0x13456..=0x1345F + | 0x143FB..=0x143FF + | 0x14647..=0x160FF + | 0x1613A..=0x167FF + | 0x16A39..=0x16A3F + | 0x16A5F + | 0x16A6A..=0x16A6D + | 0x16ABF + | 0x16ACA..=0x16ACF + | 0x16AEE..=0x16AEF + | 0x16AF6..=0x16AFF + | 0x16B46..=0x16B4F + | 0x16B5A + | 0x16B62 + | 0x16B78..=0x16B7C + | 0x16B90..=0x16D3F + | 0x16D7A..=0x16E3F + | 0x16E9B..=0x16E9F + | 0x16EB9..=0x16EBA + | 0x16ED4..=0x16EFF + | 0x16F4B..=0x16F4E + | 0x16F88..=0x16F8E + | 0x16FA0..=0x16FDF + | 0x16FE5..=0x16FEF + | 0x16FF7..=0x16FFF + | 0x18CD6..=0x18CFE + | 0x18D1F..=0x18D7F + | 0x18DF3..=0x1AFEF + | 0x1AFF4 + | 0x1AFFC + | 0x1AFFF + | 0x1B123..=0x1B131 + | 0x1B133..=0x1B14F + | 0x1B153..=0x1B154 + | 0x1B156..=0x1B163 + | 0x1B168..=0x1B16F + | 0x1B2FC..=0x1BBFF + | 0x1BC6B..=0x1BC6F + | 0x1BC7D..=0x1BC7F + | 0x1BC89..=0x1BC8F + | 0x1BC9A..=0x1BC9B + | 0x1BCA4..=0x1CBFF + | 0x1CCFD..=0x1CCFF + | 0x1CEB4..=0x1CEB9 + | 0x1CED1..=0x1CEDF + | 0x1CEF1..=0x1CEFF + | 0x1CF2E..=0x1CF2F + | 0x1CF47..=0x1CF4F + | 0x1CFC4..=0x1CFFF + | 0x1D0F6..=0x1D0FF + | 0x1D127..=0x1D128 + | 0x1D1EB..=0x1D1FF + | 0x1D246..=0x1D2BF + | 0x1D2D4..=0x1D2DF + | 0x1D2F4..=0x1D2FF + | 0x1D357..=0x1D35F + | 0x1D379..=0x1D3FF + | 0x1D455 + | 0x1D49D + | 0x1D4A0..=0x1D4A1 + | 0x1D4A3..=0x1D4A4 + | 0x1D4A7..=0x1D4A8 + | 0x1D4AD + | 0x1D4BA + | 0x1D4BC + | 0x1D4C4 + | 0x1D506 + | 0x1D50B..=0x1D50C + | 0x1D515 + | 0x1D51D + | 0x1D53A + | 0x1D53F + | 0x1D545 + | 0x1D547..=0x1D549 + | 0x1D551 + | 0x1D6A6..=0x1D6A7 + | 0x1D7CC..=0x1D7CD + | 0x1DA8C..=0x1DA9A + | 0x1DAA0 + | 0x1DAB0..=0x1DEFF + | 0x1DF1F..=0x1DF24 + | 0x1DF2B..=0x1DFFF + | 0x1E007 + | 0x1E019..=0x1E01A + | 0x1E022 + | 0x1E025 + | 0x1E02B..=0x1E02F + | 0x1E06E..=0x1E08E + | 0x1E090..=0x1E0FF + | 0x1E12D..=0x1E12F + | 0x1E13E..=0x1E13F + | 0x1E14A..=0x1E14D + | 0x1E150..=0x1E28F + | 0x1E2AF..=0x1E2BF + | 0x1E2FA..=0x1E2FE + | 0x1E300..=0x1E4CF + | 0x1E4FA..=0x1E5CF + | 0x1E5FB..=0x1E5FE + | 0x1E600..=0x1E6BF + | 0x1E6DF + | 0x1E6F6..=0x1E6FD + | 0x1E700..=0x1E7DF + | 0x1E7E7 + | 0x1E7EC + | 0x1E7EF + | 0x1E7FF + | 0x1E8C5..=0x1E8C6 + | 0x1E8D7..=0x1E8FF + | 0x1E94C..=0x1E94F + | 0x1E95A..=0x1E95D + | 0x1E960..=0x1EC70 + | 0x1ECB5..=0x1ED00 + | 0x1ED3E..=0x1EDFF + | 0x1EE04 + | 0x1EE20 + | 0x1EE23 + | 0x1EE25..=0x1EE26 + | 0x1EE28 + | 0x1EE33 + | 0x1EE38 + | 0x1EE3A + | 0x1EE3C..=0x1EE41 + | 0x1EE43..=0x1EE46 + | 0x1EE48 + | 0x1EE4A + | 0x1EE4C + | 0x1EE50 + | 0x1EE53 + | 0x1EE55..=0x1EE56 + | 0x1EE58 + | 0x1EE5A + | 0x1EE5C + | 0x1EE5E + | 0x1EE60 + | 0x1EE63 + | 0x1EE65..=0x1EE66 + | 0x1EE6B + | 0x1EE73 + | 0x1EE78 + | 0x1EE7D + | 0x1EE7F + | 0x1EE8A + | 0x1EE9C..=0x1EEA0 + | 0x1EEA4 + | 0x1EEAA + | 0x1EEBC..=0x1EEEF + | 0x1EEF2..=0x1EFFF + | 0x1F02C..=0x1F02F + | 0x1F094..=0x1F09F + | 0x1F0AF..=0x1F0B0 + | 0x1F0C0 + | 0x1F0D0 + | 0x1F0F6..=0x1F0FF + | 0x1F1AE..=0x1F1E5 + | 0x1F203..=0x1F20F + | 0x1F23C..=0x1F23F + | 0x1F249..=0x1F24F + | 0x1F252..=0x1F25F + | 0x1F266..=0x1F2FF + | 0x1F6D9..=0x1F6DB + | 0x1F6ED..=0x1F6EF + | 0x1F6FD..=0x1F6FF + | 0x1F7DA..=0x1F7DF + | 0x1F7EC..=0x1F7EF + | 0x1F7F1..=0x1F7FF + | 0x1F80C..=0x1F80F + | 0x1F848..=0x1F84F + | 0x1F85A..=0x1F85F + | 0x1F888..=0x1F88F + | 0x1F8AE..=0x1F8AF + | 0x1F8BC..=0x1F8BF + | 0x1F8C2..=0x1F8CF + | 0x1F8D9..=0x1F8FF + | 0x1FA58..=0x1FA5F + | 0x1FA6E..=0x1FA6F + | 0x1FA7D..=0x1FA7F + | 0x1FA8B..=0x1FA8D + | 0x1FAC7 + | 0x1FAC9..=0x1FACC + | 0x1FADD..=0x1FADE + | 0x1FAEB..=0x1FAEE + | 0x1FAF9..=0x1FAFF + | 0x1FB93 + | 0x1FBFB..=0x1FFFF + | 0x2A6E0..=0x2A6FF + | 0x2B81E..=0x2B81F + | 0x2CEAE..=0x2CEAF + | 0x2EBE1..=0x2EBEF + | 0x2EE5E..=0x2F7FF + | 0x2FA1E..=0x2FFFF + | 0x3134B..=0x3134F + | 0x3347A..=0xE0000 + | 0xE0002..=0xE001F + | 0xE0080..=0xE00FF + | 0xE01F0..=0xEFFFF + | 0xFFFFE..=0xFFFFF + | 0x10FFFE..=0x10FFFF + | 0x110000..=u32::MAX // above U+10FFFF — unreachable for `char` + ) +} diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index 7aa869a18bb..2558cd9c68c 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lightning" -version = "0.2.0" +version = "0.2.5" authors = ["Matt Corallo"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning/" @@ -33,9 +33,9 @@ grind_signatures = [] default = ["std", "grind_signatures"] [dependencies] -lightning-types = { version = "0.3.0", path = "../lightning-types", default-features = false } +lightning-types = { version = "0.3.5", path = "../lightning-types", default-features = false } lightning-invoice = { version = "0.34.0", path = "../lightning-invoice", default-features = false } -lightning-macros = { version = "0.2.0", path = "../lightning-macros" } +lightning-macros = { version = "0.2.1", path = "../lightning-macros" } bech32 = { version = "0.11.0", default-features = false } bitcoin = { version = "0.32.2", default-features = false, features = ["secp-recovery"] } @@ -53,7 +53,7 @@ inventory = { version = "0.3", optional = true } [dev-dependencies] regex = "1.5.6" lightning-types = { version = "0.3.0", path = "../lightning-types", features = ["_test_utils"] } -lightning-macros = { version = "0.2.0", path = "../lightning-macros" } +lightning-macros = { version = "0.2.1", path = "../lightning-macros" } parking_lot = { version = "0.12", default-features = false } [dev-dependencies.bitcoin] diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index ed55ca5dc9b..fe215332c9c 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -128,10 +128,18 @@ impl BlindedMessagePath { if let IntroductionNode::NodeId(pubkey) = &self.0.introduction_node { let node_id = NodeId::from_pubkey(pubkey); if let Some(node_info) = network_graph.node(&node_id) { + // We don't consider channels that are disabled in either direction, as it may be + // an indication that the channel has closed and simply hasn't been removed from + // our graph yet. If no such channel is found, the `NodeId` representation is + // kept. if let Some((scid, channel_info)) = node_info .channels .iter() .filter_map(|scid| network_graph.channel(*scid).map(|info| (*scid, info))) + .filter(|(_, info)| { + info.one_to_two.as_ref().map(|dir| dir.enabled).unwrap_or(false) + && info.two_to_one.as_ref().map(|dir| dir.enabled).unwrap_or(false) + }) .min_by_key(|(scid, _)| scid_utils::block_from_scid(*scid)) { let direction = if node_id == channel_info.node_one { @@ -184,6 +192,10 @@ impl BlindedMessagePath { NL::Target: NodeIdLookUp, T: secp256k1::Signing + secp256k1::Verification, { + if self.0.blinded_hops.len() <= 1 { + // The resulting blinded path has to always be left with at least one hop. + return Err(()); + } let control_tlvs_ss = node_signer.ecdh(Recipient::Node, &self.0.blinding_point, None)?; let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes()); let encrypted_control_tlvs = &self.0.blinded_hops.get(0).ok_or(())?.encrypted_payload; @@ -731,3 +743,155 @@ pub(super) fn blinded_hops( utils::construct_blinded_hops(secp_ctx, path, session_priv) } } + +#[cfg(test)] +mod tests { + use bitcoin::constants::ChainHash; + use bitcoin::network::Network; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + + use crate::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode}; + use crate::blinded_path::IntroductionNode; + use crate::ln::msgs::{UnsignedChannelUpdate, MAX_VALUE_MSAT}; + use crate::routing::gossip::{NetworkGraph, P2PGossipSync}; + use crate::routing::test_utils::{add_channel, update_channel}; + use crate::sign::ReceiveAuthKey; + use crate::sync::Arc; + use crate::types::features::ChannelFeatures; + use crate::util::test_utils::{TestKeysInterface, TestLogger}; + + fn channel_update( + short_channel_id: u64, timestamp: u32, channel_flags: u8, + ) -> UnsignedChannelUpdate { + UnsignedChannelUpdate { + chain_hash: ChainHash::using_genesis_block(Network::Testnet), + short_channel_id, + timestamp, + message_flags: 1, // Only must_be_one + channel_flags, + cltv_expiry_delta: 0, + htlc_minimum_msat: 0, + htlc_maximum_msat: MAX_VALUE_MSAT, + fee_base_msat: 0, + fee_proportional_millionths: 0, + excess_data: Vec::new(), + } + } + + fn one_hop_path( + secp_ctx: &Secp256k1, introduction_node_id: PublicKey, + recipient_node_id: PublicKey, entropy: &TestKeysInterface, + ) -> BlindedMessagePath { + let intermediate_nodes = + [MessageForwardNode { node_id: introduction_node_id, short_channel_id: None }]; + BlindedMessagePath::new( + &intermediate_nodes, + recipient_node_id, + ReceiveAuthKey([42; 32]), + MessageContext::Custom(Vec::new()), + entropy, + secp_ctx, + ) + } + + #[test] + fn compact_introduction_node_skips_disabled_channels() { + // The compact (DirectedShortChannelId) introduction node encoding must only use + // channels that are enabled in both directions: disabled or closed channels may + // linger in the local network graph (e.g., when sourcing gossip from rapid gossip + // sync, which never removes them), and must not be selected, as senders would be + // unable to resolve (or route to) the introduction node. + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(TestLogger::new()); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger))); + let gossip_sync = P2PGossipSync::new(Arc::clone(&network_graph), None, Arc::clone(&logger)); + let entropy = TestKeysInterface::new(&[0; 32], Network::Testnet); + + let node_a_privkey = SecretKey::from_slice(&[41; 32]).unwrap(); + let node_b_privkey = SecretKey::from_slice(&[43; 32]).unwrap(); + let node_a_pubkey = PublicKey::from_secret_key(&secp_ctx, &node_a_privkey); + let recipient_pubkey = PublicKey::from_secret_key(&secp_ctx, &node_b_privkey); + + let disabled_scid = 100 << 40 | 1 << 16; + let enabled_scid = 200 << 40 | 1 << 16; + + // Add an older channel which is disabled in both directions, as is the case for a + // closed channel lingering in the local graph. + add_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + &node_b_privkey, + ChannelFeatures::from_le_bytes(vec![1]), + disabled_scid, + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + channel_update(disabled_scid, 1, 2), + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_b_privkey, + channel_update(disabled_scid, 1, 3), + ); + + // Add a newer channel which is enabled in both directions. + add_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + &node_b_privkey, + ChannelFeatures::from_le_bytes(vec![2]), + enabled_scid, + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + channel_update(enabled_scid, 2, 0), + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_b_privkey, + channel_update(enabled_scid, 2, 1), + ); + + // Even though the disabled channel is older, the enabled one is selected. + { + let network_graph = network_graph.read_only(); + let mut path = one_hop_path(&secp_ctx, node_a_pubkey, recipient_pubkey, &entropy); + path.use_compact_introduction_node(&network_graph); + match path.introduction_node() { + IntroductionNode::DirectedShortChannelId(_, scid) => { + assert_eq!(*scid, enabled_scid) + }, + IntroductionNode::NodeId(..) => panic!("expected a compact introduction node"), + } + } + + // Once the enabled channel is disabled as well, the `NodeId` encoding is kept. + update_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + channel_update(enabled_scid, 3, 2), + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_b_privkey, + channel_update(enabled_scid, 3, 3), + ); + + let network_graph = network_graph.read_only(); + let mut path = one_hop_path(&secp_ctx, node_a_pubkey, recipient_pubkey, &entropy); + path.use_compact_introduction_node(&network_graph); + assert!( + matches!(path.introduction_node(), IntroductionNode::NodeId(pubkey) if *pubkey == node_a_pubkey) + ); + } +} diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 37d7a1dba7d..d8059b03c89 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -193,6 +193,11 @@ impl BlindedPaymentPath { NL::Target: NodeIdLookUp, T: secp256k1::Signing + secp256k1::Verification, { + if self.inner_path.blinded_hops.len() <= 1 { + // The resulting blinded path has to always be left with at least one hop. + return Err(()); + } + match self.decrypt_intro_payload::(node_signer) { Ok(( BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, .. }), @@ -692,7 +697,7 @@ pub(crate) fn amt_to_forward_msat( (post_base_fee_inbound_amt * 1_000_000 + 1_000_000 + prop - 1) / (prop + 1_000_000); let fee = ((amt_to_forward * prop) / 1_000_000) + base; - if inbound_amt - fee < amt_to_forward { + if inbound_amt.checked_sub(fee)? < amt_to_forward { // Rounding up the forwarded amount resulted in underpaying this node, so take an extra 1 msat // in fee to compensate. amt_to_forward -= 1; @@ -1125,4 +1130,19 @@ mod tests { .unwrap(); assert_eq!(blinded_payinfo.htlc_maximum_msat, 3997); } + + #[test] + fn amt_to_forward_msat_underflow() { + // `amt_to_forward_msat` is documented to return `None` if underflow occurs, but the + // `inbound_amt - fee` subtraction was previously unguarded. With a high proportional fee + // and a small inbound amount, rounding the forwarded amount up leaves `fee` larger than + // `inbound_amt`, so the subtraction underflows (panicking in debug builds and returning a + // nonsensical result in release). Ensure we instead return `None`. + let payment_relay = PaymentRelay { + cltv_expiry_delta: 0, + fee_proportional_millionths: u32::MAX, + fee_base_msat: 1, + }; + assert!(super::amt_to_forward_msat(2, &payment_relay).is_none()); + } } diff --git a/lightning/src/chain/chainmonitor.rs b/lightning/src/chain/chainmonitor.rs index d8d6c90921f..7c96d918e34 100644 --- a/lightning/src/chain/chainmonitor.rs +++ b/lightning/src/chain/chainmonitor.rs @@ -569,7 +569,7 @@ where channel_id_bytes[2], channel_id_bytes[3], ]); - channel_id_u32.wrapping_add(best_height.unwrap_or_default()) + best_height.map(|height| channel_id_u32.wrapping_add(height)) }; let partition_factor = if channel_count < 15 { @@ -579,7 +579,7 @@ where }; let has_pending_claims = monitor_state.monitor.has_pending_claims(); - if has_pending_claims || get_partition_key(channel_id) % partition_factor == 0 { + if has_pending_claims || get_partition_key(channel_id).map(|key| key % partition_factor == 0).unwrap_or(false) { log_trace!( logger, "Syncing Channel Monitor for channel {}", diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index ab695ee3530..7727366c2ad 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -66,7 +66,7 @@ use crate::sign::{ use crate::types::features::ChannelTypeFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::byte_utils; -use crate::util::logger::{Logger, Record}; +use crate::util::logger::{Logger, WithContext}; use crate::util::persist::MonitorName; use crate::util::ser::{ MaybeReadable, Readable, ReadableArgs, RequiredWrapper, UpgradableRequired, Writeable, Writer, @@ -1825,45 +1825,27 @@ macro_rules! _process_events_body { } pub(super) use _process_events_body as process_events_body; -pub(crate) struct WithChannelMonitor<'a, L: Deref> -where - L::Target: Logger, -{ - logger: &'a L, - peer_id: Option, - channel_id: Option, - payment_hash: Option, -} +pub(crate) struct WithChannelMonitor; -impl<'a, L: Deref> Logger for WithChannelMonitor<'a, L> -where - L::Target: Logger, -{ - fn log(&self, mut record: Record) { - record.peer_id = self.peer_id; - record.channel_id = self.channel_id; - record.payment_hash = self.payment_hash; - self.logger.log(record) - } -} - -impl<'a, L: Deref> WithChannelMonitor<'a, L> -where - L::Target: Logger, -{ - pub(crate) fn from( +impl WithChannelMonitor { + pub(crate) fn from<'a, L: Deref, S: EcdsaChannelSigner>( logger: &'a L, monitor: &ChannelMonitor, payment_hash: Option, - ) -> Self { + ) -> WithContext<'a, L> + where + L::Target: Logger, + { Self::from_impl(logger, &*monitor.inner.lock().unwrap(), payment_hash) } - #[rustfmt::skip] - pub(crate) fn from_impl(logger: &'a L, monitor_impl: &ChannelMonitorImpl, payment_hash: Option) -> Self { + pub(crate) fn from_impl<'a, L: Deref, S: EcdsaChannelSigner>( + logger: &'a L, monitor_impl: &ChannelMonitorImpl, payment_hash: Option, + ) -> WithContext<'a, L> + where + L::Target: Logger, + { let peer_id = Some(monitor_impl.counterparty_node_id); let channel_id = Some(monitor_impl.channel_id()); - WithChannelMonitor { - logger, peer_id, channel_id, payment_hash, - } + WithContext::from(logger, peer_id, channel_id, payment_hash) } } @@ -3829,7 +3811,7 @@ impl ChannelMonitorImpl { fn provide_payment_preimage( &mut self, payment_hash: &PaymentHash, payment_preimage: &PaymentPreimage, payment_info: &Option, broadcaster: &B, - fee_estimator: &LowerBoundedFeeEstimator, logger: &WithChannelMonitor) + fee_estimator: &LowerBoundedFeeEstimator, logger: &WithContext) where B::Target: BroadcasterInterface, F::Target: FeeEstimator, L::Target: Logger, @@ -4006,7 +3988,7 @@ impl ChannelMonitorImpl { /// /// [`ChannelMonitor::broadcast_latest_holder_commitment_txn`]: crate::chain::channelmonitor::ChannelMonitor::broadcast_latest_holder_commitment_txn pub(crate) fn queue_latest_holder_commitment_txn_for_broadcast( - &mut self, broadcaster: &B, fee_estimator: &LowerBoundedFeeEstimator, logger: &WithChannelMonitor, + &mut self, broadcaster: &B, fee_estimator: &LowerBoundedFeeEstimator, logger: &WithContext, require_funding_seen: bool, ) where @@ -4034,8 +4016,7 @@ impl ChannelMonitorImpl { } fn renegotiated_funding( - &mut self, logger: &WithChannelMonitor, - channel_parameters: &ChannelTransactionParameters, + &mut self, logger: &WithContext, channel_parameters: &ChannelTransactionParameters, alternative_holder_commitment_tx: &HolderCommitmentTransaction, alternative_counterparty_commitment_tx: &CommitmentTransaction, ) -> Result<(), ()> @@ -4210,7 +4191,7 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn update_monitor( - &mut self, updates: &ChannelMonitorUpdate, broadcaster: &B, fee_estimator: &F, logger: &WithChannelMonitor + &mut self, updates: &ChannelMonitorUpdate, broadcaster: &B, fee_estimator: &F, logger: &WithContext ) -> Result<(), ()> where B::Target: BroadcasterInterface, F::Target: FeeEstimator, @@ -5254,7 +5235,7 @@ impl ChannelMonitorImpl { /// Note that this includes possibly-locktimed-in-the-future transactions! #[rustfmt::skip] fn unsafe_get_latest_holder_commitment_txn( - &mut self, logger: &WithChannelMonitor + &mut self, logger: &WithContext ) -> Vec where L::Target: Logger { log_debug!(logger, "Getting signed copy of latest holder commitment transaction!"); let commitment_tx = { @@ -5307,7 +5288,7 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn block_connected( &mut self, header: &Header, txdata: &TransactionData, height: u32, broadcaster: B, - fee_estimator: F, logger: &WithChannelMonitor, + fee_estimator: F, logger: &WithContext, ) -> Vec where B::Target: BroadcasterInterface, F::Target: FeeEstimator, @@ -5327,7 +5308,7 @@ impl ChannelMonitorImpl { height: u32, broadcaster: B, fee_estimator: &LowerBoundedFeeEstimator, - logger: &WithChannelMonitor, + logger: &WithContext, ) -> Vec where B::Target: BroadcasterInterface, @@ -5360,7 +5341,7 @@ impl ChannelMonitorImpl { height: u32, broadcaster: B, fee_estimator: &LowerBoundedFeeEstimator, - logger: &WithChannelMonitor, + logger: &WithContext, ) -> Vec where B::Target: BroadcasterInterface, @@ -5648,7 +5629,7 @@ impl ChannelMonitorImpl { mut claimable_outpoints: Vec, broadcaster: &B, fee_estimator: &LowerBoundedFeeEstimator, - logger: &WithChannelMonitor, + logger: &WithContext, ) -> Vec where B::Target: BroadcasterInterface, @@ -5868,7 +5849,7 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn blocks_disconnected( - &mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithChannelMonitor + &mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithContext ) where B::Target: BroadcasterInterface, F::Target: FeeEstimator, L::Target: Logger, @@ -5921,7 +5902,7 @@ impl ChannelMonitorImpl { txid: &Txid, broadcaster: B, fee_estimator: &LowerBoundedFeeEstimator, - logger: &WithChannelMonitor, + logger: &WithContext, ) where B::Target: BroadcasterInterface, F::Target: FeeEstimator, @@ -6032,7 +6013,7 @@ impl ChannelMonitorImpl { #[rustfmt::skip] fn should_broadcast_holder_commitment_txn( - &self, logger: &WithChannelMonitor + &self, logger: &WithContext ) -> Option where L::Target: Logger { // There's no need to broadcast our commitment transaction if we've seen one confirmed (even // with 1 confirmation) as it'll be rejected as duplicate/conflicting. @@ -6114,7 +6095,7 @@ impl ChannelMonitorImpl { /// or counterparty commitment tx, if so send back the source, preimage if found and payment_hash of resolved HTLC #[rustfmt::skip] fn is_resolving_htlc_output( - &mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &WithChannelMonitor, + &mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &WithContext, ) where L::Target: Logger { let funding_spent = get_confirmed_funding_scope!(self); @@ -6137,18 +6118,6 @@ impl ChannelMonitorImpl { macro_rules! log_claim { ($tx_info: expr, $holder_tx: expr, $htlc: expr, $source_avail: expr) => { let outbound_htlc = $holder_tx == $htlc.offered; - // HTLCs must either be claimed by a matching script type or through the - // revocation path: - #[cfg(not(fuzzing))] // Note that the fuzzer is not bound by pesky things like "signatures" - debug_assert!(!$htlc.offered || offered_preimage_claim || offered_timeout_claim || revocation_sig_claim); - #[cfg(not(fuzzing))] // Note that the fuzzer is not bound by pesky things like "signatures" - debug_assert!($htlc.offered || accepted_preimage_claim || accepted_timeout_claim || revocation_sig_claim); - // Further, only exactly one of the possible spend paths should have been - // matched by any HTLC spend: - #[cfg(not(fuzzing))] // Note that the fuzzer is not bound by pesky things like "signatures" - debug_assert_eq!(accepted_preimage_claim as u8 + accepted_timeout_claim as u8 + - offered_preimage_claim as u8 + offered_timeout_claim as u8 + - revocation_sig_claim as u8, 1); if ($holder_tx && revocation_sig_claim) || (outbound_htlc && !$source_avail && (accepted_preimage_claim || offered_preimage_claim)) { log_error!(logger, "Input spending {} ({}:{}) in {} resolves {} HTLC with payment hash {} with {}!", @@ -6161,13 +6130,25 @@ impl ChannelMonitorImpl { if outbound_htlc { "outbound" } else { "inbound" }, &$htlc.payment_hash, if revocation_sig_claim { "revocation sig" } else if accepted_preimage_claim || offered_preimage_claim { "preimage" } else { "timeout" }); } + // HTLCs must either be claimed by a matching script type or through the + // revocation path: + #[cfg(not(fuzzing))] // Note that the fuzzer is not bound by pesky things like "signatures" + assert!(!$htlc.offered || offered_preimage_claim || offered_timeout_claim || revocation_sig_claim, "offered {htlc_claim:?}"); + #[cfg(not(fuzzing))] // Note that the fuzzer is not bound by pesky things like "signatures" + assert!($htlc.offered || accepted_preimage_claim || accepted_timeout_claim || revocation_sig_claim, "!offered {htlc_claim:?}"); + // Further, only exactly one of the possible spend paths should have been + // matched by any HTLC spend: + #[cfg(not(fuzzing))] // Note that the fuzzer is not bound by pesky things like "signatures" + assert_eq!(accepted_preimage_claim as u8 + accepted_timeout_claim as u8 + + offered_preimage_claim as u8 + offered_timeout_claim as u8 + + revocation_sig_claim as u8, 1); } } macro_rules! check_htlc_valid_counterparty { ($htlc_output: expr, $per_commitment_data: expr) => { for &(ref pending_htlc, ref pending_source) in $per_commitment_data { - if pending_htlc.payment_hash == $htlc_output.payment_hash && pending_htlc.amount_msat == $htlc_output.amount_msat { + if pending_htlc.offered == $htlc_output.offered && pending_htlc.payment_hash == $htlc_output.payment_hash && pending_htlc.amount_msat == $htlc_output.amount_msat { if let &Some(ref source) = pending_source { log_claim!("revoked counterparty commitment tx", false, pending_htlc, true); payment_data = Some(((**source).clone(), $htlc_output.payment_hash, $htlc_output.amount_msat)); @@ -6179,7 +6160,7 @@ impl ChannelMonitorImpl { } macro_rules! scan_commitment { - ($funding_spent: expr, $htlcs: expr, $tx_info: expr, $holder_tx: expr) => { + ($funding_spent: expr, $htlcs: expr, $tx_info: expr, $holder_tx: expr, $spent_counterparty_revoked: expr) => { for (ref htlc_output, source_option) in $htlcs { if Some(input.previous_output.vout) == htlc_output.transaction_output_index { if let Some(ref source) = source_option { @@ -6190,7 +6171,7 @@ impl ChannelMonitorImpl { // has timed out, or we screwed up. In any case, we should now // resolve the source HTLC with the original sender. payment_data = Some(((*source).clone(), htlc_output.payment_hash, htlc_output.amount_msat)); - } else if !$holder_tx { + } else if $spent_counterparty_revoked { if let Some(current_counterparty_commitment_txid) = &$funding_spent.current_counterparty_commitment_txid { check_htlc_valid_counterparty!(htlc_output, $funding_spent.counterparty_claimable_outpoints.get(current_counterparty_commitment_txid).unwrap()); } @@ -6227,21 +6208,28 @@ impl ChannelMonitorImpl { if input.previous_output.txid == funding_spent.current_holder_commitment_tx.trust().txid() { scan_commitment!( funding_spent, holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES), - "our latest holder commitment tx", true + "our latest holder commitment tx", true, false ); } if let Some(prev_holder_commitment_tx) = funding_spent.prev_holder_commitment_tx.as_ref() { if input.previous_output.txid == prev_holder_commitment_tx.trust().txid() { scan_commitment!( funding_spent, holder_commitment_htlcs!(self, PREV_WITH_SOURCES).unwrap(), - "our previous holder commitment tx", true + "our previous holder commitment tx", true, false ); } } if let Some(ref htlc_outputs) = funding_spent.counterparty_claimable_outpoints.get(&input.previous_output.txid) { + let mut spent_counterparty_revoked = true; + if funding_spent.current_counterparty_commitment_txid == Some(input.previous_output.txid) { + spent_counterparty_revoked = false; + } + if funding_spent.prev_counterparty_commitment_txid == Some(input.previous_output.txid) { + spent_counterparty_revoked = false; + } let htlcs = htlc_outputs.iter() .map(|&(ref a, ref b)| (a, b.as_ref().map(|boxed| &**boxed))); - scan_commitment!(funding_spent, htlcs, "counterparty commitment tx", false); + scan_commitment!(funding_spent, htlcs, "counterparty commitment tx", false, spent_counterparty_revoked); } // Check that scan_commitment, above, decided there is some source worth relaying an @@ -6371,7 +6359,7 @@ impl ChannelMonitorImpl { /// own. #[rustfmt::skip] fn check_tx_and_push_spendable_outputs( - &mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &WithChannelMonitor, + &mut self, tx: &Transaction, height: u32, block_hash: &BlockHash, logger: &WithContext, ) where L::Target: Logger { let funding_spent = get_confirmed_funding_scope!(self); for spendable_output in self.get_spendable_outputs(funding_spent, tx) { diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index fb65aa0f157..6b53e694ebc 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -1163,14 +1163,24 @@ impl OnchainTxHandler { if let Some(pending_claim) = self.claimable_outpoints.get(package.outpoints()[0]) { if let Some(request) = self.pending_claim_requests.get_mut(&pending_claim.0) { - assert!(request.merge_package(package, new_best_height + 1).is_ok()); - // Using a HashMap guarantee us than if we have multiple outpoints getting - // resurrected only one bump claim tx is going to be broadcast - bump_candidates.insert(pending_claim.clone(), request.clone()); + if request.can_merge_with(&package, new_best_height + 1) { + assert!(request.merge_package(package, new_best_height + 1).is_ok()); + // Using a HashMap guarantee us than if we have multiple outpoints getting + // resurrected only one bump claim tx is going to be broadcast + bump_candidates.insert(pending_claim.clone(), request.clone()); + continue; + } } } + // While we really should push the new package through the generation below + // (as it is not, in fact, locktimed to a later height), to avoid additional + // complexity we shove the package in the locktimed set instead, which will + // result in it getting handled when the reorg processing finishes and the + // new tip is connected. + let package_locktime = package.package_locktime(0); + self.locktimed_packages.entry(package_locktime).or_default().push(package); }, - _ => {}, + OnchainEvent::Claim { .. } => {}, } } else { self.onchain_events_awaiting_threshold_conf.push(entry); diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index 11f008612a2..e8c1350e862 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -74,6 +74,7 @@ impl AnchorDescriptor { chan_utils::get_keyed_anchor_redeemscript( &channel_params.broadcaster_pubkeys().funding_pubkey, ) + .to_p2wsh() } else { assert!(tx_params.channel_type_features.supports_anchor_zero_fee_commitments()); shared_anchor_script_pubkey() @@ -1383,4 +1384,27 @@ mod tests { pending_htlcs: Vec::new(), }); } + + #[test] + fn test_anchor_descriptor_previous_utxo_script_pubkey_uses_p2wsh() { + let mut transaction_parameters = ChannelTransactionParameters::test_dummy(42_000_000); + transaction_parameters.channel_type_features = + ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + + let funding_pubkey = transaction_parameters.holder_pubkeys.funding_pubkey; + let expected_script_pubkey = + chan_utils::get_keyed_anchor_redeemscript(&funding_pubkey).to_p2wsh(); + + let anchor_descriptor = AnchorDescriptor { + channel_derivation_parameters: ChannelDerivationParameters { + value_satoshis: 42_000_000, + keys_id: [42; 32], + transaction_parameters, + }, + outpoint: OutPoint::null(), + value: Amount::from_sat(ANCHOR_OUTPUT_VALUE_SATOSHI), + }; + + assert_eq!(anchor_descriptor.previous_utxo().script_pubkey, expected_script_pubkey); + } } diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index b9c4b1ca1ef..c0f252dbff1 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -25,8 +25,9 @@ use crate::blinded_path::payment::{ use crate::chain::transaction; use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS; use crate::ln::channelmanager::{InterceptId, PaymentId, RecipientOnionFields}; +use crate::ln::msgs; +use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::ln::types::ChannelId; -use crate::ln::{msgs, LocalHTLCFailureReason}; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_request::InvoiceRequest; use crate::offers::static_invoice::StaticInvoice; @@ -1048,7 +1049,8 @@ pub enum Event { /// If the recipient or an intermediate node misbehaves and gives us free money, this may /// overstate the amount paid, though this is unlikely. /// - /// This is only `None` for payments initiated on LDK versions prior to 0.0.103. + /// This is only `None` for payments abandoned but ultimately claimed when using LDK versions + /// prior to 0.3, 0.2.3, or 0.1.10. /// /// [`Route::get_total_fees`]: crate::routing::router::Route::get_total_fees fee_paid_msat: Option, diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 0496febc761..4180bf46f9b 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -2334,6 +2334,25 @@ fn refresh_static_invoices_for_used_offers() { .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); assert_eq!(recipient.node.flow.test_get_async_receive_offers().len(), 1); + // The invoice was just refreshed and persisted. A later timer tick must wait until the next + // refresh threshold before generating another invoice for the same offer. + recipient.node.timer_tick_occurred(); + let pending_oms_after = recipient.onion_messenger.release_pending_msgs(); + let mut extra_serve_invoices = 0; + if let Some(msgs) = pending_oms_after.get(&server.node.get_our_node_id()) { + for msg in msgs { + if let PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) = + server.onion_messenger.peel_onion_message(&msg).unwrap() + { + extra_serve_invoices += 1; + } + } + } + assert_eq!( + extra_serve_invoices, 0, + "used offer invoice was refreshed again immediately after a successful refresh" + ); + // Remove the peer restriction added above. server.message_router.peers_override.lock().unwrap().clear(); recipient.message_router.peers_override.lock().unwrap().clear(); diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs index 03728e28222..691a54d6e56 100644 --- a/lightning/src/ln/async_signer_tests.rs +++ b/lightning/src/ln/async_signer_tests.rs @@ -15,7 +15,7 @@ use bitcoin::secp256k1::Secp256k1; use crate::chain::channelmonitor::LATENCY_GRACE_PERIOD_BLOCKS; use crate::chain::ChannelMonitorUpdateStatus; -use crate::events::{ClosureReason, Event}; +use crate::events::{ClosureReason, Event, HTLCHandlingFailureType}; use crate::ln::chan_utils::ClosingTransaction; use crate::ln::channel::DISCONNECT_PEER_AWAITING_RESPONSE_TICKS; use crate::ln::channel_state::{ChannelDetails, ChannelShutdownState}; @@ -498,6 +498,211 @@ fn test_async_raa_peer_disconnect() { do_test_async_raa_peer_disconnect(UnblockSignerAcrossDisconnectCase::BeforeReestablish, false); } +#[test] +fn test_signer_unblocked_clears_monitor_pending_raa_after_reestablish() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + + create_announced_chan_between_nodes(&nodes, 0, 1); + let chan_bc = create_announced_chan_between_nodes(&nodes, 1, 2); + + // Rebalance so that node C can send a payment back through node B later in the test. + send_payment(&nodes[0], &[&nodes[1], &nodes[2]], 5_000_000); + + // Put the B-C channel into AwaitingRAA by having C fail a payment backwards and retaining C's + // final RAA instead of delivering it to B immediately. + let (_, payment_hash_1, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 1_000_000); + nodes[2].node.fail_htlc_backwards(&payment_hash_1); + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[2], + &[HTLCHandlingFailureType::Receive { payment_hash: payment_hash_1 }], + ); + check_added_monitors(&nodes[2], 1); + + let updates = get_htlc_update_msgs(&nodes[2], &node_b_id); + assert!(updates.update_add_htlcs.is_empty()); + assert_eq!(updates.update_fail_htlcs.len(), 1); + assert!(updates.update_fail_malformed_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + nodes[1].node.handle_update_fail_htlc(node_c_id, &updates.update_fail_htlcs[0]); + + let pending_c_raa = + commitment_signed_dance!(&nodes[1], &nodes[2], &updates.commitment_signed, false, true, false, true); + check_added_monitors(&nodes[0], 0); + + // While B is waiting for C's RAA, forward another A-to-C payment. B accepts it on the A-B + // channel, but cannot forward it over B-C yet, so it is held in B's holding cell. + let (route, payment_hash_2, _payment_preimage_2, payment_secret_2) = + get_route_and_payment_hash!(nodes[0], nodes[2], 1_000_000); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let id_2 = PaymentId(payment_hash_2.0); + nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); + check_added_monitors(&nodes[0], 1); + + let send_event = SendEvent::from_node(&nodes[0]); + assert_eq!(send_event.node_id, node_b_id); + assert_eq!(send_event.msgs.len(), 1); + nodes[1].node.handle_update_add_htlc(node_a_id, &send_event.msgs[0]); + do_commitment_signed_dance(&nodes[1], &nodes[0], &send_event.commitment_msg, false, false); + + expect_and_process_pending_htlcs(&nodes[1], false); + check_added_monitors(&nodes[1], 0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Now make B owe C an RAA whose monitor update has already completed, but whose RAA cannot be + // constructed because B's signer is unavailable. + let (route, payment_hash_3, _payment_preimage_3, payment_secret_3) = + get_route_and_payment_hash!(nodes[2], nodes[0], 1_000_000); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3); + let id_3 = PaymentId(payment_hash_3.0); + nodes[2].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); + check_added_monitors(&nodes[2], 1); + + let send_event = SendEvent::from_node(&nodes[2]); + assert_eq!(send_event.node_id, node_b_id); + assert_eq!(send_event.msgs.len(), 1); + nodes[1].node.handle_update_add_htlc(node_c_id, &send_event.msgs[0]); + nodes[1].disable_channel_signer_op(&node_c_id, &chan_bc.2, SignerOp::ReleaseCommitmentSecret); + nodes[1].node.handle_commitment_signed_batch_test(node_c_id, &send_event.commitment_msg); + check_added_monitors(&nodes[1], 1); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Deliver C's earlier RAA to B while monitor updating is blocked. This frees B's holding-cell + // HTLC and leaves a monitor update in flight. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.handle_revoke_and_ack(node_c_id, &pending_c_raa); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + check_added_monitors(&nodes[1], 1); + + nodes[1].node.peer_disconnected(node_c_id); + nodes[2].node.peer_disconnected(node_b_id); + + let init_msg = msgs::Init { + features: nodes[2].node.init_features(), + networks: None, + remote_network_address: None, + }; + nodes[1].node.peer_connected(node_c_id, &init_msg, true).unwrap(); + let bs_reestablish = get_chan_reestablish_msgs!(nodes[1], nodes[2]); + assert_eq!(bs_reestablish.len(), 1); + let init_msg = msgs::Init { + features: nodes[1].node.init_features(), + networks: None, + remote_network_address: None, + }; + nodes[2].node.peer_connected(node_b_id, &init_msg, false).unwrap(); + let cs_reestablish = get_chan_reestablish_msgs!(nodes[2], nodes[1]); + assert_eq!(cs_reestablish.len(), 1); + + nodes[1].node.handle_channel_reestablish(node_c_id, &cs_reestablish[0]); + + // The signer-pending path now generates the owed RAA before the held monitor update + // completes. + nodes[1].enable_channel_signer_op(&node_c_id, &chan_bc.2, SignerOp::ReleaseCommitmentSecret); + nodes[1].node.signer_unblocked(Some((node_c_id, chan_bc.2))); + let (_, signer_revoke_and_ack, signer_commitment_update, _, _, _, _, _) = + handle_chan_reestablish_msgs!(nodes[1], nodes[2]); + assert!(signer_revoke_and_ack.is_some()); + + // Once the held monitor update completes, B must not generate the same RAA a second time via + // the monitor-pending path. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_bc.2); + nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_bc.2, latest_update); + check_added_monitors(&nodes[1], 0); + let (_, duplicate_revoke_and_ack, monitor_commitment_update, _, _, _, _, _) = + handle_chan_reestablish_msgs!(nodes[1], nodes[2]); + assert!(duplicate_revoke_and_ack.is_none()); + + nodes[2].node.handle_channel_reestablish(node_b_id, &bs_reestablish[0]); + let (_, c_revoke_and_ack, c_commitment_update, _, _, _, _, _) = + handle_chan_reestablish_msgs!(nodes[2], nodes[1]); + assert!(c_revoke_and_ack.is_none()); + assert!(c_commitment_update.is_none()); + + nodes[2].node.handle_revoke_and_ack(node_b_id, &signer_revoke_and_ack.unwrap()); + check_added_monitors(&nodes[2], 1); + + let commitment_update = signer_commitment_update.or(monitor_commitment_update); + if let Some(commitment_update) = commitment_update { + let send_event = SendEvent::from_commitment_update(node_c_id, chan_bc.2, commitment_update); + assert_eq!(send_event.node_id, node_c_id); + for update_add in send_event.msgs { + nodes[2].node.handle_update_add_htlc(node_b_id, &update_add); + } + nodes[2].node.handle_commitment_signed_batch_test(node_b_id, &send_event.commitment_msg); + check_added_monitors(&nodes[2], 1); + let (c_raa, c_commitment_signed) = get_revoke_commit_msgs(&nodes[2], &node_b_id); + nodes[1].node.handle_revoke_and_ack(node_c_id, &c_raa); + check_added_monitors(&nodes[1], 1); + nodes[1].node.handle_commitment_signed_batch_test(node_c_id, &c_commitment_signed); + check_added_monitors(&nodes[1], 1); + let b_raa = get_event_msg!(nodes[1], MessageSendEvent::SendRevokeAndACK, node_c_id); + nodes[2].node.handle_revoke_and_ack(node_b_id, &b_raa); + check_added_monitors(&nodes[2], 1); + } + + let (route, final_payment_hash, _final_payment_preimage, final_payment_secret) = + get_route_and_payment_hash!(nodes[1], nodes[2], 100_000); + let final_payment_id = PaymentId(final_payment_hash.0); + nodes[1] + .node + .send_payment_with_route( + route, + final_payment_hash, + RecipientOnionFields::secret_only(final_payment_secret), + final_payment_id, + ) + .unwrap(); + check_added_monitors(&nodes[1], 1); + let final_payment_event = nodes[1].node.get_and_clear_pending_msg_events().remove(0); + match &final_payment_event { + MessageSendEvent::UpdateHTLCs { node_id, .. } => assert_eq!(*node_id, node_c_id), + _ => panic!("Unexpected event"), + } + do_pass_along_path( + PassAlongPathArgs::new( + &nodes[1], + &[&nodes[2]], + 100_000, + final_payment_hash, + final_payment_event, + ) + .with_payment_secret(final_payment_secret) + .without_clearing_recipient_events(), + ); + + let claimable_events = nodes[2].node.get_and_clear_pending_events(); + let final_claimable = claimable_events + .iter() + .find(|event| { + matches!( + event, + Event::PaymentClaimable { payment_hash, .. } if *payment_hash == final_payment_hash + ) + }) + .unwrap(); + check_payment_claimable( + final_claimable, + final_payment_hash, + final_payment_secret, + 100_000, + None, + node_c_id, + ); + expect_htlc_failure_conditions( + nodes[1].node.get_and_clear_pending_events(), + &[HTLCHandlingFailureType::Forward { node_id: Some(node_c_id), channel_id: chan_bc.2 }], + ); +} + fn do_test_async_raa_peer_disconnect( test_case: UnblockSignerAcrossDisconnectCase, raa_blocked_by_commit_point: bool, ) { diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index 13b39f6cabd..3af8ad5fc14 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -192,7 +192,7 @@ pub fn aggregated_htlc_timeout_input_output_pair_weight( } /// Describes the type of HTLC claim as determined by analyzing the witness. -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Debug)] pub enum HTLCClaim { /// Claims an offered output on a commitment transaction through the timeout path. OfferedTimeout, diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index 96ae8f922ac..cf3d2aa1c1c 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -48,13 +48,6 @@ use crate::prelude::*; use crate::sync::{Arc, Mutex}; use bitcoin::hashes::Hash; -fn get_latest_mon_update_id<'a, 'b, 'c>( - node: &Node<'a, 'b, 'c>, channel_id: ChannelId, -) -> (u64, u64) { - let monitor_id_state = node.chain_monitor.latest_monitor_update_id.lock().unwrap(); - monitor_id_state.get(&channel_id).unwrap().clone() -} - #[test] fn test_monitor_and_persister_update_fail() { // Test that if both updating the `ChannelMonitor` and persisting the updated @@ -212,7 +205,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { } chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[0], 0); @@ -404,7 +397,7 @@ fn do_test_monitor_temporary_update_fail(disconnect_count: usize) { // Now fix monitor updating... chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[0], 0); @@ -757,7 +750,7 @@ fn test_monitor_update_fail_cs() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[1], 0); let responses = nodes[1].node.get_and_clear_pending_msg_events(); @@ -792,7 +785,7 @@ fn test_monitor_update_fail_cs() { } chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[0], 0); @@ -868,7 +861,7 @@ fn test_monitor_update_fail_no_rebroadcast() { check_added_monitors!(nodes[1], 1); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors!(nodes[1], 0); @@ -938,7 +931,7 @@ fn test_monitor_update_raa_while_paused() { assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors!(nodes[0], 1); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[0], 0); @@ -1080,7 +1073,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // Restore monitor updating, ensuring we immediately get a fail-back update and a // update_add update. chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_2.2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_2.2); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_2.2, latest_update); check_added_monitors!(nodes[1], 0); expect_and_process_pending_htlcs_and_htlc_handling_failed( @@ -1354,7 +1347,7 @@ fn test_monitor_update_fail_reestablish() { assert_eq!(bs_channel_upd.contents.channel_flags & 2, 0); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_1.2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_1.2); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_1.2, latest_update); check_added_monitors!(nodes[1], 0); @@ -1439,7 +1432,7 @@ fn raa_no_response_awaiting_raa_state() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors!(nodes[1], 1); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); // nodes[1] should be AwaitingRAA here! check_added_monitors!(nodes[1], 0); @@ -1568,7 +1561,7 @@ fn claim_while_disconnected_monitor_update_fail() { // Now un-fail the monitor, which will result in B sending its original commitment update, // receiving the commitment update from A, and the resulting commitment dances. chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[1], 0); @@ -1697,7 +1690,7 @@ fn monitor_failed_no_reestablish_response() { get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, node_b_id); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[1], 0); let bs_responses = get_revoke_commit_msgs!(nodes[1], node_a_id); @@ -1795,7 +1788,7 @@ fn first_message_on_recv_ordering() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[1], 0); @@ -1894,7 +1887,7 @@ fn test_monitor_update_fail_claim() { // Now restore monitor updating on the 0<->1 channel and claim the funds on B. let channel_id = chan_1.2; - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); expect_payment_claimed!(nodes[1], payment_hash_1, 1_000_000); check_added_monitors!(nodes[1], 0); @@ -2022,7 +2015,7 @@ fn test_monitor_update_on_pending_forwards() { check_added_monitors!(nodes[1], 1); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_1.2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_1.2); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_1.2, latest_update); check_added_monitors!(nodes[1], 0); @@ -2093,7 +2086,7 @@ fn monitor_update_claim_fail_no_response() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); expect_payment_claimed!(nodes[1], payment_hash_1, 1_000_000); check_added_monitors!(nodes[1], 0); @@ -2165,7 +2158,7 @@ fn do_during_funding_monitor_fail( assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[0], 0); expect_channel_pending_event(&nodes[0], &node_b_id); @@ -2220,7 +2213,7 @@ fn do_during_funding_monitor_fail( } chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); check_added_monitors!(nodes[1], 0); @@ -2338,7 +2331,7 @@ fn test_path_paused_mpp() { // And check that, after we successfully update the monitor for chan_2 we can pass the second // HTLC along to nodes[3] and claim the whole payment back to nodes[0]. - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], chan_2_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(chan_2_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_2_id, latest_update); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -2784,7 +2777,7 @@ fn do_channel_holding_cell_serialize(disconnect: bool, reload_a: bool) { // If we finish updating the monitor, we should free the holding cell right away (this did // not occur prior to #756). chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (mon_id, _) = get_latest_mon_update_id(&nodes[0], chan_id); + let (mon_id, _) = nodes[0].chain_monitor.get_latest_mon_update_id(chan_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id, mon_id); expect_payment_claimed!(nodes[0], payment_hash_0, 100_000); @@ -3049,7 +3042,7 @@ fn test_temporary_error_during_shutdown() { chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[0], channel_id); + let (latest_update, _) = nodes[0].chain_monitor.get_latest_mon_update_id(channel_id); nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); nodes[1].node.handle_closing_signed( node_a_id, @@ -3059,7 +3052,7 @@ fn test_temporary_error_during_shutdown() { assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update); nodes[0].node.handle_closing_signed( @@ -3104,8 +3097,8 @@ fn double_temp_error() { chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); // `claim_funds` results in a ChannelMonitorUpdate. nodes[1].node.claim_funds(payment_preimage_1); - check_added_monitors!(nodes[1], 1); - let (latest_update_1, _) = get_latest_mon_update_id(&nodes[1], channel_id); + check_added_monitors(&nodes[1], 1); + let (latest_update_1, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); // Previously, this would've panicked due to a double-call to `Channel::monitor_update_failed`, @@ -3114,7 +3107,7 @@ fn double_temp_error() { check_added_monitors!(nodes[1], 1); chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); - let (latest_update_2, _) = get_latest_mon_update_id(&nodes[1], channel_id); + let (latest_update_2, _) = nodes[1].chain_monitor.get_latest_mon_update_id(channel_id); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(channel_id, latest_update_1); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); check_added_monitors!(nodes[1], 0); @@ -3521,7 +3514,7 @@ fn do_test_blocked_chan_preimage_release(completion_mode: BlockedUpdateComplMode reconnect_nodes(a_b_reconnect); reconnect_nodes(ReconnectArgs::new(&nodes[2], &nodes[1])); } else if completion_mode == BlockedUpdateComplMode::Async { - let (latest_update, _) = get_latest_mon_update_id(&nodes[1], chan_id_2); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_2); nodes[1] .chain_monitor .chain_monitor @@ -3701,7 +3694,7 @@ fn do_test_inverted_mon_completion_order( // (Finally) complete the A <-> B ChannelMonitorUpdate, ensuring the preimage is durably on // disk in the proper ChannelMonitor, unblocking the B <-> C ChannelMonitor updating // process. - let (_, ab_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); nodes[1] .chain_monitor .chain_monitor @@ -3734,7 +3727,7 @@ fn do_test_inverted_mon_completion_order( // ChannelMonitorUpdate hasn't yet completed. reconnect_nodes(ReconnectArgs::new(&nodes[0], &nodes[1])); - let (_, ab_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); nodes[1] .chain_monitor .chain_monitor @@ -3947,7 +3940,7 @@ fn do_test_durable_preimages_on_closed_channel( // Once the blocked `ChannelMonitorUpdate` *finally* completes, the pending // `PaymentForwarded` event will finally be released. - let (_, ab_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id_ab, ab_update_id); // If the A<->B channel was closed before we reload, we'll replay the claim against it on @@ -4059,7 +4052,7 @@ fn do_test_reload_mon_update_completion_actions(close_during_reload: bool) { mine_transaction_without_consistency_checks(&nodes[1], &as_closing_tx[0]); } - let (_, bc_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_bc); + let (_, bc_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_bc); let mut events = nodes[1].node.get_and_clear_pending_events(); assert_eq!(events.len(), if close_during_reload { 2 } else { 1 }); expect_payment_forwarded( @@ -4084,7 +4077,7 @@ fn do_test_reload_mon_update_completion_actions(close_during_reload: bool) { // Once we run event processing the monitor should free, check that it was indeed the B<->C // channel which was updated. check_added_monitors(&nodes[1], if close_during_reload { 2 } else { 1 }); - let (_, post_ev_bc_update_id) = get_latest_mon_update_id(&nodes[1], chan_id_bc); + let (_, post_ev_bc_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_bc); assert!(bc_update_id != post_ev_bc_update_id); // Finally, check that there's nothing left to do on B<->C reconnect and the channel operates @@ -4173,7 +4166,7 @@ fn do_test_glacial_peer_cant_hang(hold_chan_a: bool) { // ...but once we complete the A<->B channel preimage persistence, the B<->C channel // unlocks and we send both peers commitment updates. - let (ab_update_id, _) = get_latest_mon_update_id(&nodes[1], chan_id_ab); + let (ab_update_id, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); assert!(nodes[1] .chain_monitor .chain_monitor @@ -5130,7 +5123,7 @@ fn test_mpp_claim_to_holding_cell() { check_added_monitors(&nodes[3], 2); // Complete the B <-> D monitor update, freeing the first fulfill. - let (latest_id, _) = get_latest_mon_update_id(&nodes[3], chan_3_id); + let (latest_id, _) = nodes[3].chain_monitor.get_latest_mon_update_id(chan_3_id); nodes[3].chain_monitor.chain_monitor.channel_monitor_updated(chan_3_id, latest_id).unwrap(); let mut b_claim = get_htlc_update_msgs(&nodes[3], &node_b_id); @@ -5141,7 +5134,7 @@ fn test_mpp_claim_to_holding_cell() { // Finally, complete the C <-> D monitor update. Previously, this unlock failed to be processed // due to the existence of the blocked RAA update above. - let (latest_id, _) = get_latest_mon_update_id(&nodes[3], chan_4_id); + let (latest_id, _) = nodes[3].chain_monitor.get_latest_mon_update_id(chan_4_id); nodes[3].chain_monitor.chain_monitor.channel_monitor_updated(chan_4_id, latest_id).unwrap(); // Once we process monitor events (in this case by checking for the `PaymentClaimed` event, the // RAA monitor update blocked above will be released. diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index e47b5492efd..abe991ce88a 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -910,6 +910,10 @@ pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS pub const MIN_CHAN_DUST_LIMIT_SATOSHIS: u64 = 354; // Just a reasonable implementation-specific safe lower bound, higher than the dust limit. +// Deprecated: This constant is kept for backward compatibility. +// The minimum channel reserve is now configurable via `ChannelHandshakeConfig::min_their_channel_reserve_satoshis`. +// This constant retains its original value for API compatibility, but the actual behavior uses the config value. +#[allow(dead_code)] pub const MIN_THEIR_CHAN_RESERVE_SATOSHIS: u64 = 1000; /// Used to return a simple Error back to ChannelManager. Will get converted to a @@ -1959,9 +1963,12 @@ where let tx_abort = should_ack.then(|| { let logger = WithChannelContext::from(logger, &self.context(), None); - let reason = - types::string::UntrustedString(String::from_utf8_lossy(&msg.data).to_string()); - log_info!(logger, "Counterparty failed interactive transaction negotiation: {reason}"); + let reason = String::from_utf8_lossy(&msg.data); + log_info!( + logger, + "Counterparty failed interactive transaction negotiation: {}", + log_msg!(reason) + ); msgs::TxAbort { channel_id: msg.channel_id, data: "Acknowledged tx_abort".to_string().into_bytes(), @@ -2517,12 +2524,12 @@ impl FundingScope { prev_funding: &Self, context: &ChannelContext, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, counterparty_funding_pubkey: PublicKey, our_new_holder_keys: ChannelPublicKeys, - ) -> Self + ) -> Result where SP::Target: SignerProvider, { - debug_assert!(our_funding_contribution.abs() <= SignedAmount::MAX_MONEY); - debug_assert!(their_funding_contribution.abs() <= SignedAmount::MAX_MONEY); + debug_assert!(our_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); + debug_assert!(their_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); let post_channel_value = prev_funding.compute_post_splice_value( our_funding_contribution.to_sat(), @@ -2532,9 +2539,15 @@ impl FundingScope { let post_value_to_self_msat = AddSigned::checked_add_signed( prev_funding.value_to_self_msat, our_funding_contribution.to_sat() * 1000, - ); - debug_assert!(post_value_to_self_msat.is_some()); - let post_value_to_self_msat = post_value_to_self_msat.unwrap(); + ) + .ok_or_else(|| { + // This should have been caught in `validate_splice_contributions` + debug_assert!(false); + String::from( + "Adding our funding contribution to our balance overflowed. \ + This should never happen! Please report this bug.", + ) + })?; let channel_parameters = &prev_funding.channel_transaction_parameters; let mut post_channel_transaction_parameters = ChannelTransactionParameters { @@ -2556,14 +2569,35 @@ impl FundingScope { .funding_pubkey = counterparty_funding_pubkey; // New reserve values are based on the new channel value and are v2-specific - let counterparty_selected_channel_reserve_satoshis = - Some(get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS)); + let counterparty_selected_channel_reserve_satoshis = Some( + get_v2_channel_reserve_satoshis(post_channel_value, context.holder_dust_limit_satoshis) + .map_err(|()| { + // This should have been caught in `validate_splice_contributions` + debug_assert!(false); + format!( + "The post-splice channel value {post_channel_value} \ + is smaller than our dust limit {}. \ + This should never happen! Please report this bug.", + context.holder_dust_limit_satoshis, + ) + })?, + ); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( post_channel_value, context.counterparty_dust_limit_satoshis, - ); + ) + .map_err(|()| { + // This should have been caught in `validate_splice_contributions` + debug_assert!(false); + format!( + "The post-splice channel value {post_channel_value} is smaller than \ + their dust limit {}. \ + This should never happen! Please report this bug.", + context.counterparty_dust_limit_satoshis, + ) + })?; - Self { + Ok(Self { channel_transaction_parameters: post_channel_transaction_parameters, value_to_self_msat: post_value_to_self_msat, funding_transaction: None, @@ -2587,7 +2621,7 @@ impl FundingScope { funding_tx_confirmed_in: None, minimum_depth_override: None, short_channel_id: None, - } + }) } /// Compute the post-splice channel value from each counterparty's contributions. @@ -2985,6 +3019,9 @@ where /// We use this to close if funding is never broadcasted. pub(super) channel_creation_height: u32, + #[cfg(any(test, feature = "_test_utils"))] + pub(crate) counterparty_dust_limit_satoshis: u64, + #[cfg(not(any(test, feature = "_test_utils")))] counterparty_dust_limit_satoshis: u64, #[cfg(any(test, feature = "_test_utils"))] @@ -3010,6 +3047,7 @@ where counterparty_max_accepted_htlcs: u16, holder_max_accepted_htlcs: u16, minimum_depth: Option, + splice_minimum_depth: Option, counterparty_forwarding_info: Option, @@ -3456,9 +3494,10 @@ where } } - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - // Protocol level safety check in place, although it should never happen because - // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` + // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case) + if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS + && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 { + // Protocol level safety check in place return Err(ChannelError::close(format!("Suitable channel reserve not found. remote_channel_reserve was ({}). dust_limit_satoshis is ({}).", holder_selected_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS))); } if holder_selected_channel_reserve_satoshis * 1000 >= full_channel_value_msat { @@ -3468,7 +3507,9 @@ where log_debug!(logger, "channel_reserve_satoshis ({}) is smaller than our dust limit ({}). We can broadcast stale states without any risk, implying this channel is very insecure for our counterparty.", msg_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); } - if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis { + // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case) + if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis + && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 { return Err(ChannelError::close(format!("Dust limit ({}) too high for the channel reserve we require the remote to keep ({})", open_channel_fields.dust_limit_satoshis, holder_selected_channel_reserve_satoshis))); } @@ -3651,6 +3692,7 @@ where counterparty_max_accepted_htlcs: open_channel_fields.max_accepted_htlcs, holder_max_accepted_htlcs: cmp::min(config.channel_handshake_config.our_max_accepted_htlcs, max_htlcs(&channel_type)), minimum_depth, + splice_minimum_depth: config.channel_handshake_config.splice_minimum_depth, counterparty_forwarding_info: None, @@ -3891,6 +3933,7 @@ where counterparty_max_accepted_htlcs: 0, holder_max_accepted_htlcs: cmp::min(config.channel_handshake_config.our_max_accepted_htlcs, max_htlcs(&channel_type)), minimum_depth: None, // Filled in in accept_channel + splice_minimum_depth: config.channel_handshake_config.splice_minimum_depth, counterparty_forwarding_info: None, @@ -4181,7 +4224,9 @@ where if channel_reserve_satoshis > funding.get_value_satoshis() { return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", channel_reserve_satoshis, funding.get_value_satoshis()))); } - if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis { + // Allow bypassing dust limit when holder_selected_channel_reserve_satoshis is 0 (LSP use case) + if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis + && funding.holder_selected_channel_reserve_satoshis > 0 { return Err(ChannelError::close(format!("Dust limit ({}) is bigger than our channel reserve ({})", common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis))); } if channel_reserve_satoshis > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis { @@ -6434,6 +6479,19 @@ fn get_holder_max_htlc_value_in_flight_msat( channel_value_satoshis * 10 * configured_percent } +fn rescale_max_htlc_value_in_flight_msat( + max_htlc_value_in_flight_msat: u64, old_channel_value_satoshis: u64, + new_channel_value_satoshis: u64, +) -> u64 { + debug_assert_ne!(old_channel_value_satoshis, 0); + if old_channel_value_satoshis == 0 { + return max_htlc_value_in_flight_msat; + } + + ((max_htlc_value_in_flight_msat as u128 * new_channel_value_satoshis as u128) + / old_channel_value_satoshis as u128) as u64 +} + /// Returns a minimum channel reserve value the remote needs to maintain, /// required by us according to the configured or default /// [`ChannelHandshakeConfig::their_channel_reserve_proportional_millionths`] @@ -6441,15 +6499,26 @@ fn get_holder_max_htlc_value_in_flight_msat( /// Guaranteed to return a value no larger than channel_value_satoshis /// /// This is used both for outbound and inbound channels and has lower bound -/// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`. +/// of `ChannelHandshakeConfig::min_their_channel_reserve_satoshis`. +/// +/// Returns `Err` if `channel_value_satoshis` is smaller than +/// `ChannelHandshakeConfig::min_their_channel_reserve_satoshis`. pub(crate) fn get_holder_selected_channel_reserve_satoshis( channel_value_satoshis: u64, config: &UserConfig, -) -> u64 { - let counterparty_chan_reserve_prop_mil = - config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64; +) -> Result { + let min_their_channel_reserve_satoshis = + config.channel_handshake_config.min_their_channel_reserve_satoshis; + if channel_value_satoshis < min_their_channel_reserve_satoshis { + return Err(()); + } + // As described in the `ChannelHandshakeConfig` docs, we cap this value at 1_000_000. + let counterparty_chan_reserve_prop_mil = cmp::min( + config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64, + 1_000_000, + ); let calculated_reserve = channel_value_satoshis.saturating_mul(counterparty_chan_reserve_prop_mil) / 1_000_000; - cmp::min(channel_value_satoshis, cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS)) + Ok(cmp::max(calculated_reserve, min_their_channel_reserve_satoshis)) } /// This is for legacy reasons, present for forward-compatibility. @@ -6466,14 +6535,21 @@ pub(crate) fn get_legacy_default_holder_selected_channel_reserve_satoshis( /// Returns a minimum channel reserve value each party needs to maintain, fixed in the spec to a /// default of 1% of the total channel value. /// -/// Guaranteed to return a value no larger than channel_value_satoshis +/// Guaranteed to return a value no larger than `channel_value_satoshis` /// /// This is used both for outbound and inbound channels and has lower bound /// of `dust_limit_satoshis`. -fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satoshis: u64) -> u64 { +/// +/// Returns `Err` if `channel_value_satoshis` is smaller than `dust_limit_satoshis`. +fn get_v2_channel_reserve_satoshis( + channel_value_satoshis: u64, dust_limit_satoshis: u64, +) -> Result { + if channel_value_satoshis < dust_limit_satoshis { + return Err(()); + } // Fixed at 1% of channel value by spec. let (q, _) = channel_value_satoshis.overflowing_div(100); - cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) + Ok(cmp::max(q, dust_limit_satoshis)) } fn check_splice_contribution_sufficient( @@ -8876,7 +8952,6 @@ where update_fail_htlcs.len() + update_fail_malformed_htlcs.len(), &self.context.channel_id); } else { - debug_assert!(htlcs_to_fail.is_empty()); let reason = if self.context.channel_state.is_local_stfu_sent() { "exits quiescence" } else if self.context.channel_state.is_monitor_update_in_progress() { @@ -8931,11 +9006,17 @@ where if let Some(pending_splice) = self.pending_splice.as_mut() { self.context.channel_state.clear_quiescent(); - if let Some(FundingNegotiation::AwaitingSignatures { mut funding, .. }) = + if let Some(FundingNegotiation::AwaitingSignatures { mut funding, is_initiator }) = pending_splice.funding_negotiation.take() { funding.funding_transaction = Some(funding_tx); + if !is_initiator { + if let Some(depth) = self.context.splice_minimum_depth { + funding.minimum_depth_override = Some(depth); + } + } + let funding_txo = funding.get_funding_txo().expect("funding outpoint should be set"); let channel_type = funding.get_channel_type().clone(); @@ -9542,6 +9623,13 @@ where self.context.signer_pending_commitment_update = true; commitment_update = None; } + if revoke_and_ack.is_some() { + // If signer-pending state regenerated an RAA, the monitor update for that RAA was + // already persisted before we set `signer_pending_revoke_and_ack`. Thus, if reconnect + // also marked the same RAA monitor-pending while another monitor update was in flight, + // the RAA we're returning here satisfies that monitor-pending resend. + self.context.monitor_pending_revoke_and_ack = false; + } let (closing_signed, signed_closing_tx, shutdown_result) = if self.context.signer_pending_closing { debug_assert!(self.context.last_sent_closing_fee.is_some()); @@ -9846,6 +9934,7 @@ where // Go ahead and unmark PeerDisconnected as various calls we may make check for it (and all // remaining cases either succeed or ErrorMessage-fail). + log_info!(logger, "channel_reestablish: clearing peer_disconnected flag for channel {} - channel will now be is_live()", self.context.channel_id()); self.context.channel_state.clear_peer_disconnected(); self.mark_response_received(); @@ -11171,12 +11260,41 @@ where .find(|funding| funding.get_funding_txid() == Some(splice_txid)) .unwrap(); let prev_funding_txid = self.funding.get_funding_txid(); + let prev_channel_value_satoshis = self.funding.get_value_satoshis(); if let Some(scid) = self.funding.short_channel_id { self.context.historical_scids.push(scid); } core::mem::swap(&mut self.funding, funding); + let new_channel_value_satoshis = self.funding.get_value_satoshis(); + let prev_holder_max_htlc_value_in_flight_msat = + self.context.holder_max_htlc_value_in_flight_msat; + let prev_counterparty_max_htlc_value_in_flight_msat = + self.context.counterparty_max_htlc_value_in_flight_msat; + self.context.holder_max_htlc_value_in_flight_msat = + rescale_max_htlc_value_in_flight_msat( + self.context.holder_max_htlc_value_in_flight_msat, + prev_channel_value_satoshis, + new_channel_value_satoshis, + ); + self.context.counterparty_max_htlc_value_in_flight_msat = + rescale_max_htlc_value_in_flight_msat( + self.context.counterparty_max_htlc_value_in_flight_msat, + prev_channel_value_satoshis, + new_channel_value_satoshis, + ); + log_info!( + logger, + "Splice promotion HTLC caps for channel {}: holder {} -> {}, counterparty {} -> {}, value {} -> {} sats", + &self.context.channel_id, + prev_holder_max_htlc_value_in_flight_msat, + self.context.holder_max_htlc_value_in_flight_msat, + prev_counterparty_max_htlc_value_in_flight_msat, + self.context.counterparty_max_htlc_value_in_flight_msat, + prev_channel_value_satoshis, + new_channel_value_satoshis, + ); // The swap above places the previous `FundingScope` into `pending_funding`. pending_splice @@ -11954,7 +12072,7 @@ where // Mutually exclusive with prevtx, which is accounted for below. shared_input_txid: None, }; - let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); + let message_len = 2 + MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); if message_len > LN_MAX_MSG_LEN { return Err(APIError::APIMisuseError { err: format!( @@ -12125,20 +12243,21 @@ where let mut new_keys = self.funding.get_holder_pubkeys().clone(); new_keys.funding_pubkey = funding_pubkey; - Ok(FundingScope::for_splice( + FundingScope::for_splice( &self.funding, &self.context, our_funding_contribution, their_funding_contribution, msg.funding_pubkey, new_keys, - )) + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e)) } fn validate_splice_contributions( &self, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, ) -> Result<(), String> { - if our_funding_contribution.abs() > SignedAmount::MAX_MONEY { + if our_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { return Err(format!( "Channel {} cannot be spliced; our {} contribution exceeds the total bitcoin supply", self.context.channel_id(), @@ -12146,7 +12265,7 @@ where )); } - if their_funding_contribution.abs() > SignedAmount::MAX_MONEY { + if their_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { return Err(format!( "Channel {} cannot be spliced; their {} contribution exceeds the total bitcoin supply", self.context.channel_id(), @@ -12164,12 +12283,29 @@ where their_funding_contribution.to_sat(), ); let counterparty_selected_channel_reserve = Amount::from_sat( - get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS), + get_v2_channel_reserve_satoshis( + post_channel_value, + self.context.holder_dust_limit_satoshis, + ) + .map_err(|()| { + format!( + "The post-splice channel value {post_channel_value} is smaller than our dust limit {}", + self.context.holder_dust_limit_satoshis, + ) + })?, + ); + let holder_selected_channel_reserve = Amount::from_sat( + get_v2_channel_reserve_satoshis( + post_channel_value, + self.context.counterparty_dust_limit_satoshis, + ) + .map_err(|()| { + format!( + "The post-splice channel value {post_channel_value} is smaller than their dust limit {}", + self.context.counterparty_dust_limit_satoshis, + ) + })?, ); - let holder_selected_channel_reserve = Amount::from_sat(get_v2_channel_reserve_satoshis( - post_channel_value, - self.context.counterparty_dust_limit_satoshis, - )); // We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve @@ -12386,14 +12522,15 @@ where let mut new_keys = self.funding.get_holder_pubkeys().clone(); new_keys.funding_pubkey = *new_holder_funding_key; - Ok(FundingScope::for_splice( + FundingScope::for_splice( &self.funding, &self.context, our_funding_contribution, their_funding_contribution, msg.funding_pubkey, new_keys, - )) + ) + .map_err(|e| ChannelError::WarnAndDisconnect(e)) } fn get_holder_counterparty_balances_floor_incl_fee( @@ -12600,6 +12737,10 @@ where // disconnected during the time the previous hop was doing the commitment dance we may // end up getting here after the forwarding delay. In any case, returning an // IgnoreError will get ChannelManager to do the right thing and fail backwards now. + log_info!(logger, + "send_htlc: REJECTING HTLC on channel {} - peer_disconnected flag is set. payment_hash: {}, amount: {}msat. Channel was is_usable() at forward_intercepted_htlc time but is NOT is_live() now.", + self.context.channel_id(), payment_hash, amount_msat + ); return Err(( LocalHTLCFailureReason::PeerOffline, "Cannot send an HTLC while disconnected from channel counterparty".to_owned(), @@ -13371,10 +13512,12 @@ where F::Target: FeeEstimator, L::Target: Logger, { - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config); - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - // Protocol level safety check in place, although it should never happen because - // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` + let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config) + .map_err(|()| APIError::APIMisuseError { err: format!("The channel value {channel_value_satoshis} is smaller than {}", config.channel_handshake_config.min_their_channel_reserve_satoshis)})?; + // Allow bypassing dust limit when min_their_channel_reserve_satoshis is explicitly set to 0 (LSP use case) + if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS + && config.channel_handshake_config.min_their_channel_reserve_satoshis > 0 { + // Protocol level safety check in place return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below \ implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) }); } @@ -13743,7 +13886,8 @@ where // support this channel type. let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(msg.common_fields.funding_satoshis, config); + let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(msg.common_fields.funding_satoshis, config) + .map_err(|()| ChannelError::close(format!("The channel value {} is smaller than {}", msg.common_fields.funding_satoshis, config.channel_handshake_config.min_their_channel_reserve_satoshis)))?; let counterparty_pubkeys = ChannelPublicKeys { funding_pubkey: msg.common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint::from(msg.common_fields.revocation_basepoint), @@ -13985,7 +14129,10 @@ where }); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS + ).map_err(|()| APIError::APIMisuseError { err: format!( + "The channel value {funding_satoshis} is smaller than their dust limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}", + )})?; let funding_feerate_sat_per_1000_weight = fee_estimator.bounded_sat_per_1000_weight(funding_confirmation_target); let funding_tx_locktime = LockTime::from_height(current_chain_height) @@ -14132,9 +14279,15 @@ where let channel_value_satoshis = our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis); let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, msg.common_fields.dust_limit_satoshis); + channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS).map_err(|()| ChannelError::close(format!( + "The channel value {channel_value_satoshis} is smaller than our dust limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}" + )))?; + let their_dust_limit_satoshis = msg.common_fields.dust_limit_satoshis; let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + channel_value_satoshis, their_dust_limit_satoshis + ).map_err(|()| ChannelError::close(format!( + "The channel value {channel_value_satoshis} is smaller than their dust limit {their_dust_limit_satoshis}" + )))?; let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; @@ -14843,6 +14996,7 @@ where (65, self.quiescent_action, option), // Added in 0.2 (67, pending_outbound_held_htlc_flags, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2 + (71, self.context.splice_minimum_depth, option), }); Ok(()) @@ -15210,6 +15364,7 @@ where let mut pending_outbound_held_htlc_flags_opt: Option>> = None; let mut holding_cell_held_htlc_flags_opt: Option>> = None; + let mut splice_minimum_depth: Option = None; read_tlv_fields!(reader, { (0, announcement_sigs, option), @@ -15257,6 +15412,7 @@ where (65, quiescent_action, upgradable_option), // Added in 0.2 (67, pending_outbound_held_htlc_flags_opt, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags_opt, optional_vec), // Added in 0.2 + (71, splice_minimum_depth, option), }); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -15646,6 +15802,7 @@ where is_manual_broadcast: is_manual_broadcast.unwrap_or(false), interactive_tx_signing_session, + splice_minimum_depth, }, holder_commitment_point, pending_splice, @@ -16181,13 +16338,17 @@ mod tests { test_self_and_counterparty_channel_reserve(10_000_000, 0.60, 0.30); // Test with calculated channel reserve less than lower bound - // i.e `MIN_THEIR_CHAN_RESERVE_SATOSHIS` + // i.e `ChannelHandshakeConfig::min_their_channel_reserve_satoshis` test_self_and_counterparty_channel_reserve(100_000, 0.00002, 0.30); // Test with invalid channel reserves since sum of both is greater than or equal // to channel value test_self_and_counterparty_channel_reserve(10_000_000, 0.50, 0.50); test_self_and_counterparty_channel_reserve(10_000_000, 0.60, 0.50); + + // Make sure we correctly handle reserves greater than the channel value + test_self_and_counterparty_channel_reserve(100_000, 1.1, 0.30); + test_self_and_counterparty_channel_reserve(100_000, 0.30, 1.1); } #[rustfmt::skip] @@ -16207,7 +16368,19 @@ mod tests { outbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (outbound_selected_channel_reserve_perc * 1_000_000.0) as u32; let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&outbound_node_config), channel_value_satoshis, 100_000, 42, &outbound_node_config, 0, 42, None, &logger).unwrap(); - let expected_outbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * outbound_selected_channel_reserve_perc) as u64); + let outbound_capped_reserve_perc = if outbound_selected_channel_reserve_perc.lt(&1.0) { + outbound_selected_channel_reserve_perc + } else { + 1.0 + }; + + let inbound_capped_reserve_perc = if inbound_selected_channel_reserve_perc.lt(&1.0) { + inbound_selected_channel_reserve_perc + } else { + 1.0 + }; + + let expected_outbound_selected_chan_reserve = cmp::max(outbound_node_config.channel_handshake_config.min_their_channel_reserve_satoshis, (chan.funding.get_value_satoshis() as f64 * outbound_capped_reserve_perc) as u64); assert_eq!(chan.funding.holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve); let chan_open_channel_msg = chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap(); @@ -16217,7 +16390,7 @@ mod tests { if outbound_selected_channel_reserve_perc + inbound_selected_channel_reserve_perc < 1.0 { let chan_inbound_node = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, /*is_0conf=*/false).unwrap(); - let expected_inbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * inbound_selected_channel_reserve_perc) as u64); + let expected_inbound_selected_chan_reserve = cmp::max(inbound_node_config.channel_handshake_config.min_their_channel_reserve_satoshis, (chan.funding.get_value_satoshis() as f64 * inbound_capped_reserve_perc) as u64); assert_eq!(chan_inbound_node.funding.holder_selected_channel_reserve_satoshis, expected_inbound_selected_chan_reserve); assert_eq!(chan_inbound_node.funding.counterparty_selected_channel_reserve_satoshis.unwrap(), expected_outbound_selected_chan_reserve); @@ -16228,6 +16401,62 @@ mod tests { } } + #[test] + #[rustfmt::skip] + fn test_configurable_min_channel_reserve() { + let test_est = TestFeeEstimator::new(15000); + let fee_est = LowerBoundedFeeEstimator::new(&test_est); + let logger = test_utils::TestLogger::new(); + let secp_ctx = Secp256k1::new(); + let keys_provider = test_utils::TestKeysInterface::new(&[42; 32], Network::Testnet); + let outbound_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + + // Test with min_their_channel_reserve_satoshis set to 0 (LSP use case) + let mut config = UserConfig::default(); + config.channel_handshake_config.min_their_channel_reserve_satoshis = 0; + config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0; + + let chan = OutboundV1Channel::<&TestKeysInterface>::new( + &fee_est, &&keys_provider, &&keys_provider, outbound_node_id, + &channelmanager::provided_init_features(&config), + 1_000_000, 100_000, 42, &config, 0, 42, None, &logger + ).unwrap(); + + // With 0 minimum and 0 proportional, reserve should be 0 (bypasses dust limit) + assert_eq!(chan.funding.holder_selected_channel_reserve_satoshis, 0); + + // Test with custom minimum enforced when proportional is lower + config.channel_handshake_config.min_their_channel_reserve_satoshis = 10_000; + config.channel_handshake_config.their_channel_reserve_proportional_millionths = 10_000; // 1% + + let chan_small = OutboundV1Channel::<&TestKeysInterface>::new( + &fee_est, &&keys_provider, &&keys_provider, outbound_node_id, + &channelmanager::provided_init_features(&config), + 100_000, 100_000, 42, &config, 0, 42, None, &logger + ).unwrap(); + + // Proportional would be 1% of 100k = 1000, but minimum is 10000, so 10000 should be used + assert_eq!(chan_small.funding.holder_selected_channel_reserve_satoshis, 10_000); + + // Test that dust limit is still enforced when min_their_channel_reserve_satoshis is non-zero but below dust limit + config.channel_handshake_config.min_their_channel_reserve_satoshis = 100; // Below dust limit of 354 + config.channel_handshake_config.their_channel_reserve_proportional_millionths = 0; + + let result = OutboundV1Channel::<&TestKeysInterface>::new( + &fee_est, &&keys_provider, &&keys_provider, outbound_node_id, + &channelmanager::provided_init_features(&config), + 1_000_000, 100_000, 42, &config, 0, 42, None, &logger + ); + + // Should fail because 100 < 354 (dust limit) and min_their_channel_reserve_satoshis > 0 + assert!(result.is_err()); + if let Err(APIError::APIMisuseError { err }) = result { + assert!(err.contains("dust_limit_satoshis")); + } else { + panic!("Expected APIMisuseError"); + } + } + #[test] #[rustfmt::skip] fn channel_update() { diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index 3fd546aaff7..0292728535c 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -110,7 +110,7 @@ fn test_0conf_limiting() { }; // First, get us up to MAX_UNFUNDED_CHANNEL_PEERS so we can test at the edge - for _ in 0..MAX_UNFUNDED_CHANNEL_PEERS - 1 { + for _ in 0..MAX_UNFUNDED_CHANNEL_PEERS { let random_pk = PublicKey::from_secret_key( &nodes[0].node.secp_ctx, &SecretKey::from_slice(&nodes[1].keys_manager.get_secure_random_bytes()).unwrap(), @@ -183,6 +183,94 @@ fn test_0conf_limiting() { get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, last_random_pk); } +#[test] +fn test_unfunded_channel_peer_limit_multiple_requests() { + // Tests that a peer cannot bypass the `MAX_UNFUNDED_CHANNEL_PEERS` limit by sending us several + // `open_channel` messages in quick succession, before we get a chance to accept any of them. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut settings = test_default_channel_config(); + settings.manually_accept_inbound_channels = true; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(settings)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Note that create_network connects the nodes together for us + let node_b = nodes[1].node.get_our_node_id(); + nodes[0].node.create_channel(node_b, 100_000, 0, 42, None, None).unwrap(); + let mut open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b); + let init_msg = &msgs::Init { + features: nodes[0].node.init_features(), + networks: None, + remote_network_address: None, + }; + + // First, get us up to MAX_UNFUNDED_CHANNEL_PEERS so we can test at the edge + for _ in 0..MAX_UNFUNDED_CHANNEL_PEERS { + let random_pk = PublicKey::from_secret_key( + &nodes[0].node.secp_ctx, + &SecretKey::from_slice(&nodes[1].keys_manager.get_secure_random_bytes()).unwrap(), + ); + nodes[1].node.peer_connected(random_pk, init_msg, true).unwrap(); + + nodes[1].node.handle_open_channel(random_pk, &open_channel_msg); + let events = nodes[1].node.get_and_clear_pending_events(); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id, .. } => { + nodes[1] + .node + .accept_inbound_channel(&temporary_channel_id, &random_pk, 23, None) + .unwrap(); + }, + _ => panic!("Unexpected event"), + } + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, random_pk); + open_channel_msg.common_fields.temporary_channel_id = + ChannelId::temporary_from_entropy_source(&nodes[0].keys_manager); + } + + // Now have one more peer request two channels before we accept either of them. + let last_random_pk = PublicKey::from_secret_key( + &nodes[0].node.secp_ctx, + &SecretKey::from_slice(&nodes[1].keys_manager.get_secure_random_bytes()).unwrap(), + ); + nodes[1].node.peer_connected(last_random_pk, init_msg, true).unwrap(); + + for _ in 0..2 { + nodes[1].node.handle_open_channel(last_random_pk, &open_channel_msg); + open_channel_msg.common_fields.temporary_channel_id = + ChannelId::temporary_from_entropy_source(&nodes[0].keys_manager); + } + + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + + // Neither of them should be acceptable, as the peer still has no funded channel with us and + // we're already at the limit of peers with unfunded channels. + for event in events { + match event { + Event::OpenChannelRequest { temporary_channel_id, .. } => { + match nodes[1].node.accept_inbound_channel( + &temporary_channel_id, + &last_random_pk, + 23, + None, + ) { + Err(APIError::APIMisuseError { err }) => assert_eq!( + err, + "Too many peers with unfunded channels, refusing to accept new ones" + ), + _ => panic!(), + } + assert_eq!( + get_err_msg(&nodes[1], &last_random_pk).channel_id, + temporary_channel_id + ); + }, + _ => panic!("Unexpected event"), + } + } +} + #[test] fn test_inbound_anchors_manual_acceptance() { let mut anchors_cfg = test_default_channel_config(); @@ -513,7 +601,7 @@ pub fn test_insane_channel_opens() { // funding satoshis let channel_value_sat = 31337; // same as funding satoshis let channel_reserve_satoshis = - get_holder_selected_channel_reserve_satoshis(channel_value_sat, &cfg); + get_holder_selected_channel_reserve_satoshis(channel_value_sat, &cfg).unwrap(); let push_msat = (channel_value_sat - channel_reserve_satoshis) * 1000; // Have node0 initiate a channel to node1 with aforementioned parameters diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index 82a45dcb1a6..1aed643a214 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -1275,7 +1275,16 @@ enum BackgroundEvent { /// Some [`ChannelMonitorUpdate`] (s) completed before we were serialized but we still have /// them marked pending, thus we need to run any [`MonitorUpdateCompletionAction`] (s) pending /// on a channel. - MonitorUpdatesComplete { counterparty_node_id: PublicKey, channel_id: ChannelId }, + MonitorUpdatesComplete { + counterparty_node_id: PublicKey, + channel_id: ChannelId, + highest_update_id_completed: u64, + }, + /// A channel had blocked monitor updates waiting on startup. If the updates were blocked on + /// an MPP claim blocker not written to disk, we may be able to unblock them now. + /// + /// This event is never written to disk. + AttemptUnblockMonitorUpdates { counterparty_node_id: PublicKey, channel_id: ChannelId }, } /// A pointer to a channel that is unblocked when an event is surfaced @@ -2719,7 +2728,7 @@ pub struct ChannelManager< /// See `PendingOutboundPayment` documentation for more info. /// /// See `ChannelManager` struct-level documentation for lock order requirements. - pending_outbound_payments: OutboundPayments, + pending_outbound_payments: OutboundPayments, /// SCID/SCID Alias -> forward infos. Key of 0 means payments received. /// @@ -3137,7 +3146,8 @@ pub(crate) const ENABLE_GOSSIP_TICKS: u8 = 5; pub(super) const MAX_UNFUNDED_CHANS_PER_PEER: usize = 4; /// The maximum number of peers from which we will allow pending unfunded channels. Once we reach -/// this many peers we reject new (inbound) channels from peers with which we don't have a channel. +/// this many peers we reject new (inbound) channels from peers with which we don't have a funded +/// channel. pub(super) const MAX_UNFUNDED_CHANNEL_PEERS: usize = 50; /// The maximum allowed size for peer storage, in bytes. @@ -3977,7 +3987,7 @@ where best_block: RwLock::new(params.best_block), outbound_scid_aliases: Mutex::new(new_hash_set()), - pending_outbound_payments: OutboundPayments::new(new_hash_map(), logger.clone()), + pending_outbound_payments: OutboundPayments::new(new_hash_map()), forward_htlcs: Mutex::new(new_hash_map()), decode_update_add_htlcs: Mutex::new(new_hash_map()), claimable_payments: Mutex::new(ClaimablePayments { claimable_payments: new_hash_map(), pending_claiming_payments: new_hash_map() }), @@ -4707,6 +4717,12 @@ where /// the channel. This will spend the channel's funding transaction output, effectively replacing /// it with a new one. /// + /// # Required Feature Flags + /// + /// Initiating a splice requires that the channel counterparty supports splicing. Any + /// channel (no matter the type) can be spliced, as long as the counterparty is currently + /// connected. + /// /// # Arguments /// /// Provide a `contribution` to determine if value is spliced in or out. The splice initiator is @@ -4767,8 +4783,21 @@ where Err(e) => return Err(e), }; - let mut peer_state_lock = peer_state_mutex.lock().unwrap(); - let peer_state = &mut *peer_state_lock; + let mut peer_state = peer_state_mutex.lock().unwrap(); + // Accept the legacy prototype bit too: un-upgraded clients advertise + // splicing on bit 155 and speak the same splice protocol. See MDK-799. + if !peer_state.latest_features.supports_splicing() + && !peer_state.latest_features.supports_splicing_legacy() + { + return Err(APIError::ChannelUnavailable { + err: "Peer does not support splicing".to_owned(), + }); + } + if !peer_state.latest_features.supports_quiescence() { + return Err(APIError::ChannelUnavailable { + err: "Peer does not support quiescence, a splicing prerequisite".to_owned(), + }); + } // Look for the channel match peer_state.channel_by_id.entry(*channel_id) { @@ -5378,17 +5407,18 @@ where // Create a dummy route params since they're a required parameter but unused in this case let (payee_node_id, cltv_delta) = route.paths.first() .and_then(|path| path.hops.last().map(|hop| (hop.pubkey, hop.cltv_expiry_delta as u32))) - .unwrap_or_else(|| (PublicKey::from_slice(&[2; 32]).unwrap(), MIN_FINAL_CLTV_EXPIRY_DELTA as u32)); + .unwrap_or_else(|| (PublicKey::from_slice(&[2; 33]).unwrap(), MIN_FINAL_CLTV_EXPIRY_DELTA as u32)); let dummy_payment_params = PaymentParameters::from_node_id(payee_node_id, cltv_delta); RouteParameters::from_payment_params_and_value(dummy_payment_params, route.get_total_amount()) }); if route.route_params.is_none() { route.route_params = Some(route_params.clone()); } let router = FixedRouter::new(route); + let logger = WithContext::from(&self.logger, None, None, Some(payment_hash)); self.pending_outbound_payments .send_payment(payment_hash, recipient_onion, payment_id, Retry::Attempts(0), route_params, &&router, self.list_usable_channels(), || self.compute_inflight_htlcs(), &self.entropy_source, &self.node_signer, best_block_height, - &self.pending_events, |args| self.send_payment_along_path(args)) + &self.pending_events, |args| self.send_payment_along_path(args), &logger) } /// Sends a payment to the route found using the provided [`RouteParameters`], retrying failed @@ -5448,6 +5478,7 @@ where best_block_height, &self.pending_events, |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, Some(payment_hash)), ) } @@ -5532,6 +5563,7 @@ where ) -> Result<(), Bolt11PaymentError> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); self.pending_outbound_payments.pay_for_bolt11_invoice( invoice, payment_id, @@ -5546,6 +5578,7 @@ where best_block_height, &self.pending_events, |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, Some(payment_hash)), ) } @@ -5615,6 +5648,7 @@ where best_block_height, &self.pending_events, |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, None), ) } @@ -5797,6 +5831,7 @@ where best_block_height, &self.pending_events, |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, None), ) } @@ -5875,6 +5910,7 @@ where ) -> Result { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let payment_hash = payment_preimage.map(|preimage| preimage.into()); self.pending_outbound_payments.send_spontaneous_payment( payment_preimage, recipient_onion, @@ -5889,6 +5925,7 @@ where best_block_height, &self.pending_events, |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, payment_hash), ) } @@ -6713,6 +6750,9 @@ where &self, intercept_id: InterceptId, next_hop_channel_id: &ChannelId, next_node_id: PublicKey, amt_to_forward_msat: u64, ) -> Result<(), APIError> { + let entry_logger = WithContext::from(&self.logger, Some(next_node_id), Some(*next_hop_channel_id), None); + log_info!(entry_logger, "forward_intercepted_htlc called: intercept_id {:?} next_hop_channel_id {} next_node_id {} amt_to_forward_msat {}", + intercept_id, next_hop_channel_id, next_node_id, amt_to_forward_msat); let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); let outbound_scid_alias = { @@ -6728,13 +6768,26 @@ where match peer_state.channel_by_id.get(next_hop_channel_id) { Some(chan) => { if let Some(funded_chan) = chan.as_funded() { - if !funded_chan.context.is_usable() { + let is_usable = funded_chan.context.is_usable(); + let is_live = funded_chan.context.is_live(); + let is_connected = funded_chan.context.is_connected(); + log_info!(entry_logger, + "forward_intercepted_htlc: channel {} - is_usable: {}, is_live: {}, is_connected: {}, peer_state.is_connected: {}", + next_hop_channel_id, is_usable, is_live, is_connected, peer_state.is_connected + ); + if !is_usable { return Err(APIError::ChannelUnavailable { err: format!( "Channel with id {next_hop_channel_id} not fully established" ), }); } + if !is_live { + log_info!(entry_logger, + "forward_intercepted_htlc: WARNING - channel {} is_usable but NOT is_live (peer_disconnected flag set). HTLC will be queued but will fail at send_htlc time.", + next_hop_channel_id + ); + } funded_chan.context.outbound_scid_alias() } else { return Err(APIError::ChannelUnavailable { @@ -7148,6 +7201,7 @@ where best_block_height, &self.pending_events, |args| self.send_payment_along_path(args), + &WithContext::from(&self.logger, None, None, None), ); if needs_persist { should_persist = NotifyOption::DoPersist; @@ -7497,8 +7551,10 @@ where } else { "alternate" }; - log_trace!(logger, "Forwarding HTLC from SCID {} with payment_hash {} and next hop SCID {} over {} channel {} with corresponding peer {}", - prev_outbound_scid_alias, &payment_hash, short_chan_id, channel_description, optimal_channel.context.channel_id(), &counterparty_node_id); + log_info!(logger, "process_forward_htlcs: forwarding HTLC from SCID {} with payment_hash {} and next hop SCID {} over {} channel {} with peer {} - is_live: {}, is_usable: {}, is_connected: {}", + prev_outbound_scid_alias, &payment_hash, short_chan_id, channel_description, + optimal_channel.context.channel_id(), &counterparty_node_id, + optimal_channel.context.is_live(), optimal_channel.context.is_usable(), optimal_channel.context.is_connected()); if let Err((reason, msg)) = optimal_channel.queue_add_htlc( *outgoing_amt_msat, *payment_hash, @@ -8001,9 +8057,11 @@ where /// Free the background events, generally called from [`PersistenceNotifierGuard`] constructors. /// /// Expects the caller to have a total_consistency_lock read lock. - #[rustfmt::skip] fn process_background_events(&self) -> NotifyOption { - debug_assert_ne!(self.total_consistency_lock.held_by_thread(), LockHeldState::NotHeldByThread); + debug_assert_ne!( + self.total_consistency_lock.held_by_thread(), + LockHeldState::NotHeldByThread + ); self.background_events_processed_since_startup.store(true, Ordering::Release); @@ -8015,11 +8073,40 @@ where for event in background_events.drain(..) { match event { - BackgroundEvent::MonitorUpdateRegeneratedOnStartup { counterparty_node_id, funding_txo, channel_id, update } => { - self.apply_post_close_monitor_update(counterparty_node_id, channel_id, funding_txo, update); + BackgroundEvent::MonitorUpdateRegeneratedOnStartup { + counterparty_node_id, + funding_txo, + channel_id, + update, + } => { + self.apply_post_close_monitor_update( + counterparty_node_id, + channel_id, + funding_txo, + update, + ); }, - BackgroundEvent::MonitorUpdatesComplete { counterparty_node_id, channel_id } => { - self.channel_monitor_updated(&channel_id, None, &counterparty_node_id); + BackgroundEvent::MonitorUpdatesComplete { + counterparty_node_id, + channel_id, + highest_update_id_completed, + } => { + // Now that we can finally handle the background event, remove all in-flight + // monitor updates for this channel that we've known to complete, as they have + // already been persisted to the monitor and can be applied to our internal + // state such that the channel resumes operation if no new updates have been + // made since. + self.channel_monitor_updated( + &channel_id, + Some(highest_update_id_completed), + &counterparty_node_id, + ); + }, + BackgroundEvent::AttemptUnblockMonitorUpdates { + counterparty_node_id, + channel_id, + } => { + self.handle_monitor_update_release(counterparty_node_id, channel_id, None); }, } } @@ -8304,26 +8391,23 @@ where debug_assert!(false); return false; } - if let OnionPayload::Invoice { .. } = payment.htlcs[0].onion_payload { - // Check if we've received all the parts we need for an MPP (the value of the parts adds to total_msat). - // In this case we're not going to handle any timeouts of the parts here. - // This condition determining whether the MPP is complete here must match - // exactly the condition used in `process_pending_htlc_forwards`. - let htlc_total_msat = - payment.htlcs.iter().map(|h| h.sender_intended_value).sum(); - if payment.htlcs[0].total_msat <= htlc_total_msat { - return true; - } else if payment.htlcs.iter_mut().any(|htlc| { - htlc.timer_ticks += 1; - return htlc.timer_ticks >= MPP_TIMEOUT_TICKS; - }) { - let htlcs = payment - .htlcs - .drain(..) - .map(|htlc: ClaimableHTLC| (htlc.prev_hop, *payment_hash)); - timed_out_mpp_htlcs.extend(htlcs); - return false; - } + // Check if we've received all the parts we need for an MPP. + // This condition determining whether the MPP is complete here must match + // exactly the condition used in `process_pending_htlc_forwards`. + let htlc_total_msat = + payment.htlcs.iter().map(|h| h.sender_intended_value).sum(); + if payment.htlcs[0].total_msat <= htlc_total_msat { + return true; + } else if payment.htlcs.iter_mut().any(|htlc| { + htlc.timer_ticks += 1; + return htlc.timer_ticks >= MPP_TIMEOUT_TICKS; + }) { + let htlcs = payment + .htlcs + .drain(..) + .map(|htlc: ClaimableHTLC| (htlc.prev_hop, *payment_hash)); + timed_out_mpp_htlcs.extend(htlcs); + return false; } true }, @@ -8523,6 +8607,7 @@ where // being fully configured. See the docs for `ChannelManagerReadArgs` for more. match source { HTLCSource::OutboundRoute { ref path, ref session_priv, ref payment_id, .. } => { + let logger = WithContext::from(&self.logger, None, None, Some(*payment_hash)); self.pending_outbound_payments.fail_htlc( source, payment_hash, @@ -8534,6 +8619,7 @@ where &self.secp_ctx, &self.pending_events, &mut from_monitor_update_completion, + &logger, ); if let Some(update) = from_monitor_update_completion { // If `fail_htlc` didn't `take` the post-event action, we should go ahead and @@ -9022,12 +9108,12 @@ where { if let Some(peer_state_mtx) = per_peer_state.get(&node_id) { let mut peer_state = peer_state_mtx.lock().unwrap(); - if let Some(blockers) = peer_state + let entry = peer_state .actions_blocking_raa_monitor_updates - .get_mut(&channel_id) - { + .entry(channel_id); + if let btree_map::Entry::Occupied(mut entry) = entry { let mut found_blocker = false; - blockers.retain(|iter| { + entry.get_mut().retain(|iter| { // Note that we could actually be blocked, in // which case we need to only remove the one // blocker which was added duplicatively. @@ -9037,6 +9123,9 @@ where } *iter != blocker || !first_blocker }); + if entry.get().is_empty() { + entry.remove(); + } debug_assert!(found_blocker); } } else { @@ -9214,6 +9303,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ from_onchain, &mut ev_completion_action, &self.pending_events, + &WithContext::from(&self.logger, None, None, Some(payment_preimage.into())), ); // If an event was generated, `claim_htlc` set `ev_completion_action` to None, if // not, we should go ahead and run it now (as the claim was duplicative), at least @@ -9303,6 +9393,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ channel_id, .. } => *channel_id == prev_channel_id, + BackgroundEvent::AttemptUnblockMonitorUpdates { .. } => false, } }); assert!( @@ -9854,7 +9945,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - let is_only_peer_channel = peer_state.total_channel_count() == 1; + let peer_lacks_funded_channels = + Self::unfunded_channel_count(peer_state, self.best_block.read().unwrap().height) + == peer_state.total_channel_count(); // Find (and remove) the channel in the unaccepted table. If it's not there, something weird is // happening and return an error. N.B. that we create channel with an outbound SCID of zero so @@ -9944,10 +10037,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ return Err(APIError::APIMisuseError { err: err_str }); } else { - // If this peer already has some channels, a new channel won't increase our number of peers - // with unfunded channels, so as long as we aren't over the maximum number of unfunded - // channels per-peer we can accept channels from a peer with existing ones. - if is_only_peer_channel && peers_without_funded_channels >= MAX_UNFUNDED_CHANNEL_PEERS { + // If this peer already has a funded channel with us, accepting another channel won't + // increase the number of unfunded channels. Otherwise, make sure we don't end up with + // too many peers with unfunded channels afterwards. + if peer_lacks_funded_channels + && peers_without_funded_channels > MAX_UNFUNDED_CHANNEL_PEERS + { let send_msg_err_event = MessageSendEvent::HandleError { node_id: channel.context().get_counterparty_node_id(), action: msgs::ErrorAction::SendErrorMessage{ @@ -11266,38 +11361,43 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // Pull this now to avoid introducing a lock order with `forward_htlcs`. let is_our_scid = self.short_to_chan_info.read().unwrap().contains_key(&scid); - let payment_hash = forward_info.payment_hash; - let logger = WithContext::from( - &self.logger, - None, - Some(prev_channel_id), - Some(payment_hash), + + let payment_hash = forward_info.payment_hash; + let is_intercept = fake_scid::is_valid_intercept(&self.fake_scid_rand_bytes, scid, &self.chain_hash); + let is_phantom = fake_scid::is_valid_phantom(&self.fake_scid_rand_bytes, scid, &self.chain_hash); + let logger = WithContext::from( + &self.logger, + None, + Some(prev_channel_id), + Some(payment_hash), + ); + log_debug!(logger, "Forward HTLC decision: scid {} is_our_scid={} is_intercept={} is_phantom={} incoming_amt_msat={:?} outgoing_amt_msat={} prev_short_channel_id={} prev_channel_id={}", + scid, is_our_scid, is_intercept, is_phantom, forward_info.incoming_amt_msat, forward_info.outgoing_amt_msat, prev_outbound_scid_alias, prev_channel_id); + let pending_add = PendingAddHTLCInfo { + prev_outbound_scid_alias, + prev_counterparty_node_id, + prev_funding_outpoint, + prev_channel_id, + prev_htlc_id, + prev_user_channel_id, + forward_info, + }; + let mut fail_intercepted_htlc = |pending_add: PendingAddHTLCInfo| { + let htlc_source = + HTLCSource::PreviousHopData(pending_add.htlc_previous_hop_data()); + let reason = HTLCFailReason::from_failure_code( + LocalHTLCFailureReason::UnknownNextPeer, ); - let pending_add = PendingAddHTLCInfo { - prev_outbound_scid_alias, - prev_counterparty_node_id, - prev_funding_outpoint, - prev_channel_id, - prev_htlc_id, - prev_user_channel_id, - forward_info, - }; - let mut fail_intercepted_htlc = |pending_add: PendingAddHTLCInfo| { - let htlc_source = - HTLCSource::PreviousHopData(pending_add.htlc_previous_hop_data()); - let reason = HTLCFailReason::from_failure_code( - LocalHTLCFailureReason::UnknownNextPeer, - ); - let failure_type = HTLCHandlingFailureType::InvalidForward { - requested_forward_scid: scid, - }; - failed_intercept_forwards.push(( - htlc_source, - payment_hash, - reason, - failure_type, - )); + let failure_type = HTLCHandlingFailureType::InvalidForward { + requested_forward_scid: scid, }; + failed_intercept_forwards.push(( + htlc_source, + payment_hash, + reason, + failure_type, + )); + }; // In the case that we have an HTLC that we're supposed to hold onto until the // recipient comes online *and* the outbound scid is encoded as @@ -11392,6 +11492,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } if !new_intercept_events.is_empty() { + log_info!(WithContext::from(&self.logger, None, None, None), "Queueing {} HTLCIntercepted event(s)", new_intercept_events.len()); let mut events = self.pending_events.lock().unwrap(); events.append(&mut new_intercept_events); } @@ -13531,10 +13632,12 @@ where let peer_state = &mut *peer_state_lck; if let Some(blocker) = completed_blocker.take() { // Only do this on the first iteration of the loop. - if let Some(blockers) = peer_state.actions_blocking_raa_monitor_updates - .get_mut(&channel_id) - { - blockers.retain(|iter| iter != &blocker); + let entry = peer_state.actions_blocking_raa_monitor_updates.entry(channel_id); + if let btree_map::Entry::Occupied(mut entry) = entry { + entry.get_mut().retain(|iter| iter != &blocker); + if entry.get().is_empty() { + entry.remove(); + } } } @@ -13687,8 +13790,10 @@ where provided_node_features(&self.config.read().unwrap()) } - fn provided_init_features(&self, _their_init_features: PublicKey) -> InitFeatures { - provided_init_features(&self.config.read().unwrap()) + fn provided_init_features(&self, their_node_id: PublicKey) -> InitFeatures { + let mut features = provided_init_features(&self.config.read().unwrap()); + strip_acinq_splice_legacy(&mut features, &their_node_id); + features } #[rustfmt::skip] @@ -15677,6 +15782,26 @@ pub(crate) fn provided_channel_type_features(config: &UserConfig) -> ChannelType ChannelTypeFeatures::from_init(&provided_init_features(config)) } +/// ACINQ mainnet node id. Eclair chokes on an `Init` advertising both the +/// legacy splice prototype bit (155, which eclair used for its own +/// pre-standard splicing) and the production bit (63), so strip the legacy +/// bit when peering with this specific node. ACINQ is on bit 63. The +/// carve-out is intentionally per-peer `Init` only — gossip +/// `node_announcement`s are broadcast, not per-peer. See MDK-799. +const ACINQ_MAINNET_NODE_ID: [u8; 33] = [ + 0x03, 0x86, 0x4e, 0xf0, 0x25, 0xfd, 0xe8, 0xfb, 0x58, 0x7d, 0x98, 0x91, 0x86, 0xce, 0x6a, 0x4a, + 0x18, 0x68, 0x95, 0xee, 0x44, 0xa9, 0x26, 0xbf, 0xc3, 0x70, 0xe2, 0xc3, 0x66, 0x59, 0x7a, 0x3f, + 0x8f, +]; + +/// If `their_node_id` is the ACINQ mainnet node, clear the legacy splice +/// prototype (bit 155) feature. See [`ACINQ_MAINNET_NODE_ID`] for context. +fn strip_acinq_splice_legacy(features: &mut InitFeatures, their_node_id: &PublicKey) { + if their_node_id.serialize() == ACINQ_MAINNET_NODE_ID { + features.clear_splicing_legacy(); + } +} + /// Fetches the set of [`InitFeatures`] flags that are provided by or required by /// [`ChannelManager`]. pub fn provided_init_features(config: &UserConfig) -> InitFeatures { @@ -15701,6 +15826,10 @@ pub fn provided_init_features(config: &UserConfig) -> InitFeatures { features.set_simple_close_optional(); features.set_quiescence_optional(); features.set_splicing_optional(); + // Dual-advertise the pre-0.2.2 prototype bit so un-upgraded clients still + // negotiate splicing. Stripped per-peer for ACINQ in the BaseMessageHandler + // impl. See MDK-799. + features.set_splicing_legacy_optional(); if config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx { features.set_anchors_zero_fee_htlc_tx_optional(); @@ -17151,8 +17280,7 @@ where } pending_outbound_payments = Some(outbounds); } - let pending_outbounds = - OutboundPayments::new(pending_outbound_payments.unwrap(), args.logger.clone()); + let pending_outbounds = OutboundPayments::new(pending_outbound_payments.unwrap()); for (peer_pubkey, peer_storage) in peer_storage_dir { if let Some(peer_state) = per_peer_state.get_mut(&peer_pubkey) { @@ -17198,39 +17326,58 @@ where ($counterparty_node_id: expr, $chan_in_flight_upds: expr, $monitor: expr, $peer_state: expr, $logger: expr, $channel_info_log: expr ) => { { + // When all in-flight updates have completed after we were last serialized, we + // need to remove them. However, we can't guarantee that the next serialization + // will have happened after processing the + // `BackgroundEvent::MonitorUpdatesComplete`, so removing them now could lead to the + // channel never being resumed as the event would not be regenerated after another + // reload. At the same time, we don't want to resume the channel now because there + // may be post-update actions to handle. Therefore, we're forced to keep tracking + // the completed in-flight updates (but only when they have all completed) until we + // are processing the `BackgroundEvent::MonitorUpdatesComplete`. let mut max_in_flight_update_id = 0; - let starting_len = $chan_in_flight_upds.len(); - $chan_in_flight_upds.retain(|upd| upd.update_id > $monitor.get_latest_update_id()); - if $chan_in_flight_upds.len() < starting_len { + let num_updates_completed = $chan_in_flight_upds + .iter() + .filter(|update| { + max_in_flight_update_id = cmp::max(max_in_flight_update_id, update.update_id); + update.update_id <= $monitor.get_latest_update_id() + }) + .count(); + if num_updates_completed > 0 { log_debug!( $logger, "{} ChannelMonitorUpdates completed after ChannelManager was last serialized", - starting_len - $chan_in_flight_upds.len() + num_updates_completed, ); } + let all_updates_completed = num_updates_completed == $chan_in_flight_upds.len(); + let funding_txo = $monitor.get_funding_txo(); - for update in $chan_in_flight_upds.iter() { - log_debug!($logger, "Replaying ChannelMonitorUpdate {} for {}channel {}", - update.update_id, $channel_info_log, &$monitor.channel_id()); - max_in_flight_update_id = cmp::max(max_in_flight_update_id, update.update_id); - pending_background_events.push( - BackgroundEvent::MonitorUpdateRegeneratedOnStartup { - counterparty_node_id: $counterparty_node_id, - funding_txo: funding_txo, - channel_id: $monitor.channel_id(), - update: update.clone(), - }); - } - if $chan_in_flight_upds.is_empty() { - // We had some updates to apply, but it turns out they had completed before we - // were serialized, we just weren't notified of that. Thus, we may have to run - // the completion actions for any monitor updates, but otherwise are done. + if all_updates_completed { + log_debug!($logger, "All monitor updates completed since the ChannelManager was last serialized"); pending_background_events.push( BackgroundEvent::MonitorUpdatesComplete { counterparty_node_id: $counterparty_node_id, channel_id: $monitor.channel_id(), + highest_update_id_completed: max_in_flight_update_id, }); } else { + $chan_in_flight_upds.retain(|update| { + let replay = update.update_id > $monitor.get_latest_update_id(); + if replay { + log_debug!($logger, "Replaying ChannelMonitorUpdate {} for {}channel {}", + update.update_id, $channel_info_log, &$monitor.channel_id()); + pending_background_events.push( + BackgroundEvent::MonitorUpdateRegeneratedOnStartup { + counterparty_node_id: $counterparty_node_id, + funding_txo: funding_txo, + channel_id: $monitor.channel_id(), + update: update.clone(), + } + ); + } + replay + }); $peer_state.closed_channel_monitor_update_ids.entry($monitor.channel_id()) .and_modify(|v| *v = cmp::max(max_in_flight_update_id, *v)) .or_insert(max_in_flight_update_id); @@ -17292,6 +17439,14 @@ where log_error!(logger, " Please ensure the chain::Watch API requirements are met and file a bug report at https://github.com/lightningdevkit/rust-lightning"); return Err(DecodeError::DangerousValue); } + if funded_chan.blocked_monitor_updates_pending() > 0 { + pending_background_events.push( + BackgroundEvent::AttemptUnblockMonitorUpdates { + counterparty_node_id: *counterparty_id, + channel_id: *chan_id, + }, + ); + } } else { // We shouldn't have persisted (or read) any unfunded channel types so none should have been // created in this `channel_by_id` map. @@ -17458,6 +17613,7 @@ where session_priv_bytes, &path, best_block_height, + &logger, ); } } @@ -17557,6 +17713,7 @@ where true, &mut compl_action, &pending_events, + &logger, ); // If the completion action was not consumed, then there was no // payment to claim, and we need to tell the `ChannelMonitor` @@ -17601,8 +17758,10 @@ where } } for (htlc_source, payment_hash) in monitor.get_onchain_failed_outbound_htlcs() { + let logger = + WithChannelMonitor::from(&args.logger, monitor, Some(payment_hash)); log_info!( - args.logger, + logger, "Failing HTLC with payment hash {} as it was resolved on-chain.", payment_hash ); @@ -17671,6 +17830,11 @@ where // inbound edge of the payment's monitor has already claimed // the HTLC) we skip trying to replay the claim. let htlc_payment_hash: PaymentHash = payment_preimage.into(); + let logger = WithChannelMonitor::from( + &args.logger, + monitor, + Some(htlc_payment_hash), + ); let balance_could_incl_htlc = |bal| match bal { &Balance::ClaimableOnChannelClose { .. } => { // The channel is still open, assume we can still @@ -17693,7 +17857,7 @@ where // edge monitor but the channel is closed (and thus we'll // immediately panic if we call claim_funds_from_hop). if short_to_chan_info.get(&prev_hop.prev_outbound_scid_alias).is_none() { - log_error!(args.logger, + log_error!(logger, "We need to replay the HTLC claim for payment_hash {} (preimage {}) but cannot do so as the HTLC was forwarded prior to LDK 0.0.124.\ All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1", htlc_payment_hash, @@ -17708,7 +17872,7 @@ where // of panicking at runtime. The user ideally should have read // the release notes and we wouldn't be here, but we go ahead // and let things run in the hope that it'll all just work out. - log_error!(args.logger, + log_error!(logger, "We need to replay the HTLC claim for payment_hash {} (preimage {}) but don't have all the required information to do so reliably.\ As long as the channel for the inbound edge of the forward remains open, this may work okay, but we may panic at runtime!\ All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1\ @@ -18374,6 +18538,28 @@ mod tests { use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use core::sync::atomic::Ordering; + #[test] + fn test_splice_legacy_dual_advertise_and_acinq_strip() { + let config = crate::util::config::UserConfig::default(); + + let mut features = super::provided_init_features(&config); + assert!(features.supports_splicing()); + assert!(features.supports_splicing_legacy()); + + // ACINQ gets the legacy bit stripped but keeps the production bit. + let acinq = PublicKey::from_slice(&super::ACINQ_MAINNET_NODE_ID).unwrap(); + super::strip_acinq_splice_legacy(&mut features, &acinq); + assert!(features.supports_splicing()); + assert!(!features.supports_splicing_legacy()); + + // Anyone else keeps both bits. + let mut features = super::provided_init_features(&config); + let other = PublicKey::from_slice(&[2; 33]).unwrap(); + super::strip_acinq_splice_legacy(&mut features, &other); + assert!(features.supports_splicing()); + assert!(features.supports_splicing_legacy()); + } + #[test] #[rustfmt::skip] fn test_notify_limits() { diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index 7af632e0351..bd4403fd3fe 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -971,7 +971,7 @@ pub fn get_revoke_commit_msgs>( assert_eq!(node_id, recipient); (*msg).clone() }, - _ => panic!("Unexpected event"), + _ => panic!("Unexpected event: {events:?}"), }, match events[1] { MessageSendEvent::UpdateHTLCs { ref node_id, ref channel_id, ref updates } => { @@ -984,7 +984,7 @@ pub fn get_revoke_commit_msgs>( assert!(updates.commitment_signed.iter().all(|cs| cs.channel_id == *channel_id)); updates.commitment_signed.clone() }, - _ => panic!("Unexpected event"), + _ => panic!("Unexpected event: {events:?}"), }, ) } @@ -3486,7 +3486,7 @@ pub fn send_along_route_with_secret<'a, 'b, 'c>( payment_id } -fn fail_payment_along_path<'a, 'b, 'c>(expected_path: &[&Node<'a, 'b, 'c>]) { +pub fn fail_payment_along_path<'a, 'b, 'c>(expected_path: &[&Node<'a, 'b, 'c>]) { let origin_node_id = expected_path[0].node.get_our_node_id(); // iterate from the receiving node to the origin node and handle update fail htlc. diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index c161a9664c0..9fd2e634d65 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -407,7 +407,7 @@ pub fn test_inbound_outbound_capacity_is_not_zero() { assert_eq!(channels0.len(), 1); assert_eq!(channels1.len(), 1); - let reserve = get_holder_selected_channel_reserve_satoshis(100_000, &default_config); + let reserve = get_holder_selected_channel_reserve_satoshis(100_000, &default_config).unwrap(); assert_eq!(channels0[0].inbound_capacity_msat, 95000000 - reserve * 1000); assert_eq!(channels1[0].outbound_capacity_msat, 95000000 - reserve * 1000); @@ -8405,7 +8405,7 @@ pub fn test_inconsistent_mpp_params() { pass_along_path(&nodes[0], path_b, real_amt, hash, Some(payment_secret), event, true, None); do_claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path_a, path_b], preimage)); - expect_payment_sent(&nodes[0], preimage, Some(None), true, true); + expect_payment_sent(&nodes[0], preimage, Some(Some(2000)), true, true); } #[xtest(feature = "_externalize_tests")] @@ -9848,3 +9848,220 @@ pub fn test_multi_post_event_actions() { do_test_multi_post_event_actions(true); do_test_multi_post_event_actions(false); } + +#[xtest(feature = "_externalize_tests")] +pub fn test_dust_exposure_holding_cell_assertion() { + // Test that we properly move forward if we pop an HTLC-add from the holding cell but fail to + // add it to the channel. In 0.2 this cause a (harmless in prod) debug assertion failure. We + // try to ensure that this won't happen by checking that an HTLC will be able to be added + // before we add it to the holding cell, so getting into this state takes a bit of work. + // + // Here we accomplish this by using the dust exposure limit. This has the unique feature that + // node C can increase node B's dust exposure on the B <-> C channel without B doing anything. + // To exploit this, we get node B one HTLC away from being over-exposed to dust, give it one + // more HTLC in the holding cell, then have node C add an HTLC. By the time the holding-cell + // HTLC is released we are at max-dust-exposure and will fail it. + + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + + // Configure nodes with specific dust limits + let mut config = test_default_channel_config(); + // Use a fixed dust exposure limit to make the test simpler + const DUST_HTLC_VALUE_MSAT: u64 = 500_000; + config.channel_config.max_dust_htlc_exposure = MaxDustHTLCExposure::FixedLimitMsat(5_000_000); + config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + let configs = [Some(config.clone()), Some(config.clone()), Some(config.clone())]; + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &configs); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + + // Create channels: A <-> B <-> C + create_announced_chan_between_nodes(&nodes, 0, 1); + let bc_chan_id = create_announced_chan_between_nodes(&nodes, 1, 2).2; + send_payment(&nodes[0], &[&nodes[1], &nodes[2]], 10_000_000); + + // Send multiple dust HTLCs from B to C to approach the dust limit (including transaction fees) + for _ in 0..4 { + route_payment(&nodes[1], &[&nodes[2]], DUST_HTLC_VALUE_MSAT); + } + + // At this point we shouldn't be over the dust limit, and should still be able to send HTLCs. + let bs_chans = nodes[1].node.list_channels(); + let bc_chan = bs_chans.iter().find(|chan| chan.counterparty.node_id == node_c_id).unwrap(); + assert_eq!( + bc_chan.next_outbound_htlc_minimum_msat, + config.channel_handshake_config.our_htlc_minimum_msat + ); + + // Add a further HTLC from B to C, but don't deliver the send messages. + // After this we'll only have the ability to add one more HTLC, but by not delivering the send + // messages (leaving B waiting on C's RAA) the next HTLC will go into B's holding cell. + let (route_bc, payment_hash_bc, _payment_preimage_bc, payment_secret_bc) = + get_route_and_payment_hash!(nodes[1], nodes[2], DUST_HTLC_VALUE_MSAT); + let onion_bc = RecipientOnionFields::secret_only(payment_secret_bc); + let id = PaymentId(payment_hash_bc.0); + nodes[1].node.send_payment_with_route(route_bc, payment_hash_bc, onion_bc, id).unwrap(); + check_added_monitors(&nodes[1], 1); + let send_bc = SendEvent::from_node(&nodes[1]); + + let bs_chans = nodes[1].node.list_channels(); + let bc_chan = bs_chans.iter().find(|chan| chan.counterparty.node_id == node_c_id).unwrap(); + assert_eq!( + bc_chan.next_outbound_htlc_minimum_msat, + config.channel_handshake_config.our_htlc_minimum_msat + ); + + // Forward an additional HTLC from A through B to C. This will go in B's holding cell for node + // C as it is waiting on a response to the above messages. + let payment_params_ac = PaymentParameters::from_node_id(node_c_id, TEST_FINAL_CLTV) + .with_bolt11_features(nodes[2].node.bolt11_invoice_features()) + .unwrap(); + let (route_ac, payment_hash_cell, _, payment_secret_ac) = + get_route_and_payment_hash!(nodes[0], nodes[2], payment_params_ac, DUST_HTLC_VALUE_MSAT); + let onion_ac = RecipientOnionFields::secret_only(payment_secret_ac); + let id = PaymentId(payment_hash_cell.0); + nodes[0].node.send_payment_with_route(route_ac, payment_hash_cell, onion_ac, id).unwrap(); + check_added_monitors(&nodes[0], 1); + + let send_ab = SendEvent::from_node(&nodes[0]); + nodes[1].node.handle_update_add_htlc(node_a_id, &send_ab.msgs[0]); + do_commitment_signed_dance(&nodes[1], &nodes[0], &send_ab.commitment_msg, false, true); + + // At this point when we process pending forwards the HTLC will go into the holding cell and no + // further messages will be generated. Node B will also be at its maximum dust exposure and + // will refuse to send any dust HTLCs (when it includes the holding cell HTLC). + expect_and_process_pending_htlcs(&nodes[1], false); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + let bs_chans = nodes[1].node.list_channels(); + let bc_chan = bs_chans.iter().find(|chan| chan.counterparty.node_id == node_c_id).unwrap(); + assert!(bc_chan.next_outbound_htlc_minimum_msat > DUST_HTLC_VALUE_MSAT); + + // Send an additional HTLC from C to B. This will make B unable to forward the HTLC already in + // its holding cell as it would be over-exposed to dust. + let (route_cb, payment_hash_cb, payment_preimage_cb, payment_secret_cb) = + get_route_and_payment_hash!(nodes[2], nodes[1], DUST_HTLC_VALUE_MSAT); + let onion_cb = RecipientOnionFields::secret_only(payment_secret_cb); + let id = PaymentId(payment_hash_cb.0); + nodes[2].node.send_payment_with_route(route_cb, payment_hash_cb, onion_cb, id).unwrap(); + check_added_monitors(&nodes[2], 1); + + // Now deliver all the messages and make sure that the HTLC is failed-back. + let send_event_cb = SendEvent::from_node(&nodes[2]); + nodes[1].node.handle_update_add_htlc(node_c_id, &send_event_cb.msgs[0]); + nodes[1].node.handle_commitment_signed_batch_test(node_c_id, &send_event_cb.commitment_msg); + check_added_monitors(&nodes[1], 1); + + nodes[2].node.handle_update_add_htlc(node_b_id, &send_bc.msgs[0]); + nodes[2].node.handle_commitment_signed_batch_test(node_b_id, &send_bc.commitment_msg); + check_added_monitors(&nodes[2], 1); + + let cs_raa = get_event_msg!(nodes[2], MessageSendEvent::SendRevokeAndACK, node_b_id); + nodes[1].node.handle_revoke_and_ack(node_c_id, &cs_raa); + check_added_monitors(&nodes[1], 1); + let (bs_raa, bs_cs) = get_revoke_commit_msgs(&nodes[1], &node_c_id); + + // When we delivered the RAA above, we attempted (and failed) to add the HTLC to the channel, + // causing it to be ready to fail-back, which we do here: + let next_hop = + HTLCHandlingFailureType::Forward { node_id: Some(node_c_id), channel_id: bc_chan_id }; + expect_htlc_forwarding_fails(&nodes[1], &[next_hop]); + check_added_monitors(&nodes[1], 1); + fail_payment_along_path(&[&nodes[0], &nodes[1]]); + let conditions = PaymentFailedConditions::new(); + expect_payment_failed_conditions(&nodes[0], payment_hash_cell, false, conditions); + + nodes[2].node.handle_revoke_and_ack(node_b_id, &bs_raa); + check_added_monitors(&nodes[2], 1); + let cs_cs = get_htlc_update_msgs(&nodes[2], &node_b_id); + + nodes[2].node.handle_commitment_signed_batch_test(node_b_id, &bs_cs); + check_added_monitors(&nodes[2], 1); + let cs_raa = get_event_msg!(nodes[2], MessageSendEvent::SendRevokeAndACK, node_b_id); + + nodes[1].node.handle_commitment_signed_batch_test(node_c_id, &cs_cs.commitment_signed); + check_added_monitors(&nodes[1], 1); + let bs_raa = get_event_msg!(nodes[1], MessageSendEvent::SendRevokeAndACK, node_c_id); + + nodes[1].node.handle_revoke_and_ack(node_c_id, &cs_raa); + check_added_monitors(&nodes[1], 1); + expect_and_process_pending_htlcs(&nodes[1], false); + expect_payment_claimable!(nodes[1], payment_hash_cb, payment_secret_cb, DUST_HTLC_VALUE_MSAT); + + nodes[2].node.handle_revoke_and_ack(node_b_id, &bs_raa); + check_added_monitors(&nodes[2], 1); + + // Now that everything has settled, make sure the channels still work with a simple claim. + claim_payment(&nodes[2], &[&nodes[1]], payment_preimage_cb); +} + +#[test] +fn test_dup_htlc_claim_onchain_and_offchain() { + // Tests what happens if we receive a claim first offchain, then see a counterparty broadcast + // their commitment transaction and re-claim the same HTLC on-chain. This was never broken, but + // the very specific ordering in this test did hit a debug assertion failure. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let legacy_cfg = test_default_channel_config(); + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(legacy_cfg.clone()), Some(legacy_cfg.clone()), Some(legacy_cfg)], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + + create_announced_chan_between_nodes(&nodes, 0, 1); + let chan_bc = create_announced_chan_between_nodes(&nodes, 1, 2); + + // Route payment A -> B -> C. + let (payment_preimage, payment_hash, _, _) = + route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 1_000_000); + + // C claims the payment. + nodes[2].node.claim_funds(payment_preimage); + expect_payment_claimed!(nodes[2], payment_hash, 1_000_000); + check_added_monitors(&nodes[2], 1); + + // Deliver only C's update_fulfill_htlc to B (NOT the commitment_signed). B learns + // the preimage and claims from A (adding an RAA blocker on B-C via + // internal_update_fulfill_htlc, then removing it when the A-B monitor update completes + // and the EmitEventOptionAndFreeOtherChannel action runs). + let cs_updates = get_htlc_update_msgs(&nodes[2], &node_b_id); + nodes[1].node.handle_update_fulfill_htlc(node_c_id, cs_updates.update_fulfill_htlcs[0].clone()); + check_added_monitors(&nodes[1], 1); + + // Ignore B's attempts to claim the HTLC from A. + nodes[1].node.get_and_clear_pending_msg_events(); + + // Get C's commitment transactions. C's commitment includes the HTLC and C has + // an HTLC-success transaction (claiming with preimage). Mine both on B. + let cs_txn = get_local_commitment_txn!(nodes[2], chan_bc.2); + assert!(cs_txn.len() >= 2, "Expected commitment + HTLC-success tx, got {}", cs_txn.len()); + + // Mine C's commitment on B. B sees the counterparty commitment on-chain. + mine_transaction(&nodes[1], &cs_txn[0]); + check_closed_broadcast(&nodes[1], 1, true); + check_added_monitors(&nodes[1], 1); + let events = nodes[1].node.get_and_clear_pending_events(); + assert!( + events.iter().any(|e| matches!(e, Event::ChannelClosed { .. })), + "Expected ChannelClosed event" + ); + + // Mine C's HTLC-success transaction. B's monitor sees the preimage being used on-chain + // and generates an HTLCEvent with the preimage. + mine_transaction(&nodes[1], &cs_txn[1]); + + // Advance past ANTI_REORG_DELAY so the on-chain HTLC resolution matures. This triggers + // the monitor to generate an HTLCEvent with the preimage via process_pending_monitor_events, + // which calls claim_funds_internal a second time. + connect_blocks(&nodes[1], ANTI_REORG_DELAY); +} diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index dc5d07c180e..533a1099741 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -48,7 +48,8 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) { push_amt -= feerate_per_kw as u64 * (commitment_tx_base_weight(&channel_type_features) + 4 * COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000 * 1000; - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config).unwrap() * 1000; let push = if send_from_initiator { 0 } else { push_amt }; let temp_channel_id = @@ -993,7 +994,8 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() { &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config).unwrap() * 1000; let _ = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt); @@ -1035,7 +1037,8 @@ pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { MIN_AFFORDABLE_HTLC_COUNT as u64, &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config).unwrap() * 1000; let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt); // Send four HTLCs to cover the initial push_msat buffer we're required to include @@ -1111,7 +1114,8 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() { MIN_AFFORDABLE_HTLC_COUNT as u64, &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config).unwrap() * 1000; create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, push_amt); let (htlc_success_tx_fee_sat, _) = diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 7c0190a23a9..1258e699114 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -1288,7 +1288,7 @@ mod test { assert!(!invoice.features().unwrap().supports_basic_mpp()); let payment_params = PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), + invoice.get_payee_pub_key(), invoice.min_final_cltv_expiry_delta() as u32, ) .with_bolt11_features(invoice.features().unwrap().clone()) @@ -1350,7 +1350,7 @@ mod test { payment_secret, payment_amt, payment_preimage_opt, - invoice.recover_payee_pub_key(), + invoice.get_payee_pub_key(), ); do_claim_payment_along_route(ClaimAlongRouteArgs::new( &nodes[0], diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index 9473142cfed..2e6b279965f 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -41,7 +41,7 @@ pub mod channel; #[cfg(not(fuzzing))] pub(crate) mod channel; -pub(crate) mod onion_utils; +pub mod onion_utils; mod outbound_payment; pub mod wire; @@ -53,14 +53,6 @@ pub use onion_utils::{create_payment_onion, LocalHTLCFailureReason}; // without the node parameter being mut. This is incorrect, and thus newer rustcs will complain // about an unnecessary mut. Thus, we silence the unused_mut warning in two test modules below. -#[cfg(fuzzing)] -pub use onion_utils::decode_fulfill_attribution_data; -#[cfg(fuzzing)] -pub use onion_utils::process_onion_failure; - -#[cfg(fuzzing)] -pub use onion_utils::AttributionData; - #[cfg(test)] #[allow(unused_mut)] mod async_payments_tests; diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 354273f7170..81fa2fc41fa 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -4357,7 +4357,7 @@ mod tests { InboundOnionForwardPayload, InboundOnionReceivePayload, OutboundTrampolinePayload, TrampolineOnionPacket, }; - use crate::ln::onion_utils::{AttributionData, HMAC_COUNT, HMAC_LEN, HOLD_TIME_LEN, MAX_HOPS}; + use crate::ln::onion_utils::AttributionData; use crate::ln::types::ChannelId; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::types::features::{ @@ -5890,13 +5890,10 @@ mod tests { channel_id: ChannelId::from_bytes([2; 32]), htlc_id: 2316138423780173, reason: [1; 32].to_vec(), - attribution_data: Some(AttributionData { - hold_times: [3; MAX_HOPS * HOLD_TIME_LEN], - hmacs: [3; HMAC_LEN * HMAC_COUNT], - }), + attribution_data: Some(AttributionData::new()), }; let encoded_value = update_fail_htlc.encode(); - let target_value = >::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d0020010101010101010101010101010101010101010101010101010101010101010101fd03980303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303").unwrap(); + let target_value = >::from_hex("020202020202020202020202020202020202020202020202020202020202020200083a840000034d0020010101010101010101010101010101010101010101010101010101010101010101fd03980000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(); assert_eq!(encoded_value, target_value); } diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index b7d64df4063..300066c5c5f 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -53,7 +53,7 @@ use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Inv use crate::ln::channelmanager::{Bolt12PaymentError, PaymentId, RecentPaymentDetails, RecipientOnionFields, Retry, self}; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; -use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; +use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, OnionMessage, OnionMessageHandler}; use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_error::InvoiceError; @@ -62,9 +62,8 @@ use crate::offers::nonce::Nonce; use crate::offers::parse::Bolt12SemanticError; use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, PADDED_PATH_LENGTH}; use crate::onion_message::offers::OffersMessage; -use crate::routing::gossip::{NodeAlias, NodeId}; use crate::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; -use crate::sign::{NodeSigner, Recipient}; +use crate::sign::NodeSigner; use crate::util::ser::Writeable; /// This used to determine whether we built a compact path or not, but now its just a random @@ -115,38 +114,6 @@ fn disconnect_peers<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, peers: &[&Node<'a, 'b } } -fn announce_node_address<'a, 'b, 'c>( - node: &Node<'a, 'b, 'c>, peers: &[&Node<'a, 'b, 'c>], address: SocketAddress, -) { - let features = node.onion_messenger.provided_node_features() - | node.gossip_sync.provided_node_features(); - let rgb = [0u8; 3]; - let announcement = UnsignedNodeAnnouncement { - features, - timestamp: 1000, - node_id: NodeId::from_pubkey(&node.keys_manager.get_node_id(Recipient::Node).unwrap()), - rgb, - alias: NodeAlias([0u8; 32]), - addresses: vec![address], - excess_address_data: Vec::new(), - excess_data: Vec::new(), - }; - let signature = node.keys_manager.sign_gossip_message( - UnsignedGossipMessage::NodeAnnouncement(&announcement) - ).unwrap(); - - let msg = NodeAnnouncement { - signature, - contents: announcement - }; - - let node_pubkey = node.node.get_our_node_id(); - node.gossip_sync.handle_node_announcement(None, &msg).unwrap(); - for peer in peers { - peer.gossip_sync.handle_node_announcement(Some(node_pubkey), &msg).unwrap(); - } -} - fn resolve_introduction_node<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, path: &BlindedMessagePath) -> PublicKey { path.public_introduction_node_id(&node.network_graph.read_only()) .and_then(|node_id| node_id.as_pubkey().ok()) @@ -315,126 +282,6 @@ fn create_refund_with_no_blinded_path() { assert!(refund.paths().is_empty()); } -/// Checks that blinded paths without Tor-only nodes are preferred when constructing an offer. -#[test] -fn prefers_non_tor_nodes_in_blinded_paths() { - let mut accept_forward_cfg = test_default_channel_config(); - accept_forward_cfg.accept_forwards_to_priv_channels = true; - - let mut features = channelmanager::provided_init_features(&accept_forward_cfg); - features.set_onion_messages_optional(); - features.set_route_blinding_optional(); - - let chanmon_cfgs = create_chanmon_cfgs(6); - let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); - - *node_cfgs[1].override_init_features.borrow_mut() = Some(features); - - let node_chanmgrs = create_node_chanmgrs( - 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] - ); - let nodes = create_network(6, &node_cfgs, &node_chanmgrs); - - create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); - - // Add an extra channel so that more than one of Bob's peers have MIN_PEER_CHANNELS. - create_announced_chan_between_nodes_with_value(&nodes, 4, 5, 10_000_000, 1_000_000_000); - - let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); - let bob_id = bob.node.get_our_node_id(); - let charlie_id = charlie.node.get_our_node_id(); - - disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); - disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - - let tor = SocketAddress::OnionV2([255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 38, 7]); - announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone()); - - let offer = bob.node - .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); - assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); - assert!(!offer.paths().is_empty()); - for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_ne!(introduction_node_id, bob_id); - assert_ne!(introduction_node_id, charlie_id); - } - - // Use a one-hop blinded path when Bob is announced and all his peers are Tor-only. - announce_node_address(&nodes[4], &[alice, bob, charlie, david, &nodes[5]], tor.clone()); - announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone()); - - let offer = bob.node - .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); - assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); - assert!(!offer.paths().is_empty()); - for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_eq!(introduction_node_id, bob_id); - } -} - -/// Checks that blinded paths prefer an introduction node that is the most connected. -#[test] -fn prefers_more_connected_nodes_in_blinded_paths() { - let mut accept_forward_cfg = test_default_channel_config(); - accept_forward_cfg.accept_forwards_to_priv_channels = true; - - let mut features = channelmanager::provided_init_features(&accept_forward_cfg); - features.set_onion_messages_optional(); - features.set_route_blinding_optional(); - - let chanmon_cfgs = create_chanmon_cfgs(6); - let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); - - *node_cfgs[1].override_init_features.borrow_mut() = Some(features); - - let node_chanmgrs = create_node_chanmgrs( - 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] - ); - let nodes = create_network(6, &node_cfgs, &node_chanmgrs); - - create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); - - // Add extra channels so that more than one of Bob's peers have MIN_PEER_CHANNELS and one has - // more than the others. - create_announced_chan_between_nodes_with_value(&nodes, 0, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 3, 4, 10_000_000, 1_000_000_000); - - let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); - let bob_id = bob.node.get_our_node_id(); - - disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); - disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - - let offer = bob.node - .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); - assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); - assert!(!offer.paths().is_empty()); - for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_eq!(introduction_node_id, nodes[4].node.get_our_node_id()); - } -} - /// Tests the dummy hop behavior of Offers based on the message router used: /// - Compact paths (`DefaultMessageRouter`) should not include dummy hops. /// - Node ID paths (`NodeIdMessageRouter`) may include 0 to [`MAX_DUMMY_HOPS_COUNT`] dummy hops. diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 6bba2b59e10..029b7cf5952 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -7,6 +7,8 @@ // You may not use this file except in accordance with one or both of these // licenses. +//! Low-level onion manipulation logic and fields + use super::msgs::OnionErrorPacket; use crate::blinded_path::BlindedHop; use crate::crypto::chacha20::ChaCha20; @@ -15,6 +17,9 @@ use crate::events::HTLCHandlingFailureReason; use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::channelmanager::{HTLCSource, RecipientOnionFields}; use crate::ln::msgs::{self, DecodeError}; +use crate::ln::types::ChannelId; +use crate::ln::wire::Encode; +use crate::ln::LN_MAX_MSG_LEN; use crate::offers::invoice_request::InvoiceRequest; use crate::routing::gossip::NetworkUpdate; use crate::routing::router::{BlindedTail, Path, RouteHop, RouteParameters, TrampolineHop}; @@ -24,7 +29,7 @@ use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::errors::APIError; use crate::util::logger::Logger; use crate::util::ser::{ - LengthCalculatingWriter, Readable, ReadableArgs, VecWriter, Writeable, Writer, + BigSize, LengthCalculatingWriter, Readable, ReadableArgs, VecWriter, Writeable, Writer, }; use bitcoin::hashes::cmp::fixed_time_eq; @@ -894,7 +899,6 @@ fn build_unencrypted_failure_packet( hold_time: u32, min_packet_len: usize, ) -> OnionErrorPacket { assert_eq!(shared_secret.len(), 32); - assert!(failure_data.len() <= 64531); // Failure len is 2 bytes type plus the data. let failure_len = 2 + failure_data.len(); @@ -930,6 +934,9 @@ fn build_unencrypted_failure_packet( let mut packet = OnionErrorPacket { data: writer.0, attribution_data: None }; update_attribution_data(&mut packet, shared_secret, hold_time); + // The failure packets we build ourselves must always be small enough to send to our peer. + debug_assert!(update_fail_htlc_wire_len(&packet) <= LN_MAX_MSG_LEN); + packet } @@ -979,49 +986,102 @@ mod fuzzy_onion_utils { #[cfg(test)] pub(crate) attribution_failed_channel: Option, } + + pub fn process_onion_failure( + secp_ctx: &Secp256k1, logger: &L, htlc_source: &HTLCSource, + encrypted_packet: OnionErrorPacket, + ) -> DecodedOnionFailure + where + L::Target: Logger, + { + let (path, primary_session_priv) = match htlc_source { + HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => (path, session_priv), + _ => unreachable!(), + }; + + if path.has_trampoline_hops() { + // If we have Trampoline hops, the outer onion session_priv is a hash of the inner one. + let session_priv_hash = + Sha256::hash(&primary_session_priv.secret_bytes()).to_byte_array(); + let outer_session_priv = + SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!"); + process_onion_failure_inner( + secp_ctx, + logger, + path, + &outer_session_priv, + Some(primary_session_priv), + encrypted_packet, + ) + } else { + process_onion_failure_inner( + secp_ctx, + logger, + path, + primary_session_priv, + None, + encrypted_packet, + ) + } + } + + /// Decodes the attribution data that we got back from upstream on a payment we sent. + pub fn decode_fulfill_attribution_data( + secp_ctx: &Secp256k1, logger: &L, path: &Path, outer_session_priv: &SecretKey, + mut attribution_data: AttributionData, + ) -> Vec + where + L::Target: Logger, + { + let mut hold_times = Vec::new(); + + // Only consider hops in the regular path for attribution data. Blinded path attribution data isn't accessible. + let shared_secrets = + construct_onion_keys_generic(secp_ctx, &path.hops, None, outer_session_priv) + .map(|(shared_secret, _, _, _, _)| shared_secret); + + // Path length can reach 27 hops, but attribution data can only be conveyed back to the sender from the first 20 + // hops. Determine the number of hops to be used for attribution data. + let attributable_hop_count = usize::min(path.hops.len(), MAX_HOPS); + + for (route_hop_idx, shared_secret) in + shared_secrets.enumerate().take(attributable_hop_count) + { + attribution_data.crypt(shared_secret.as_ref()); + + // Calculate position relative to the last attributable hop. The last attributable hop is at position 0. We need + // to look at the chain of HMACs that does include all data up to the last attributable hop. Hold times beyond + // the last attributable hop will not be available. + let position = attributable_hop_count - route_hop_idx - 1; + let res = attribution_data.verify(&Vec::new(), shared_secret.as_ref(), position); + match res { + Ok(hold_time) => { + hold_times.push(hold_time); + + // Shift attribution data to prepare for processing the next hop. + attribution_data.shift_left(); + }, + Err(()) => { + // We will hit this if there is a node on the path that does not support fulfill attribution data. + log_debug!( + logger, + "Invalid fulfill HMAC in attribution data for node at pos {}", + route_hop_idx + ); + + break; + }, + } + } + + hold_times + } } #[cfg(fuzzing)] pub use self::fuzzy_onion_utils::*; #[cfg(not(fuzzing))] pub(crate) use self::fuzzy_onion_utils::*; -pub fn process_onion_failure( - secp_ctx: &Secp256k1, logger: &L, htlc_source: &HTLCSource, - encrypted_packet: OnionErrorPacket, -) -> DecodedOnionFailure -where - L::Target: Logger, -{ - let (path, primary_session_priv) = match htlc_source { - HTLCSource::OutboundRoute { ref path, ref session_priv, .. } => (path, session_priv), - _ => unreachable!(), - }; - - if path.has_trampoline_hops() { - // If we have Trampoline hops, the outer onion session_priv is a hash of the inner one. - let session_priv_hash = Sha256::hash(&primary_session_priv.secret_bytes()).to_byte_array(); - let outer_session_priv = - SecretKey::from_slice(&session_priv_hash[..]).expect("You broke SHA-256!"); - process_onion_failure_inner( - secp_ctx, - logger, - path, - &outer_session_priv, - Some(primary_session_priv), - encrypted_packet, - ) - } else { - process_onion_failure_inner( - secp_ctx, - logger, - path, - primary_session_priv, - None, - encrypted_packet, - ) - } -} - /// Process failure we got back from upstream on a payment we sent (implying htlc_source is an /// OutboundRoute). fn process_onion_failure_inner( @@ -1466,56 +1526,6 @@ where } } -/// Decodes the attribution data that we got back from upstream on a payment we sent. -pub fn decode_fulfill_attribution_data( - secp_ctx: &Secp256k1, logger: &L, path: &Path, outer_session_priv: &SecretKey, - mut attribution_data: AttributionData, -) -> Vec -where - L::Target: Logger, -{ - let mut hold_times = Vec::new(); - - // Only consider hops in the regular path for attribution data. Blinded path attribution data isn't accessible. - let shared_secrets = - construct_onion_keys_generic(secp_ctx, &path.hops, None, outer_session_priv) - .map(|(shared_secret, _, _, _, _)| shared_secret); - - // Path length can reach 27 hops, but attribution data can only be conveyed back to the sender from the first 20 - // hops. Determine the number of hops to be used for attribution data. - let attributable_hop_count = usize::min(path.hops.len(), MAX_HOPS); - - for (route_hop_idx, shared_secret) in shared_secrets.enumerate().take(attributable_hop_count) { - attribution_data.crypt(shared_secret.as_ref()); - - // Calculate position relative to the last attributable hop. The last attributable hop is at position 0. We need - // to look at the chain of HMACs that does include all data up to the last attributable hop. Hold times beyond - // the last attributable hop will not be available. - let position = attributable_hop_count - route_hop_idx - 1; - let res = attribution_data.verify(&Vec::new(), shared_secret.as_ref(), position); - match res { - Ok(hold_time) => { - hold_times.push(hold_time); - - // Shift attribution data to prepare for processing the next hop. - attribution_data.shift_left(); - }, - Err(()) => { - // We will hit this if there is a node on the path that does not support fulfill attribution data. - log_debug!( - logger, - "Invalid fulfill HMAC in attribution data for node at pos {}", - route_hop_idx - ); - - break; - }, - } - } - - hold_times -} - const BADONION: u16 = 0x8000; const PERM: u16 = 0x4000; const NODE: u16 = 0x2000; @@ -2520,6 +2530,7 @@ where } /// Build a payment onion, returning the first hop msat and cltv values as well. +/// /// `cur_block_height` should be set to the best known block height + 1. pub fn create_payment_onion( secp_ctx: &Secp256k1, path: &Path, session_priv: &SecretKey, total_msat: u64, @@ -2708,22 +2719,53 @@ fn decode_next_hop, N: NextPacketBytes>( } } -pub const HOLD_TIME_LEN: usize = 4; -pub const MAX_HOPS: usize = 20; -pub const HMAC_LEN: usize = 4; +pub(crate) const HOLD_TIME_LEN: usize = 4; +pub(crate) const MAX_HOPS: usize = 20; +pub(crate) const HMAC_LEN: usize = 4; // Define the number of HMACs in the attributable data block. For the first node, there are 20 HMACs, and then for every // subsequent node, the number of HMACs decreases by 1. 20 + 19 + 18 + ... + 1 = 20 * 21 / 2 = 210. -pub const HMAC_COUNT: usize = MAX_HOPS * (MAX_HOPS + 1) / 2; +pub(crate) const HMAC_COUNT: usize = MAX_HOPS * (MAX_HOPS + 1) / 2; + +fn update_fail_htlc_wire_len(onion_error: &OnionErrorPacket) -> usize { + let empty_msg_len = msgs::UpdateFailHTLC { + channel_id: ChannelId([0; 32]), + htlc_id: 0, + reason: Vec::new(), + attribution_data: None, + } + .serialized_length(); + + let attribution_data_len = + onion_error.attribution_data.as_ref().map_or(0, |attribution_data| { + let value_len = attribution_data.serialized_length(); + // Attribution data is written as a TLV of type 1 + BigSize(1).serialized_length() + + BigSize(value_len as u64).serialized_length() + + value_len + }); + + // Messages are prefixed with their two-byte type on the wire. + msgs::UpdateFailHTLC::TYPE.serialized_length() + + empty_msg_len + + onion_error.data.len() + + attribution_data_len +} #[derive(Clone, Debug, Hash, PartialEq, Eq)] +/// Attribution data allows the sender of an HTLC to identify which hop failed an HTLC robustly, +/// preventing earlier hops from corrupting the HTLC failure information (or at least allowing the +/// sender to identify the earliest hop which corrupted HTLC failure information). +/// +/// Additionally, it allows a sender to identify how long each hop along a path held an HTLC, with +/// 100ms granularity. pub struct AttributionData { - pub hold_times: [u8; MAX_HOPS * HOLD_TIME_LEN], - pub hmacs: [u8; HMAC_LEN * HMAC_COUNT], + hold_times: [u8; MAX_HOPS * HOLD_TIME_LEN], + hmacs: [u8; HMAC_LEN * HMAC_COUNT], } impl AttributionData { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self { hold_times: [0; MAX_HOPS * HOLD_TIME_LEN], hmacs: [0; HMAC_LEN * HMAC_COUNT] } } } @@ -2772,7 +2814,7 @@ impl AttributionData { /// Writes the HMACs corresponding to the given position that have been added already by downstream hops. Position is /// relative to the final node. The final node is at position 0. - pub fn write_downstream_hmacs(&self, position: usize, w: &mut HmacEngine) { + pub(crate) fn write_downstream_hmacs(&self, position: usize, w: &mut HmacEngine) { // Set the index to the first downstream HMAC that we need to include. Note that we skip the first MAX_HOPS HMACs // because this is space reserved for the HMACs that we are producing for the current node. let mut hmac_idx = MAX_HOPS + MAX_HOPS - position - 1; @@ -2895,6 +2937,12 @@ fn process_failure_packet( // Add this node's attribution data. update_attribution_data(onion_error, shared_secret, hold_time); + + // A downstream peer could send us a maximum-sized failure packet without attribution data, in + // which case adding ours would push the `update_fail_htlc` over the message size limit. + if update_fail_htlc_wire_len(onion_error) > LN_MAX_MSG_LEN { + onion_error.attribution_data = None; + } } /// Updates fulfill attribution data with the given hold time for an intermediate or final node. If no downstream @@ -3968,29 +4016,20 @@ mod tests { #[test] fn test_failure_packet_max_size() { - // Create a failure message of the maximum size of 65535 bytes. It is composed of: - // - 32 bytes channel id - // - 8 bytes htlc id - // - 2 bytes reason length - // - 32 bytes of hmac - // - 2 bytes of failure type - // - 2 bytes of failure length - // - 64531 bytes of failure data - // - 2 bytes of pad len (0) - // - 1 byte attribution data tlv type - // - 3 bytes attribution data tlv length - // - 80 bytes of attribution data hold times - // - 840 bytes of attribution data hmacs - let failure_data = vec![0; 64531]; - let shared_secret = [0; 32]; + let reason = LocalHTLCFailureReason::TemporaryNodeFailure; + + let empty = super::build_unencrypted_failure_packet(&shared_secret, reason, &[], 0, 0); + let failure_data = vec![0; LN_MAX_MSG_LEN - update_fail_htlc_wire_len(&empty)]; + let onion_error = super::build_unencrypted_failure_packet( &shared_secret, - LocalHTLCFailureReason::TemporaryNodeFailure, + reason, &failure_data, 0, DEFAULT_MIN_FAILURE_PACKET_LEN, ); + assert!(onion_error.attribution_data.is_some()); let msg = UpdateFailHTLC { channel_id: ChannelId([0; 32]), @@ -4000,8 +4039,26 @@ mod tests { }; let mut buffer = Vec::new(); + msgs::UpdateFailHTLC::TYPE.write(&mut buffer).unwrap(); msg.write(&mut buffer).unwrap(); + assert_eq!(buffer.len(), LN_MAX_MSG_LEN); + assert_eq!(update_fail_htlc_wire_len(&msg.into()), buffer.len()); + } + + #[test] + fn relaying_max_sized_failure_packet_does_not_exceed_wire_limit() { + let empty = OnionErrorPacket { data: Vec::new(), attribution_data: None }; + let msg = UpdateFailHTLC { + channel_id: ChannelId([0; 32]), + htlc_id: 0, + reason: vec![0; LN_MAX_MSG_LEN - update_fail_htlc_wire_len(&empty)], + attribution_data: None, + }; + + let onion_error = + HTLCFailReason::from_msg(&msg).get_encrypted_failure_packet(&[1; 32], &None); + assert!(onion_error.attribution_data.is_none()); - assert_eq!(buffer.len(), 65535); + assert_eq!(update_fail_htlc_wire_len(&onion_error), LN_MAX_MSG_LEN); } } diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 75fe55bfeac..975f5fb76fe 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -34,7 +34,7 @@ use crate::sign::{EntropySource, NodeSigner, Recipient}; use crate::types::features::Bolt12InvoiceFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::util::errors::APIError; -use crate::util::logger::Logger; +use crate::util::logger::{Logger, WithContext}; use crate::util::ser::ReadableArgs; #[cfg(feature = "std")] use crate::util::time::Instant; @@ -156,6 +156,9 @@ pub(crate) enum PendingOutboundPayment { /// The total payment amount across all paths, used to be able to issue `PaymentSent` if /// an HTLC still happens to succeed after we marked the payment as abandoned. total_msat: Option, + /// Preserved from `Retryable` so we can still report `fee_paid_msat` if an HTLC succeeds after + /// the payment was abandoned. Added in 0.3. + pending_fee_msat: Option, }, } @@ -244,6 +247,7 @@ impl PendingOutboundPayment { fn get_pending_fee_msat(&self) -> Option { match self { PendingOutboundPayment::Retryable { pending_fee_msat, .. } => pending_fee_msat.clone(), + PendingOutboundPayment::Abandoned { pending_fee_msat, .. } => pending_fee_msat.clone(), _ => None, } } @@ -300,6 +304,7 @@ impl PendingOutboundPayment { _ => new_hash_set(), }; let total_msat = self.total_msat(); + let pending_fee_msat = self.get_pending_fee_msat(); match self { Self::Retryable { payment_hash, .. } | Self::InvoiceReceived { payment_hash, .. } | @@ -310,6 +315,7 @@ impl PendingOutboundPayment { payment_hash: *payment_hash, reason: Some(reason), total_msat, + pending_fee_msat, }; }, _ => {} @@ -837,22 +843,15 @@ pub(super) struct SendAlongPathArgs<'a> { pub hold_htlc_at_next_hop: bool, } -pub(super) struct OutboundPayments -where - L::Target: Logger, -{ +pub(super) struct OutboundPayments { pub(super) pending_outbound_payments: Mutex>, awaiting_invoice: AtomicBool, retry_lock: Mutex<()>, - logger: L, } -impl OutboundPayments -where - L::Target: Logger, -{ +impl OutboundPayments { pub(super) fn new( - pending_outbound_payments: HashMap, logger: L, + pending_outbound_payments: HashMap, ) -> Self { let has_invoice_requests = pending_outbound_payments.values().any(|payment| { matches!( @@ -867,17 +866,19 @@ where pending_outbound_payments: Mutex::new(pending_outbound_payments), awaiting_invoice: AtomicBool::new(has_invoice_requests), retry_lock: Mutex::new(()), - logger, } } +} +impl OutboundPayments { #[rustfmt::skip] - pub(super) fn send_payment( + pub(super) fn send_payment( &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, retry_strategy: Retry, route_params: RouteParameters, router: &R, first_hops: Vec, compute_inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, pending_events: &Mutex)>>, send_payment_along_path: SP, + logger: &WithContext, ) -> Result<(), RetryableSendFailure> where R::Target: Router, @@ -885,19 +886,21 @@ where NS::Target: NodeSigner, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + L::Target: Logger, { self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, None, retry_strategy, route_params, router, first_hops, &compute_inflight_htlcs, entropy_source, node_signer, - best_block_height, pending_events, &send_payment_along_path) + best_block_height, pending_events, &send_payment_along_path, logger) } #[rustfmt::skip] - pub(super) fn send_spontaneous_payment( + pub(super) fn send_spontaneous_payment( &self, payment_preimage: Option, recipient_onion: RecipientOnionFields, payment_id: PaymentId, retry_strategy: Retry, route_params: RouteParameters, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, - pending_events: &Mutex)>>, send_payment_along_path: SP + pending_events: &Mutex)>>, send_payment_along_path: SP, + logger: &WithContext, ) -> Result where R::Target: Router, @@ -905,18 +908,20 @@ where NS::Target: NodeSigner, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + L::Target: Logger, { let preimage = payment_preimage .unwrap_or_else(|| PaymentPreimage(entropy_source.get_secure_random_bytes())); let payment_hash = PaymentHash(Sha256::hash(&preimage.0).to_byte_array()); self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, Some(preimage), retry_strategy, route_params, router, first_hops, inflight_htlcs, entropy_source, - node_signer, best_block_height, pending_events, send_payment_along_path) - .map(|()| payment_hash) + node_signer, best_block_height, pending_events, send_payment_along_path, logger, + ) + .map(|()| payment_hash) } #[rustfmt::skip] - pub(super) fn pay_for_bolt11_invoice( + pub(super) fn pay_for_bolt11_invoice( &self, invoice: &Bolt11Invoice, payment_id: PaymentId, amount_msats: Option, route_params_config: RouteParametersConfig, @@ -925,6 +930,7 @@ where first_hops: Vec, compute_inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, pending_events: &Mutex)>>, send_payment_along_path: SP, + logger: &WithContext, ) -> Result<(), Bolt11PaymentError> where R::Target: Router, @@ -932,6 +938,7 @@ where NS::Target: NodeSigner, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + L::Target: Logger, { let payment_hash = PaymentHash((*invoice.payment_hash()).to_byte_array()); @@ -957,20 +964,20 @@ where self.send_payment_for_non_bolt12_invoice(payment_id, payment_hash, recipient_onion, None, retry_strategy, route_params, router, first_hops, compute_inflight_htlcs, entropy_source, node_signer, best_block_height, - pending_events, send_payment_along_path + pending_events, send_payment_along_path, logger, ).map_err(|err| Bolt11PaymentError::SendingFailed(err)) } #[rustfmt::skip] pub(super) fn send_payment_for_bolt12_invoice< - R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP + R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref, >( &self, invoice: &Bolt12Invoice, payment_id: PaymentId, router: &R, first_hops: Vec, features: Bolt12InvoiceFeatures, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, pending_events: &Mutex)>>, - send_payment_along_path: SP, + send_payment_along_path: SP, logger: &WithContext, ) -> Result<(), Bolt12PaymentError> where R::Target: Router, @@ -979,6 +986,7 @@ where NL::Target: NodeIdLookUp, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + L::Target: Logger, { let (payment_hash, retry_strategy, params_config, _) = self @@ -1002,13 +1010,13 @@ where self.send_payment_for_bolt12_invoice_internal( payment_id, payment_hash, None, None, invoice, route_params, retry_strategy, false, router, first_hops, inflight_htlcs, entropy_source, node_signer, node_id_lookup, secp_ctx, - best_block_height, pending_events, send_payment_along_path + best_block_height, pending_events, send_payment_along_path, logger, ) } #[rustfmt::skip] fn send_payment_for_bolt12_invoice_internal< - R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP + R: Deref, ES: Deref, NS: Deref, NL: Deref, IH, SP, L: Deref, >( &self, payment_id: PaymentId, payment_hash: PaymentHash, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, @@ -1017,7 +1025,7 @@ where first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, pending_events: &Mutex)>>, - send_payment_along_path: SP, + send_payment_along_path: SP, logger: &WithContext, ) -> Result<(), Bolt12PaymentError> where R::Target: Router, @@ -1026,6 +1034,7 @@ where NL::Target: NodeIdLookUp, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + L::Target: Logger, { // Advance any blinded path where the introduction node is our node. if let Ok(our_node_id) = node_signer.get_node_id(Recipient::Node) { @@ -1040,7 +1049,14 @@ where }, }; if introduction_node_id == our_node_id { - let _ = path.advance_path_by_one(node_signer, node_id_lookup, secp_ctx); + // TODO: Switch this to + // Bolt12PaymentError::SendingFailed(RetryableSendFailure::UnpayableInstructions) + // once we add it. + if let Err(()) = path.advance_path_by_one(node_signer, node_id_lookup, secp_ctx) { + let reason = PaymentFailureReason::RouteNotFound; + self.abandon_payment(payment_id, reason, pending_events); + Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::RouteNotFound))? + } } } } @@ -1053,6 +1069,7 @@ where let route = match self.find_initial_route( payment_id, payment_hash, &recipient_onion, keysend_preimage, invoice_request, &mut route_params, router, &first_hops, &inflight_htlcs, node_signer, best_block_height, + logger, ) { Ok(route) => route, Err(e) => { @@ -1102,14 +1119,14 @@ where best_block_height, &send_payment_along_path ); log_info!( - self.logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, + logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, result ); if let Err(e) = result { self.handle_pay_route_err( e, payment_id, payment_hash, route, route_params, onion_session_privs, router, first_hops, &inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, - &send_payment_along_path + &send_payment_along_path, logger, ); } Ok(()) @@ -1231,12 +1248,13 @@ where NL: Deref, IH, SP, + L: Deref, >( &self, payment_id: PaymentId, hold_htlcs_at_next_hop: bool, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, node_id_lookup: &NL, secp_ctx: &Secp256k1, best_block_height: u32, pending_events: &Mutex)>>, - send_payment_along_path: SP, + send_payment_along_path: SP, logger: &WithContext, ) -> Result<(), Bolt12PaymentError> where R::Target: Router, @@ -1245,6 +1263,7 @@ where NL::Target: NodeIdLookUp, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + L::Target: Logger, { let ( payment_hash, @@ -1303,15 +1322,16 @@ where best_block_height, pending_events, send_payment_along_path, + logger, ) } // Returns whether the data changed and needs to be repersisted. - pub(super) fn check_retry_payments( + pub(super) fn check_retry_payments( &self, router: &R, first_hops: FH, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, pending_events: &Mutex)>>, - send_payment_along_path: SP, + send_payment_along_path: SP, logger: &WithContext, ) -> bool where R::Target: Router, @@ -1320,6 +1340,7 @@ where SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, IH: Fn() -> InFlightHtlcs, FH: Fn() -> Vec, + L::Target: Logger, { let _single_thread = self.retry_lock.lock().unwrap(); let mut should_persist = false; @@ -1369,6 +1390,7 @@ where best_block_height, pending_events, &send_payment_along_path, + logger, ); should_persist = true; } else { @@ -1414,11 +1436,11 @@ where } #[rustfmt::skip] - fn find_initial_route( + fn find_initial_route( &self, payment_id: PaymentId, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, keysend_preimage: Option, invoice_request: Option<&InvoiceRequest>, route_params: &mut RouteParameters, router: &R, first_hops: &Vec, - inflight_htlcs: &IH, node_signer: &NS, best_block_height: u32, + inflight_htlcs: &IH, node_signer: &NS, best_block_height: u32, logger: &WithContext, ) -> Result where R::Target: Router, @@ -1428,7 +1450,7 @@ where { #[cfg(feature = "std")] { if has_expired(&route_params) { - log_error!(self.logger, "Payment with id {} and hash {} had expired before we started paying", + log_error!(logger, "Payment with id {} and hash {} had expired before we started paying", payment_id, payment_hash); return Err(RetryableSendFailure::PaymentExpired) } @@ -1438,7 +1460,7 @@ where route_params, recipient_onion, keysend_preimage, invoice_request, best_block_height ) .map_err(|()| { - log_error!(self.logger, "Can't construct an onion packet without exceeding 1300-byte onion \ + log_error!(logger, "Can't construct an onion packet without exceeding 1300-byte onion \ hop_data length for payment with id {} and hash {}", payment_id, payment_hash); RetryableSendFailure::OnionPacketSizeExceeded })?; @@ -1448,7 +1470,7 @@ where Some(&first_hops.iter().collect::>()), inflight_htlcs(), payment_hash, payment_id, ).map_err(|_| { - log_error!(self.logger, "Failed to find route for payment with id {} and hash {}", + log_error!(logger, "Failed to find route for payment with id {} and hash {}", payment_id, payment_hash); RetryableSendFailure::RouteNotFound })?; @@ -1469,12 +1491,13 @@ where /// [`Event::PaymentPathFailed`]: crate::events::Event::PaymentPathFailed /// [`Event::PaymentFailed`]: crate::events::Event::PaymentFailed #[rustfmt::skip] - fn send_payment_for_non_bolt12_invoice( + fn send_payment_for_non_bolt12_invoice( &self, payment_id: PaymentId, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option, retry_strategy: Retry, mut route_params: RouteParameters, router: &R, first_hops: Vec, inflight_htlcs: IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, pending_events: &Mutex)>>, send_payment_along_path: SP, + logger: &WithContext, ) -> Result<(), RetryableSendFailure> where R::Target: Router, @@ -1486,14 +1509,14 @@ where { let route = self.find_initial_route( payment_id, payment_hash, &recipient_onion, keysend_preimage, None, &mut route_params, router, - &first_hops, &inflight_htlcs, node_signer, best_block_height, + &first_hops, &inflight_htlcs, node_signer, best_block_height, logger, )?; let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, keysend_preimage, &route, Some(retry_strategy), Some(route_params.payment_params.clone()), entropy_source, best_block_height, None) .map_err(|_| { - log_error!(self.logger, "Payment with id {} is already pending. New payment had payment hash {}", + log_error!(logger, "Payment with id {} is already pending. New payment had payment hash {}", payment_id, payment_hash); RetryableSendFailure::DuplicatePayment })?; @@ -1501,24 +1524,25 @@ where let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, None, None, payment_id, None, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); - log_info!(self.logger, "Sending payment with id {} and hash {} returned {:?}", + log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, res); if let Err(e) = res { self.handle_pay_route_err( e, payment_id, payment_hash, route, route_params, onion_session_privs, router, first_hops, &inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, - &send_payment_along_path + &send_payment_along_path, logger, ); } Ok(()) } #[rustfmt::skip] - fn find_route_and_send_payment( + fn find_route_and_send_payment( &self, payment_hash: PaymentHash, payment_id: PaymentId, route_params: RouteParameters, router: &R, first_hops: Vec, inflight_htlcs: &IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, - pending_events: &Mutex)>>, send_payment_along_path: &SP, + pending_events: &Mutex)>>, + send_payment_along_path: &SP, logger: &WithContext, ) where R::Target: Router, @@ -1530,7 +1554,7 @@ where { #[cfg(feature = "std")] { if has_expired(&route_params) { - log_error!(self.logger, "Payment params expired on retry, abandoning payment {}", &payment_id); + log_error!(logger, "Payment params expired on retry, abandoning payment {}", &payment_id); self.abandon_payment(payment_id, PaymentFailureReason::PaymentExpired, pending_events); return } @@ -1543,7 +1567,7 @@ where ) { Ok(route) => route, Err(e) => { - log_error!(self.logger, "Failed to find a route on retry, abandoning payment {}: {:#?}", &payment_id, e); + log_error!(logger, "Failed to find a route on retry, abandoning payment {}: {:#?}", &payment_id, e); self.abandon_payment(payment_id, PaymentFailureReason::RouteNotFound, pending_events); return } @@ -1557,7 +1581,7 @@ where for path in route.paths.iter() { if path.hops.len() == 0 { - log_error!(self.logger, "Unusable path in route (path.hops.len() must be at least 1"); + log_error!(logger, "Unusable path in route (path.hops.len() must be at least 1"); self.abandon_payment(payment_id, PaymentFailureReason::UnexpectedError, pending_events); return } @@ -1590,13 +1614,13 @@ where const RETRY_OVERFLOW_PERCENTAGE: u64 = 10; let retry_amt_msat = route.get_total_amount(); if retry_amt_msat + *pending_amt_msat > *total_msat * (100 + RETRY_OVERFLOW_PERCENTAGE) / 100 { - log_error!(self.logger, "retry_amt_msat of {} will put pending_amt_msat (currently: {}) more than 10% over total_payment_amt_msat of {}", retry_amt_msat, pending_amt_msat, total_msat); + log_error!(logger, "retry_amt_msat of {} will put pending_amt_msat (currently: {}) more than 10% over total_payment_amt_msat of {}", retry_amt_msat, pending_amt_msat, total_msat); abandon_with_entry!(payment, PaymentFailureReason::UnexpectedError); return } if !payment.get().is_retryable_now() { - log_error!(self.logger, "Retries exhausted for payment id {}", &payment_id); + log_error!(logger, "Retries exhausted for payment id {}", &payment_id); abandon_with_entry!(payment, PaymentFailureReason::RetriesExhausted); return } @@ -1625,38 +1649,38 @@ where (total_msat, recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) }, PendingOutboundPayment::Legacy { .. } => { - log_error!(self.logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); + log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); return }, PendingOutboundPayment::AwaitingInvoice { .. } | PendingOutboundPayment::AwaitingOffer { .. } => { - log_error!(self.logger, "Payment not yet sent"); + log_error!(logger, "Payment not yet sent"); debug_assert!(false); return }, PendingOutboundPayment::InvoiceReceived { .. } => { - log_error!(self.logger, "Payment already initiating"); + log_error!(logger, "Payment already initiating"); debug_assert!(false); return }, PendingOutboundPayment::StaticInvoiceReceived { .. } => { - log_error!(self.logger, "Payment already initiating"); + log_error!(logger, "Payment already initiating"); debug_assert!(false); return }, PendingOutboundPayment::Fulfilled { .. } => { - log_error!(self.logger, "Payment already completed"); + log_error!(logger, "Payment already completed"); return }, PendingOutboundPayment::Abandoned { .. } => { - log_error!(self.logger, "Payment already abandoned (with some HTLCs still pending)"); + log_error!(logger, "Payment already abandoned (with some HTLCs still pending)"); return }, } }, hash_map::Entry::Vacant(_) => { - log_error!(self.logger, "Payment with ID {} not found", &payment_id); + log_error!(logger, "Payment with ID {} not found", &payment_id); return } } @@ -1664,24 +1688,24 @@ where let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, Some(total_msat), &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); - log_info!(self.logger, "Result retrying payment id {}: {:?}", &payment_id, res); + log_info!(logger, "Result retrying payment id {}: {:?}", &payment_id, res); if let Err(e) = res { self.handle_pay_route_err( e, payment_id, payment_hash, route, route_params, onion_session_privs, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, - send_payment_along_path + send_payment_along_path, logger ); } } #[rustfmt::skip] - fn handle_pay_route_err( + fn handle_pay_route_err( &self, err: PaymentSendFailure, payment_id: PaymentId, payment_hash: PaymentHash, route: Route, mut route_params: RouteParameters, onion_session_privs: Vec<[u8; 32]>, router: &R, first_hops: Vec, inflight_htlcs: &IH, entropy_source: &ES, node_signer: &NS, best_block_height: u32, pending_events: &Mutex)>>, - send_payment_along_path: &SP, + send_payment_along_path: &SP, logger: &WithContext, ) where R::Target: Router, @@ -1689,12 +1713,13 @@ where NS::Target: NodeSigner, IH: Fn() -> InFlightHtlcs, SP: Fn(SendAlongPathArgs) -> Result<(), APIError>, + L::Target: Logger, { match err { PaymentSendFailure::AllFailedResendSafe(errs) => { self.remove_session_privs(payment_id, route.paths.iter().zip(onion_session_privs.iter())); - Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut route_params, route.paths, errs.into_iter().map(|e| Err(e)), &self.logger, pending_events); - self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, send_payment_along_path); + Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut route_params, route.paths, errs.into_iter().map(|e| Err(e)), pending_events, logger); + self.find_route_and_send_payment(payment_hash, payment_id, route_params, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, send_payment_along_path, logger); }, PaymentSendFailure::PartialFailure { failed_paths_retry: Some(mut retry), results, .. } => { debug_assert_eq!(results.len(), route.paths.len()); @@ -1710,11 +1735,11 @@ where } }); self.remove_session_privs(payment_id, failed_paths); - Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut retry, route.paths, results.into_iter(), &self.logger, pending_events); + Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut retry, route.paths, results.into_iter(), pending_events, logger); // Some paths were sent, even if we failed to send the full MPP value our recipient may // misbehave and claim the funds, at which point we have to consider the payment sent, so // return `Ok()` here, ignoring any retry errors. - self.find_route_and_send_payment(payment_hash, payment_id, retry, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, send_payment_along_path); + self.find_route_and_send_payment(payment_hash, payment_id, retry, router, first_hops, inflight_htlcs, entropy_source, node_signer, best_block_height, pending_events, send_payment_along_path, logger); }, PaymentSendFailure::PartialFailure { failed_paths_retry: None, .. } => { // This may happen if we send a payment and some paths fail, but only due to a temporary @@ -1722,13 +1747,13 @@ where // initial HTLC-Add messages yet. }, PaymentSendFailure::PathParameterError(results) => { - log_error!(self.logger, "Failed to send to route due to parameter error in a single path. Your router is buggy"); + log_error!(logger, "Failed to send to route due to parameter error in a single path. Your router is buggy"); self.remove_session_privs(payment_id, route.paths.iter().zip(onion_session_privs.iter())); - Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut route_params, route.paths, results.into_iter(), &self.logger, pending_events); + Self::push_path_failed_evs_and_scids(payment_id, payment_hash, &mut route_params, route.paths, results.into_iter(), pending_events, logger); self.abandon_payment(payment_id, PaymentFailureReason::UnexpectedError, pending_events); }, PaymentSendFailure::ParameterError(e) => { - log_error!(self.logger, "Failed to send to route due to parameter error: {:?}. Your router is buggy", e); + log_error!(logger, "Failed to send to route due to parameter error: {:?}. Your router is buggy", e); self.remove_session_privs(payment_id, route.paths.iter().zip(onion_session_privs.iter())); self.abandon_payment(payment_id, PaymentFailureReason::UnexpectedError, pending_events); }, @@ -1738,11 +1763,15 @@ where fn push_path_failed_evs_and_scids< I: ExactSizeIterator + Iterator>, + L: Deref, >( payment_id: PaymentId, payment_hash: PaymentHash, route_params: &mut RouteParameters, - paths: Vec, path_results: I, logger: &L, + paths: Vec, path_results: I, pending_events: &Mutex)>>, - ) { + logger: &WithContext, + ) where + L::Target: Logger, + { let mut events = pending_events.lock().unwrap(); debug_assert_eq!(paths.len(), path_results.len()); for (path, path_res) in paths.into_iter().zip(path_results) { @@ -2216,11 +2245,15 @@ where } #[rustfmt::skip] - pub(super) fn claim_htlc( + pub(super) fn claim_htlc( &self, payment_id: PaymentId, payment_preimage: PaymentPreimage, bolt12_invoice: Option, session_priv: SecretKey, path: Path, from_onchain: bool, ev_completion_action: &mut Option, pending_events: &Mutex)>>, - ) { + logger: &WithContext, + ) + where + L::Target: Logger, + { let mut session_priv_bytes = [0; 32]; session_priv_bytes.copy_from_slice(&session_priv[..]); let mut outbounds = self.pending_outbound_payments.lock().unwrap(); @@ -2228,7 +2261,7 @@ where if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(payment_id) { if !payment.get().is_fulfilled() { let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); - log_info!(self.logger, "Payment with id {} and hash {} sent!", payment_id, payment_hash); + log_info!(logger, "Payment with id {} and hash {} sent!", payment_id, payment_hash); let fee_paid_msat = payment.get().get_pending_fee_msat(); let amount_msat = payment.get().total_msat(); pending_events.push_back((events::Event::PaymentSent { @@ -2258,7 +2291,7 @@ where } } } else { - log_trace!(self.logger, "Received duplicative fulfill for HTLC with payment_preimage {}", &payment_preimage); + log_trace!(logger, "Received duplicative fulfill for HTLC with payment_preimage {}", &payment_preimage); } } @@ -2378,13 +2411,15 @@ where }); } - pub(super) fn fail_htlc( + pub(super) fn fail_htlc( &self, source: &HTLCSource, payment_hash: &PaymentHash, onion_error: &HTLCFailReason, path: &Path, session_priv: &SecretKey, payment_id: &PaymentId, probing_cookie_secret: [u8; 32], secp_ctx: &Secp256k1, pending_events: &Mutex)>>, - completion_action: &mut Option, - ) { + completion_action: &mut Option, logger: &WithContext, + ) where + L::Target: Logger, + { #[cfg(any(test, feature = "_test_utils"))] let DecodedOnionFailure { network_update, @@ -2395,7 +2430,7 @@ where failed_within_blinded_path, hold_times, .. - } = onion_error.decode_onion_failure(secp_ctx, &self.logger, &source); + } = onion_error.decode_onion_failure(secp_ctx, &logger, &source); #[cfg(not(any(test, feature = "_test_utils")))] let DecodedOnionFailure { network_update, @@ -2404,7 +2439,7 @@ where failed_within_blinded_path, hold_times, .. - } = onion_error.decode_onion_failure(secp_ctx, &self.logger, &source); + } = onion_error.decode_onion_failure(secp_ctx, &logger, &source); let payment_is_probe = payment_is_probe(payment_hash, &payment_id, probing_cookie_secret); let mut session_priv_bytes = [0; 32]; @@ -2429,7 +2464,7 @@ where if let hash_map::Entry::Occupied(mut payment) = outbounds.entry(*payment_id) { if !payment.get_mut().remove(&session_priv_bytes, Some(&path)) { log_trace!( - self.logger, + logger, "Received duplicative fail for HTLC with payment_hash {}", &payment_hash ); @@ -2437,7 +2472,7 @@ where } if payment.get().is_fulfilled() { log_trace!( - self.logger, + logger, "Received failure of HTLC with payment_hash {} after payment completion", &payment_hash ); @@ -2485,18 +2520,13 @@ where is_retryable_now } else { log_trace!( - self.logger, - "Received duplicative fail for HTLC with payment_hash {}", - &payment_hash + logger, + "Received duplicative fail for HTLC with payment_hash {payment_hash}" ); return; }; core::mem::drop(outbounds); - log_trace!( - self.logger, - "Failing outbound payment HTLC with payment_hash {}", - &payment_hash - ); + log_trace!(logger, "Failing outbound payment HTLC with payment_hash {payment_hash}"); let path_failure = { if payment_is_probe { @@ -2618,10 +2648,12 @@ where invoice_requests } - pub(super) fn insert_from_monitor_on_startup( + pub(super) fn insert_from_monitor_on_startup( &self, payment_id: PaymentId, payment_hash: PaymentHash, session_priv_bytes: [u8; 32], - path: &Path, best_block_height: u32, - ) { + path: &Path, best_block_height: u32, logger: &WithContext, + ) where + L::Target: Logger, + { let path_amt = path.final_value_msat(); let path_fee = path.fee_msat(); @@ -2670,12 +2702,12 @@ where entry.get_mut().insert(session_priv_bytes, &path) }, }; - log_info!(self.logger, "{} a pending payment path for {} msat for session priv {} on an existing pending payment with payment hash {}", + log_info!(logger, "{} a pending payment path for {} msat for session priv {} on an existing pending payment with payment hash {}", if newly_added { "Added" } else { "Had" }, path_amt, log_bytes!(session_priv_bytes), payment_hash); }, hash_map::Entry::Vacant(entry) => { entry.insert(new_retryable!()); - log_info!(self.logger, "Added a pending payment for {} msat with payment hash {} for path with session priv {}", + log_info!(logger, "Added a pending payment for {} msat with payment hash {} for path with session priv {}", path_amt, payment_hash, log_bytes!(session_priv_bytes)); }, } @@ -2734,6 +2766,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (1, reason, upgradable_option), (2, payment_hash, required), (3, total_msat, option), + (5, pending_fee_msat, option), }, (5, AwaitingInvoice) => { (0, expiration, required), @@ -2834,6 +2867,7 @@ mod tests { use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::errors::APIError; use crate::util::hash_tables::new_hash_map; + use crate::util::logger::WithContext; use crate::util::test_utils; use alloc::collections::VecDeque; @@ -2871,7 +2905,9 @@ mod tests { #[rustfmt::skip] fn do_fails_paying_after_expiration(on_retry: bool) { let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let logger_ref = &logger; + let log = WithContext::from(&logger_ref, None, None, Some(PaymentHash([0; 32]))); + let outbound_payments = OutboundPayments::new(new_hash_map()); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); @@ -2893,7 +2929,7 @@ mod tests { outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), expired_route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, - &|_| Ok(())); + &|_| Ok(()), &log); let events = pending_events.lock().unwrap(); assert_eq!(events.len(), 1); if let Event::PaymentFailed { ref reason, .. } = events[0].0 { @@ -2903,7 +2939,7 @@ mod tests { let err = outbound_payments.send_payment( PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), Retry::Attempts(0), expired_route_params, &&router, vec![], || InFlightHtlcs::new(), - &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(())).unwrap_err(); + &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(()), &log).unwrap_err(); if let RetryableSendFailure::PaymentExpired = err { } else { panic!("Unexpected error"); } } } @@ -2916,7 +2952,9 @@ mod tests { #[rustfmt::skip] fn do_find_route_error(on_retry: bool) { let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let logger_ref = &logger; + let log = WithContext::from(&logger_ref, None, None, Some(PaymentHash([0; 32]))); + let outbound_payments = OutboundPayments::new(new_hash_map()); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); @@ -2937,7 +2975,7 @@ mod tests { outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), route_params, &&router, vec![], &|| InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, - &|_| Ok(())); + &|_| Ok(()), &log); let events = pending_events.lock().unwrap(); assert_eq!(events.len(), 1); if let Event::PaymentFailed { .. } = events[0].0 { } else { panic!("Unexpected event"); } @@ -2945,7 +2983,7 @@ mod tests { let err = outbound_payments.send_payment( PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), Retry::Attempts(0), route_params, &&router, vec![], || InFlightHtlcs::new(), - &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(())).unwrap_err(); + &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(()), &log).unwrap_err(); if let RetryableSendFailure::RouteNotFound = err { } else { panic!("Unexpected error"); } } @@ -2955,7 +2993,9 @@ mod tests { #[rustfmt::skip] fn initial_send_payment_path_failed_evs() { let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let logger_ref = &logger; + let log = WithContext::from(&logger_ref, None, None, Some(PaymentHash([0; 32]))); + let outbound_payments = OutboundPayments::new(new_hash_map()); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); @@ -2995,7 +3035,7 @@ mod tests { PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, - |_| Err(APIError::ChannelUnavailable { err: "test".to_owned() })).unwrap(); + |_| Err(APIError::ChannelUnavailable { err: "test".to_owned() }), &log).unwrap(); let mut events = pending_events.lock().unwrap(); assert_eq!(events.len(), 2); if let Event::PaymentPathFailed { @@ -3013,7 +3053,7 @@ mod tests { PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, - |_| Err(APIError::MonitorUpdateInProgress)).unwrap(); + |_| Err(APIError::MonitorUpdateInProgress), &log).unwrap(); assert_eq!(pending_events.lock().unwrap().len(), 0); // Ensure that any other error will result in a PaymentPathFailed event but no blamed scid. @@ -3021,7 +3061,7 @@ mod tests { PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([1; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, - |_| Err(APIError::APIMisuseError { err: "test".to_owned() })).unwrap(); + |_| Err(APIError::APIMisuseError { err: "test".to_owned() }), &log).unwrap(); let events = pending_events.lock().unwrap(); assert_eq!(events.len(), 2); if let Event::PaymentPathFailed { @@ -3037,8 +3077,7 @@ mod tests { #[rustfmt::skip] fn removes_stale_awaiting_invoice_using_absolute_timeout() { let pending_events = Mutex::new(VecDeque::new()); - let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let payment_id = PaymentId([0; 32]); let absolute_expiry = 100; let tick_interval = 10; @@ -3093,8 +3132,7 @@ mod tests { #[rustfmt::skip] fn removes_stale_awaiting_invoice_using_timer_ticks() { let pending_events = Mutex::new(VecDeque::new()); - let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let payment_id = PaymentId([0; 32]); let timer_ticks = 3; let expiration = StaleExpiration::TimerTicks(timer_ticks); @@ -3148,8 +3186,7 @@ mod tests { #[rustfmt::skip] fn removes_abandoned_awaiting_invoice() { let pending_events = Mutex::new(VecDeque::new()); - let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let payment_id = PaymentId([0; 32]); let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); @@ -3180,6 +3217,8 @@ mod tests { #[rustfmt::skip] fn fails_sending_payment_for_expired_bolt12_invoice() { let logger = test_utils::TestLogger::new(); + let logger_ref = &logger; + let log = WithContext::from(&logger_ref, None, None, Some(PaymentHash([0; 32]))); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); @@ -3189,7 +3228,7 @@ mod tests { let nonce = Nonce([0; 16]); let pending_events = Mutex::new(VecDeque::new()); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let payment_id = PaymentId([0; 32]); let expiration = StaleExpiration::AbsoluteTimeout(Duration::from_secs(100)); @@ -3214,7 +3253,7 @@ mod tests { outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], Bolt12InvoiceFeatures::empty(), || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, &EmptyNodeIdLookUp {}, - &secp_ctx, 0, &pending_events, |_| panic!() + &secp_ctx, 0, &pending_events, |_| panic!(), &log ), Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::PaymentExpired)), ); @@ -3235,6 +3274,8 @@ mod tests { #[rustfmt::skip] fn fails_finding_route_for_bolt12_invoice() { let logger = test_utils::TestLogger::new(); + let logger_ref = &logger; + let log = WithContext::from(&logger_ref, None, None, Some(PaymentHash([0; 32]))); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); @@ -3242,7 +3283,7 @@ mod tests { let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); let pending_events = Mutex::new(VecDeque::new()); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let expanded_key = ExpandedKey::new([42; 32]); let nonce = Nonce([0; 16]); let payment_id = PaymentId([0; 32]); @@ -3277,7 +3318,7 @@ mod tests { outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], Bolt12InvoiceFeatures::empty(), || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, &EmptyNodeIdLookUp {}, - &secp_ctx, 0, &pending_events, |_| panic!() + &secp_ctx, 0, &pending_events, |_| panic!(), &log ), Err(Bolt12PaymentError::SendingFailed(RetryableSendFailure::RouteNotFound)), ); @@ -3298,6 +3339,8 @@ mod tests { #[rustfmt::skip] fn sends_payment_for_bolt12_invoice() { let logger = test_utils::TestLogger::new(); + let logger_ref = &logger; + let log = WithContext::from(&logger_ref, None, None, Some(PaymentHash([0; 32]))); let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &logger)); let scorer = RwLock::new(test_utils::TestScorer::new()); let router = test_utils::TestRouter::new(network_graph, &logger, &scorer); @@ -3305,7 +3348,7 @@ mod tests { let keys_manager = test_utils::TestKeysInterface::new(&[0; 32], Network::Testnet); let pending_events = Mutex::new(VecDeque::new()); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let expanded_key = ExpandedKey::new([42; 32]); let nonce = Nonce([0; 16]); let payment_id = PaymentId([0; 32]); @@ -3353,7 +3396,7 @@ mod tests { outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], Bolt12InvoiceFeatures::empty(), || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, &EmptyNodeIdLookUp {}, - &secp_ctx, 0, &pending_events, |_| panic!() + &secp_ctx, 0, &pending_events, |_| panic!(), &log ), Err(Bolt12PaymentError::UnexpectedInvoice), ); @@ -3373,7 +3416,7 @@ mod tests { outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], Bolt12InvoiceFeatures::empty(), || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, &EmptyNodeIdLookUp {}, - &secp_ctx, 0, &pending_events, |_| Ok(()) + &secp_ctx, 0, &pending_events, |_| Ok(()), &log ), Ok(()), ); @@ -3384,7 +3427,7 @@ mod tests { outbound_payments.send_payment_for_bolt12_invoice( &invoice, payment_id, &&router, vec![], Bolt12InvoiceFeatures::empty(), || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, &EmptyNodeIdLookUp {}, - &secp_ctx, 0, &pending_events, |_| panic!() + &secp_ctx, 0, &pending_events, |_| panic!(), &log ), Err(Bolt12PaymentError::DuplicateInvoice), ); @@ -3413,8 +3456,7 @@ mod tests { #[rustfmt::skip] fn time_out_unreleased_async_payments() { let pending_events = Mutex::new(VecDeque::new()); - let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let payment_id = PaymentId([0; 32]); let absolute_expiry = 60; @@ -3464,8 +3506,7 @@ mod tests { #[rustfmt::skip] fn abandon_unreleased_async_payment() { let pending_events = Mutex::new(VecDeque::new()); - let logger = test_utils::TestLogger::new(); - let outbound_payments = OutboundPayments::new(new_hash_map(), &logger); + let outbound_payments = OutboundPayments::new(new_hash_map()); let payment_id = PaymentId([0; 32]); let absolute_expiry = 60; diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index bab2a16bef9..1aa6ecdde7f 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -332,7 +332,7 @@ fn mpp_retry_overpay() { expect_payment_sent!(&nodes[0], payment_preimage, Some(expected_total_fee_msat)); } -fn do_mpp_receive_timeout(send_partial_mpp: bool) { +fn do_mpp_receive_timeout(send_partial_mpp: bool, keysend: bool) { let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); @@ -348,8 +348,12 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { let (chan_3_update, _, chan_3_id, _) = create_announced_chan_between_nodes(&nodes, 1, 3); let (chan_4_update, _, _, _) = create_announced_chan_between_nodes(&nodes, 2, 3); - let (mut route, hash, payment_preimage, payment_secret) = - get_route_and_payment_hash!(nodes[0], nodes[3], 100_000); + let (mut route, hash, payment_preimage, payment_secret) = if keysend { + let payment_params = PaymentParameters::for_keysend(node_d_id, TEST_FINAL_CLTV, true); + get_route_and_payment_hash!(nodes[0], nodes[3], payment_params, 100_000) + } else { + get_route_and_payment_hash!(nodes[0], nodes[3], 100_000) + }; let path = route.paths[0].clone(); route.paths.push(path); route.paths[0].hops[0].pubkey = node_b_id; @@ -361,8 +365,23 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { // Initiate the MPP payment. let onion = RecipientOnionFields::secret_only(payment_secret); - nodes[0].node.send_payment_with_route(route, hash, onion, PaymentId(hash.0)).unwrap(); - check_added_monitors!(nodes[0], 2); // one monitor per path + if keysend { + let route_params = route.route_params.clone().unwrap(); + nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); + nodes[0] + .node + .send_spontaneous_payment( + Some(payment_preimage), + onion, + PaymentId(hash.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + } else { + nodes[0].node.send_payment_with_route(route, hash, onion, PaymentId(hash.0)).unwrap(); + } + check_added_monitors(&nodes[0], 2); // one monitor per path let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 2); @@ -408,7 +427,17 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { let node_2_msgs = remove_first_msg_event_to_node(&node_c_id, &mut events); let path = &[&nodes[2], &nodes[3]]; let payment_secret = Some(payment_secret); - pass_along_path(&nodes[0], path, 200_000, hash, payment_secret, node_2_msgs, true, None); + let expected_preimage = if keysend { Some(payment_preimage) } else { None }; + pass_along_path( + &nodes[0], + path, + 200_000, + hash, + payment_secret, + node_2_msgs, + true, + expected_preimage, + ); // Even after MPP_TIMEOUT_TICKS we should not timeout the MPP if we have all the parts for _ in 0..MPP_TIMEOUT_TICKS { @@ -422,8 +451,14 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { #[test] fn mpp_receive_timeout() { - do_mpp_receive_timeout(true); - do_mpp_receive_timeout(false); + do_mpp_receive_timeout(true, false); + do_mpp_receive_timeout(false, false); +} + +#[test] +fn keysend_mpp_receive_timeout() { + do_mpp_receive_timeout(true, true); + do_mpp_receive_timeout(false, true); } #[test] @@ -1991,6 +2026,36 @@ fn abandoned_send_payment_idempotent() { claim_payment(&nodes[0], &[&nodes[1]], second_payment_preimage); } +#[test] +fn abandoned_payment_fulfilled_preserves_fee_paid_msat() { + // Previously, if we abandoned a payment with HTLCs in-flight and the payment eventually + // succeeded, we would set the `Event::PaymentSent::fee_paid_msat` to None, even though we had + // docs guaranteeing that it would always be Some after 0.0.103. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes(&nodes, 0, 1); + create_announced_chan_between_nodes(&nodes, 1, 2); + + let amt_msat = 5_000_000; + let (route, payment_hash, payment_preimage, payment_secret) = + get_route_and_payment_hash!(&nodes[0], nodes[2], amt_msat); + let payment_id = PaymentId(payment_hash.0); + let onion = RecipientOnionFields::secret_only(payment_secret); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + check_added_monitors(&nodes[0], 1); + + let path: &[&Node] = &[&nodes[1], &nodes[2]]; + pass_along_route(&nodes[0], &[path], amt_msat, payment_hash, payment_secret); + + nodes[0].node.abandon_payment(payment_id); + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], payment_preimage)); +} + #[derive(PartialEq)] enum InterceptTest { Forward, @@ -4951,7 +5016,7 @@ fn test_htlc_forward_considers_anchor_outputs_value() { create_announced_chan_between_nodes_with_value(&nodes, 1, 2, CHAN_AMT, PUSH_MSAT); let channel_reserve_msat = - get_holder_selected_channel_reserve_satoshis(CHAN_AMT, &config) * 1000; + get_holder_selected_channel_reserve_satoshis(CHAN_AMT, &config).unwrap() * 1000; let commitment_fee_msat = chan_utils::commit_tx_fee_sat( *nodes[1].fee_estimator.sat_per_kw.lock().unwrap(), 2, diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs index 09b970a9ab2..a0945c06ea3 100644 --- a/lightning/src/ln/peer_channel_encryptor.rs +++ b/lightning/src/ln/peer_channel_encryptor.rs @@ -10,7 +10,7 @@ use crate::prelude::*; use crate::ln::msgs; -use crate::ln::msgs::LightningError; +use crate::ln::msgs::{ErrorAction, LightningError}; use crate::ln::wire; use crate::sign::{NodeSigner, Recipient}; @@ -524,10 +524,11 @@ impl PeerChannelEncryptor { /// /// For effeciency, the [`Vec::capacity`] should be at least 16 bytes larger than the /// [`Vec::len`], to avoid reallocating for the message MAC, which will be appended to the vec. - fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec) { + fn encrypt_message_with_header_0s(&mut self, msgbuf: &mut Vec) -> Result<(), ()> { let msg_len = msgbuf.len() - 16 - 2; if msg_len > LN_MAX_MSG_LEN { - panic!("Attempted to encrypt message longer than 65535 bytes!"); + debug_assert!(false, "Attempted to encrypt message longer than 65535 bytes!"); + return Err(()); } match self.noise_state { @@ -550,30 +551,40 @@ impl PeerChannelEncryptor { Self::encrypt_in_place_with_ad(msgbuf, 16 + 2, *sn, sk, &[0; 0]); *sn += 1; + Ok(()) + }, + _ => { + debug_assert!( + false, + "Tried to encrypt a message prior to noise handshake completion" + ); + Err(()) }, - _ => panic!("Tried to encrypt a message prior to noise handshake completion"), } } /// Encrypts the given pre-serialized message, returning the encrypted version. - /// panics if msg.len() > 65535 or Noise handshake has not finished. pub fn encrypt_buffer(&mut self, mut msg: MessageBuf) -> Vec { - self.encrypt_message_with_header_0s(&mut msg.0); + self.encrypt_message_with_header_0s(&mut msg.0) + .expect("Length was checked in buf constructor and peer should be live"); msg.0 } /// Encrypts the given message, returning the encrypted version. - /// panics if the length of `message`, once encoded, is greater than 65535 or if the Noise - /// handshake has not finished. - pub fn encrypt_message(&mut self, message: &M) -> Vec { + /// + /// Returns `Err(())` if the length of `message`, once encoded, is greater than 65535 or if the + /// Noise handshake has not finished. + pub(crate) fn encrypt_message( + &mut self, message: &M, + ) -> Result, ()> { // Allocate a buffer with 2KB, fitting most common messages. Reserve the first 16+2 bytes // for the 2-byte message type prefix and its MAC. let mut res = VecWriter(Vec::with_capacity(MSG_BUF_ALLOC_SIZE)); res.0.resize(16 + 2, 0); wire::write(message, &mut res).expect("In-memory messages must never fail to serialize"); - self.encrypt_message_with_header_0s(&mut res.0); - res.0 + self.encrypt_message_with_header_0s(&mut res.0)?; + Ok(res.0) } /// Decrypts a message length header from the remote peer. @@ -602,10 +613,14 @@ impl PeerChannelEncryptor { /// Decrypts the given message up to msg.len() - 16. Bytes after msg.len() - 16 will be left /// undefined (as they contain the Poly1305 tag bytes). /// - /// panics if msg.len() > 65535 + 16 + /// Returns an error if `msg.len() > 65535 + 16`. pub fn decrypt_message(&mut self, msg: &mut [u8]) -> Result<(), LightningError> { if msg.len() > LN_MAX_MSG_LEN + 16 { - panic!("Attempted to decrypt message longer than 65535 + 16 bytes!"); + debug_assert!(false, "Attempted to decrypt message longer than 65535 + 16 bytes!"); + return Err(LightningError { + err: "Somehow had an oversized message to decrypt".to_owned(), + action: ErrorAction::DisconnectPeer { msg: None }, + }); } match self.noise_state { @@ -649,17 +664,18 @@ impl MessageBuf { /// Creates a new buffer from an encoded message (i.e. the two message-type bytes followed by /// the message contents). /// - /// Panics if the message is longer than 2^16. - pub fn from_encoded(encoded_msg: &[u8]) -> Self { + /// Returns `Err(())` if the message is longer than 2^16 - 1. + pub fn from_encoded(encoded_msg: &[u8]) -> Result { if encoded_msg.len() > LN_MAX_MSG_LEN { - panic!("Attempted to encrypt message longer than 65535 bytes!"); + debug_assert!(false, "Attempted to encrypt message longer than 65535 bytes!"); + return Err(()); } // In addition to the message (continaing the two message type bytes), we also have to add // the message length header (and its MAC) and the message MAC. let mut res = Vec::with_capacity(encoded_msg.len() + 16 * 2 + 2); res.resize(encoded_msg.len() + 16 + 2, 0); res[16 + 2..].copy_from_slice(&encoded_msg); - Self(res) + Ok(Self(res)) } #[cfg(test)] @@ -1015,7 +1031,8 @@ mod tests { for i in 0..1005 { let msg = [0x68, 0x65, 0x6c, 0x6c, 0x6f]; - let mut res = outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg)); + let msgbuf = MessageBuf::from_encoded(&msg).unwrap(); + let mut res = outbound_peer.encrypt_buffer(msgbuf); assert_eq!(res.len(), 5 + 2 * 16 + 2); let len_header = res[0..2 + 16].to_vec(); @@ -1056,20 +1073,38 @@ mod tests { } #[test] + #[cfg(debug_assertions)] #[should_panic(expected = "Attempted to encrypt message longer than 65535 bytes!")] fn max_message_len_encryption() { - let mut outbound_peer = get_outbound_peer_for_initiator_test_vectors(); let msg = [4u8; LN_MAX_MSG_LEN + 1]; - outbound_peer.encrypt_buffer(MessageBuf::from_encoded(&msg)); + let _ = MessageBuf::from_encoded(&msg); + } + + #[test] + #[cfg(not(debug_assertions))] + fn max_message_len_encryption() { + let msg = [4u8; LN_MAX_MSG_LEN + 1]; + assert!(MessageBuf::from_encoded(&msg).is_err()); } #[test] + #[cfg(debug_assertions)] #[should_panic(expected = "Attempted to decrypt message longer than 65535 + 16 bytes!")] fn max_message_len_decryption() { let mut inbound_peer = get_inbound_peer_for_test_vectors(); // MSG should not exceed LN_MAX_MSG_LEN + 16 let mut msg = [4u8; LN_MAX_MSG_LEN + 17]; - inbound_peer.decrypt_message(&mut msg).unwrap(); + let _ = inbound_peer.decrypt_message(&mut msg); + } + + #[test] + #[cfg(not(debug_assertions))] + fn max_message_len_decryption() { + let mut inbound_peer = get_inbound_peer_for_test_vectors(); + + // MSG should not exceed LN_MAX_MSG_LEN + 16 + let mut msg = [4u8; LN_MAX_MSG_LEN + 17]; + assert!(inbound_peer.decrypt_message(&mut msg).is_err()); } } diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 74f081b03ae..be0ea6c1dc6 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -45,7 +45,6 @@ use crate::onion_message::packet::OnionMessageContents; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::sign::{NodeSigner, Recipient}; use crate::types::features::{InitFeatures, NodeFeatures}; -use crate::types::string::PrintableString; use crate::util::atomic_counter::AtomicCounter; use crate::util::logger::{Level, Logger, WithContext}; use crate::util::ser::{VecWriter, Writeable, Writer}; @@ -1554,7 +1553,8 @@ where if let Some(next_onion_message) = handler.next_onion_message_for_peer(peer_node_id) { - self.enqueue_message(peer, &next_onion_message); + // OMs are delivered on a best-effort basis, drop if unsendable. + let _ = self.enqueue_message(peer, &next_onion_message); } } } @@ -1572,12 +1572,12 @@ where if let Some((announce, update_a_option, update_b_option)) = self.message_handler.route_handler.get_next_channel_announcement(c) { - self.enqueue_message(peer, &announce); + let _ = self.enqueue_message(peer, &announce); if let Some(update_a) = update_a_option { - self.enqueue_message(peer, &update_a); + let _ = self.enqueue_message(peer, &update_a); } if let Some(update_b) = update_b_option { - self.enqueue_message(peer, &update_b); + let _ = self.enqueue_message(peer, &update_b); } peer.sync_status = InitSyncTracker::ChannelsSyncing( announce.contents.short_channel_id + 1, @@ -1590,7 +1590,7 @@ where InitSyncTracker::ChannelsSyncing(c) if c == 0xffff_ffff_ffff_ffff => { let handler = &self.message_handler.route_handler; if let Some(msg) = handler.get_next_node_announcement(None) { - self.enqueue_message(peer, &msg); + let _ = self.enqueue_message(peer, &msg); peer.sync_status = InitSyncTracker::NodesSyncing(msg.contents.node_id); } else { peer.sync_status = InitSyncTracker::NoSyncRequested; @@ -1600,7 +1600,7 @@ where InitSyncTracker::NodesSyncing(sync_node_id) => { let handler = &self.message_handler.route_handler; if let Some(msg) = handler.get_next_node_announcement(Some(&sync_node_id)) { - self.enqueue_message(peer, &msg); + let _ = self.enqueue_message(peer, &msg); peer.sync_status = InitSyncTracker::NodesSyncing(msg.contents.node_id); } else { peer.sync_status = InitSyncTracker::NoSyncRequested; @@ -1709,10 +1709,13 @@ where } /// Append a message to a peer's pending outbound/write buffer - fn enqueue_message(&self, peer: &mut Peer, message: &M) { + /// + /// Returns `Err(())` if the message was too large to send (and was thus dropped). This may + /// result in us getting out of sync with the peer! + fn enqueue_message(&self, peer: &mut Peer, message: &M) -> Result<(), ()> { let their_node_id = peer.their_node_id.map(|p| p.0); + let logger = WithContext::from(&self.logger, their_node_id, None, None); if let Some(node_id) = their_node_id { - let logger = WithContext::from(&self.logger, their_node_id, None, None); if is_gossip_msg(message.type_id()) { log_gossip!(logger, "Enqueueing message {:?} to {}", message, node_id); } else { @@ -1722,7 +1725,17 @@ where debug_assert!(false, "node_id should be set by the time we send a message"); } peer.msgs_sent_since_pong += 1; - peer.pending_outbound_buffer.push_back(peer.channel_encryptor.encrypt_message(message)); + let msg_ty = message.type_id(); + match peer.channel_encryptor.encrypt_message(message) { + Ok(encrypted_msg) => { + peer.pending_outbound_buffer.push_back(encrypted_msg); + Ok(()) + }, + Err(()) => { + log_error!(logger, "Failed to encrypt a message of type {}, dropping it!", msg_ty); + Err(()) + }, + } } fn do_read_event( @@ -1774,12 +1787,12 @@ where }, msgs::ErrorAction::SendErrorMessage { msg } => { log_debug!(logger, "Error handling message{}; sending error message with: {}", OptionalFromDebugger(&peer_node_id), e.err); - self.enqueue_message($peer, &msg); + let _ = self.enqueue_message($peer, &msg); continue; }, msgs::ErrorAction::SendWarningMessage { msg, log_level } => { log_given_level!(logger, log_level, "Error handling message{}; sending warning message with: {}", OptionalFromDebugger(&peer_node_id), e.err); - self.enqueue_message($peer, &msg); + let _ = self.enqueue_message($peer, &msg); continue; }, } @@ -1874,7 +1887,7 @@ where peer.their_socket_address.clone(), ), }; - self.enqueue_message(peer, &resp); + self.enqueue_message(peer, &resp).map_err(|()| PeerHandleError {})?; }, NextNoiseStep::ActThree => { let res = peer @@ -1894,7 +1907,7 @@ where peer.their_socket_address.clone(), ), }; - self.enqueue_message(peer, &resp); + self.enqueue_message(peer, &resp).map_err(|()| PeerHandleError {})?; }, NextNoiseStep::NoiseComplete => { if peer.pending_read_is_header { @@ -1955,7 +1968,7 @@ where let data = "Unsupported message compression: zlib" .to_owned(); let msg = msgs::WarningMessage { channel_id, data }; - self.enqueue_message(peer, &msg); + let _ = self.enqueue_message(peer, &msg); continue; }, (_, Some(ty)) if is_gossip_msg(ty) => { @@ -1966,7 +1979,7 @@ where ty ); let msg = msgs::WarningMessage { channel_id, data }; - self.enqueue_message(peer, &msg); + let _ = self.enqueue_message(peer, &msg); continue; }, (msgs::DecodeError::UnknownRequiredFeature, _) => { @@ -2415,7 +2428,7 @@ where logger, "Got Err message from {}: {}", their_node_id, - PrintableString(&msg.data) + log_msg!(msg.data) ); self.message_handler.chan_handler.handle_error(their_node_id, &msg); if msg.channel_id.is_zero() { @@ -2427,14 +2440,14 @@ where logger, "Got warning message from {}: {}", their_node_id, - PrintableString(&msg.data) + log_msg!(msg.data) ); }, wire::Message::Ping(msg) => { if msg.ponglen < 65532 { let resp = msgs::Pong { byteslen: msg.ponglen }; - self.enqueue_message(&mut *peer_mutex.lock().unwrap(), &resp); + let _ = self.enqueue_message(&mut *peer_mutex.lock().unwrap(), &resp); } }, wire::Message::Pong(_msg) => { @@ -2692,8 +2705,9 @@ where { continue; } - let encoded_message = MessageBuf::from_encoded(&encoded_msg); - peer.gossip_broadcast_buffer.push_back(encoded_message); + if let Ok(encoded_message) = MessageBuf::from_encoded(&encoded_msg) { + peer.gossip_broadcast_buffer.push_back(encoded_message); + } } }, wire::Message::NodeAnnouncement(ref msg) => { @@ -2734,8 +2748,9 @@ where { continue; } - let encoded_message = MessageBuf::from_encoded(&encoded_msg); - peer.gossip_broadcast_buffer.push_back(encoded_message); + if let Ok(encoded_message) = MessageBuf::from_encoded(&encoded_msg) { + peer.gossip_broadcast_buffer.push_back(encoded_message); + } } }, wire::Message::ChannelUpdate(ref msg) => { @@ -2771,8 +2786,9 @@ where { continue; } - let encoded_message = MessageBuf::from_encoded(&encoded_msg); - peer.gossip_broadcast_buffer.push_back(encoded_message); + if let Ok(encoded_message) = MessageBuf::from_encoded(&encoded_msg) { + peer.gossip_broadcast_buffer.push_back(encoded_message); + } } }, _ => { @@ -2850,6 +2866,27 @@ where }}; } + macro_rules! enqueue_message_to_peer { + ($peer: expr, $node_id: expr, $msg: expr) => {{ + if self.enqueue_message($peer, $msg).is_err() { + peers_to_disconnect + .insert(*$node_id, (None, "a message we failed to send")); + None + } else { + Some(()) + } + }}; + } + + macro_rules! enqueue_message_to { + ($node_id: expr, $msg: expr) => {{ + match get_peer_for_forwarding!($node_id) { + Some(mut peer) => enqueue_message_to_peer!(&mut *peer, $node_id, $msg), + None => None, + } + }}; + } + let route_handler = &self.message_handler.route_handler; let chan_handler = &self.message_handler.chan_handler; let onion_message_handler = &self.message_handler.onion_message_handler; @@ -2866,7 +2903,7 @@ where "Handling SendPeerStorage event in peer_handler for {}", node_id, ); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendPeerStorageRetrieval { ref node_id, ref msg } => { log_debug!( @@ -2874,31 +2911,31 @@ where "Handling SendPeerStorageRetrieval event in peer_handler for {}", node_id, ); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendAcceptChannel { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.common_fields.temporary_channel_id), None), "Handling SendAcceptChannel event in peer_handler for node {} for channel {}", node_id, &msg.common_fields.temporary_channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendAcceptChannelV2 { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.common_fields.temporary_channel_id), None), "Handling SendAcceptChannelV2 event in peer_handler for node {} for channel {}", node_id, &msg.common_fields.temporary_channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendOpenChannel { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.common_fields.temporary_channel_id), None), "Handling SendOpenChannel event in peer_handler for node {} for channel {}", node_id, &msg.common_fields.temporary_channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendOpenChannelV2 { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.common_fields.temporary_channel_id), None), "Handling SendOpenChannelV2 event in peer_handler for node {} for channel {}", node_id, &msg.common_fields.temporary_channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendFundingCreated { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.temporary_channel_id), None), "Handling SendFundingCreated event in peer_handler for node {} for channel {} (which becomes {})", @@ -2907,19 +2944,19 @@ where ChannelId::v1_from_funding_txid(msg.funding_txid.as_byte_array(), msg.funding_output_index)); // TODO: If the peer is gone we should generate a DiscardFunding event // indicating to the wallet that they should just throw away this funding transaction - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendFundingSigned { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendFundingSigned event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendChannelReady { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendChannelReady event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendStfu { ref node_id, ref msg } => { let logger = WithContext::from( @@ -2931,7 +2968,7 @@ where log_debug!(logger, "Handling SendStfu event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendSpliceInit { ref node_id, ref msg } => { let logger = WithContext::from( @@ -2943,7 +2980,7 @@ where log_debug!(logger, "Handling SendSpliceInit event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendSpliceAck { ref node_id, ref msg } => { let logger = WithContext::from( @@ -2955,7 +2992,7 @@ where log_debug!(logger, "Handling SendSpliceAck event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendSpliceLocked { ref node_id, ref msg } => { let logger = WithContext::from( @@ -2967,67 +3004,67 @@ where log_debug!(logger, "Handling SendSpliceLocked event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxAddInput { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxAddInput event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxAddOutput { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxAddOutput event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxRemoveInput { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxRemoveInput event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxRemoveOutput { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxRemoveOutput event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxComplete { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxComplete event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxSignatures { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxSignatures event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxInitRbf { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxInitRbf event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxAckRbf { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxAckRbf event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendTxAbort { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendTxAbort event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendAnnouncementSignatures { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendAnnouncementSignatures event in peer_handler for node {} for channel {})", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::UpdateHTLCs { ref node_id, @@ -3051,19 +3088,19 @@ where channel_id); let mut peer = get_peer_for_forwarding!(node_id)?; for msg in update_fulfill_htlcs { - self.enqueue_message(&mut *peer, msg); + enqueue_message_to_peer!(&mut *peer, node_id, msg)?; } for msg in update_fail_htlcs { - self.enqueue_message(&mut *peer, msg); + enqueue_message_to_peer!(&mut *peer, node_id, msg)?; } for msg in update_fail_malformed_htlcs { - self.enqueue_message(&mut *peer, msg); + enqueue_message_to_peer!(&mut *peer, node_id, msg)?; } for msg in update_add_htlcs { - self.enqueue_message(&mut *peer, msg); + enqueue_message_to_peer!(&mut *peer, node_id, msg)?; } - if let &Some(ref msg) = update_fee { - self.enqueue_message(&mut *peer, msg); + if let Some(msg) = update_fee { + enqueue_message_to_peer!(&mut *peer, node_id, msg)?; } if commitment_signed.len() > 1 { let msg = msgs::StartBatch { @@ -3071,47 +3108,47 @@ where batch_size: commitment_signed.len() as u16, message_type: Some(msgs::CommitmentSigned::TYPE), }; - self.enqueue_message(&mut *peer, &msg); + enqueue_message_to_peer!(&mut *peer, node_id, &msg)?; } for msg in commitment_signed { - self.enqueue_message(&mut *peer, msg); + enqueue_message_to_peer!(&mut *peer, node_id, msg)?; } }, MessageSendEvent::SendRevokeAndACK { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendRevokeAndACK event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendClosingSigned { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendClosingSigned event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendClosingComplete { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendClosingComplete event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendClosingSig { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendClosingSig event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendShutdown { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling Shutdown event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendChannelReestablish { ref node_id, ref msg } => { log_debug!(WithContext::from(&self.logger, Some(*node_id), Some(msg.channel_id), None), "Handling SendChannelReestablish event in peer_handler for node {} for channel {}", node_id, &msg.channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendChannelAnnouncement { ref node_id, @@ -3121,11 +3158,8 @@ where log_debug!(WithContext::from(&self.logger, Some(*node_id), None, None), "Handling SendChannelAnnouncement event in peer_handler for node {} for short channel id {}", node_id, msg.contents.short_channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); - self.enqueue_message( - &mut *get_peer_for_forwarding!(node_id)?, - update_msg, - ); + enqueue_message_to!(node_id, msg)?; + enqueue_message_to!(node_id, update_msg)?; }, MessageSendEvent::BroadcastChannelAnnouncement { msg, update_msg } => { log_debug!(self.logger, "Handling BroadcastChannelAnnouncement event in peer_handler for short channel id {}", msg.contents.short_channel_id); @@ -3205,7 +3239,7 @@ where MessageSendEvent::SendChannelUpdate { ref node_id, ref msg } => { log_trace!(WithContext::from(&self.logger, Some(*node_id), None, None), "Handling SendChannelUpdate event in peer_handler for node {} for channel {}", node_id, msg.contents.short_channel_id); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::HandleError { node_id, action } => { let logger = WithContext::from(&self.logger, Some(node_id), None, None); @@ -3213,7 +3247,7 @@ where msgs::ErrorAction::DisconnectPeer { msg } => { if let Some(msg) = msg.as_ref() { log_trace!(logger, "Handling DisconnectPeer HandleError event in peer_handler for node {} with message {}", - node_id, msg.data); + node_id, log_msg!(msg.data)); } else { log_trace!(logger, "Handling DisconnectPeer HandleError event in peer_handler for node {}", node_id); @@ -3224,16 +3258,20 @@ where let msg = msg.map(|msg| { wire::Message::<<::Target as wire::CustomMessageReader>::CustomMessage>::Error(msg) }); - peers_to_disconnect.insert(node_id, msg); + peers_to_disconnect.insert(node_id, (msg, "DisconnectPeer HandleError")); }, msgs::ErrorAction::DisconnectPeerWithWarning { msg } => { log_trace!(logger, "Handling DisconnectPeer HandleError event in peer_handler for node {} with message {}", - node_id, msg.data); + node_id, log_msg!(msg.data)); // We do not have the peers write lock, so we just store that we're // about to disconnect the peer and do it after we finish // processing most messages. - peers_to_disconnect - .insert(node_id, Some(wire::Message::Warning(msg))); + peers_to_disconnect.insert( + node_id, ( + Some(wire::Message::Warning(msg)), + "DisconnectPeerWithWarning HandleError", + ), + ); }, msgs::ErrorAction::IgnoreAndLog(level) => { log_given_level!( @@ -3254,11 +3292,8 @@ where msgs::ErrorAction::SendErrorMessage { ref msg } => { log_trace!(logger, "Handling SendErrorMessage HandleError event in peer_handler for node {} with message {}", node_id, - msg.data); - self.enqueue_message( - &mut *get_peer_for_forwarding!(&node_id)?, - msg, - ); + log_msg!(msg.data)); + enqueue_message_to!(&node_id, msg)?; }, msgs::ErrorAction::SendWarningMessage { ref msg, @@ -3266,11 +3301,8 @@ where } => { log_given_level!(logger, *log_level, "Handling SendWarningMessage HandleError event in peer_handler for node {} with message {}", node_id, - msg.data); - self.enqueue_message( - &mut *get_peer_for_forwarding!(&node_id)?, - msg, - ); + log_msg!(msg.data)); + enqueue_message_to!(&node_id, msg)?; }, } }, @@ -3279,13 +3311,13 @@ where node_id, msg.first_blocknum, msg.number_of_blocks); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendShortIdsQuery { ref node_id, ref msg } => { log_gossip!(WithContext::from(&self.logger, Some(*node_id), None, None), "Handling SendShortIdsQuery event in peer_handler for node {} with num_scids={}", node_id, msg.short_channel_ids.len()); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendReplyChannelRange { ref node_id, ref msg } => { log_gossip!(WithContext::from(&self.logger, Some(*node_id), None, None), "Handling SendReplyChannelRange event in peer_handler for node {} with num_scids={} first_blocknum={} number_of_blocks={}, sync_complete={}", @@ -3294,14 +3326,14 @@ where msg.first_blocknum, msg.number_of_blocks, msg.sync_complete); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, MessageSendEvent::SendGossipTimestampFilter { ref node_id, ref msg } => { log_gossip!(WithContext::from(&self.logger, Some(*node_id), None, None), "Handling SendGossipTimestampFilter event in peer_handler for node {} with first_timestamp={}, timestamp_range={}", node_id, msg.first_timestamp, msg.timestamp_range); - self.enqueue_message(&mut *get_peer_for_forwarding!(node_id)?, msg); + enqueue_message_to!(node_id, msg)?; }, } Some(()) @@ -3328,15 +3360,7 @@ where } for (node_id, msg) in custom_message_handler.get_and_clear_pending_msg() { - if peers_to_disconnect.get(&node_id).is_some() { - continue; - } - let mut peer = if let Some(peer) = get_peer_for_forwarding!(&node_id) { - peer - } else { - continue; - }; - self.enqueue_message(&mut peer, &msg); + enqueue_message_to!(&node_id, &msg); } for (descriptor, peer_mutex) in peers.iter() { @@ -3354,7 +3378,7 @@ where if !peers_to_disconnect.is_empty() { let mut peers_lock = self.peers.write().unwrap(); let peers = &mut *peers_lock; - for (node_id, msg) in peers_to_disconnect.drain() { + for (node_id, (msg, reason)) in peers_to_disconnect.drain() { // Note that since we are holding the peers *write* lock we can // remove from node_id_to_descriptor immediately (as no other // thread can be holding the peer lock if we have the global write @@ -3366,12 +3390,12 @@ where if let Some(peer_mutex) = peers.remove(&descriptor) { let mut peer = peer_mutex.lock().unwrap(); if let Some(msg) = msg { - self.enqueue_message(&mut *peer, &msg); + let _ = self.enqueue_message(&mut *peer, &msg); // This isn't guaranteed to work, but if there is enough free // room in the send buffer, put the error message there... self.do_attempt_write_data(&mut descriptor, &mut *peer, false); } - self.do_disconnect(descriptor, &*peer, "DisconnectPeer HandleError"); + self.do_disconnect(descriptor, &*peer, reason); } else { debug_assert!(false, "Missing connection for peer"); } @@ -3497,7 +3521,7 @@ where if peer.awaiting_pong_timer_tick_intervals == 0 { peer.awaiting_pong_timer_tick_intervals = -1; let ping = msgs::Ping { ponglen: 0, byteslen: 64 }; - self.enqueue_message(peer, &ping); + let _ = self.enqueue_message(peer, &ping); } } @@ -3568,7 +3592,7 @@ where peer.awaiting_pong_timer_tick_intervals = 1; let ping = msgs::Ping { ponglen: 0, byteslen: 64 }; - self.enqueue_message(&mut *peer, &ping); + let _ = self.enqueue_message(&mut *peer, &ping); break; } self.do_attempt_write_data( @@ -4252,7 +4276,7 @@ mod tests { peers[0].read_event(&mut fd_dup, &act_three).unwrap(); let not_init_msg = msgs::Ping { ponglen: 4, byteslen: 0 }; - let msg_bytes = dup_encryptor.encrypt_message(¬_init_msg); + let msg_bytes = dup_encryptor.encrypt_message(¬_init_msg).unwrap(); assert!(peers[0].read_event(&mut fd_dup, &msg_bytes).is_err()); } @@ -4359,6 +4383,39 @@ mod tests { } } + // An oversized message trips a `debug_assert` in the encryptor before the error makes it back to + // us, so we can only observe the graceful handling with debug assertions disabled. + #[test] + #[cfg(not(debug_assertions))] + fn test_message_send_failure_disconnects() { + // Test that a peer we failed to send a message to gets disconnected, ensuring they know to + // resync rather than waiting forever on a message which is never going to arrive. + let cfgs = create_peermgr_cfgs(2); + let peers = create_network(2, &cfgs); + let (fd_a, _fd_b) = establish_connection(&peers[0], &peers[1]); + + let id_b = peers[1].node_signer.get_node_id(Recipient::Node).unwrap(); + assert!(peers[0].peer_by_node_id(&id_b).is_some()); + assert!(!fd_a.disconnect.load(Ordering::Acquire)); + + // Queue an `error` which is too long to fit on the wire, which we'll thus fail to send. + let len = crate::ln::peer_channel_encryptor::LN_MAX_MSG_LEN + 1; + let msg = msgs::ErrorMessage { channel_id: ChannelId([0; 32]), data: "A".repeat(len) }; + let action = msgs::ErrorAction::SendErrorMessage { msg }; + let event = MessageSendEvent::HandleError { node_id: id_b, action }; + cfgs[0].chan_handler.pending_events.lock().unwrap().push(event); + + peers[0].process_events(); + + assert!(fd_a.disconnect.load(Ordering::Acquire)); + assert!(peers[0].list_peers().is_empty()); + assert!(peers[0].peer_by_node_id(&id_b).is_none()); + + // The message handlers should have been informed of the disconnection. + assert!(cfgs[0].chan_handler.conn_tracker.connected_peers.lock().unwrap().is_empty()); + assert!(cfgs[0].routing_handler.conn_tracker.connected_peers.lock().unwrap().is_empty()); + } + #[test] fn test_do_attempt_write_data() { // Create 2 peers with custom TestRoutingMessageHandlers and connect them. @@ -4630,7 +4687,7 @@ mod tests { channel_id: ChannelId([0; 32]), data: "no disconnect plz".to_string(), }, - ); + ).unwrap(); } peer_a.process_events(); let msg = fd_a.outbound_data.lock().unwrap().split_off(0); diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs index 3e2de1da833..9669ff5f05f 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -919,6 +919,363 @@ fn test_partial_claim_before_restart() { do_test_partial_claim_before_restart(true, true); } +#[test] +fn test_mpp_claim_htlc_fulfills_unblocked_on_reload() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let persister; + let new_chain_monitor; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes_1_deserialized; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Open two independent channels between the same nodes. The payment below is large enough to + // force the router to split it across both channels, which is what makes the MPP claim depend + // on both ChannelMonitors durably learning the preimage. + let chan_b = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + let chan_a = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + let chan_id_a = chan_a.2; + let chan_id_b = chan_b.2; + let scid_a = chan_a.0.contents.short_channel_id; + let scid_b = chan_b.0.contents.short_channel_id; + + // Send an MPP payment to nodes[1]. `send_along_route_with_secret` leaves the payment + // claimable but unclaimed, so nodes[1] still has both inbound HTLCs live when we start + // manipulating monitor persistence below. + let amt_msat = 20_000_000; + let (route, payment_hash, payment_preimage, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], amt_msat); + assert_eq!(route.paths.len(), 2); + send_along_route_with_secret( + &nodes[0], route, &[&[&nodes[1]], &[&nodes[1]]], amt_msat, payment_hash, + payment_secret, + ); + + // Move both channels into `AWAITING_REMOTE_REVOKE` by having nodes[0] send fee updates and + // withholding nodes[1]'s responding `commitment_signed`s. When nodes[1] later claims the + // payment, the fulfill updates cannot be sent immediately and instead sit in each channel's + // holding cell. + { + let mut fee_est = chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap(); + *fee_est *= 2; + } + nodes[0].node.timer_tick_occurred(); + check_added_monitors(&nodes[0], 2); + + let node_0_id = nodes[0].node.get_our_node_id(); + let node_1_id = nodes[1].node.get_our_node_id(); + + let fee_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(fee_msgs.len(), 2); + for ev in &fee_msgs { + match ev { + MessageSendEvent::UpdateHTLCs { updates, .. } => { + nodes[1].node.handle_update_fee(node_0_id, updates.update_fee.as_ref().unwrap()); + nodes[1].node.handle_commitment_signed_batch_test( + node_0_id, &updates.commitment_signed, + ); + check_added_monitors(&nodes[1], 1); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + // nodes[1] responds to each fee update with a `revoke_and_ack` and a new + // `commitment_signed`. Deliver only the `revoke_and_ack`s for now. The held + // `commitment_signed`s are delivered after nodes[1] claims the payment, creating the blocked + // post-claim monitor updates whose release is exercised after reload. + let node_1_msgs = nodes[1].node.get_and_clear_pending_msg_events(); + let mut commitment_signed_msgs = Vec::new(); + for ev in &node_1_msgs { + match ev { + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + nodes[0].node.handle_revoke_and_ack(node_1_id, msg); + check_added_monitors(&nodes[0], 1); + }, + MessageSendEvent::UpdateHTLCs { updates, .. } => { + commitment_signed_msgs.push(updates.commitment_signed.clone()); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + let node_0_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + for ev in &node_0_msgs { + match ev { + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + nodes[1].node.handle_revoke_and_ack(node_0_id, msg); + check_added_monitors(&nodes[1], 1); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + // Snapshot channel B before the claim. The in-memory ChainMonitor applies updates even when + // the persister returns `InProgress`, so taking this snapshot after the claim would not model a + // crash between two separate monitor writes. + let mon_b_serialized = get_monitor!(nodes[1], chan_id_b).encode(); + + // Make both preimage monitor writes asynchronous. `claim_funds` attaches an in-memory MPP RAA + // blocker so neither channel can release later monitor updates until all channels have the + // preimage durably persisted. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.claim_funds(payment_preimage); + check_added_monitors(&nodes[1], 2); + + // Complete only channel A's preimage update. Channel B will be reloaded from the stale snapshot + // above, simulating a crash where one monitor write completed and the other did not. + let (update_id_a, _) = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap().get(&chan_id_a).unwrap().clone(); + nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id_a, update_id_a); + + // Now finish the fee-update commitment dance we held back. nodes[1] receives nodes[0]'s + // `revoke_and_ack`s while the MPP RAA blocker is still in place, so the resulting monitor + // updates are blocked behind state that is not serialized in the ChannelManager. + for commitment_signed in &commitment_signed_msgs { + nodes[0].node.handle_commitment_signed_batch_test(node_1_id, commitment_signed); + check_added_monitors(&nodes[0], 1); + } + let node_0_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + for ev in &node_0_msgs { + match ev { + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + nodes[1].node.handle_revoke_and_ack(node_0_id, msg); + check_added_monitors(&nodes[1], 0); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + // Persist the ChannelManager after the blocked post-claim monitor updates have been recorded. + // Reload with channel A's up-to-date monitor and channel B's stale monitor. The preimage update + // for B is replayed during reload, putting both channels' preimages on disk. The remaining state + // under test is the blocked post-claim `revoke_and_ack` monitor updates after the in-memory MPP + // RAA blocker that created them is gone. + let node_1_serialized = nodes[1].node.encode(); + let mon_a_serialized = get_monitor!(nodes[1], chan_id_a).encode(); + + nodes[0].node.peer_disconnected(node_1_id); + reload_node!( + nodes[1], + node_1_serialized, + &[&mon_a_serialized, &mon_b_serialized], + persister, + new_chain_monitor, + nodes_1_deserialized + ); + + // Reconnect both peers by manually exchanging `channel_reestablish`s. This avoids relying on a + // more general reconnect helper while the channels intentionally have asymmetric monitor state. + let node_1_id = nodes[1].node.get_our_node_id(); + nodes[0].node.peer_connected(node_1_id, &msgs::Init { + features: nodes[1].node.init_features(), networks: None, remote_network_address: None, + }, true).unwrap(); + nodes[1].node.peer_connected(node_0_id, &msgs::Init { + features: nodes[0].node.init_features(), networks: None, remote_network_address: None, + }, false).unwrap(); + + let reestablish_0 = nodes[0].node.get_and_clear_pending_msg_events(); + let reestablish_1 = nodes[1].node.get_and_clear_pending_msg_events(); + let mut reestablish_0_chan_ids = Vec::new(); + let mut reestablish_1_chan_ids = Vec::new(); + for ev in &reestablish_1 { + match ev { + MessageSendEvent::SendChannelReestablish { node_id, msg } => { + assert_eq!(*node_id, node_0_id); + reestablish_1_chan_ids.push(msg.channel_id); + nodes[0].node.handle_channel_reestablish(node_1_id, msg); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + for ev in &reestablish_0 { + match ev { + MessageSendEvent::SendChannelReestablish { node_id, msg } => { + assert_eq!(*node_id, node_1_id); + reestablish_0_chan_ids.push(msg.channel_id); + nodes[1].node.handle_channel_reestablish(node_0_id, msg); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + assert_eq!(reestablish_0_chan_ids.len(), 2); + assert!(reestablish_0_chan_ids.contains(&chan_id_a)); + assert!(reestablish_0_chan_ids.contains(&chan_id_b)); + assert_eq!(reestablish_1_chan_ids.len(), 2); + assert!(reestablish_1_chan_ids.contains(&chan_id_a)); + assert!(reestablish_1_chan_ids.contains(&chan_id_b)); + // Only nodes[1] was reloaded with stale monitor state. nodes[0] responds to the + // `channel_reestablish`s without touching its monitors. nodes[1] applies the replayed channel B + // preimage update, releases channel A's held RAA update, and frees channel A's held fulfill + // during startup processing. + // Note that unlike the test in 0.3, we only generate the last monitor update for node B after + // get_and_clear_pending_msg_events as we only free the holding cell then. + check_added_monitors(&nodes[0], 0); + check_added_monitors(&nodes[1], 2); + + // The first message batch after reconnect contains channel updates from both nodes. nodes[1] + // also sends the channel A fulfill that startup processing released from the holding cell. + let restart_msgs_0 = nodes[0].node.get_and_clear_pending_msg_events(); + let restart_msgs_1 = nodes[1].node.get_and_clear_pending_msg_events(); + check_added_monitors(&nodes[1], 1); + let mut restart_scids_0 = Vec::new(); + let mut restart_scids_1 = Vec::new(); + let mut startup_fulfill_chan_ids = Vec::new(); + for ev in &restart_msgs_0 { + match ev { + MessageSendEvent::SendChannelUpdate { node_id, msg } => { + assert_eq!(*node_id, node_1_id); + restart_scids_0.push(msg.contents.short_channel_id); + }, + _ => panic!("Unexpected restart message from node 0: {:?}", ev), + } + } + for ev in &restart_msgs_1 { + match ev { + MessageSendEvent::SendChannelUpdate { node_id, msg } => { + assert_eq!(*node_id, node_0_id); + restart_scids_1.push(msg.contents.short_channel_id); + }, + MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates } => { + assert_eq!(*node_id, node_0_id); + startup_fulfill_chan_ids.push(*channel_id); + assert_eq!(updates.update_fulfill_htlcs.len(), 1); + assert!(updates.update_add_htlcs.is_empty()); + assert!(updates.update_fail_htlcs.is_empty()); + assert!(updates.update_fail_malformed_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + for fulfill in &updates.update_fulfill_htlcs { + nodes[0].node.handle_update_fulfill_htlc(node_1_id, fulfill.clone()); + } + // Complete the standard commitment handshake for the released fulfill. The helper + // checks nodes[0]'s incoming commitment monitor update, nodes[1]'s response monitor + // updates, and nodes[0]'s held final monitor update. + do_commitment_signed_dance( + &nodes[0], &nodes[1], &updates.commitment_signed, false, false, + ); + }, + _ => panic!("Unexpected restart message from node 1: {:?}", ev), + } + } + assert_eq!(restart_scids_0.len(), 2); + assert!(restart_scids_0.contains(&scid_a)); + assert!(restart_scids_0.contains(&scid_b)); + assert_eq!(restart_scids_1.len(), 2); + assert!(restart_scids_1.contains(&scid_a)); + assert!(restart_scids_1.contains(&scid_b)); + assert_eq!(startup_fulfill_chan_ids, vec![chan_id_a]); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + check_added_monitors(&nodes[0], 0); + check_added_monitors(&nodes[1], 0); + + // Receiving the startup-released fulfill gives nodes[0] the payment preimage. That is enough to + // emit `PaymentSent`, even though channel B's path-level success still needs its own fulfill. + let startup_payment_events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(startup_payment_events.len(), 2); + let mut saw_startup_payment_sent = false; + let mut startup_success_scids = Vec::new(); + for ev in &startup_payment_events { + match ev { + Event::PaymentSent { + payment_preimage: sent_preimage, + payment_hash: sent_hash, + amount_msat: sent_amount, + fee_paid_msat, + .. + } => { + assert_eq!(*sent_preimage, payment_preimage); + assert_eq!(*sent_hash, payment_hash); + assert_eq!(*sent_amount, Some(amt_msat)); + assert_eq!(*fee_paid_msat, Some(0)); + saw_startup_payment_sent = true; + }, + Event::PaymentPathSuccessful { payment_hash: Some(path_hash), path, .. } => { + assert_eq!(*path_hash, payment_hash); + assert_eq!(path.hops.len(), 1); + startup_success_scids.push(path.hops[0].short_channel_id); + }, + _ => panic!("Unexpected startup payment event: {:?}", ev), + } + } + assert!(saw_startup_payment_sent); + assert_eq!(startup_success_scids, vec![scid_a]); + + // Handling the claim event runs the event-completion action that releases the remaining + // RAA-blocked monitor update. The startup unblock path already released channel A, so channel B + // is the only fulfill that should be emitted here. + let claim_events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(claim_events.len(), 1); + match &claim_events[0] { + Event::PaymentClaimed { payment_hash: claimed_hash, amount_msat, htlcs, .. } => { + assert_eq!(*claimed_hash, payment_hash); + assert_eq!(*amount_msat, amt_msat); + assert_eq!(htlcs.len(), 2); + }, + _ => panic!("Unexpected event: {:?}", claim_events[0]), + } + // The `PaymentSent` event above releases the monitor update that nodes[0] held after the final + // channel A startup revocation. + check_added_monitors(&nodes[0], 1); + // Handling `PaymentClaimed` releases channel B's held revocation update and then the fulfill + // that was waiting behind it (unlike this test in 0.3, after we free the holding cell in + // get_and_clear_pending_msg_events below). + check_added_monitors(&nodes[1], 1); + + // Channel A's fulfill was already sent during startup. The `PaymentClaimed` completion action + // now frees channel B's held fulfill, and no other HTLC update should be bundled with it. + let fulfill_msgs = nodes[1].node.get_and_clear_pending_msg_events(); + check_added_monitors(&nodes[1], 1); + assert_eq!(fulfill_msgs.len(), 1); + match &fulfill_msgs[0] { + MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates } => { + assert_eq!(*node_id, node_0_id); + assert_eq!(*channel_id, chan_id_b); + assert_eq!(updates.update_fulfill_htlcs.len(), 1); + assert!(updates.update_add_htlcs.is_empty()); + assert!(updates.update_fail_htlcs.is_empty()); + assert!(updates.update_fail_malformed_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + for fulfill in &updates.update_fulfill_htlcs { + nodes[0].node.handle_update_fulfill_htlc(node_1_id, fulfill.clone()); + } + // Complete the same commitment handshake for channel B. Here nodes[0]'s final monitor + // update is persisted immediately because `PaymentSent` already ran for channel A. + do_commitment_signed_dance( + &nodes[0], &nodes[1], &updates.commitment_signed, false, false, + ); + }, + _ => panic!("Unexpected fulfill message: {:?}", fulfill_msgs[0]), + } + check_added_monitors(&nodes[1], 0); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + let final_payment_events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(final_payment_events.len(), 1); + match &final_payment_events[0] { + Event::PaymentPathSuccessful { payment_hash: Some(path_hash), path, .. } => { + assert_eq!(*path_hash, payment_hash); + assert_eq!(path.hops.len(), 1); + assert_eq!(path.hops[0].short_channel_id, scid_b); + }, + _ => panic!("Unexpected final payment event: {:?}", final_payment_events[0]), + } + check_added_monitors(&nodes[0], 0); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + check_added_monitors(&nodes[0], 0); + check_added_monitors(&nodes[1], 0); + + // Both MPP parts should have been fulfilled back to nodes[0]. If either channel still has a + // pending outbound HTLC, its fulfill remained stuck in nodes[1]'s holding cell after reload. + let pending: Vec<_> = nodes[0].node.list_channels().iter() + .filter(|channel| channel.channel_id == chan_id_a || channel.channel_id == chan_id_b) + .filter(|channel| !channel.pending_outbound_htlcs.is_empty()) + .map(|channel| channel.channel_id) + .collect(); + assert!(pending.is_empty(), "HTLC fulfills remained stuck on channels {:?}", pending); +} + fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_htlc: bool, use_intercept: bool) { if !use_cs_commitment { assert!(!claim_htlc); } // If we go to forward a payment, and the ChannelMonitor persistence completes, but the @@ -1420,3 +1777,84 @@ fn test_peer_storage() { assert!(res.is_err()); } +#[test] +fn test_hold_completed_inflight_monitor_updates_upon_manager_reload() { + // Test that if a `ChannelMonitorUpdate` completes after the `ChannelManager` is serialized, + // but before it is deserialized, we hold any completed in-flight updates until background event + // processing. Previously, we would remove completed monitor updates from + // `in_flight_monitor_updates` during deserialization, relying on + // [`ChannelManager::process_background_events`] to eventually be called before the + // `ChannelManager` is serialized again such that the channel is resumed and further updates can + // be made. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister_a, persister_b); + let (chain_monitor_a, chain_monitor_b); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes_0_deserialized_a; + let nodes_0_deserialized_b; + + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let chan_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + + // Send a payment that will be pending due to an async monitor update. + let (route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); + let payment_id = PaymentId(payment_hash.0); + let onion = RecipientOnionFields::secret_only(payment_secret); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + check_added_monitors(&nodes[0], 1); + + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Serialize the ChannelManager while the monitor update is still in-flight. + let node_0_serialized = nodes[0].node.encode(); + + // Now complete the monitor update by calling force_channel_monitor_updated. + // This updates the monitor's state, but the ChannelManager still thinks it's pending. + let (_, latest_update_id) = nodes[0].chain_monitor.get_latest_mon_update_id(chan_id); + nodes[0].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id, latest_update_id); + let monitor_serialized_updated = get_monitor!(nodes[0], chan_id).encode(); + + // Reload the node with the updated monitor. Upon deserialization, the ChannelManager will + // detect that the monitor update completed (monitor's update_id >= the in-flight update_id) + // and queue a `BackgroundEvent::MonitorUpdatesComplete`. + nodes[0].node.peer_disconnected(nodes[1].node.get_our_node_id()); + nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); + reload_node!( + nodes[0], + test_default_channel_config(), + &node_0_serialized, + &[&monitor_serialized_updated[..]], + persister_a, + chain_monitor_a, + nodes_0_deserialized_a + ); + + // If we serialize again, even though we haven't processed any background events yet, we should + // still see the `BackgroundEvent::MonitorUpdatesComplete` be regenerated on startup. + let node_0_serialized = nodes[0].node.encode(); + reload_node!( + nodes[0], + test_default_channel_config(), + &node_0_serialized, + &[&monitor_serialized_updated[..]], + persister_b, + chain_monitor_b, + nodes_0_deserialized_b + ); + + // Reconnect the nodes. We should finally see the `update_add_htlc` go out, as the reconnection + // should first process `BackgroundEvent::MonitorUpdatesComplete, allowing the channel to be + // resumed. + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.pending_htlc_adds = (0, 1); + reconnect_nodes(reconnect_args); +} + diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs index ede6acfb639..32041905139 100644 --- a/lightning/src/ln/reorg_tests.rs +++ b/lightning/src/ln/reorg_tests.rs @@ -1230,3 +1230,207 @@ fn test_split_htlc_expiry_tracking() { do_test_split_htlc_expiry_tracking(true, false, true); do_test_split_htlc_expiry_tracking(false, false, true); } + +fn do_test_reorg_resurrect_split_htlc_package_with_future_locktime(style: ConnectStyle) { + // Exercises a failed `request.merge_package(..)` in `OnchainTxHandler::blocks_disconnected`. + // + // Two of our outbound HTLCs land on the counterparty's commitment with `cltv_expiry` two + // blocks apart. If the commitment confirms at a height at or past the *later* expiry, both + // timeout claims are immediately spendable, so their `package_locktime` agree and they + // aggregate into a single claim. The counterparty then claims only the earlier-expiring HTLC + // with its preimage, which splits that input back out into a `ContentiousOutpoint`. On a reorg + // back below the later expiry, the split-off package's locktime requirement has been met but + // the surviving one's has not, so the two are no longer mergeable and the assert's condition + // is false. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + // A legacy (non-anchor) channel is required: with anchors the counterparty aggregates both of + // its HTLC-success claims into one transaction, which would spend *every* outpoint in our + // claim and take the `is_claim_subset_of_tx` path instead of splitting. + let legacy_cfg = test_default_channel_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg.clone()), Some(legacy_cfg)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + *nodes[0].connect_style.borrow_mut() = style; + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, chan_id, funding_tx) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 0); + + // Two outbound HTLCs with different amounts (so we can tell their commitment outputs apart) + // and `cltv_expiry` exactly two blocks apart. + let amt_a_msat = 100_000_000; + let amt_b_msat = 200_000_000; + let (preimage_a, payment_hash_a, ..) = route_payment(&nodes[0], &[&nodes[1]], amt_a_msat); + connect_blocks(&nodes[0], 2); + connect_blocks(&nodes[1], 2); + let (preimage_b, payment_hash_b, ..) = route_payment(&nodes[0], &[&nodes[1]], amt_b_msat); + + let (cltv_a, cltv_b) = { + let chans = nodes[0].node.list_channels(); + assert_eq!(chans.len(), 1); + let mut htlcs = chans[0].pending_outbound_htlcs.clone(); + assert_eq!(htlcs.len(), 2, "{htlcs:?}"); + htlcs.sort_by_key(|h| h.cltv_expiry); + assert_eq!(htlcs[0].amount_msat, amt_a_msat); + assert_eq!(htlcs[1].amount_msat, amt_b_msat); + (htlcs[0].cltv_expiry, htlcs[1].cltv_expiry) + }; + println!("[repro] A height after routing = {}", nodes[0].best_block_info().1); + println!("[repro] cltv_a = {cltv_a}, cltv_b = {cltv_b}"); + assert_eq!(cltv_b, cltv_a + 2, "need the expiries exactly 2 apart"); + + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + // Give node B both preimages so it will claim both HTLCs on-chain. + nodes[1].node.claim_funds(preimage_a); + expect_payment_claimed!(nodes[1], payment_hash_a, amt_a_msat); + nodes[1].node.claim_funds(preimage_b); + expect_payment_claimed!(nodes[1], payment_hash_b, amt_b_msat); + check_added_monitors(&nodes[1], 2); + + // Node B force-closes, giving us its commitment transaction. + let err = "Channel force-closed".to_string(); + nodes[1].node.force_close_broadcasting_latest_txn(&chan_id, &node_id_0, err).unwrap(); + check_closed_broadcast(&nodes[1], 1, false); + check_added_monitors(&nodes[1], 1); + let message = "Channel force-closed".to_owned(); + let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; + check_closed_event(&nodes[1], 1, reason, false, &[node_id_0], 10_000_000); + + // On a legacy channel node B knows both preimages, so it broadcasts its commitment together + // with a separate, pre-signed HTLC-success transaction per HTLC. + let mut bs_txn = nodes[1].tx_broadcaster.txn_broadcast(); + println!("[repro] node B broadcast {} txn on force-close", bs_txn.len()); + assert_eq!(bs_txn.len(), 3, "{bs_txn:?}"); + let commitment_tx = bs_txn.remove(0); + check_spends!(commitment_tx, funding_tx); + let commitment_txid = commitment_tx.compute_txid(); + for tx in bs_txn.iter() { + check_spends!(tx, commitment_tx); + assert_eq!(tx.input.len(), 1); + } + // Take only the HTLC-success transaction for the *earlier*-expiring HTLC, identified by the + // value of the commitment output it spends. + let partial_claim_tx = bs_txn + .into_iter() + .find(|tx| { + tx.input.len() == 1 + && tx.input[0].previous_output.txid == commitment_txid + && commitment_tx.output[tx.input[0].previous_output.vout as usize].value.to_sat() + == amt_a_msat / 1000 + }) + .expect("node B should have a single-input HTLC-success tx for the earlier HTLC"); + println!( + "[repro] node B partial claim spends {}:{} (value {} sat)", + partial_claim_tx.input[0].previous_output.txid, + partial_claim_tx.input[0].previous_output.vout, + commitment_tx.output[partial_claim_tx.input[0].previous_output.vout as usize] + .value + .to_sat(), + ); + + // Advance node A so that the block containing the commitment lands at exactly `cltv_b`. Note + // this stays below `cltv_a + LATENCY_GRACE_PERIOD_BLOCKS`, so node A does not force-close on + // its own first. + let a_height = nodes[0].best_block_info().1; + assert!(a_height < cltv_b - 1, "a_height {a_height} cltv_b {cltv_b}"); + connect_blocks(&nodes[0], cltv_b - 1 - a_height); + assert_eq!(nodes[0].best_block_info().1, cltv_b - 1); + assert_eq!(nodes[0].tx_broadcaster.txn_broadcast().len(), 0); + + mine_transaction(&nodes[0], &commitment_tx); + let commitment_conf_height = nodes[0].best_block_info().1; + println!("[repro] commitment confirmed on A at height {commitment_conf_height}"); + assert_eq!(commitment_conf_height, cltv_b); + check_closed_broadcast(&nodes[0], 1, false); + let reason = ClosureReason::CommitmentTxConfirmed; + check_closed_event(&nodes[0], 1, reason, false, &[node_id_1], 10_000_000); + check_added_monitors(&nodes[0], 1); + + // Both timeout claims must have aggregated into a single transaction, otherwise the path we + // are trying to reach does not exist. + { + let txn = nodes[0].tx_broadcaster.txn_broadcast(); + println!("[repro] node A broadcast {} txn when the commitment confirmed:", txn.len()); + for tx in txn.iter() { + println!( + "[repro] txid {} locktime {} inputs {:?}", + tx.compute_txid(), + tx.lock_time.to_consensus_u32(), + tx.input.iter().map(|i| i.previous_output).collect::>(), + ); + } + let aggregated = txn + .iter() + .find(|tx| { + tx.input.len() == 2 + && tx.input.iter().all(|i| i.previous_output.txid == commitment_txid) + }) + .expect("node A must aggregate both HTLC timeout claims into one transaction"); + assert_eq!(aggregated.lock_time.to_consensus_u32(), cltv_b); + } + + // Now confirm the counterparty's preimage claim of the earlier HTLC, splitting it back out of + // our aggregated claim. + mine_transaction(&nodes[0], &partial_claim_tx); + let spend_conf_height = nodes[0].best_block_info().1; + println!("[repro] node B's partial claim confirmed on A at height {spend_conf_height}"); + { + let txn = nodes[0].tx_broadcaster.txn_broadcast(); + println!("[repro] node A broadcast {} txn after the split:", txn.len()); + for tx in txn.iter() { + println!( + "[repro] txid {} locktime {} inputs {:?}", + tx.compute_txid(), + tx.lock_time.to_consensus_u32(), + tx.input.iter().map(|i| i.previous_output).collect::>(), + ); + } + } + + // Node A learned both preimages from node B's on-chain claim, so drain the resulting payment + // events; we want the only possible failure below to be the one we are testing for. + nodes[0].node.get_and_clear_pending_events(); + check_added_monitors(&nodes[0], 1); + + // Reorg back to `cltv_b - 2`, which is `cltv_a`. The resurrected package (locktime `cltv_a`) + // is spendable, but the surviving request still requires locktime `cltv_b`. + let disconnect = spend_conf_height - cltv_b + 2; + let new_best_height = spend_conf_height - disconnect; + println!( + "[repro] disconnecting {disconnect} blocks: new_best_height will be {new_best_height} \ + (cltv_a {cltv_a}, cltv_b {cltv_b}); re-merge is evaluated at {}", + new_best_height + 1, + ); + assert!(cltv_a <= new_best_height); + assert!(new_best_height + 1 < cltv_b); + disconnect_blocks(&nodes[0], disconnect); + println!("[repro] survived the disconnect, height now {}", nodes[0].best_block_info().1); + + // The failed merge must not discard the resurrected package. On the next block it should be + // registered as an independent claim and broadcast on its own. + connect_blocks(&nodes[0], 1); + let early_htlc_outpoint = partial_claim_tx.input[0].previous_output; + let txn = nodes[0].tx_broadcaster.txn_broadcast(); + let independent_claim = txn + .iter() + .find(|tx| tx.input.len() == 1 && tx.input[0].previous_output == early_htlc_outpoint) + .expect("the resurrected HTLC package must be broadcast as an independent claim"); + assert_eq!(independent_claim.lock_time.to_consensus_u32(), new_best_height + 1); +} + +#[test] +fn test_reorg_resurrect_split_htlc_package_with_future_locktime() { + // `FullBlockDisconnectionsSkippingViaListen` issues a single `blocks_disconnected` call with + // the fork point, which is what `lightning-block-sync` does after detecting a reorg. The + // styles which instead walk the chain backwards one block at a time re-merge successfully on + // the first (shallowest) disconnection, and the `*ReorgsOnlyTip` styles go through + // `transaction_unconfirmed`, which disconnects only to `entry.height - 1`; neither reaches the + // case below. + do_test_reorg_resurrect_split_htlc_package_with_future_locktime( + ConnectStyle::FullBlockDisconnectionsSkippingViaListen, + ); +} diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index e387ac3bfdd..e60ecf7dddc 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -17,7 +17,9 @@ use crate::events::bump_transaction::sync::WalletSourceSync; use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType}; use crate::ln::chan_utils; use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY; -use crate::ln::channelmanager::{PaymentId, RecipientOnionFields, BREAKDOWN_TIMEOUT}; +use crate::ln::channelmanager::{ + provided_init_features, PaymentId, RecipientOnionFields, BREAKDOWN_TIMEOUT, +}; use crate::ln::functional_test_utils::*; use crate::ln::funding::{FundingTxInput, SpliceContribution}; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; @@ -30,6 +32,70 @@ use crate::util::test_channel_signer::SignerOp; use bitcoin::secp256k1::PublicKey; use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut}; +#[test] +fn test_splicing_not_supported_api_error() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut features = provided_init_features(&test_default_channel_config()); + features.clear_splicing(); + features.clear_splicing_legacy(); + *node_cfgs[0].override_init_features.borrow_mut() = Some(features); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1); + + let bs_contribution = SpliceContribution::SpliceIn { + value: Amount::ZERO, + inputs: Vec::new(), + change_script: None, + }; + + let res = nodes[1].node.splice_channel( + &channel_id, + &node_id_0, + bs_contribution.clone(), + 0, // funding_feerate_per_kw, + None, // locktime + ); + match res { + Err(APIError::ChannelUnavailable { err }) => { + assert!(err.contains("Peer does not support splicing")) + }, + _ => panic!("Wrong error {:?}", res.err().unwrap()), + } + + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + let mut features = nodes[0].node.init_features(); + features.set_splicing_optional(); + features.clear_quiescence(); + *nodes[0].override_init_features.borrow_mut() = Some(features); + + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_channel_ready = (true, true); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); + + let res = nodes[1].node.splice_channel( + &channel_id, + &node_id_0, + bs_contribution, + 0, // funding_feerate_per_kw, + None, // locktime + ); + match res { + Err(APIError::ChannelUnavailable { err }) => { + assert!(err.contains("Peer does not support quiescence, a splicing prerequisite")) + }, + _ => panic!("Wrong error {:?}", res.err().unwrap()), + } +} + #[test] fn test_v1_splice_in_negative_insufficient_inputs() { let chanmon_cfgs = create_chanmon_cfgs(2); @@ -759,15 +825,17 @@ fn test_splice_in() { mine_transaction(&nodes[0], &splice_tx); mine_transaction(&nodes[1], &splice_tx); - let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert!(htlc_limit_msat < initial_channel_value_sat * 1000); - let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + let pre_splice_htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(pre_splice_htlc_limit_msat < initial_channel_value_sat * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], pre_splice_htlc_limit_msat); lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); - let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert!(htlc_limit_msat > initial_channel_value_sat); - let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + let post_splice_htlc_limit_msat = + nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert!(post_splice_htlc_limit_msat > pre_splice_htlc_limit_msat); + assert!(post_splice_htlc_limit_msat > initial_channel_value_sat * 1000); + let _ = send_payment(&nodes[0], &[&nodes[1]], post_splice_htlc_limit_msat); } #[test] @@ -2054,3 +2122,529 @@ fn test_splice_with_inflight_htlc_forward_and_resolution() { do_test_splice_with_inflight_htlc_forward_and_resolution(true); do_test_splice_with_inflight_htlc_forward_and_resolution(false); } + +/// We previously allowed a splice initiator to splice out funds past their channel reserve if the +/// the acceptor had no balance in the channel, and there were no HTLCs in the channel +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +enum AcceptorBalance { + NoBalance, + BalanceInHTLC, + SettledBalance, +} + +#[cfg(test)] +#[derive(Clone, Copy, Debug)] +enum ValidationCase { + Passes, + FailsAtHolder, + FailsAtCounterparty, +} + +#[test] +fn test_splice_out_initiator_reserve_breach_zero_fee_commitments() { + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::NoBalance, + ValidationCase::Passes, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::BalanceInHTLC, + ValidationCase::Passes, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::SettledBalance, + ValidationCase::Passes, + ); + + // We used to fail this case here + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::NoBalance, + ValidationCase::FailsAtHolder, + ); + + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::BalanceInHTLC, + ValidationCase::FailsAtHolder, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::SettledBalance, + ValidationCase::FailsAtHolder, + ); + + // We used to fail this case here + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::NoBalance, + ValidationCase::FailsAtCounterparty, + ); + + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::BalanceInHTLC, + ValidationCase::FailsAtCounterparty, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::SettledBalance, + ValidationCase::FailsAtCounterparty, + ); +} + +#[cfg(test)] +fn do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + acceptor_balance: AcceptorBalance, validation_case: ValidationCase, +) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + // This reserve breach was only possible in 0FC channels + config.manually_accept_inbound_channels = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + config.channel_handshake_config.our_htlc_minimum_msat = 1; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + provide_anchor_reserves(&nodes); + + // Node 0 is initiator, node 1 is acceptor + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + let node_1_settled_balance_msat = + if matches!(acceptor_balance, AcceptorBalance::SettledBalance) { 1 } else { 0 }; + let node_1_htlc_balance_msat = + if matches!(acceptor_balance, AcceptorBalance::BalanceInHTLC) { 1 } else { 0 }; + let node_0_balance_msat = + channel_value_sat * 1000 - node_1_settled_balance_msat - node_1_htlc_balance_msat; + + // Bump initiator's dust limit to the highest value we allow in anchor channels + let high_dust_limit_satoshis = 10_000; + + let (_, _, channel_id, _tx) = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + channel_value_sat, + node_1_settled_balance_msat, + ); + + if matches!(acceptor_balance, AcceptorBalance::BalanceInHTLC) { + let _ = route_payment(&nodes[0], &[&nodes[1]], node_1_htlc_balance_msat); + } + + { + let per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id); + if let Some(chan) = channel.as_funded_mut() { + chan.context.holder_dust_limit_satoshis = high_dust_limit_satoshis; + } else { + panic!("Unexpected Channel phase"); + } + } + + { + let per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id); + if let Some(chan) = channel.as_funded_mut() { + chan.context.counterparty_dust_limit_satoshis = high_dust_limit_satoshis; + } else { + panic!("Unexpected Channel phase"); + } + } + + if matches!(validation_case, ValidationCase::Passes) { + let node_0_balance_leftover_amount = Amount::from_sat(high_dust_limit_satoshis); + // Estimated fees of a splice_out at 253sat/kw + let estimated_fees = 183; + // Note in 0FC we've got no fee spike buffer, no commit tx fee, no anchors + let splice_out_output_sat = + node_0_balance_msat / 1000 - node_0_balance_leftover_amount.to_sat() - estimated_fees; + let splice_out_output_amount = Amount::from_sat(splice_out_output_sat); + let outputs = vec![TxOut { + value: splice_out_output_amount, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let contribution = SpliceContribution::SpliceOut { outputs }; + + let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + } else { + let node_0_balance_leftover_amount = Amount::from_sat(high_dust_limit_satoshis - 1); + let post_splice_channel_value_sat = node_0_balance_leftover_amount.to_sat(); + // Note in 0FC we've got no fee spike buffer, no commit tx fee, no anchors + let funding_contribution_sat = + -((node_0_balance_msat / 1000 - node_0_balance_leftover_amount.to_sat()) as i64); + let value = if matches!(validation_case, ValidationCase::FailsAtHolder) { + Amount::from_sat(funding_contribution_sat.unsigned_abs() - 183) + } else if matches!(validation_case, ValidationCase::FailsAtCounterparty) { + // Splice out some dummy amount to get past the initiator's validation, + // we'll modify the message in-flight. + Amount::from_sat(1000) + } else { + panic!("Unexpected test case"); + }; + let outputs = vec![TxOut { + value, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let contribution = SpliceContribution::SpliceOut { outputs }; + + let res = nodes[0].node.splice_channel(&channel_id, &node_id_1, contribution, 253, None); + + match (validation_case, acceptor_balance) { + (ValidationCase::FailsAtHolder, AcceptorBalance::NoBalance) => { + let err = format!( + "The post-splice channel value {post_splice_channel_value_sat} \ + is smaller than our dust limit {high_dust_limit_satoshis}" + ); + assert_eq!(res.unwrap_err(), APIError::APIMisuseError { err }); + return; + }, + (ValidationCase::FailsAtHolder, _) => { + let v2_reserve_amount = Amount::from_sat(high_dust_limit_satoshis); + let err = format!( + "Channel {channel_id} cannot be spliced out; our \ + post-splice channel balance {node_0_balance_leftover_amount} \ + is smaller than their selected v2 reserve {v2_reserve_amount}" + ); + assert_eq!(res.unwrap_err(), APIError::APIMisuseError { err }); + return; + }, + _ => (), + } + + // The dummy contribution should have passed the holder's validation + assert!(res.is_ok()); + + // When acceptor has no balance, the reserve the initiator should keep should remain + // clamped at its dust limit. We previously allowed the initiator to withdraw past + // this point. + let v2_channel_reserve = Amount::from_sat(high_dust_limit_satoshis); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + let mut splice_init = + get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + // Make the modification here, acceptor should now complain. If the acceptor has no + // balance, we previously would not complain. + splice_init.funding_contribution_satoshis = funding_contribution_sat; + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + if let MessageSendEvent::HandleError { action, .. } = &msg_events[0] { + assert!(matches!(action, msgs::ErrorAction::DisconnectPeerWithWarning { .. })); + } else { + panic!("Expected MessageSendEvent::HandleError"); + } + let cannot_splice_out = if matches!(acceptor_balance, AcceptorBalance::NoBalance) { + format!( + "Got non-closing error: The post-splice channel value \ + {post_splice_channel_value_sat} is smaller than their dust limit \ + {high_dust_limit_satoshis}" + ) + } else { + // As soon as we've pushed any sats out of our balance, the channel value + // is now at the dust limit, so we don't complain when determining the new + // dust limits, but later when we check the balances against those new + // dust limits + assert_eq!( + (channel_value_sat as i64 + funding_contribution_sat) as u64, + high_dust_limit_satoshis + ); + format!( + "Got non-closing error: Channel {channel_id} cannot \ + be spliced out; their post-splice channel balance \ + {node_0_balance_leftover_amount} is smaller than our selected v2 reserve \ + {v2_channel_reserve}" + ) + }; + acceptor.logger.assert_log("lightning::ln::channelmanager", cannot_splice_out, 1); + } +} + +#[test] +fn test_splice_minimum_depth_for_counterparty_initiated() { + // When a 0-conf channel is spliced by the counterparty, the LSP (acceptor) should gate + // splice_locked behind the configured splice_minimum_depth rather than inheriting 0-conf. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + + // node_a (LSP): accepts 0-conf, but requires 6 confirmations for counterparty-initiated splices. + let mut lsp_config = test_default_channel_config(); + lsp_config.manually_accept_inbound_channels = true; + lsp_config.channel_handshake_limits.trust_own_funding_0conf = true; + lsp_config.channel_handshake_config.splice_minimum_depth = Some(6); + + // node_b (client): vanilla 0-conf, no splice_minimum_depth. + let mut client_config = test_default_channel_config(); + client_config.manually_accept_inbound_channels = true; + client_config.channel_handshake_limits.trust_own_funding_0conf = true; + + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(lsp_config), Some(client_config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_a = nodes[0].node.get_our_node_id(); + let node_id_b = nodes[1].node.get_our_node_id(); + + // Open a 0-conf channel: node_b initiates, node_a accepts 0-conf. + let initial_channel_value_sat = 1_000_000; + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[1], &nodes[0], None, initial_channel_value_sat, 0); + mine_transaction(&nodes[0], &funding_tx); + mine_transaction(&nodes[1], &funding_tx); + + // node_b (client) initiates the splice → is_initiator=true on node_b, is_initiator=false on node_a. + let coinbase_tx = provide_anchor_reserves(&nodes); + let splice_in_sats = 500_000; + let initiator_contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(splice_in_sats), + inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 1).unwrap()], + change_script: Some(nodes[1].wallet_source.get_change_script().unwrap()), + }; + + let new_funding_script = + complete_splice_handshake(&nodes[1], &nodes[0], channel_id, initiator_contribution.clone()); + + let initial_commit_sig = complete_interactive_funding_negotiation( + &nodes[1], + &nodes[0], + channel_id, + initiator_contribution, + new_funding_script, + ); + + // --- Sign the splice tx (custom flow for asymmetric splice_locked) --- + // The standard sign_interactive_funding_tx helper expects symmetric 0-conf behavior. + // Here, node_b (initiator) will send splice_locked, but node_a (acceptor) will not. + + // acceptor (node_a) receives initiator's commitment_signed, responds with commitment_signed + tx_signatures. + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_commitment_signed(node_id_b, &initial_commit_sig); + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { + nodes[1].node.handle_commitment_signed(node_id_a, &updates.commitment_signed[0]); + } else { + panic!("Expected UpdateHTLCs, got {:?}", &msg_events[0]); + } + if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[1] { + nodes[1].node.handle_tx_signatures(node_id_a, msg); + } else { + panic!("Expected SendTxSignatures, got {:?}", &msg_events[1]); + } + + // initiator (node_b) signs the funding tx. + let event = get_event!(nodes[1], Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id: cid, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + assert_eq!(cid, channel_id); + let signed_tx = nodes[1].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[1].node.funding_transaction_signed(&cid, &counterparty_node_id, signed_tx).unwrap(); + } + + // node_b (initiator, client) should emit TxSignatures + SpliceLocked (0-conf, no override). + let mut initiator_msgs = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!( + initiator_msgs.len(), + 2, + "client should send TxSignatures + SpliceLocked: {initiator_msgs:?}" + ); + let initiator_splice_locked = + if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &initiator_msgs[0] { + // Forward TxSignatures to acceptor (node_a). + nodes[0].node.handle_tx_signatures(node_id_b, msg); + match initiator_msgs.remove(1) { + MessageSendEvent::SendSpliceLocked { msg, .. } => msg, + ref e => panic!("Expected SendSpliceLocked, got {:?}", e), + } + } else { + panic!("Expected SendTxSignatures, got {:?}", &initiator_msgs[0]); + }; + + check_added_monitors(&nodes[1], 1); + check_added_monitors(&nodes[0], 1); + + let splice_tx = { + let mut txn = nodes[1].tx_broadcaster.txn_broadcast(); + assert_eq!(txn.len(), 1); + assert_eq!(nodes[0].tx_broadcaster.txn_broadcast(), txn); + txn.remove(0) + }; + + expect_splice_pending_event(&nodes[1], &node_id_a); + expect_splice_pending_event(&nodes[0], &node_id_b); + + // --- Key assertion: node_a (LSP/acceptor) should NOT have emitted SpliceLocked --- + let acceptor_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + assert!( + acceptor_msgs.is_empty(), + "LSP should not send splice_locked before required depth, got: {acceptor_msgs:?}" + ); + + // Confirm the splice tx and mine blocks up to (but not including) the required depth. + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + // 1 conf from mine_transaction. We need 6 total, so connect 4 more (will be at 5 confs). + connect_blocks(&nodes[0], 4); + connect_blocks(&nodes[1], 4); + + // Still not enough depth — node_a should still not have emitted splice_locked. + // (Other messages like SendAnnouncementSignatures may appear.) + let acceptor_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + assert!( + !acceptor_msgs.iter().any(|m| matches!(m, MessageSendEvent::SendSpliceLocked { .. })), + "LSP should not send splice_locked at 5 confs (need 6), got: {acceptor_msgs:?}" + ); + // Clear any announcement signatures from node_b as well. + nodes[1].node.get_and_clear_pending_msg_events(); + + // Mine one more block → 6 confirmations on both. node_a should now emit splice_locked. + connect_blocks(&nodes[0], 1); + connect_blocks(&nodes[1], 1); + + let acceptor_splice_locked = + get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_b); + + // Save old funding info for chain source cleanup BEFORE any splice_locked handling, + // since handle_splice_locked updates the monitor's funding outpoint. + let (prev_funding_outpoint, prev_funding_script) = nodes[0] + .chain_monitor + .chain_monitor + .get_monitor(channel_id) + .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) + .unwrap(); + + // Send node_b's splice_locked (from earlier) to node_a. + nodes[0].node.handle_splice_locked(node_id_b, &initiator_splice_locked); + + // Send node_a's splice_locked to node_b. node_b responds with SendAnnouncementSignatures. + nodes[1].node.handle_splice_locked(node_id_a, &acceptor_splice_locked); + let mut b_msgs = nodes[1].node.get_and_clear_pending_msg_events(); + // node_b already sent splice_locked earlier, so it only emits SendAnnouncementSignatures. + assert_eq!(b_msgs.len(), 1, "{b_msgs:?}"); + if let MessageSendEvent::SendAnnouncementSignatures { ref msg, .. } = b_msgs.remove(0) { + nodes[0].node.handle_announcement_signatures(node_id_b, msg); + } else { + panic!("Expected SendAnnouncementSignatures, got {:?}", b_msgs); + } + + expect_channel_ready_event(&nodes[0], &node_id_b); + check_added_monitors(&nodes[0], 1); + expect_channel_ready_event(&nodes[1], &node_id_a); + check_added_monitors(&nodes[1], 1); + + // node_a now emits its own announcement_signatures + BroadcastChannelAnnouncement. + let mut a_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(a_msgs.len(), 2, "{a_msgs:?}"); + if let MessageSendEvent::SendAnnouncementSignatures { ref msg, .. } = a_msgs.remove(0) { + nodes[1].node.handle_announcement_signatures(node_id_a, msg); + } else { + panic!("Expected SendAnnouncementSignatures"); + } + assert!(matches!(a_msgs.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + let mut b_msgs = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(b_msgs.len(), 1, "{b_msgs:?}"); + assert!(matches!(b_msgs.remove(0), MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + // Cleanup chain source watches for old funding. + nodes[0] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); + nodes[1] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); + + // Verify the channel is usable. + send_payment(&nodes[1], &[&nodes[0]], 100_000); +} + +#[test] +fn test_splice_minimum_depth_not_applied_for_self_initiated() { + // When the LSP itself initiates a splice, the splice_minimum_depth override should NOT apply + // (is_initiator=true on LSP side), and the splice should lock immediately (0-conf). + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + + // node_a (LSP): 0-conf + splice_minimum_depth. + let mut lsp_config = test_default_channel_config(); + lsp_config.manually_accept_inbound_channels = true; + lsp_config.channel_handshake_limits.trust_own_funding_0conf = true; + lsp_config.channel_handshake_config.splice_minimum_depth = Some(6); + + // node_b (client): vanilla 0-conf. + let mut client_config = test_default_channel_config(); + client_config.manually_accept_inbound_channels = true; + client_config.channel_handshake_limits.trust_own_funding_0conf = true; + + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(lsp_config), Some(client_config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_a = nodes[0].node.get_our_node_id(); + let node_id_b = nodes[1].node.get_our_node_id(); + + // Open a 0-conf channel: node_b initiates channel, node_a accepts 0-conf. + let initial_channel_value_sat = 1_000_000; + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[1], &nodes[0], None, initial_channel_value_sat, 0); + mine_transaction(&nodes[0], &funding_tx); + mine_transaction(&nodes[1], &funding_tx); + + // node_a (LSP) initiates the splice → is_initiator=true on node_a, override should NOT apply. + let coinbase_tx = provide_anchor_reserves(&nodes); + let splice_in_sats = 500_000; + let initiator_contribution = SpliceContribution::SpliceIn { + value: Amount::from_sat(splice_in_sats), + inputs: vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], + change_script: Some(nodes[0].wallet_source.get_change_script().unwrap()), + }; + + let new_funding_script = + complete_splice_handshake(&nodes[0], &nodes[1], channel_id, initiator_contribution.clone()); + + let initial_commit_sig = complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + initiator_contribution, + new_funding_script, + ); + + // Use the standard helper — both sides should be 0-conf since the override doesn't apply + // to self-initiated splices. + let (_splice_tx, splice_locked) = + sign_interactive_funding_tx(&nodes[0], &nodes[1], initial_commit_sig, true); + + // node_a (LSP, initiator) DOES emit SpliceLocked immediately — the override is not applied + // because is_initiator=true. + let (splice_locked_msg, for_node_id) = splice_locked.unwrap(); + assert_eq!(for_node_id, node_id_b); + + expect_splice_pending_event(&nodes[0], &node_id_b); + expect_splice_pending_event(&nodes[1], &node_id_a); + + lock_splice(&nodes[0], &nodes[1], &splice_locked_msg, true); + + // Verify the channel is usable. + send_payment(&nodes[0], &[&nodes[1]], 100_000); +} diff --git a/lightning/src/ln/update_fee_tests.rs b/lightning/src/ln/update_fee_tests.rs index acb913294ab..35d02752fea 100644 --- a/lightning/src/ln/update_fee_tests.rs +++ b/lightning/src/ln/update_fee_tests.rs @@ -414,7 +414,7 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann let channel_id = chan.2; let secp_ctx = Secp256k1::new(); let bs_channel_reserve_sats = - get_holder_selected_channel_reserve_satoshis(channel_value, &default_config); + get_holder_selected_channel_reserve_satoshis(channel_value, &default_config).unwrap(); let (anchor_outputs_value_sats, outputs_num_no_htlcs) = if channel_type_features.supports_anchors_zero_fee_htlc_tx() { (ANCHOR_OUTPUT_VALUE_SATOSHI * 2, 4) @@ -892,7 +892,8 @@ pub fn test_chan_init_feerate_unaffordability() { // During open, we don't have a "counterparty channel reserve" to check against, so that // requirement only comes into play on the open_channel handling side. - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= + get_holder_selected_channel_reserve_satoshis(100_000, &default_config).unwrap() * 1000; nodes[0].node.create_channel(node_b_id, 100_000, push_amt, 42, None, None).unwrap(); let mut open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index 8c1887ad139..9c7652c782b 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -491,7 +491,7 @@ impl AsyncReceiveOfferCache { match offer.status { OfferStatus::Used { invoice_created_at: ref mut inv_created_at } | OfferStatus::Ready { invoice_created_at: ref mut inv_created_at } => { - *inv_created_at = core::cmp::min(invoice_created_at, *inv_created_at); + *inv_created_at = core::cmp::max(invoice_created_at, *inv_created_at); }, OfferStatus::Pending => offer.status = OfferStatus::Ready { invoice_created_at }, } diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 15e744e1a7a..d8fca367dff 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -210,10 +210,12 @@ where /// /// Must be called whenever a new chain tip becomes available. May be skipped /// for intermediary blocks. - pub fn best_block_updated(&self, header: &Header, _height: u32) { + pub fn best_block_updated(&self, header: &Header, height: u32) { let timestamp = &self.highest_seen_timestamp; let block_time = header.time as usize; + *self.best_block.write().unwrap() = BestBlock::new(header.block_hash(), height); + loop { // Update timestamp to be the max of its current value and the block // timestamp. This should keep us close to the current time without relying on @@ -235,7 +237,7 @@ where #[cfg(feature = "dnssec")] { let updated_time = timestamp.load(Ordering::Acquire) as u32; - self.hrn_resolver.new_best_block(_height, updated_time); + self.hrn_resolver.new_best_block(height, updated_time); } } } diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index d5d3c4d75a8..9d5d09d2a24 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -2127,6 +2127,19 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidQuantity), } + match OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Bounded(ten)) + .build() + .unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .unwrap() + .quantity(0) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidQuantity), + } + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 7eb719c104a..e9c3deb7d9a 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -402,6 +402,9 @@ macro_rules! offer_builder_methods { ( pub fn build($($self_mut)* $self: $self_type) -> Result { match $self.offer.amount { Some(Amount::Bitcoin { amount_msats }) => { + if amount_msats == 0 { + $self.offer.amount = None; + } if amount_msats > MAX_VALUE_MSAT { return Err(Bolt12SemanticError::InvalidAmount); } @@ -975,7 +978,7 @@ impl OfferContents { fn is_valid_quantity(&self, quantity: u64) -> bool { match self.supported_quantity { - Quantity::Bounded(n) => quantity <= n.get(), + Quantity::Bounded(n) => quantity > 0 && quantity <= n.get(), Quantity::Unbounded => quantity > 0, Quantity::One => quantity == 1, } @@ -1306,11 +1309,12 @@ impl TryFrom for OfferContents { let amount = match (currency, amount) { (None, None) => None, - (None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => { + (None, Some(amount_msats)) if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT => { return Err(Bolt12SemanticError::InvalidAmount); }, (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }), (Some(_), None) => return Err(Bolt12SemanticError::MissingAmount), + (Some(_), Some(0)) => return Err(Bolt12SemanticError::InvalidAmount), (Some(currency_bytes), Some(amount)) => { let iso4217_code = CurrencyCode::new(currency_bytes) .map_err(|_| Bolt12SemanticError::InvalidCurrencyCode)?; @@ -1702,6 +1706,12 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } + + // An amount of 0 is rejected per BOLT 12, so we map it to `None` instead. + match OfferBuilder::new(pubkey(42)).amount_msats(0).build() { + Ok(offer) => assert_eq!(offer.amount(), None), + Err(_) => panic!("expected offer"), + } } #[test] @@ -1974,6 +1984,59 @@ mod tests { Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidCurrencyCode) ), } + + // An offer with amount=0 must be rejected per BOLT 12. + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.0.amount = Some(0); + tlv_stream.0.currency = None; + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + // An offer with amount=0 and a currency must also be rejected. + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.0.amount = Some(0); + tlv_stream.0.currency = Some(b"USD"); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + // BOLT 12 test vectors: verify rejection of offers with amount=0 from their + // bech32 encoding (see bolt12/offers-test.json). + match "lno1pqqq5qqkyyp4he0fg7pqje62jmnq78cr0ashv4q06qql58tyd9rhp3t2wuyugtq".parse::() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + match "lno1qcp4256ypqqq5qqkyyp4he0fg7pqje62jmnq78cr0ashv4q06qql58tyd9rhp3t2wuyugtq" + .parse::() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } } #[test] diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 77f486a6a06..8d4b75bb627 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -400,7 +400,7 @@ impl StaticInvoice { /// Whether the [`Offer`] that this invoice is based on is expired. #[cfg(feature = "std")] pub fn is_offer_expired(&self) -> bool { - self.contents.is_expired() + self.contents.is_offer_expired() } /// Whether the [`Offer`] that this invoice is based on is expired, given the current time as @@ -993,6 +993,43 @@ mod tests { } } + #[cfg(feature = "std")] + #[test] + fn is_offer_expired_does_not_check_invoice_expiry() { + // Regression test: `StaticInvoice::is_offer_expired` must reflect the offer's expiry, + // not the invoice's own expiry. Build an invoice whose offer has no absolute expiry + // (so the offer never expires) but whose own `created_at + relative_expiry` lies in + // the past (so the invoice itself is expired). + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + Duration::from_secs(0), + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .relative_expiry(1) + .build_and_sign(&secp_ctx) + .unwrap(); + + assert!(invoice.is_expired()); + assert!(!invoice.is_offer_expired()); + } + #[test] fn builds_invoice_from_offer_using_derived_key() { let node_id = recipient_pubkey(); diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index 54eb16b5266..c88526c0932 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -520,7 +520,8 @@ impl OMNameResolver { .filter_map(|data| String::from_utf8(data).ok()) .filter(|data_string| data_string.len() > URI_PREFIX.len()) .filter(|data_string| { - data_string[..URI_PREFIX.len()].eq_ignore_ascii_case(URI_PREFIX) + let pfx = &data_string.as_bytes()[..URI_PREFIX.len()]; + pfx.eq_ignore_ascii_case(URI_PREFIX.as_bytes()) }); // Check that there is exactly one TXT record that begins with // bitcoin: as required by BIP 353 (and is valid UTF-8). diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 605a81a4f95..128c30a9473 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -784,6 +784,38 @@ fn we_are_intro_node() { pass_along_path(&nodes); } +#[test] +fn malformed_one_hop_reply_path_does_not_panic_when_we_are_intro_node() { + // A reply path may be provided by an untrusted sender. If its only hop is a forward hop for us, + // advancing it must reject the path instead of leaving a zero-hop destination behind. + let nodes = create_nodes(2); + let secp_ctx = Secp256k1::new(); + let intermediate_nodes = + [MessageForwardNode { node_id: nodes[0].node_id, short_channel_id: None }]; + let valid_reply_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[1].node_id, + nodes[1].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + &*nodes[1].entropy_source, + &secp_ctx, + ); + let malformed_reply_path = BlindedMessagePath::from_blinded_path( + nodes[0].node_id, + valid_reply_path.blinding_point(), + vec![valid_reply_path.blinded_hops()[0].clone()], + ); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + nodes[0].messenger.handle_onion_message_response( + TestCustomMessage::Pong, + Responder::new(malformed_reply_path).respond(), + ) + })); + assert!(result.is_ok(), "responding over a malformed reply path must not panic"); + assert_eq!(result.unwrap(), Err(SendError::BlindedPathAdvanceFailed)); +} + #[test] fn invalid_blinded_path_error() { // Make sure we error as expected if a provided blinded path has 0 hops. diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index 9a2c06bb72f..251c2ae84be 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -584,40 +584,10 @@ where // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; - // Ensure peers have at least three channels so that it is more difficult to infer the - // recipient's node_id. - const MIN_PEER_CHANNELS: usize = 3; - let network_graph = network_graph.deref().read_only(); let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); - let has_one_peer = peers.len() == 1; - let mut peer_info = peers - .map(|peer| MessageForwardNode { - short_channel_id: if compact_paths { peer.short_channel_id } else { None }, - ..peer - }) - // Limit to peers with announced channels unless the recipient is unannounced. - .filter_map(|peer| { - network_graph - .node(&NodeId::from_pubkey(&peer.node_id)) - .filter(|info| { - !is_recipient_announced || info.channels.len() >= MIN_PEER_CHANNELS - }) - .map(|info| (peer, info.is_tor_only(), info.channels.len())) - // Allow messages directly with the only peer when unannounced. - .or_else(|| (!is_recipient_announced && has_one_peer).then(|| (peer, false, 0))) - }) - // Exclude Tor-only nodes when the recipient is announced. - .filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced)) - .collect::>(); - - // Prefer using non-Tor nodes with the most channels as the introduction node. - peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| { - a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse()) - }); - let build_path = |intermediate_hops: &[MessageForwardNode]| { let dummy_hops_count = if compact_paths { 0 @@ -637,12 +607,37 @@ where ) }; - // Try to create paths from peer info, fall back to direct path if needed - let mut paths = peer_info - .into_iter() - .map(|(peer, _, _)| build_path(&[peer])) - .take(MAX_PATHS) - .collect::>(); + let has_one_peer = peers.len() == 1; + let mut paths = if !is_recipient_announced { + let mut peer_info = peers + .map(|peer| MessageForwardNode { + short_channel_id: if compact_paths { peer.short_channel_id } else { None }, + ..peer + }) + .filter_map(|peer| { + network_graph + .node(&NodeId::from_pubkey(&peer.node_id)) + .map(|info| (peer, info.is_tor_only(), info.channels.len())) + // Allow messages directly with the only peer. + .or_else(|| has_one_peer.then(|| (peer, false, 0))) + }) + .collect::>(); + + // Prefer using non-Tor nodes with the most channels as the introduction node. + peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| { + a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse()) + }); + + // Try to create paths from peer info, fall back to direct path if needed + peer_info + .into_iter() + .map(|(peer, _, _)| build_path(&[peer])) + .take(MAX_PATHS) + .collect::>() + } else { + vec![] + }; + if paths.is_empty() { if is_recipient_announced { paths = vec![build_path(&[])]; diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 80ffbf9fb6c..51a9a59e47f 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -1757,14 +1757,16 @@ where } } -// In Jan, 2025 there were about 49K channels. -// We over-allocate by a bit because 20% more is better than the double we get if we're slightly -// too low -const CHAN_COUNT_ESTIMATE: usize = 60_000; -// In Jan, 2025 there were about 15K nodes -// We over-allocate by a bit because 33% more is better than the double we get if we're slightly -// too low -const NODE_COUNT_ESTIMATE: usize = 20_000; +/// In Jan, 2025 there were about 49K channels. +/// +/// We over-allocate by a bit because 20% more is better than the double we get if we're slightly +/// too low +pub const CHAN_COUNT_ESTIMATE: usize = 60_000; +/// In Jan, 2025 there were about 15K nodes +/// +/// We over-allocate by a bit because 33% more is better than the double we get if we're slightly +/// too low +pub const NODE_COUNT_ESTIMATE: usize = 20_000; impl NetworkGraph where diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index 8ea3ea068b3..12ba06eff64 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -980,7 +980,7 @@ impl PaymentParameters { /// [`PaymentParameters::expiry_time`]. pub fn from_bolt11_invoice(invoice: &Bolt11Invoice) -> Self { let mut payment_params = Self::from_node_id( - invoice.recover_payee_pub_key(), + invoice.get_payee_pub_key(), invoice.min_final_cltv_expiry_delta() as u32, ) .with_route_hints(invoice.route_hints()) @@ -2231,10 +2231,12 @@ impl<'a> PaymentPath<'a> { /// contribution this path can make to the final value of the payment. /// May be slightly lower than the actual max due to rounding errors when aggregating fees /// along the path. + /// Returns an error with the index of a later hop to discard if the following hops' aggregate + /// fees overflow. #[rustfmt::skip] fn max_final_value_msat( &self, used_liquidities: &HashMap, channel_saturation_pow_half: u8 - ) -> (usize, u64) { + ) -> Result<(usize, u64), usize> { let mut max_path_contribution = (0, u64::MAX); for (idx, (hop, _)) in self.hops.iter().enumerate() { let hop_effective_capacity_msat = hop.candidate.effective_capacity(); @@ -2250,7 +2252,8 @@ impl<'a> PaymentPath<'a> { // Aggregate the fees of the hops that come after this one, and use those fees to compute the // maximum amount that this hop can contribute to the final value received by the payee. let (next_hops_aggregated_base, next_hops_aggregated_prop) = - crate::blinded_path::payment::compute_aggregated_base_prop_fee(next_hops_feerates_iter).unwrap(); + crate::blinded_path::payment::compute_aggregated_base_prop_fee(next_hops_feerates_iter) + .map_err(|_| idx + 1)?; // floor(((hop_max_msat - agg_base) * 1_000_000) / (1_000_000 + agg_prop)) let hop_max_final_value_contribution = (hop_max_msat as u128) @@ -2267,7 +2270,19 @@ impl<'a> PaymentPath<'a> { } else { debug_assert!(false); } } - max_path_contribution + Ok(max_path_contribution) + } +} + +fn mark_candidate_liquidity_exhausted( + used_liquidities: &mut HashMap, candidate: &CandidateRouteHop, +) { + let exhausted = u64::max_value(); + if let Some(scid) = candidate.short_channel_id() { + *used_liquidities.entry(CandidateHopId::Clear((scid, false))).or_default() = exhausted; + *used_liquidities.entry(CandidateHopId::Clear((scid, true))).or_default() = exhausted; + } else { + *used_liquidities.entry(candidate.id()).or_default() = exhausted; } } @@ -3448,7 +3463,17 @@ where L::Target: Logger { // underpaid htlc_minimum_msat with fees. debug_assert_eq!(payment_path.get_value_msat(), value_contribution_msat); let (lowest_value_contrib_hop, max_path_contribution_msat) = - payment_path.max_final_value_msat(&used_liquidities, channel_saturation_pow_half); + match payment_path.max_final_value_msat(&used_liquidities, channel_saturation_pow_half) { + Ok(contribution) => contribution, + Err(candidate_idx_to_skip) => { + let candidate = &payment_path.hops[candidate_idx_to_skip].0.candidate; + log_trace!(logger, + "Ignoring path because aggregate fees including hop {} overflow.", + LoggedCandidateHop(candidate)); + mark_candidate_liquidity_exhausted(&mut used_liquidities, candidate); + continue 'paths_collection; + } + }; let desired_value_contribution = cmp::min(max_path_contribution_msat, final_value_msat); value_contribution_msat = payment_path.update_value_and_recompute_fees(desired_value_contribution); @@ -3922,8 +3947,10 @@ mod tests { use crate::util::test_utils as ln_test_utils; use bitcoin::amount::Amount; + use bitcoin::bech32::primitives::decode::CheckedHrpstring; + use bitcoin::bech32::{ByteIterExt, Fe32IterExt}; use bitcoin::constants::ChainHash; - use bitcoin::hashes::Hash; + use bitcoin::hashes::{Hash, sha256::Hash as Sha256}; use bitcoin::hex::FromHex; use bitcoin::network::Network; use bitcoin::opcodes; @@ -3932,9 +3959,58 @@ mod tests { use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::TxOut; + use lightning_invoice::{Bolt11Bech32, Bolt11Invoice, Currency, InvoiceBuilder}; + use crate::io::Cursor; use crate::prelude::*; use crate::sync::Arc; + use crate::types::payment::PaymentSecret; + + fn invoice_with_included_payee_pub_key_and_bad_recovery_id() -> (Bolt11Invoice, PublicKey) { + let secp_ctx = Secp256k1::new(); + let private_key = SecretKey::from_slice(&[42; 32]).unwrap(); + let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); + + let invoice = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".to_string()) + .amount_milli_satoshis(1000) + .payment_hash(Sha256::from_slice(&[0; 32][..]).unwrap()) + .payment_secret(PaymentSecret([21; 32])) + .payee_pub_key(public_key) + .min_final_cltv_expiry_delta(144) + .duration_since_epoch(core::time::Duration::from_secs(1234567)) + .build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key)) + .unwrap(); + + let invoice_string = invoice.to_string(); + let parsed = CheckedHrpstring::new::(&invoice_string).unwrap(); + let hrp = parsed.hrp(); + let mut data: Vec<_> = parsed.fe32_iter::<&mut dyn Iterator>().collect(); + let signature_start = data.len() - 104; + let mut signature_bytes: Vec = + data[signature_start..].iter().copied().fes_to_bytes().collect(); + signature_bytes[64] = 2; + let signature_data: Vec<_> = signature_bytes.into_iter().bytes_to_fes().collect(); + data.splice(signature_start.., signature_data); + + let bad_invoice_string = data + .into_iter() + .with_checksum::(&hrp) + .chars() + .collect::(); + (bad_invoice_string.parse().unwrap(), public_key) + } + + #[test] + fn payment_params_from_bolt11_invoice_uses_included_payee_pub_key() { + let (invoice, public_key) = invoice_with_included_payee_pub_key_and_bad_recovery_id(); + let payment_params = PaymentParameters::from_bolt11_invoice(&invoice); + + match payment_params.payee { + super::Payee::Clear { node_id, .. } => assert_eq!(node_id, public_key), + super::Payee::Blinded { .. } => panic!("BOLT11 invoice should create a clear payee"), + } + } #[rustfmt::skip] fn get_channel_details(short_channel_id: Option, node_id: PublicKey, @@ -9044,6 +9120,67 @@ mod tests { assert_eq!(route.paths[0].hops[0].short_channel_id, 44); } + #[test] + fn aggregated_prop_fee_overflow_fails_route() { + // If the fee cap is disabled, we may consider invoice hints with very large + // proportional fees. Aggregating those fees can overflow, in which case we should fail + // routing cleanly rather than panic. + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(ln_test_utils::TestLogger::new()); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger))); + let scorer = ln_test_utils::TestScorer::new(); + let random_seed_bytes = [42; 32]; + let config = UserConfig::default(); + + let (_, our_node_id, _, nodes) = get_nodes(&secp_ctx); + let route_hint = RouteHint(vec![ + RouteHintHop { + src_node_id: nodes[0], + short_channel_id: 100, + fees: RoutingFees { base_msat: 0, proportional_millionths: u32::MAX }, + cltv_expiry_delta: 10, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }, + RouteHintHop { + src_node_id: nodes[1], + short_channel_id: 101, + fees: RoutingFees { base_msat: 0, proportional_millionths: u32::MAX }, + cltv_expiry_delta: 10, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }, + ]); + + let payment_params = PaymentParameters::from_node_id(nodes[2], 42) + .with_route_hints(vec![route_hint]) + .unwrap() + .with_bolt11_features(channelmanager::provided_bolt11_invoice_features(&config)) + .unwrap(); + let first_hops = [get_channel_details( + Some(1), + nodes[0], + channelmanager::provided_init_features(&config), + 100_000_000, + )]; + let route_params = RouteParameters { + payment_params, + final_value_msat: 1, + max_total_routing_fee_msat: None, + }; + let route = get_route( + &our_node_id, + &route_params, + &network_graph.read_only(), + Some(&first_hops.iter().collect::>()), + Arc::clone(&logger), + &scorer, + &Default::default(), + &random_seed_bytes, + ); + assert!(route.is_err()); + } + #[test] fn prefers_paths_by_cost_amt_ratio() { // Previously, we preferred paths during MPP selection based on their absolute cost, rather diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 74941ec8a87..e6d598f1aab 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -194,7 +194,6 @@ impl TxBuilder for SpecTxBuilder { if channel_type.supports_anchor_zero_fee_commitments() { debug_assert_eq!(feerate_per_kw, 0); debug_assert_eq!(excess_feerate, 0); - debug_assert_eq!(addl_nondust_htlc_count, 0); } // Calculate inbound htlc count diff --git a/lightning/src/util/anchor_channel_reserves.rs b/lightning/src/util/anchor_channel_reserves.rs index e50e103211f..29e24c9f727 100644 --- a/lightning/src/util/anchor_channel_reserves.rs +++ b/lightning/src/util/anchor_channel_reserves.rs @@ -25,7 +25,7 @@ use crate::chain::chainmonitor::ChainMonitor; use crate::chain::chainmonitor::Persist; use crate::chain::Filter; use crate::events::bump_transaction::Utxo; -use crate::ln::chan_utils::max_htlcs; +use crate::ln::chan_utils::{max_htlcs, BASE_INPUT_WEIGHT}; use crate::ln::channelmanager::AChannelManager; use crate::prelude::new_hash_set; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -240,11 +240,11 @@ pub fn get_supportable_anchor_channels( let mut total_fractional_amount = Amount::from_sat(0); let mut num_whole_utxos = 0; for utxo in utxos { - let satisfaction_fee = context + let spend_fee = context .upper_bound_fee_rate - .fee_wu(Weight::from_wu(utxo.satisfaction_weight)) + .fee_wu(Weight::from_wu(BASE_INPUT_WEIGHT + utxo.satisfaction_weight)) .unwrap_or(Amount::MAX); - let amount = utxo.output.value.checked_sub(satisfaction_fee).unwrap_or(Amount::MIN); + let amount = utxo.output.value.checked_sub(spend_fee).unwrap_or(Amount::MIN); if amount >= reserve_per_channel { num_whole_utxos += 1; } else { @@ -260,6 +260,13 @@ pub fn get_supportable_anchor_channels( num_whole_utxos + total_fractional_amount.to_sat() / reserve_per_channel.to_sat() / 2 } +/// Returns whether a channel of the given type requires an on-chain anchor reserve, i.e. uses +/// either the `anchors_zero_fee_htlc_tx` or `anchor_zero_fee_commitments` (TRUC / 0FC) variant. +fn is_anchor_channel_type(channel_type: &ChannelTypeFeatures) -> bool { + channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_anchor_zero_fee_commitments() +} + /// Verifies whether the anchor channel reserve provided by `utxos` is sufficient to support /// an additional anchor channel. /// @@ -311,7 +318,7 @@ where } else { continue; }; - if channel_monitor.channel_type_features().supports_anchors_zero_fee_htlc_tx() + if is_anchor_channel_type(&channel_monitor.channel_type_features()) && !channel_monitor.get_claimable_balances().is_empty() { anchor_channels.insert(channel_id); @@ -320,7 +327,7 @@ where // Also include channels that are in the middle of negotiation or anchor channels that don't have // a ChannelMonitor yet. for channel in a_channel_manager.get_cm().list_channels() { - if channel.channel_type.map_or(true, |ct| ct.supports_anchors_zero_fee_htlc_tx()) { + if channel.channel_type.map_or(true, |ct| is_anchor_channel_type(&ct)) { anchor_channels.insert(channel.channel_id); } } @@ -330,6 +337,7 @@ where #[cfg(test)] mod test { use super::*; + use crate::ln::functional_test_utils::*; use bitcoin::{OutPoint, ScriptBuf, TxOut, Txid}; use std::str::FromStr; @@ -376,6 +384,15 @@ mod test { assert_eq!(get_supportable_anchor_channels(&context, utxos.as_slice()), 3); } + #[test] + fn test_get_supportable_anchor_channels_accounts_for_input_weight() { + let context = AnchorChannelReserveContext::default(); + let reserve = get_reserve_per_channel(&context); + let utxo = make_p2wpkh_utxo(reserve - Amount::from_sat(1)); + + assert_eq!(get_supportable_anchor_channels(&context, &[utxo]), 0); + } + #[test] fn test_anchor_output_spend_transaction_weight() { // Example with smaller signatures: @@ -439,4 +456,49 @@ mod test { 1068 ); } + + #[test] + fn test_can_support_additional_anchor_channel_zero_fee_commitments() { + // Regression test: a channel that uses the `anchor_zero_fee_commitments` + // (option 41) variant is just as much an anchor channel — and requires + // the same on-chain reserve — as one using `anchors_zero_fee_htlc_tx`. + // The reserve check must therefore count it as an existing anchor + // channel when deciding whether the wallet can safely support an + // additional one. Currently `can_support_additional_anchor_channel` + // only counts channels whose features set `anchors_zero_fee_htlc_tx`, + // so a node whose reserves are exhausted by zero-fee-commitment + // channels is incorrectly told it can open another anchor channel. + let mut cfg = test_default_channel_config(); + cfg.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + cfg.manually_accept_inbound_channels = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(cfg.clone()), Some(cfg)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_chan_between_nodes(&nodes[0], &nodes[1]); + + let channels = nodes[0].node.list_channels(); + assert_eq!(channels.len(), 1); + let channel_type = channels[0].channel_type.as_ref().unwrap(); + assert!(channel_type.supports_anchor_zero_fee_commitments()); + // Sanity check: a zero-fee-commitments channel does not also set the + // older anchors_zero_fee_htlc_tx feature. + assert!(!channel_type.supports_anchors_zero_fee_htlc_tx()); + + let context = AnchorChannelReserveContext::default(); + let reserve = get_reserve_per_channel(&context); + // Provide a single UTXO with enough value to cover one channel reserve. + let utxos = vec![make_p2wpkh_utxo(reserve * 2)]; + + // We already have one TRUC anchor channel and only enough reserve for + // a single channel; we must not authorize an additional one. + assert!(!can_support_additional_anchor_channel( + &context, + &utxos, + nodes[0].node, + &nodes[0].chain_monitor.chain_monitor, + )); + } } diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index 0856dc96394..281f3b63a8b 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -154,14 +154,40 @@ pub struct ChannelHandshakeConfig { /// /// Default value: `10_000` millionths (i.e., 1% of channel value) /// - /// Minimum value: If the calculated proportional value is less than `1000` sats, it will be - /// treated as `1000` sats instead, which is a safe implementation-specific lower - /// bound. + /// Minimum value: If the calculated proportional value is less than `min_their_channel_reserve_satoshis`, + /// it will be treated as `min_their_channel_reserve_satoshis` instead. /// /// Maximum value: `1_000_000` (i.e., 100% of channel value. Any values larger than one million /// will be treated as one million instead, although channel negotiations will /// fail in that case.) pub their_channel_reserve_proportional_millionths: u32, + /// The minimum absolute channel reserve value in satoshis that will be enforced regardless of + /// the proportional reserve calculation. + /// + /// This ensures that even if the proportional reserve calculation results in a very small value + /// (or zero), at least this minimum amount will be required as a channel reserve. This provides + /// a safety mechanism to ensure some minimum reserve is always maintained. + /// + /// **Special case: Setting to `0`** + /// + /// Setting this value to `0` allows the counterparty to have no channel reserve, enabling them + /// to use their entire channel balance for payments. This is useful for LSP use cases where the + /// LSP wants to allow clients to be able to fully withdraw their funds from the channel without + /// closing it. + /// + /// **Security Warning:** + /// + /// When set to `0`, the channel reserve no longer provides economic security. If the counterparty + /// broadcasts a revoked state, there is no reserve to claim as punishment. This removes the + /// economic disincentive for the counterparty to attempt cheating. Only use this setting with + /// trusted counterparties (e.g., known LSP clients) or when other trust mechanisms are in place. + /// + /// When set to `0`, the dust limit check is bypassed, allowing reserves below the protocol + /// minimum dust limit (354 sats). For any non-zero value below the dust limit, the dust limit + /// check will still be enforced. + /// + /// Default value: `1000` sats + pub min_their_channel_reserve_satoshis: u64, /// If set, we attempt to negotiate the `anchors_zero_fee_htlc_tx`option for all future /// channels. This feature requires having a reserve of onchain funds readily available to bump /// transactions in the event of a channel force close to avoid the possibility of losing funds. @@ -241,6 +267,15 @@ pub struct ChannelHandshakeConfig { /// /// [`max_htlcs`]: crate::ln::chan_utils::max_htlcs pub our_max_accepted_htlcs: u16, + + /// If set, counterparty-initiated splices on 0-conf channels will require this many + /// confirmations before we send `splice_locked`. This prevents inheriting 0-conf trust + /// on funding outputs we didn't construct. + /// + /// Has no effect on self-initiated splices or non-0-conf channels. + /// + /// Default value: `None` (splice inherits the channel's `minimum_depth`) + pub splice_minimum_depth: Option, } impl Default for ChannelHandshakeConfig { @@ -254,9 +289,11 @@ impl Default for ChannelHandshakeConfig { announce_for_forwarding: false, commit_upfront_shutdown_pubkey: true, their_channel_reserve_proportional_millionths: 10_000, + min_their_channel_reserve_satoshis: 1_000, negotiate_anchors_zero_fee_htlc_tx: false, negotiate_anchor_zero_fee_commitments: false, our_max_accepted_htlcs: 50, + splice_minimum_depth: None, } } } @@ -276,9 +313,11 @@ impl Readable for ChannelHandshakeConfig { announce_for_forwarding: Readable::read(reader)?, commit_upfront_shutdown_pubkey: Readable::read(reader)?, their_channel_reserve_proportional_millionths: Readable::read(reader)?, + min_their_channel_reserve_satoshis: Readable::read(reader)?, negotiate_anchors_zero_fee_htlc_tx: Readable::read(reader)?, negotiate_anchor_zero_fee_commitments: Readable::read(reader)?, our_max_accepted_htlcs: Readable::read(reader)?, + splice_minimum_depth: Readable::read(reader)?, }) } } diff --git a/lightning/src/util/macro_logger.rs b/lightning/src/util/macro_logger.rs index ec9eb14ba38..fac68f19c59 100644 --- a/lightning/src/util/macro_logger.rs +++ b/lightning/src/util/macro_logger.rs @@ -169,6 +169,33 @@ macro_rules! log_spendable { }; } +/// The maximum number of characters to display in a network message log entry. +pub(crate) const LOG_MSG_MAX_LEN: usize = 512; + +/// Wraps a string slice for Display, truncating to [`LOG_MSG_MAX_LEN`] characters and +/// delegating sanitization to [`crate::types::string::PrintableString`]. +/// Useful for logging counterparty-provided messages. +pub(crate) struct DebugMsg<'a>(pub &'a str); +impl<'a> core::fmt::Display for DebugMsg<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + let (msg, was_truncated) = match self.0.char_indices().nth(LOG_MSG_MAX_LEN) { + Some((idx, _)) => (&self.0[..idx], true), + None => (self.0, false), + }; + core::fmt::Display::fmt(&crate::types::string::PrintableString(msg), f)?; + if was_truncated { + f.write_str("...")?; + } + Ok(()) + } +} + +macro_rules! log_msg { + ($obj: expr) => { + $crate::util::macro_logger::DebugMsg(&$obj) + }; +} + /// Create a new Record and log it. You probably don't want to use this macro directly, /// but it needs to be exported so `log_trace` etc can use it in external crates. #[doc(hidden)] @@ -226,3 +253,61 @@ macro_rules! log_gossip { $crate::log_given_level!($logger, $crate::util::logger::Level::Gossip, $($arg)*); ) } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + + #[test] + fn debug_msg_short_string() { + let s = "hello world"; + assert_eq!(DebugMsg(s).to_string(), "hello world"); + } + + #[test] + fn debug_msg_truncates_at_limit() { + let s: String = core::iter::repeat('a').take(LOG_MSG_MAX_LEN + 100).collect(); + let result = DebugMsg(&s).to_string(); + // Should be exactly LOG_MSG_MAX_LEN 'a's followed by "..." + assert_eq!(result.len(), LOG_MSG_MAX_LEN + 3); + assert!(result.ends_with("...")); + } + + #[test] + fn debug_msg_no_truncation_at_exact_limit() { + let s: String = core::iter::repeat('a').take(LOG_MSG_MAX_LEN).collect(); + let result = DebugMsg(&s).to_string(); + assert_eq!(result.len(), LOG_MSG_MAX_LEN); + assert!(!result.ends_with("...")); + } + + #[test] + fn debug_msg_replaces_control_characters() { + let s = "hello\x00world\nfoo"; + let result = DebugMsg(s).to_string(); + assert_eq!(result, "hello\u{FFFD}world\u{FFFD}foo"); + } + + #[test] + fn debug_msg_uses_printable_string_sanitization() { + let s = "safe\u{202E}cipsxe.exe"; + assert_eq!(DebugMsg(s).to_string(), crate::types::string::PrintableString(s).to_string()); + } + + #[test] + fn debug_msg_multibyte_unicode() { + // Each emoji is multiple bytes but one character + let s: String = core::iter::repeat('\u{1F600}').take(LOG_MSG_MAX_LEN + 10).collect(); + let result = DebugMsg(&s).to_string(); + let char_count: usize = result.chars().count(); + // LOG_MSG_MAX_LEN emoji chars + 3 chars for "..." + assert_eq!(char_count, LOG_MSG_MAX_LEN + 3); + assert!(result.ends_with("...")); + } + + #[test] + fn debug_msg_empty_string() { + assert_eq!(DebugMsg("").to_string(), ""); + } +} diff --git a/lightning/src/util/persist.rs b/lightning/src/util/persist.rs index d00e29e686a..43bd55c7fd7 100644 --- a/lightning/src/util/persist.rs +++ b/lightning/src/util/persist.rs @@ -247,6 +247,35 @@ where } } +impl KVStoreSync for KVStoreSyncWrapper +where + K::Target: KVStoreSync, +{ + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result> { + self.0.read(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> io::Result<()> { + self.0.write(primary_namespace, secondary_namespace, key, buf.to_vec()) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> io::Result<()> { + self.0.remove(primary_namespace, secondary_namespace, key, lazy) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> io::Result> { + self.0.list(primary_namespace, secondary_namespace) + } +} + /// Provides an interface that allows storage and retrieval of persisted values that are associated /// with given keys. /// diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index f821aa5afc0..92b1e224ac6 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -925,7 +925,9 @@ macro_rules! impl_for_map { #[inline] fn read(r: &mut R) -> Result { let len: CollectionLength = Readable::read(r)?; - let mut ret = $constr(len.0 as usize); + let entry_size = ::core::mem::size_of::() + ::core::mem::size_of::(); + let max_alloc = MAX_BUF_SIZE / (entry_size + 1); + let mut ret = $constr(cmp::min(len.0 as usize, max_alloc)); for _ in 0..len.0 { let k = K::read(r)?; let v_opt = V::read(r)?; diff --git a/lightning/src/util/sweep.rs b/lightning/src/util/sweep.rs index b31dab1ccf6..eb1fc09f2c2 100644 --- a/lightning/src/util/sweep.rs +++ b/lightning/src/util/sweep.rs @@ -473,12 +473,18 @@ where return Ok(()); } - let result = self.regenerate_and_broadcast_spend_if_necessary_internal().await; - - // Release the pending sweep flag again, regardless of result. - self.pending_sweep.store(false, Ordering::Release); + // Use an RAII guard so the flag is released even if this future is dropped mid-await + // (e.g. cancelled by `tokio::time::timeout` or `select!`). A bare `store(false)` after + // the await would never run on cancellation, leaving the sweeper permanently disabled. + struct PendingSweepGuard<'a>(&'a AtomicBool); + impl<'a> Drop for PendingSweepGuard<'a> { + fn drop(&mut self) { + self.0.store(false, Ordering::Release); + } + } + let _guard = PendingSweepGuard(&self.pending_sweep); - result + self.regenerate_and_broadcast_spend_if_necessary_internal().await } /// Regenerates and broadcasts the spending transaction for any outputs that are pending @@ -1111,3 +1117,140 @@ where Ok((best_block, OutputSweeperSync { sweeper })) } } + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::*; + use crate::chain::transaction::OutPoint; + use crate::sign::{ChangeDestinationSource, OutputSpender}; + use crate::util::async_poll::dummy_waker; + use crate::util::logger::Record; + + use bitcoin::hashes::Hash as _; + use bitcoin::secp256k1::All; + use bitcoin::transaction::Version; + use bitcoin::{Amount, BlockHash, ScriptBuf, Transaction, TxOut, Txid}; + + use core::future as core_future; + use core::sync::atomic::Ordering; + use core::task::Poll; + + struct DummyBroadcaster; + impl BroadcasterInterface for DummyBroadcaster { + fn broadcast_transactions(&self, _: &[&Transaction]) {} + } + + struct DummyFeeEstimator; + impl FeeEstimator for DummyFeeEstimator { + fn get_est_sat_per_1000_weight(&self, _: ConfirmationTarget) -> u32 { + 1000 + } + } + + struct DummyFilter; + impl Filter for DummyFilter { + fn register_tx(&self, _: &Txid, _: &bitcoin::Script) {} + fn register_output(&self, _: WatchedOutput) {} + } + + struct DummyLogger; + impl Logger for DummyLogger { + fn log(&self, _: Record) {} + } + + struct DummyOutputSpender; + impl OutputSpender for DummyOutputSpender { + fn spend_spendable_outputs( + &self, _: &[&SpendableOutputDescriptor], _: Vec, _: ScriptBuf, _: u32, + _: Option, _: &Secp256k1, + ) -> Result { + Ok(Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: Vec::new(), + output: Vec::new(), + }) + } + } + + struct DummyChangeDestSource; + impl ChangeDestinationSource for DummyChangeDestSource { + fn get_change_destination_script<'a>( + &'a self, + ) -> AsyncResult<'a, ScriptBuf, ()> { + Box::pin(core_future::ready(Ok(ScriptBuf::new()))) + } + } + + struct PendingKVStore; + impl KVStore for PendingKVStore { + fn read( + &self, _: &str, _: &str, _: &str, + ) -> AsyncResult<'static, Vec, io::Error> { + Box::pin(core_future::ready(Err(io::Error::new(io::ErrorKind::NotFound, "")))) + } + fn write( + &self, _: &str, _: &str, _: &str, _: Vec, + ) -> AsyncResult<'static, (), io::Error> { + Box::pin(core_future::pending()) + } + fn remove( + &self, _: &str, _: &str, _: &str, _: bool, + ) -> AsyncResult<'static, (), io::Error> { + Box::pin(core_future::ready(Ok(()))) + } + fn list( + &self, _: &str, _: &str, + ) -> AsyncResult<'static, Vec, io::Error> { + Box::pin(core_future::ready(Ok(Vec::new()))) + } + } + + #[test] + fn pending_sweep_flag_resets_after_future_drop() { + let best_block = BestBlock::new(BlockHash::all_zeros(), 1_000); + + let sweeper: OutputSweeper<_, _, _, _, _, _, _> = OutputSweeper::new( + best_block, + &DummyBroadcaster, + &DummyFeeEstimator, + None::<&DummyFilter>, + &DummyOutputSpender, + Box::new(DummyChangeDestSource), + &PendingKVStore, + &DummyLogger, + ); + + // Inject a tracked output directly so the sweep loop has work to do. + let descriptor = SpendableOutputDescriptor::StaticOutput { + outpoint: OutPoint { txid: Txid::all_zeros(), index: 0 }, + output: TxOut { value: Amount::from_sat(100_000), script_pubkey: ScriptBuf::new() }, + channel_keys_id: None, + }; + sweeper.sweeper_state.lock().unwrap().outputs.push(TrackedSpendableOutput { + descriptor, + channel_id: None, + status: OutputSpendStatus::PendingInitialBroadcast { delayed_until_height: None }, + }); + + // Start a sweep, poll once (the persist step stays Pending because our KVStore's + // `write` future is `future::pending()`), then drop the future to mimic + // cancellation - the sort of thing a `tokio::time::timeout` wrapper produces. + { + let mut fut = Box::pin(sweeper.regenerate_and_broadcast_spend_if_necessary()); + let waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&waker); + assert!(matches!(fut.as_mut().poll(&mut ctx), Poll::Pending)); + } + + // Once the future has been dropped, `pending_sweep` must be cleared. The bug + // is that the flag is only ever cleared after the inner future returns, so a + // dropped future leaves it stuck `true` and every subsequent call to + // `regenerate_and_broadcast_spend_if_necessary` short-circuits with `Ok(())`, + // permanently disabling the sweeper. + assert!( + !sweeper.pending_sweep.load(Ordering::Acquire), + "pending_sweep flag was not reset when the future was dropped", + ); + } +} diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index c9f9ba2d086..db0b0503118 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -550,6 +550,11 @@ impl<'a> TestChainMonitor<'a> { self.added_monitors.lock().unwrap().push((channel_id, monitor)); self.chain_monitor.load_existing_monitor(channel_id, new_monitor) } + + pub fn get_latest_mon_update_id(&self, channel_id: ChannelId) -> (u64, u64) { + let monitor_id_state = self.latest_monitor_update_id.lock().unwrap(); + monitor_id_state.get(&channel_id).unwrap().clone() + } } impl<'a> chain::Watch for TestChainMonitor<'a> { fn watch_channel( diff --git a/possiblyrandom/Cargo.toml b/possiblyrandom/Cargo.toml index 4508f690129..91c0f201238 100644 --- a/possiblyrandom/Cargo.toml +++ b/possiblyrandom/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "possiblyrandom" -version = "0.2.0" +version = "0.2.1" authors = ["Matt Corallo"] license = "MIT OR Apache-2.0" repository = "https://github.com/lightningdevkit/rust-lightning/" diff --git a/possiblyrandom/src/lib.rs b/possiblyrandom/src/lib.rs index 9cbbad7f13d..6ddbc6de1a2 100644 --- a/possiblyrandom/src/lib.rs +++ b/possiblyrandom/src/lib.rs @@ -20,16 +20,19 @@ #![no_std] -#[cfg(feature = "getrandom")] +#[cfg(any( + feature = "getrandom", + not(any(target_os = "unknown", target_os = "none")) +))] extern crate getrandom; /// Possibly fills `dest` with random data. May fill it with zeros. #[inline] pub fn getpossiblyrandom(dest: &mut [u8]) { - #[cfg(feature = "getrandom")] - if getrandom::getrandom(dest).is_err() { - dest.fill(0); - } - #[cfg(not(feature = "getrandom"))] dest.fill(0); + #[cfg(any( + feature = "getrandom", + not(any(target_os = "unknown", target_os = "none")) + ))] + let _ = getrandom::getrandom(dest); }