diff --git a/.github/workflows/lnd-integration.yml b/.github/workflows/lnd-integration.yml new file mode 100644 index 0000000000..219e929b1b --- /dev/null +++ b/.github/workflows/lnd-integration.yml @@ -0,0 +1,52 @@ +name: CI Checks - LND Integration Tests + +on: [push, pull_request] + +jobs: + check-lnd: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check and install CMake if needed + # lnd_grpc_rust (via prost-build v0.10.4) requires CMake >= 3.5 but is incompatible with CMake >= 4.0. + # This step checks if CMake is missing, below 3.5, or 4.0 or higher, and installs CMake 3.31.6 if needed, + # ensuring compatibility with prost-build in ubuntu-latest. + run: | + if ! command -v cmake &> /dev/null || + [ "$(cmake --version | head -n1 | cut -d' ' -f3)" \< "3.5" ] || + [ "$(cmake --version | head -n1 | cut -d' ' -f3)" \> "4.0" ]; then + sudo apt-get update + sudo apt-get remove -y cmake + wget https://github.com/Kitware/CMake/releases/download/v3.31.6/cmake-3.31.6-Linux-x86_64.sh + echo "518c76bd18cc4ca5faab891db69b1289dc1bf134f394f0983a19576711b95210 cmake-3.31.6-Linux-x86_64.sh" | sha256sum -c - || { + echo "Error: The checksum of the downloaded file does not match the expected value!" + exit 1 + } + chmod +x cmake-3.31.6-Linux-x86_64.sh + sudo ./cmake-3.31.6-Linux-x86_64.sh --prefix=/usr/local --skip-license + fi + + - name: Create temporary directory for LND data + id: create-temp-dir + run: echo "LND_DATA_DIR=$(mktemp -d)" >> $GITHUB_ENV + + - name: Start bitcoind, electrs, and LND + run: docker compose -f docker-compose-lnd.yml up -d + env: + LND_DATA_DIR: ${{ env.LND_DATA_DIR }} + + - name: Set permissions for LND data directory + # In PR 4622 (https://github.com/lightningnetwork/lnd/pull/4622), + # LND sets file permissions to 0700, preventing test code from accessing them. + # This step ensures the test suite has the necessary permissions. + run: sudo chmod -R 755 $LND_DATA_DIR + env: + LND_DATA_DIR: ${{ env.LND_DATA_DIR }} + + - name: Run LND integration tests + run: LND_CERT_PATH=$LND_DATA_DIR/tls.cert LND_MACAROON_PATH=$LND_DATA_DIR/data/chain/bitcoin/regtest/admin.macaroon + RUSTFLAGS="--cfg lnd_test" cargo test --test integration_tests_lnd -- --exact --show-output + env: + LND_DATA_DIR: ${{ env.LND_DATA_DIR }} \ No newline at end of file diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish-android.yml deleted file mode 100644 index b6b24ac90e..0000000000 --- a/.github/workflows/publish-android.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish ldk-node-android to Maven Central -on: [workflow_dispatch] - -jobs: - build: - runs-on: ubuntu-20.04 - steps: - - name: "Check out PR branch" - uses: actions/checkout@v2 - - - name: "Cache" - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ./target - key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} - - - name: "Set up JDK" - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - - - name: "Install Rust Android targets" - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi - - - name: "Build ldk-node-android library" - run: | - export PATH=$PATH:$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin - ./scripts/uniffi_bindgen_generate_kotlin_android.sh - - - name: "Publish to Maven Local and Maven Central" - env: - ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} - ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} - run: | - cd bindings/kotlin/ldk-node-android - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.github/workflows/publish-jvm.yml b/.github/workflows/publish-jvm.yml deleted file mode 100644 index 0ae40e0a11..0000000000 --- a/.github/workflows/publish-jvm.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Publish ldk-node-jvm to Maven Central -on: [workflow_dispatch] - -jobs: - build-jvm-macOS-M1-native-lib: - name: "Create M1 and x86_64 JVM native binaries" - runs-on: macos-12 - steps: - - name: "Checkout publishing branch" - uses: actions/checkout@v2 - - - name: Cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ./target - key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} - - - name: Set up JDK - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - - - name: Install aarch64 Rust target - run: rustup target add aarch64-apple-darwin - - - name: Build ldk-node-jvm library - run: | - ./scripts/uniffi_bindgen_generate_kotlin.sh - - # build aarch64 + x86_64 native libraries and upload - - name: Upload macOS native libraries for reuse in publishing job - uses: actions/upload-artifact@v3 - with: - # name: no name is required because we upload the entire directory - # the default name "artifact" will be used - path: /Users/runner/work/ldk-node/ldk-node/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/ - - build-jvm-full-library: - name: "Create full ldk-node-jvm library" - needs: [build-jvm-macOS-M1-native-lib] - runs-on: ubuntu-20.04 - steps: - - name: "Check out PR branch" - uses: actions/checkout@v2 - - - name: "Cache" - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ./target - key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} - - - name: "Set up JDK" - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - - - name: "Build ldk-node-jvm library" - run: | - ./scripts/uniffi_bindgen_generate_kotlin.sh - - - name: Download macOS native libraries from previous job - uses: actions/download-artifact@v4.1.7 - id: download - with: - # download the artifact created in the prior job (named "artifact") - name: artifact - path: ./bindings/kotlin/ldk-node-jvm/lib/src/main/resources/ - - - name: "Publish to Maven Local and Maven Central" - env: - ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} - ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} - run: | - cd bindings/kotlin/ldk-node-jvm - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0bf3ac49c1..aff6109089 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: toolchain: [ stable, beta, - 1.75.0, # Our MSRV + 1.85.0, # Our MSRV ] include: - toolchain: stable @@ -29,7 +29,7 @@ jobs: platform: macos-latest - toolchain: stable platform: windows-latest - - toolchain: 1.75.0 + - toolchain: 1.85.0 msrv: true runs-on: ${{ matrix.platform }} steps: @@ -42,10 +42,6 @@ jobs: - name: Check formatting on Rust ${{ matrix.toolchain }} if: matrix.check-fmt run: rustup component add rustfmt && cargo fmt --all -- --check - - name: Pin packages to allow for MSRV - if: matrix.msrv - run: | - cargo update -p home --precise "0.5.9" --verbose # home v0.5.11 requires rustc 1.81 or newer - name: Set RUSTFLAGS to deny warnings if: "matrix.toolchain == 'stable'" run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" @@ -78,7 +74,7 @@ jobs: if: matrix.build-uniffi run: cargo build --features uniffi --verbose --color always - name: Build documentation on Rust ${{ matrix.toolchain }} - if: "matrix.platform != 'windows-latest' || matrix.toolchain != '1.75.0'" + if: "matrix.platform != 'windows-latest' || matrix.toolchain != '1.85.0'" run: | cargo doc --release --verbose --color always cargo doc --document-private-items --verbose --color always diff --git a/.github/workflows/vss-integration.yml b/.github/workflows/vss-integration.yml index 2a6c63704e..81b63fdf93 100644 --- a/.github/workflows/vss-integration.yml +++ b/.github/workflows/vss-integration.yml @@ -18,7 +18,7 @@ jobs: env: POSTGRES_DB: postgres POSTGRES_USER: postgres - POSTGRES_PASSWORD: YOU_MUST_CHANGE_THIS_PASSWORD + POSTGRES_PASSWORD: postgres options: >- --health-cmd pg_isready --health-interval 10s @@ -36,47 +36,13 @@ jobs: repository: lightningdevkit/vss-server path: vss-server - - name: Set up Java - uses: actions/setup-java@v3 - with: - distribution: 'corretto' - java-version: '17' - - - name: Start Tomcat + - name: Build and Deploy VSS Server run: | - docker run -d --network=host --name tomcat tomcat:latest - - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - gradle-version: release-candidate - - - name: Create database table - run: | - psql -h localhost -U postgres -d postgres -f ./vss-server/java/app/src/main/java/org/vss/impl/postgres/sql/v0_create_vss_db.sql - env: - PGPASSWORD: YOU_MUST_CHANGE_THIS_PASSWORD - - - name: Build and Deploy VSS - run: | - # Print Info - java -version - gradle --version - - cd vss-server/java - gradle wrapper --gradle-version 8.1.1 - ./gradlew --version - ./gradlew build - - docker cp app/build/libs/vss-1.0.war tomcat:/usr/local/tomcat/webapps/vss.war - cd ../ - - name: Run VSS Integration tests against vss-instance. + cd vss-server/rust + cargo run server/vss-server-config.toml& + - name: Run VSS Integration tests run: | cd ldk-node export TEST_VSS_BASE_URL="http://localhost:8080/vss" RUSTFLAGS="--cfg vss_test" cargo build --verbose --color always RUSTFLAGS="--cfg vss_test" cargo test --test integration_tests_vss - - - name: Cleanup - run: | - docker stop tomcat && docker rm tomcat diff --git a/.gitignore b/.gitignore index d97a301b21..f77ecc60ff 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ swift.swiftdoc .build/arm64-apple-macosx/debug/index/db/v13/p69904--d2c184/data.mdb .build/arm64-apple-macosx/debug/index/db/v13/p69904--d2c184/lock.mdb .build/arm64-apple-macosx/debug/LDKNode.build/module.modulemap + +# Hermit +.hermit/ +bin/ diff --git a/CHANGELOG.md b/CHANGELOG.md index f6edb23fd4..361e2a3a8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,127 @@ +# 0.6.2 - Aug. 14, 2025 +This patch release fixes a panic that could have been hit when syncing to a +TLS-enabled Electrum server, as well as some minor issues when shutting down +the node. + +## Bug Fixes and Improvements +- If not set by the user, we now install a default `CryptoProvider` for the + `rustls` TLS library. This fixes an issue that would have the node panic + whenever they first try to access an Electrum server behind an `ssl://` + address. (#600) +- We improved robustness of the shutdown procedure. In particular, we now + wait for more background tasks to finish processing before shutting down + LDK background processing. Previously some tasks were kept running which + could have lead to race conditions. (#613) + +In total, this release features 12 files changed, 198 insertions, 92 +deletions in 13 commits from 2 authors in alphabetical order: + +- Elias Rohrer +- moisesPomilio + +# 0.6.1 - Jun. 19, 2025 +This patch release fixes minor issues with the recently-exposed `Bolt11Invoice` +type in bindings. + +## Feature and API updates +- The `Bolt11Invoice::description` method is now exposed as + `Bolt11Invoice::invoice_description` in bindings, to avoid collisions with a + Swift standard method of same name (#576) + +## Bug Fixes and Improvements +- The `Display` implementation of `Bolt11Invoice` is now exposed in bindings, + (re-)allowing to render the invoice as a string. (#574) + +In total, this release features 9 files changed, 549 insertions, 83 deletions, +in 8 commits from 1 author in alphabetical order: + +- Elias Rohrer + +# 0.6.0 - Jun. 9, 2025 +This sixth minor release mainly fixes an issue that could have left the +on-chain wallet unable to spend funds if transactions that had previously been +accepted to the mempool ended up being evicted. + +## Feature and API updates +- Onchain addresses are now validated against the expected network before use (#519). +- The API methods on the `Bolt11Invoice` type are now exposed in bindings (#522). +- The `UnifiedQrPayment::receive` flow no longer aborts if we're unable to generate a BOLT12 offer (#548). + +## Bug Fixes and Improvements +- Previously, the node could potentially enter a state that would have left the + onchain wallet unable spend any funds if previously-generated transactions + had been first accepted, and then evicted from the mempool. This has been + fixed in BDK 2.0.0, to which we upgrade as part of this release. (#551) +- A bug that had us fail `OnchainPayment::send_all` in the `retrain_reserves` + mode when requiring sub-dust-limit anchor reserves has been fixed (#540). +- The output of the `log` facade logger has been corrected (#547). + +## Compatibility Notes +- The BDK dependency has been bumped to `bdk_wallet` v2.0 (#551). + +In total, this release features 20 files changed, 1188 insertions, 447 deletions, in 18 commits from 3 authors in alphabetical order: + +- alexanderwiederin +- Camillarhi +- Elias Rohrer + +# 0.5.0 - Apr. 29, 2025 +Besides numerous API improvements and bugfixes this fifth minor release notably adds support for sourcing chain and fee rate data from an Electrum backend, requesting channels via the [bLIP-51 / LSPS1](https://github.com/lightning/blips/blob/master/blip-0051.md) protocol, as well as experimental support for operating as a [bLIP-52 / LSPS2](https://github.com/lightning/blips/blob/master/blip-0052.md) service. + +## Feature and API updates +- The `PaymentSuccessful` event now exposes a `payment_preimage` field (#392). +- The node now emits `PaymentForwarded` events for forwarded payments (#404). +- The ability to send custom TLVs as part of spontaneous payments has been added (#411). +- The ability to override the used fee rates for on-chain sending has been added (#434). +- The ability to set a description hash when creating a BOLT11 invoice has been added (#438). +- The ability to export pathfinding scores has been added (#458). +- The ability to request inbound channels from an LSP via the bLIP-51 / LSPS1 protocol has been added (#418). +- The `ChannelDetails` returned by `Node::list_channels` now exposes fields for the channel's SCIDs (#444). +- Lightning peer-to-peer gossip data is now being verified when syncing from a Bitcoin Core RPC backend (#428). +- The logging sub-system was reworked to allow logging to backends using the Rust [`log`](https://crates.io/crates/log) facade, as well as via a custom logger trait (#407, #450, #454). +- On-chain transactions are now added to the internal payment store and exposed via `Node::list_payments` (#432). +- Inbound announced channels are now rejected if not all requirements for operating as a forwarding node (set listening addresses and node alias) have been met (#467). +- Initial support for operating as an bLIP-52 / LSPS2 service has been added (#420). + - **Note**: bLIP-52 / LSPS2 support is considered 'alpha'/'experimental' and should *not* yet be used in production. +- The `Builder::set_entropy_seed_bytes` method now takes an array rather than a `Vec` (#493). +- The builder will now return a `NetworkMismatch` error in case of network switching (#485). +- The `Bolt11Jit` payment variant now exposes a field telling how much fee the LSP withheld (#497). +- The ability to disable syncing Lightning and on-chain wallets in the background has been added. If it is disabled, the user is responsible for running `Node::sync_wallets` manually (#508). +- The ability to configure the node's announcement addresses independently from the listening addresses has been added (#484). +- The ability to choose whether to honor the Anchor reserves when calling `send_all_to_address` has been added (#345). +- The ability to sync the node via an Electrum backend has been added (#486). + +## Bug Fixes and Improvements +- When syncing from Bitcoin Core RPC, syncing mempool entries has been made more efficient (#410, #465). +- We now ensure the our configured fallback rates are used when the configured chain source would return huge bogus values during fee estimation (#430). +- We now re-enabled trying to bump Anchor channel transactions for trusted counterparties in the `ContentiousClaimable` case to reduce the risk of losing funds in certain edge cases (#461). +- An issue that would potentially have us panic on retrying the chain listening initialization when syncing from Bitcoin Core RPC has been fixed (#471). +- The `Node::remove_payment` now also removes the respective entry from the in-memory state, not only from the persisted payment store (#514). + +## Compatibility Notes +- The filesystem logger was simplified and its default path changed to `ldk_node.log` in the configured storage directory (#394). +- The BDK dependency has been bumped to `bdk_wallet` v1.0 (#426). +- The LDK dependency has been bumped to `lightning` v0.1 (#426). +- The `rusqlite` dependency has been bumped to v0.31 (#403). +- The minimum supported Rust version (MSRV) has been bumped to v1.75 (#429). + +In total, this release features 53 files changed, 6147 insertions, 1193 deletions, in 191 commits from 14 authors in alphabetical order: + +- alexanderwiederin +- Andrei +- Artur Gontijo +- Ayla Greystone +- Elias Rohrer +- elnosh +- Enigbe Ochekliye +- Evan Feenstra +- G8XSU +- Joost Jager +- maan2003 +- moisesPompilio +- Rob N +- Vincenzo Palazzo + # 0.4.3 - Jan. 23, 2025 This patch release fixes the broken Rust build resulting from `cargo` treating the recent v0.1.0 release of `lightning-liquidity` as API-compatible with the previous v0.1.0-alpha.6 release (even though it's not). diff --git a/Cargo.toml b/Cargo.toml old mode 100644 new mode 100755 index a1cf4bbec6..3877c7e31d --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ldk-node" -version = "0.5.0+git" +version = "0.7.0+git" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" @@ -28,46 +28,63 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -# lightning = { version = "0.1.0", features = ["std"] } -# lightning-types = { version = "0.2.0" } -# lightning-invoice = { version = "0.33.0", features = ["std"] } -# lightning-net-tokio = { version = "0.1.0" } -# lightning-persister = { version = "0.1.0" } -# lightning-background-processor = { version = "0.1.0", features = ["futures"] } -# lightning-rapid-gossip-sync = { version = "0.1.0" } -# lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "tokio"] } -# lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time"] } -# lightning-liquidity = { version = "0.1.0", features = ["std"] } - -lightning = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched", features = ["std"] } -lightning-types = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched" } -lightning-invoice = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched" } -lightning-persister = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched" } -lightning-background-processor = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched", features = ["futures"] } -lightning-rapid-gossip-sync = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched" } -lightning-block-sync = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched", features = ["rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched", features = ["esplora-async-https", "time"] } -lightning-liquidity = { git = "https://github.com/cequals/rust-lightning", branch = "amackillop/0.1.0-patched" } +lightning = { version = "0.2.0-beta1", features = ["std"] } +lightning-types = { version = "0.3.0-beta1" } +lightning-invoice = { version = "0.34.0-beta1", features = ["std"] } +lightning-net-tokio = { version = "0.2.0-beta1" } +lightning-persister = { version = "0.2.0-beta1", features = ["tokio"] } +lightning-background-processor = { version = "0.2.0-beta1" } +lightning-rapid-gossip-sync = { version = "0.2.0-beta1" } +lightning-block-sync = { version = "0.2.0-beta1", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { version = "0.2.0-beta1", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { version = "0.2.0-beta1", features = ["std"] } +lightning-macros = { version = "0.2.0-beta1" } + +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } +#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } +#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["tokio"] } +#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rest-client", "rpc-client", "tokio"] } +#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +#lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } + +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } +#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } +#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["tokio"] } +#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["rest-client", "rpc-client", "tokio"] } +#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +#lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } #lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } #lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } -#lightning-persister = { path = "../rust-lightning/lightning-persister" } -#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor", features = ["futures"] } +#lightning-persister = { path = "../rust-lightning/lightning-persister", features = ["tokio"] } +#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } #lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rpc-client", "tokio"] } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "time"] } +#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } +#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } +#lightning-macros = { path = "../rust-lightning/lightning-macros" } -bdk_chain = { version = "0.21.1", default-features = false, features = ["std"] } -bdk_esplora = { version = "0.20.1", default-features = false, features = ["async-https-rustls", "tokio"]} -bdk_wallet = { version = "1.0.0", default-features = false, features = ["std", "keys-bip39"]} +bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } +bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} +bdk_electrum = { version = "0.23.0", default-features = false, features = ["use-rustls-ring"]} +bdk_wallet = { version = "2.2.0", default-features = false, features = ["std", "keys-bip39"]} -reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +rustls = { version = "0.23", default-features = false } rusqlite = { version = "0.31.0", features = ["bundled"] } -bitcoin = "0.32.2" +bitcoin = "0.32.7" bip39 = "2.0.0" bip21 = { version = "0.5", features = ["std"], default-features = false } @@ -75,9 +92,10 @@ base64 = { version = "0.22.1", default-features = false, features = ["std"] } rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] } -esplora-client = { version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } +esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"] } +electrum-client = { version = "0.24.0", default-features = false, features = ["proxy", "use-rustls-ring"] } libc = "0.2" -uniffi = { version = "0.27.3", features = ["build"], optional = true } +uniffi = { version = "0.28.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0.128", default-features = false, features = ["std"] } log = { version = "0.4.22", default-features = false, features = ["std"]} @@ -89,25 +107,29 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -# lightning = { version = "0.1.0", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/cequals/rust-lightning", branch="amackillop/0.1.0-patched", features = ["std", "_test_utils"] } +lightning = { version = "0.2.0-beta1", features = ["std", "_test_utils"] } +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } -electrum-client = { version = "0.21.0", default-features = true } -bitcoincore-rpc = { version = "0.19.0", default-features = false } proptest = "1.0.0" regex = "1.5.6" [target.'cfg(not(no_download))'.dev-dependencies] -electrsd = { version = "0.29.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] } +electrsd = { version = "0.35.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] } [target.'cfg(no_download)'.dev-dependencies] -electrsd = { version = "0.29.0", features = ["legacy"] } +electrsd = { version = "0.35.0", default-features = false, features = ["legacy"] } +corepc-node = { version = "0.8.0", default-features = false, features = ["27_2"] } [target.'cfg(cln_test)'.dev-dependencies] clightningrpc = { version = "0.3.0-beta.8", default-features = false } +[target.'cfg(lnd_test)'.dev-dependencies] +lnd_grpc_rust = { version = "2.10.0", default-features = false } +tokio = { version = "1.37", features = ["fs"] } + [build-dependencies] -uniffi = { version = "0.27.3", features = ["build"], optional = true } +uniffi = { version = "0.28.3", features = ["build"], optional = true } [profile.release] panic = "abort" @@ -123,4 +145,5 @@ check-cfg = [ "cfg(ldk_bench)", "cfg(tokio_unstable)", "cfg(cln_test)", -] + "cfg(lnd_test)", +] \ No newline at end of file diff --git a/Package.swift b/Package.swift index 0590524929..00f3eeb845 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.4.2" -let checksum = "95ea5307eb3a99203e39cfa21d962bfe3e879e62429e8c7cdf5292cae5dc35cc" +let tag = "v0.6.2" +let checksum = "dee28eb2bc019eeb61cc28ca5c19fdada465a6eb2b5169d2dbaa369f0c63ba03" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( diff --git a/README.md b/README.md index f8a3664d4e..d11c5fc8e3 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ fn main() { LDK Node currently comes with a decidedly opinionated set of design choices: - On-chain data is handled by the integrated [BDK][bdk] wallet. -- Chain data may currently be sourced from the Bitcoin Core RPC interface or an [Esplora][esplora] server, while support for Electrum will follow soon. +- Chain data may currently be sourced from the Bitcoin Core RPC interface, or from an [Electrum][electrum] or [Esplora][esplora] server. - Wallet and channel state may be persisted to an [SQLite][sqlite] database, to file system, or to a custom back-end to be implemented by the user. - Gossip data may be sourced via Lightning's peer-to-peer network or the [Rapid Gossip Sync](https://docs.rs/lightning-rapid-gossip-sync/*/lightning_rapid_gossip_sync/) protocol. - Entropy for the Lightning and on-chain wallets may be sourced from raw bytes or a [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic. In addition, LDK Node offers the means to generate and persist the entropy bytes to disk. @@ -64,7 +64,7 @@ LDK Node currently comes with a decidedly opinionated set of design choices: LDK Node itself is written in [Rust][rust] and may therefore be natively added as a library dependency to any `std` Rust program. However, beyond its Rust API it also offers language bindings for [Swift][swift], [Kotlin][kotlin], and [Python][python] based on the [UniFFI](https://github.com/mozilla/uniffi-rs/). Moreover, [Flutter bindings][flutter_bindings] are also available. ## MSRV -The Minimum Supported Rust Version (MSRV) is currently 1.75.0. +The Minimum Supported Rust Version (MSRV) is currently 1.85.0. [api_docs]: https://docs.rs/ldk-node/*/ldk_node/ [api_docs_node]: https://docs.rs/ldk-node/*/ldk_node/struct.Node.html @@ -72,6 +72,7 @@ The Minimum Supported Rust Version (MSRV) is currently 1.75.0. [rust_crate]: https://crates.io/ [ldk]: https://lightningdevkit.org/ [bdk]: https://bitcoindevkit.org/ +[electrum]: https://github.com/spesmilo/electrum-protocol [esplora]: https://github.com/Blockstream/esplora [sqlite]: https://sqlite.org/ [rust]: https://www.rust-lang.org/ diff --git a/bindings/kotlin/ldk-node-android/build.gradle.kts b/bindings/kotlin/ldk-node-android/build.gradle.kts index ab7262dd75..bb38991d37 100644 --- a/bindings/kotlin/ldk-node-android/build.gradle.kts +++ b/bindings/kotlin/ldk-node-android/build.gradle.kts @@ -1,6 +1,7 @@ buildscript { repositories { google() + mavenCentral() } dependencies { classpath("com.android.tools.build:gradle:7.1.2") @@ -8,29 +9,10 @@ buildscript { } plugins { - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" } // library version is defined in gradle.properties val libraryVersion: String by project -// These properties are required here so that the nexus publish-plugin -// finds a staging profile with the correct group (group is otherwise set as "") -// and knows whether to publish to a SNAPSHOT repository or not -// https://github.com/gradle-nexus/publish-plugin#applying-the-plugin group = "org.lightningdevkit" version = libraryVersion - -nexusPublishing { - repositories { - create("sonatype") { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - - val ossrhUsername: String? by project - val ossrhPassword: String? by project - username.set(ossrhUsername) - password.set(ossrhPassword) - } - } -} \ No newline at end of file diff --git a/bindings/kotlin/ldk-node-android/gradle.properties b/bindings/kotlin/ldk-node-android/gradle.properties index 44a51cfafb..578c3308b3 100644 --- a/bindings/kotlin/ldk-node-android/gradle.properties +++ b/bindings/kotlin/ldk-node-android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official -libraryVersion=0.4.2 +libraryVersion=0.6.0 diff --git a/bindings/kotlin/ldk-node-jvm/build.gradle.kts b/bindings/kotlin/ldk-node-jvm/build.gradle.kts index ab7262dd75..faf316ef06 100644 --- a/bindings/kotlin/ldk-node-jvm/build.gradle.kts +++ b/bindings/kotlin/ldk-node-jvm/build.gradle.kts @@ -1,36 +1,17 @@ buildscript { repositories { google() + mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.1.2") } } plugins { - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" } // library version is defined in gradle.properties val libraryVersion: String by project -// These properties are required here so that the nexus publish-plugin -// finds a staging profile with the correct group (group is otherwise set as "") -// and knows whether to publish to a SNAPSHOT repository or not -// https://github.com/gradle-nexus/publish-plugin#applying-the-plugin group = "org.lightningdevkit" version = libraryVersion - -nexusPublishing { - repositories { - create("sonatype") { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - - val ossrhUsername: String? by project - val ossrhPassword: String? by project - username.set(ossrhUsername) - password.set(ossrhPassword) - } - } -} \ No newline at end of file diff --git a/bindings/kotlin/ldk-node-jvm/gradle.properties b/bindings/kotlin/ldk-node-jvm/gradle.properties index 338b60d965..913b5caeac 100644 --- a/bindings/kotlin/ldk-node-jvm/gradle.properties +++ b/bindings/kotlin/ldk-node-jvm/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official -libraryVersion=0.4.2 +libraryVersion=0.6.0 diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 09ffebe63d..a0fb9780e0 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -7,11 +7,12 @@ dictionary Config { string storage_dir_path; Network network; sequence? listening_addresses; + sequence? announcement_addresses; NodeAlias? node_alias; sequence trusted_peers_0conf; u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; - SendingParameters? sending_parameters; + RouteParametersConfig? route_parameters; }; dictionary AnchorChannelsConfig { @@ -19,12 +20,32 @@ dictionary AnchorChannelsConfig { u64 per_channel_reserve_sats; }; -dictionary EsploraSyncConfig { +dictionary BackgroundSyncConfig { u64 onchain_wallet_sync_interval_secs; u64 lightning_wallet_sync_interval_secs; u64 fee_rate_cache_update_interval_secs; }; +dictionary EsploraSyncConfig { + BackgroundSyncConfig? background_sync_config; +}; + +dictionary ElectrumSyncConfig { + BackgroundSyncConfig? background_sync_config; +}; + +dictionary LSPS2ServiceConfig { + string? require_token; + boolean advertise_service; + u32 channel_opening_fee_ppm; + u32 channel_over_provisioning_ppm; + u64 min_channel_opening_fee_msat; + u32 min_channel_lifetime; + u32 max_client_to_self_delay; + u64 min_payment_size_msat; + u64 max_payment_size_msat; +}; + enum LogLevel { "Gossip", "Trace", @@ -43,7 +64,7 @@ dictionary LogRecord { [Trait, WithForeign] interface LogWriter { - void log(LogRecord record); + void log(LogRecord record); }; interface Builder { @@ -55,21 +76,27 @@ interface Builder { void set_entropy_seed_bytes(sequence seed_bytes); void set_entropy_bip39_mnemonic(Mnemonic mnemonic, string? passphrase); void set_chain_source_esplora(string server_url, EsploraSyncConfig? config); + void set_chain_source_electrum(string server_url, ElectrumSyncConfig? config); void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); + void set_chain_source_bitcoind_rest(string rest_host, u16 rest_port, string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); void set_liquidity_source_lsps1(PublicKey node_id, SocketAddress address, string? token); void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token); void set_storage_dir_path(string storage_dir_path); void set_filesystem_logger(string? log_file_path, LogLevel? max_log_level); - void set_log_facade_logger(LogLevel? max_log_level); + void set_log_facade_logger(); void set_custom_logger(LogWriter log_writer); void set_network(Network network); [Throws=BuildError] void set_listening_addresses(sequence listening_addresses); [Throws=BuildError] + void set_announcement_addresses(sequence announcement_addresses); + [Throws=BuildError] void set_node_alias(string node_alias); [Throws=BuildError] + void set_async_payments_role(AsyncPaymentsRole? role); + [Throws=BuildError] Node build(); [Throws=BuildError] Node build_with_fs_store(); @@ -92,9 +119,11 @@ interface Node { Event wait_next_event(); [Async] Event next_event_async(); + [Throws=NodeError] void event_handled(); PublicKey node_id(); sequence? listening_addresses(); + sequence? announcement_addresses(); NodeAlias? node_alias(); Bolt11Payment bolt11_payment(); Bolt12Payment bolt12_payment(); @@ -134,19 +163,19 @@ interface Node { [Enum] interface Bolt11InvoiceDescription { - Hash(string hash); - Direct(string description); + Hash(string hash); + Direct(string description); }; interface Bolt11Payment { [Throws=NodeError] - PaymentId send([ByRef]Bolt11Invoice invoice, SendingParameters? sending_parameters); + PaymentId send([ByRef]Bolt11Invoice invoice, RouteParametersConfig? route_parameters); [Throws=NodeError] - PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, SendingParameters? sending_parameters); + PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, RouteParametersConfig? route_parameters); [Throws=NodeError] - void send_probes([ByRef]Bolt11Invoice invoice); + void send_probes([ByRef]Bolt11Invoice invoice, RouteParametersConfig? route_parameters); [Throws=NodeError] - void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, RouteParametersConfig? route_parameters); [Throws=NodeError] void claim_for_hash(PaymentHash payment_hash, u64 claimable_amount_msat, PaymentPreimage preimage); [Throws=NodeError] @@ -162,7 +191,11 @@ interface Bolt11Payment { [Throws=NodeError] Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); [Throws=NodeError] + Bolt11Invoice receive_via_jit_channel_for_hash(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat, PaymentHash payment_hash); + [Throws=NodeError] Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); + [Throws=NodeError] + Bolt11Invoice receive_variable_amount_via_jit_channel_for_hash([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat, PaymentHash payment_hash); }; interface Bolt12Payment { @@ -178,13 +211,23 @@ interface Bolt12Payment { Bolt12Invoice request_refund_payment([ByRef]Refund refund); [Throws=NodeError] Refund initiate_refund(u64 amount_msat, u32 expiry_secs, u64? quantity, string? payer_note); + [Throws=NodeError] + Offer receive_async(); + [Throws=NodeError] + void set_paths_to_static_invoice_server(bytes paths); + [Throws=NodeError] + bytes blinded_paths_for_async_recipient(bytes recipient_id); }; interface SpontaneousPayment { [Throws=NodeError] - PaymentId send(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters); + PaymentId send(u64 amount_msat, PublicKey node_id, RouteParametersConfig? route_parameters); + [Throws=NodeError] + PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, RouteParametersConfig? route_parameters, sequence custom_tlvs); + [Throws=NodeError] + PaymentId send_with_preimage(u64 amount_msat, PublicKey node_id, PaymentPreimage preimage, RouteParametersConfig? route_parameters); [Throws=NodeError] - PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters, sequence custom_tlvs); + PaymentId send_with_preimage_and_custom_tlvs(u64 amount_msat, PublicKey node_id, sequence custom_tlvs, PaymentPreimage preimage, RouteParametersConfig? route_parameters); [Throws=NodeError] void send_probes(u64 amount_msat, PublicKey node_id); }; @@ -219,7 +262,7 @@ interface LSPS1Liquidity { [Throws=NodeError] LSPS1OrderStatus request_channel(u64 lsp_balance_sat, u64 client_balance_sat, u32 channel_expiry_blocks, boolean announce_channel); [Throws=NodeError] - LSPS1OrderStatus check_order_status(OrderId order_id); + LSPS1OrderStatus check_order_status(LSPS1OrderId order_id); }; [Error] @@ -276,11 +319,12 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "InvalidBlindedPaths", + "AsyncPaymentServicesDisabled", }; dictionary NodeStatus { boolean is_running; - boolean is_listening; BestBlock current_best_block; u64? latest_lightning_wallet_sync_timestamp; u64? latest_onchain_wallet_sync_timestamp; @@ -302,13 +346,17 @@ enum BuildError { "InvalidSystemTime", "InvalidChannelMonitor", "InvalidListeningAddresses", + "InvalidAnnouncementAddresses", "InvalidNodeAlias", + "RuntimeSetupFailed", "ReadFailed", "WriteFailed", "StoragePathAccessFailed", "KVStoreSetupFailed", "WalletSetupFailed", "LoggerSetupFailed", + "NetworkMismatch", + "AsyncPaymentsConfigMismatch", }; [Trait] @@ -356,7 +404,7 @@ enum PaymentFailureReason { [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); - HolderForceClosed(boolean? broadcasted_latest_txn); + HolderForceClosed(boolean? broadcasted_latest_txn, string message); LegacyCooperativeClosure(); CounterpartyInitiatedCooperativeClosure(); LocallyInitiatedCooperativeClosure(); @@ -366,8 +414,9 @@ interface ClosureReason { DisconnectedPeer(); OutdatedChannelManager(); CounterpartyCoopClosedUnfundedChannel(); + LocallyCoopClosedUnfundedChannel(); FundingBatchClosure(); - HTLCsTimedOut(); + HTLCsTimedOut( PaymentHash? payment_hash ); PeerFeerateTooLow(u32 peer_feerate_sat_per_kw, u32 required_feerate_sat_per_kw); }; @@ -375,7 +424,7 @@ interface ClosureReason { interface PaymentKind { Onchain(Txid txid, ConfirmationStatus status); Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret); - Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, LSPFeeLimits lsp_fee_limits); + Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits); Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity); Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); @@ -414,16 +463,17 @@ dictionary PaymentDetails { PaymentId id; PaymentKind kind; u64? amount_msat; + u64? fee_paid_msat; PaymentDirection direction; PaymentStatus status; u64 latest_update_timestamp; }; -dictionary SendingParameters { - MaxTotalRoutingFeeLimit? max_total_routing_fee_msat; - u32? max_total_cltv_expiry_delta; - u8? max_path_count; - u8? max_channel_saturation_power_of_half; +dictionary RouteParametersConfig { + u64? max_total_routing_fee_msat; + u32 max_total_cltv_expiry_delta; + u8 max_path_count; + u8 max_channel_saturation_power_of_half; }; dictionary CustomTlvRecord { @@ -432,13 +482,13 @@ dictionary CustomTlvRecord { }; dictionary LSPS1OrderStatus { - OrderId order_id; - OrderParameters order_params; - PaymentInfo payment_options; - ChannelOrderInfo? channel_state; + LSPS1OrderId order_id; + LSPS1OrderParams order_params; + LSPS1PaymentInfo payment_options; + LSPS1ChannelInfo? channel_state; }; -dictionary OrderParameters { +dictionary LSPS1OrderParams { u64 lsp_balance_sat; u64 client_balance_sat; u16 required_channel_confirmations; @@ -448,22 +498,22 @@ dictionary OrderParameters { boolean announce_channel; }; -dictionary PaymentInfo { - Bolt11PaymentInfo? bolt11; - OnchainPaymentInfo? onchain; +dictionary LSPS1PaymentInfo { + LSPS1Bolt11PaymentInfo? bolt11; + LSPS1OnchainPaymentInfo? onchain; }; -dictionary Bolt11PaymentInfo { - PaymentState state; - DateTime expires_at; +dictionary LSPS1Bolt11PaymentInfo { + LSPS1PaymentState state; + LSPSDateTime expires_at; u64 fee_total_sat; u64 order_total_sat; Bolt11Invoice invoice; }; -dictionary OnchainPaymentInfo { - PaymentState state; - DateTime expires_at; +dictionary LSPS1OnchainPaymentInfo { + LSPS1PaymentState state; + LSPSDateTime expires_at; u64 fee_total_sat; u64 order_total_sat; Address address; @@ -472,24 +522,18 @@ dictionary OnchainPaymentInfo { Address? refund_onchain_address; }; -dictionary ChannelOrderInfo { - DateTime funded_at; +dictionary LSPS1ChannelInfo { + LSPSDateTime funded_at; OutPoint funding_outpoint; - DateTime expires_at; + LSPSDateTime expires_at; }; -enum PaymentState { +enum LSPS1PaymentState { "ExpectPayment", "Paid", "Refunded", }; -[Enum] -interface MaxTotalRoutingFeeLimit { - None (); - Some ( u64 amount_msat ); -}; - [NonExhaustive] enum Network { "Bitcoin", @@ -671,6 +715,115 @@ dictionary NodeAnnouncementInfo { sequence addresses; }; +enum Currency { + "Bitcoin", + "BitcoinTestnet", + "Regtest", + "Simnet", + "Signet", +}; + +enum AsyncPaymentsRole { + "Client", + "Server", +}; + +dictionary RouteHintHop { + PublicKey src_node_id; + u64 short_channel_id; + u16 cltv_expiry_delta; + u64? htlc_minimum_msat; + u64? htlc_maximum_msat; + RoutingFees fees; +}; + +[Traits=(Debug, Display, Eq)] +interface Bolt11Invoice { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string invoice_str); + sequence signable_hash(); + PaymentHash payment_hash(); + PaymentSecret payment_secret(); + u64? amount_milli_satoshis(); + u64 expiry_time_seconds(); + u64 seconds_since_epoch(); + u64 seconds_until_expiry(); + boolean is_expired(); + boolean would_expire(u64 at_time_seconds); + Bolt11InvoiceDescription invoice_description(); + u64 min_final_cltv_expiry_delta(); + Network network(); + Currency currency(); + sequence
fallback_addresses(); + sequence> route_hints(); + PublicKey recover_payee_pub_key(); +}; + +[Enum] +interface OfferAmount { + Bitcoin(u64 amount_msats); + Currency(string iso4217_code, u64 amount); +}; + +[Traits=(Debug, Display, Eq)] +interface Offer { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string offer_str); + OfferId id(); + boolean is_expired(); + string? description(); + string? issuer(); + OfferAmount? amount(); + boolean is_valid_quantity(u64 quantity); + boolean expects_quantity(); + boolean supports_chain(Network chain); + sequence chains(); + sequence? metadata(); + u64? absolute_expiry_seconds(); + PublicKey? issuer_signing_pubkey(); +}; + +[Traits=(Debug, Display, Eq)] +interface Refund { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string refund_str); + string description(); + u64? absolute_expiry_seconds(); + boolean is_expired(); + string? issuer(); + sequence payer_metadata(); + Network? chain(); + u64 amount_msats(); + u64? quantity(); + PublicKey payer_signing_pubkey(); + string? payer_note(); +}; + +interface Bolt12Invoice { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string invoice_str); + PaymentHash payment_hash(); + u64 amount_msats(); + OfferAmount? amount(); + PublicKey signing_pubkey(); + u64 created_at(); + u64? absolute_expiry_seconds(); + u64 relative_expiry(); + boolean is_expired(); + string? description(); + string? issuer(); + string? payer_note(); + sequence? metadata(); + u64? quantity(); + sequence signable_hash(); + PublicKey payer_signing_pubkey(); + PublicKey? issuer_signing_pubkey(); + sequence chain(); + sequence>? offer_chains(); + sequence
fallback_addresses(); + sequence encode(); +}; + [Custom] typedef string Txid; @@ -689,18 +842,6 @@ typedef string NodeId; [Custom] typedef string Address; -[Custom] -typedef string Bolt11Invoice; - -[Custom] -typedef string Offer; - -[Custom] -typedef string Refund; - -[Custom] -typedef string Bolt12Invoice; - [Custom] typedef string OfferId; @@ -732,7 +873,7 @@ typedef string UntrustedString; typedef string NodeAlias; [Custom] -typedef string OrderId; +typedef string LSPS1OrderId; [Custom] -typedef string DateTime; +typedef string LSPSDateTime; diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 781542ec3f..496781a6a5 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldk_node" -version = "0.4.2" +version = "0.6.0" authors = [ { name="Elias Rohrer", email="dev@tnull.de" }, ] diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 4c5cdd828d..f71e89df8e 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -50,27 +50,43 @@ def mine_and_wait(esplora_endpoint, blocks): def wait_for_block(esplora_endpoint, block_hash): url = esplora_endpoint + "/block/" + block_hash + "/status" - esplora_picked_up_block = False - while not esplora_picked_up_block: - res = requests.get(url) + attempts = 0 + max_attempts = 30 + + while attempts < max_attempts: try: + res = requests.get(url, timeout=10) json = res.json() - esplora_picked_up_block = json['in_best_chain'] - except: - pass - time.sleep(1) + if json.get('in_best_chain'): + return + + except Exception as e: + print(f"Error: {e}") + + attempts += 1 + time.sleep(0.5) + + raise Exception(f"Failed to confirm block {block_hash} after {max_attempts} attempts") def wait_for_tx(esplora_endpoint, txid): url = esplora_endpoint + "/tx/" + txid - esplora_picked_up_tx = False - while not esplora_picked_up_tx: - res = requests.get(url) + attempts = 0 + max_attempts = 30 + + while attempts < max_attempts: try: + res = requests.get(url, timeout=10) json = res.json() - esplora_picked_up_tx = json['txid'] == txid - except: - pass - time.sleep(1) + if json.get('txid') == txid: + return + + except Exception as e: + print(f"Error: {e}") + + attempts += 1 + time.sleep(0.5) + + raise Exception(f"Failed to confirm transaction {txid} after {max_attempts} attempts") def send_to_address(address, amount_sats): amount_btc = amount_sats/100000000.0 diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index 835816b9f5..20ad658d7b 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -493,6 +493,295 @@ fileprivate struct FfiConverterString: FfiConverter { } } +fileprivate struct FfiConverterData: FfiConverterRustBuffer { + typealias SwiftType = Data + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { + let len: Int32 = try readInt(&buf) + return Data(try readBytes(&buf, count: Int(len))) + } + + public static func write(_ value: Data, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + writeBytes(&buf, value) + } +} + + + + +public protocol Bolt11InvoiceProtocol : AnyObject { + + func amountMilliSatoshis() -> UInt64? + + func currency() -> Currency + + func expiryTimeSeconds() -> UInt64 + + func fallbackAddresses() -> [Address] + + func invoiceDescription() -> Bolt11InvoiceDescription + + func isExpired() -> Bool + + func minFinalCltvExpiryDelta() -> UInt64 + + func network() -> Network + + func paymentHash() -> PaymentHash + + func paymentSecret() -> PaymentSecret + + func recoverPayeePubKey() -> PublicKey + + func routeHints() -> [[RouteHintHop]] + + func secondsSinceEpoch() -> UInt64 + + func secondsUntilExpiry() -> UInt64 + + func signableHash() -> [UInt8] + + func wouldExpire(atTimeSeconds: UInt64) -> Bool + +} + +open class Bolt11Invoice: + CustomDebugStringConvertible, + CustomStringConvertible, + Equatable, + Bolt11InvoiceProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_bolt11invoice(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_bolt11invoice(pointer, $0) } + } + + +public static func fromStr(invoiceStr: String)throws -> Bolt11Invoice { + return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_constructor_bolt11invoice_from_str( + FfiConverterString.lower(invoiceStr),$0 + ) +}) +} + + + +open func amountMilliSatoshis() -> UInt64? { + return try! FfiConverterOptionUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_amount_milli_satoshis(self.uniffiClonePointer(),$0 + ) +}) +} + +open func currency() -> Currency { + return try! FfiConverterTypeCurrency.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_currency(self.uniffiClonePointer(),$0 + ) +}) +} + +open func expiryTimeSeconds() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_expiry_time_seconds(self.uniffiClonePointer(),$0 + ) +}) +} + +open func fallbackAddresses() -> [Address] { + return try! FfiConverterSequenceTypeAddress.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_fallback_addresses(self.uniffiClonePointer(),$0 + ) +}) +} + +open func invoiceDescription() -> Bolt11InvoiceDescription { + return try! FfiConverterTypeBolt11InvoiceDescription.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_invoice_description(self.uniffiClonePointer(),$0 + ) +}) +} + +open func isExpired() -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_is_expired(self.uniffiClonePointer(),$0 + ) +}) +} + +open func minFinalCltvExpiryDelta() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_min_final_cltv_expiry_delta(self.uniffiClonePointer(),$0 + ) +}) +} + +open func network() -> Network { + return try! FfiConverterTypeNetwork.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_network(self.uniffiClonePointer(),$0 + ) +}) +} + +open func paymentHash() -> PaymentHash { + return try! FfiConverterTypePaymentHash.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_payment_hash(self.uniffiClonePointer(),$0 + ) +}) +} + +open func paymentSecret() -> PaymentSecret { + return try! FfiConverterTypePaymentSecret.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_payment_secret(self.uniffiClonePointer(),$0 + ) +}) +} + +open func recoverPayeePubKey() -> PublicKey { + return try! FfiConverterTypePublicKey.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_recover_payee_pub_key(self.uniffiClonePointer(),$0 + ) +}) +} + +open func routeHints() -> [[RouteHintHop]] { + return try! FfiConverterSequenceSequenceTypeRouteHintHop.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_route_hints(self.uniffiClonePointer(),$0 + ) +}) +} + +open func secondsSinceEpoch() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_seconds_since_epoch(self.uniffiClonePointer(),$0 + ) +}) +} + +open func secondsUntilExpiry() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_seconds_until_expiry(self.uniffiClonePointer(),$0 + ) +}) +} + +open func signableHash() -> [UInt8] { + return try! FfiConverterSequenceUInt8.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_signable_hash(self.uniffiClonePointer(),$0 + ) +}) +} + +open func wouldExpire(atTimeSeconds: UInt64) -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_would_expire(self.uniffiClonePointer(), + FfiConverterUInt64.lower(atTimeSeconds),$0 + ) +}) +} + + open var debugDescription: String { + return try! FfiConverterString.lift( + try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_uniffi_trait_debug(self.uniffiClonePointer(),$0 + ) +} + ) + } + open var description: String { + return try! FfiConverterString.lift( + try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_uniffi_trait_display(self.uniffiClonePointer(),$0 + ) +} + ) + } + public static func == (self: Bolt11Invoice, other: Bolt11Invoice) -> Bool { + return try! FfiConverterBool.lift( + try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_uniffi_trait_eq_eq(self.uniffiClonePointer(), + FfiConverterTypeBolt11Invoice.lower(other),$0 + ) +} + ) + } + +} + +public struct FfiConverterTypeBolt11Invoice: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = Bolt11Invoice + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Bolt11Invoice { + return Bolt11Invoice(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: Bolt11Invoice) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11Invoice { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: Bolt11Invoice, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeBolt11Invoice_lift(_ pointer: UnsafeMutableRawPointer) throws -> Bolt11Invoice { + return try FfiConverterTypeBolt11Invoice.lift(pointer) +} + +public func FfiConverterTypeBolt11Invoice_lower(_ value: Bolt11Invoice) -> UnsafeMutableRawPointer { + return FfiConverterTypeBolt11Invoice.lower(value) +} + @@ -502,17 +791,17 @@ public protocol Bolt11PaymentProtocol : AnyObject { func failForHash(paymentHash: PaymentHash) throws - func receive(amountMsat: UInt64, description: String, expirySecs: UInt32) throws -> Bolt11Invoice + func receive(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32) throws -> Bolt11Invoice - func receiveForHash(amountMsat: UInt64, description: String, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice + func receiveForHash(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice - func receiveVariableAmount(description: String, expirySecs: UInt32) throws -> Bolt11Invoice + func receiveVariableAmount(description: Bolt11InvoiceDescription, expirySecs: UInt32) throws -> Bolt11Invoice - func receiveVariableAmountForHash(description: String, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice + func receiveVariableAmountForHash(description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice - func receiveVariableAmountViaJitChannel(description: String, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?) throws -> Bolt11Invoice + func receiveVariableAmountViaJitChannel(description: Bolt11InvoiceDescription, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?) throws -> Bolt11Invoice - func receiveViaJitChannel(amountMsat: UInt64, description: String, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?) throws -> Bolt11Invoice + func receiveViaJitChannel(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?) throws -> Bolt11Invoice func send(invoice: Bolt11Invoice, sendingParameters: SendingParameters?) throws -> PaymentId @@ -581,61 +870,61 @@ open func failForHash(paymentHash: PaymentHash)throws {try rustCallWithError(Ff } } -open func receive(amountMsat: UInt64, description: String, expirySecs: UInt32)throws -> Bolt11Invoice { +open func receive(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive(self.uniffiClonePointer(), FfiConverterUInt64.lower(amountMsat), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs),$0 ) }) } -open func receiveForHash(amountMsat: UInt64, description: String, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { +open func receiveForHash(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_for_hash(self.uniffiClonePointer(), FfiConverterUInt64.lower(amountMsat), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterTypePaymentHash.lower(paymentHash),$0 ) }) } -open func receiveVariableAmount(description: String, expirySecs: UInt32)throws -> Bolt11Invoice { +open func receiveVariableAmount(description: Bolt11InvoiceDescription, expirySecs: UInt32)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_variable_amount(self.uniffiClonePointer(), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs),$0 ) }) } -open func receiveVariableAmountForHash(description: String, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { +open func receiveVariableAmountForHash(description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_variable_amount_for_hash(self.uniffiClonePointer(), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterTypePaymentHash.lower(paymentHash),$0 ) }) } -open func receiveVariableAmountViaJitChannel(description: String, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?)throws -> Bolt11Invoice { +open func receiveVariableAmountViaJitChannel(description: Bolt11InvoiceDescription, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_variable_amount_via_jit_channel(self.uniffiClonePointer(), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterOptionUInt64.lower(maxProportionalLspFeeLimitPpmMsat),$0 ) }) } -open func receiveViaJitChannel(amountMsat: UInt64, description: String, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?)throws -> Bolt11Invoice { +open func receiveViaJitChannel(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_via_jit_channel(self.uniffiClonePointer(), FfiConverterUInt64.lower(amountMsat), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterOptionUInt64.lower(maxLspFeeLimitMsat),$0 ) @@ -901,24 +1190,36 @@ public protocol BuilderProtocol : AnyObject { func buildWithVssStoreAndHeaderProvider(vssUrl: String, storeId: String, headerProvider: VssHeaderProvider) throws -> Node + func setAnnouncementAddresses(announcementAddresses: [SocketAddress]) throws + func setChainSourceBitcoindRpc(rpcHost: String, rpcPort: UInt16, rpcUser: String, rpcPassword: String) + func setChainSourceElectrum(serverUrl: String, config: ElectrumSyncConfig?) + func setChainSourceEsplora(serverUrl: String, config: EsploraSyncConfig?) + func setCustomLogger(logWriter: LogWriter) + func setEntropyBip39Mnemonic(mnemonic: Mnemonic, passphrase: String?) func setEntropySeedBytes(seedBytes: [UInt8]) throws func setEntropySeedPath(seedPath: String) + func setFilesystemLogger(logFilePath: String?, maxLogLevel: LogLevel?) + func setGossipSourceP2p() func setGossipSourceRgs(rgsServerUrl: String) - func setLiquiditySourceLsps2(address: SocketAddress, nodeId: PublicKey, token: String?) + func setLiquiditySourceLsps1(nodeId: PublicKey, address: SocketAddress, token: String?) + + func setLiquiditySourceLsps2(nodeId: PublicKey, address: SocketAddress, token: String?) func setListeningAddresses(listeningAddresses: [SocketAddress]) throws + func setLogFacadeLogger() + func setNetwork(network: Network) func setNodeAlias(nodeAlias: String) throws @@ -1028,6 +1329,13 @@ open func buildWithVssStoreAndHeaderProvider(vssUrl: String, storeId: String, he }) } +open func setAnnouncementAddresses(announcementAddresses: [SocketAddress])throws {try rustCallWithError(FfiConverterTypeBuildError.lift) { + uniffi_ldk_node_fn_method_builder_set_announcement_addresses(self.uniffiClonePointer(), + FfiConverterSequenceTypeSocketAddress.lower(announcementAddresses),$0 + ) +} +} + open func setChainSourceBitcoindRpc(rpcHost: String, rpcPort: UInt16, rpcUser: String, rpcPassword: String) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_chain_source_bitcoind_rpc(self.uniffiClonePointer(), FfiConverterString.lower(rpcHost), @@ -1038,6 +1346,14 @@ open func setChainSourceBitcoindRpc(rpcHost: String, rpcPort: UInt16, rpcUser: S } } +open func setChainSourceElectrum(serverUrl: String, config: ElectrumSyncConfig?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_chain_source_electrum(self.uniffiClonePointer(), + FfiConverterString.lower(serverUrl), + FfiConverterOptionTypeElectrumSyncConfig.lower(config),$0 + ) +} +} + open func setChainSourceEsplora(serverUrl: String, config: EsploraSyncConfig?) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_chain_source_esplora(self.uniffiClonePointer(), FfiConverterString.lower(serverUrl), @@ -1046,6 +1362,13 @@ open func setChainSourceEsplora(serverUrl: String, config: EsploraSyncConfig?) { } } +open func setCustomLogger(logWriter: LogWriter) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_custom_logger(self.uniffiClonePointer(), + FfiConverterTypeLogWriter.lower(logWriter),$0 + ) +} +} + open func setEntropyBip39Mnemonic(mnemonic: Mnemonic, passphrase: String?) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_entropy_bip39_mnemonic(self.uniffiClonePointer(), FfiConverterTypeMnemonic.lower(mnemonic), @@ -1068,6 +1391,14 @@ open func setEntropySeedPath(seedPath: String) {try! rustCall() { } } +open func setFilesystemLogger(logFilePath: String?, maxLogLevel: LogLevel?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_filesystem_logger(self.uniffiClonePointer(), + FfiConverterOptionString.lower(logFilePath), + FfiConverterOptionTypeLogLevel.lower(maxLogLevel),$0 + ) +} +} + open func setGossipSourceP2p() {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_gossip_source_p2p(self.uniffiClonePointer(),$0 ) @@ -1081,10 +1412,19 @@ open func setGossipSourceRgs(rgsServerUrl: String) {try! rustCall() { } } -open func setLiquiditySourceLsps2(address: SocketAddress, nodeId: PublicKey, token: String?) {try! rustCall() { - uniffi_ldk_node_fn_method_builder_set_liquidity_source_lsps2(self.uniffiClonePointer(), +open func setLiquiditySourceLsps1(nodeId: PublicKey, address: SocketAddress, token: String?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_liquidity_source_lsps1(self.uniffiClonePointer(), + FfiConverterTypePublicKey.lower(nodeId), FfiConverterTypeSocketAddress.lower(address), + FfiConverterOptionString.lower(token),$0 + ) +} +} + +open func setLiquiditySourceLsps2(nodeId: PublicKey, address: SocketAddress, token: String?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_liquidity_source_lsps2(self.uniffiClonePointer(), FfiConverterTypePublicKey.lower(nodeId), + FfiConverterTypeSocketAddress.lower(address), FfiConverterOptionString.lower(token),$0 ) } @@ -1097,6 +1437,12 @@ open func setListeningAddresses(listeningAddresses: [SocketAddress])throws {try } } +open func setLogFacadeLogger() {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_log_facade_logger(self.uniffiClonePointer(),$0 + ) +} +} + open func setNetwork(network: Network) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_network(self.uniffiClonePointer(), FfiConverterTypeNetwork.lower(network),$0 @@ -1166,20 +1512,18 @@ public func FfiConverterTypeBuilder_lower(_ value: Builder) -> UnsafeMutableRawP -public protocol NetworkGraphProtocol : AnyObject { - - func channel(shortChannelId: UInt64) -> ChannelInfo? +public protocol FeeRateProtocol : AnyObject { - func listChannels() -> [UInt64] + func toSatPerKwu() -> UInt64 - func listNodes() -> [NodeId] + func toSatPerVbCeil() -> UInt64 - func node(nodeId: NodeId) -> NodeInfo? + func toSatPerVbFloor() -> UInt64 } -open class NetworkGraph: - NetworkGraphProtocol { +open class FeeRate: + FeeRateProtocol { fileprivate let pointer: UnsafeMutableRawPointer! /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. @@ -1204,7 +1548,7 @@ open class NetworkGraph: } public func uniffiClonePointer() -> UnsafeMutableRawPointer { - return try! rustCall { uniffi_ldk_node_fn_clone_networkgraph(self.pointer, $0) } + return try! rustCall { uniffi_ldk_node_fn_clone_feerate(self.pointer, $0) } } // No primary constructor declared for this class. @@ -1213,30 +1557,440 @@ open class NetworkGraph: return } - try! rustCall { uniffi_ldk_node_fn_free_networkgraph(pointer, $0) } + try! rustCall { uniffi_ldk_node_fn_free_feerate(pointer, $0) } } - - -open func channel(shortChannelId: UInt64) -> ChannelInfo? { - return try! FfiConverterOptionTypeChannelInfo.lift(try! rustCall() { - uniffi_ldk_node_fn_method_networkgraph_channel(self.uniffiClonePointer(), - FfiConverterUInt64.lower(shortChannelId),$0 +public static func fromSatPerKwu(satKwu: UInt64) -> FeeRate { + return try! FfiConverterTypeFeeRate.lift(try! rustCall() { + uniffi_ldk_node_fn_constructor_feerate_from_sat_per_kwu( + FfiConverterUInt64.lower(satKwu),$0 ) }) } -open func listChannels() -> [UInt64] { - return try! FfiConverterSequenceUInt64.lift(try! rustCall() { - uniffi_ldk_node_fn_method_networkgraph_list_channels(self.uniffiClonePointer(),$0 +public static func fromSatPerVbUnchecked(satVb: UInt64) -> FeeRate { + return try! FfiConverterTypeFeeRate.lift(try! rustCall() { + uniffi_ldk_node_fn_constructor_feerate_from_sat_per_vb_unchecked( + FfiConverterUInt64.lower(satVb),$0 ) }) } -open func listNodes() -> [NodeId] { - return try! FfiConverterSequenceTypeNodeId.lift(try! rustCall() { - uniffi_ldk_node_fn_method_networkgraph_list_nodes(self.uniffiClonePointer(),$0 + + +open func toSatPerKwu() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_feerate_to_sat_per_kwu(self.uniffiClonePointer(),$0 + ) +}) +} + +open func toSatPerVbCeil() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_feerate_to_sat_per_vb_ceil(self.uniffiClonePointer(),$0 + ) +}) +} + +open func toSatPerVbFloor() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_feerate_to_sat_per_vb_floor(self.uniffiClonePointer(),$0 + ) +}) +} + + +} + +public struct FfiConverterTypeFeeRate: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = FeeRate + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> FeeRate { + return FeeRate(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: FeeRate) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> FeeRate { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: FeeRate, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeFeeRate_lift(_ pointer: UnsafeMutableRawPointer) throws -> FeeRate { + return try FfiConverterTypeFeeRate.lift(pointer) +} + +public func FfiConverterTypeFeeRate_lower(_ value: FeeRate) -> UnsafeMutableRawPointer { + return FfiConverterTypeFeeRate.lower(value) +} + + + + +public protocol Lsps1LiquidityProtocol : AnyObject { + + func checkOrderStatus(orderId: OrderId) throws -> Lsps1OrderStatus + + func requestChannel(lspBalanceSat: UInt64, clientBalanceSat: UInt64, channelExpiryBlocks: UInt32, announceChannel: Bool) throws -> Lsps1OrderStatus + +} + +open class Lsps1Liquidity: + Lsps1LiquidityProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_lsps1liquidity(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_lsps1liquidity(pointer, $0) } + } + + + + +open func checkOrderStatus(orderId: OrderId)throws -> Lsps1OrderStatus { + return try FfiConverterTypeLSPS1OrderStatus.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_lsps1liquidity_check_order_status(self.uniffiClonePointer(), + FfiConverterTypeOrderId.lower(orderId),$0 + ) +}) +} + +open func requestChannel(lspBalanceSat: UInt64, clientBalanceSat: UInt64, channelExpiryBlocks: UInt32, announceChannel: Bool)throws -> Lsps1OrderStatus { + return try FfiConverterTypeLSPS1OrderStatus.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_lsps1liquidity_request_channel(self.uniffiClonePointer(), + FfiConverterUInt64.lower(lspBalanceSat), + FfiConverterUInt64.lower(clientBalanceSat), + FfiConverterUInt32.lower(channelExpiryBlocks), + FfiConverterBool.lower(announceChannel),$0 + ) +}) +} + + +} + +public struct FfiConverterTypeLSPS1Liquidity: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = Lsps1Liquidity + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Lsps1Liquidity { + return Lsps1Liquidity(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: Lsps1Liquidity) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Lsps1Liquidity { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: Lsps1Liquidity, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeLSPS1Liquidity_lift(_ pointer: UnsafeMutableRawPointer) throws -> Lsps1Liquidity { + return try FfiConverterTypeLSPS1Liquidity.lift(pointer) +} + +public func FfiConverterTypeLSPS1Liquidity_lower(_ value: Lsps1Liquidity) -> UnsafeMutableRawPointer { + return FfiConverterTypeLSPS1Liquidity.lower(value) +} + + + + +public protocol LogWriter : AnyObject { + + func log(record: LogRecord) + +} + +open class LogWriterImpl: + LogWriter { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_logwriter(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_logwriter(pointer, $0) } + } + + + + +open func log(record: LogRecord) {try! rustCall() { + uniffi_ldk_node_fn_method_logwriter_log(self.uniffiClonePointer(), + FfiConverterTypeLogRecord.lower(record),$0 + ) +} +} + + +} +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +private let IDX_CALLBACK_FREE: Int32 = 0 +// Callback return codes +private let UNIFFI_CALLBACK_SUCCESS: Int32 = 0 +private let UNIFFI_CALLBACK_ERROR: Int32 = 1 +private let UNIFFI_CALLBACK_UNEXPECTED_ERROR: Int32 = 2 + +// Put the implementation in a struct so we don't pollute the top-level namespace +fileprivate struct UniffiCallbackInterfaceLogWriter { + + // Create the VTable using a series of closures. + // Swift automatically converts these into C callback functions. + static var vtable: UniffiVTableCallbackInterfaceLogWriter = UniffiVTableCallbackInterfaceLogWriter( + log: { ( + uniffiHandle: UInt64, + record: RustBuffer, + uniffiOutReturn: UnsafeMutableRawPointer, + uniffiCallStatus: UnsafeMutablePointer + ) in + let makeCall = { + () throws -> () in + guard let uniffiObj = try? FfiConverterTypeLogWriter.handleMap.get(handle: uniffiHandle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return uniffiObj.log( + record: try FfiConverterTypeLogRecord.lift(record) + ) + } + + + let writeReturn = { () } + uniffiTraitInterfaceCall( + callStatus: uniffiCallStatus, + makeCall: makeCall, + writeReturn: writeReturn + ) + }, + uniffiFree: { (uniffiHandle: UInt64) -> () in + let result = try? FfiConverterTypeLogWriter.handleMap.remove(handle: uniffiHandle) + if result == nil { + print("Uniffi callback interface LogWriter: handle missing in uniffiFree") + } + } + ) +} + +private func uniffiCallbackInitLogWriter() { + uniffi_ldk_node_fn_init_callback_vtable_logwriter(&UniffiCallbackInterfaceLogWriter.vtable) +} + +public struct FfiConverterTypeLogWriter: FfiConverter { + fileprivate static var handleMap = UniffiHandleMap() + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = LogWriter + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> LogWriter { + return LogWriterImpl(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: LogWriter) -> UnsafeMutableRawPointer { + guard let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: handleMap.insert(obj: value))) else { + fatalError("Cast to UnsafeMutableRawPointer failed") + } + return ptr + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LogWriter { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: LogWriter, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeLogWriter_lift(_ pointer: UnsafeMutableRawPointer) throws -> LogWriter { + return try FfiConverterTypeLogWriter.lift(pointer) +} + +public func FfiConverterTypeLogWriter_lower(_ value: LogWriter) -> UnsafeMutableRawPointer { + return FfiConverterTypeLogWriter.lower(value) +} + + + + +public protocol NetworkGraphProtocol : AnyObject { + + func channel(shortChannelId: UInt64) -> ChannelInfo? + + func listChannels() -> [UInt64] + + func listNodes() -> [NodeId] + + func node(nodeId: NodeId) -> NodeInfo? + +} + +open class NetworkGraph: + NetworkGraphProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_networkgraph(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_networkgraph(pointer, $0) } + } + + + + +open func channel(shortChannelId: UInt64) -> ChannelInfo? { + return try! FfiConverterOptionTypeChannelInfo.lift(try! rustCall() { + uniffi_ldk_node_fn_method_networkgraph_channel(self.uniffiClonePointer(), + FfiConverterUInt64.lower(shortChannelId),$0 + ) +}) +} + +open func listChannels() -> [UInt64] { + return try! FfiConverterSequenceUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_networkgraph_list_channels(self.uniffiClonePointer(),$0 + ) +}) +} + +open func listNodes() -> [NodeId] { + return try! FfiConverterSequenceTypeNodeId.lift(try! rustCall() { + uniffi_ldk_node_fn_method_networkgraph_list_nodes(self.uniffiClonePointer(),$0 ) }) } @@ -1299,6 +2053,8 @@ public func FfiConverterTypeNetworkGraph_lower(_ value: NetworkGraph) -> UnsafeM public protocol NodeProtocol : AnyObject { + func announcementAddresses() -> [SocketAddress]? + func bolt11Payment() -> Bolt11Payment func bolt12Payment() -> Bolt12Payment @@ -1311,7 +2067,9 @@ public protocol NodeProtocol : AnyObject { func disconnect(nodeId: PublicKey) throws - func eventHandled() + func eventHandled() throws + + func exportPathfindingScores() throws -> Data func forceCloseChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, reason: String?) throws @@ -1325,6 +2083,8 @@ public protocol NodeProtocol : AnyObject { func listeningAddresses() -> [SocketAddress]? + func lsps1Liquidity() -> Lsps1Liquidity + func networkGraph() -> NetworkGraph func nextEvent() -> Event? @@ -1408,6 +2168,13 @@ open class Node: +open func announcementAddresses() -> [SocketAddress]? { + return try! FfiConverterOptionSequenceTypeSocketAddress.lift(try! rustCall() { + uniffi_ldk_node_fn_method_node_announcement_addresses(self.uniffiClonePointer(),$0 + ) +}) +} + open func bolt11Payment() -> Bolt11Payment { return try! FfiConverterTypeBolt11Payment.lift(try! rustCall() { uniffi_ldk_node_fn_method_node_bolt11_payment(self.uniffiClonePointer(),$0 @@ -1453,12 +2220,19 @@ open func disconnect(nodeId: PublicKey)throws {try rustCallWithError(FfiConvert } } -open func eventHandled() {try! rustCall() { +open func eventHandled()throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_node_event_handled(self.uniffiClonePointer(),$0 ) } } +open func exportPathfindingScores()throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_export_pathfinding_scores(self.uniffiClonePointer(),$0 + ) +}) +} + open func forceCloseChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, reason: String?)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_node_force_close_channel(self.uniffiClonePointer(), FfiConverterTypeUserChannelId.lower(userChannelId), @@ -1503,6 +2277,13 @@ open func listeningAddresses() -> [SocketAddress]? { }) } +open func lsps1Liquidity() -> Lsps1Liquidity { + return try! FfiConverterTypeLSPS1Liquidity.lift(try! rustCall() { + uniffi_ldk_node_fn_method_node_lsps1_liquidity(self.uniffiClonePointer(),$0 + ) +}) +} + open func networkGraph() -> NetworkGraph { return try! FfiConverterTypeNetworkGraph.lift(try! rustCall() { uniffi_ldk_node_fn_method_node_network_graph(self.uniffiClonePointer(),$0 @@ -1720,9 +2501,9 @@ public protocol OnchainPaymentProtocol : AnyObject { func newAddress() throws -> Address - func sendAllToAddress(address: Address) throws -> Txid + func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRate?) throws -> Txid - func sendToAddress(address: Address, amountSats: UInt64) throws -> Txid + func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?) throws -> Txid } @@ -1774,19 +2555,22 @@ open func newAddress()throws -> Address { }) } -open func sendAllToAddress(address: Address)throws -> Txid { +open func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRate?)throws -> Txid { return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_onchainpayment_send_all_to_address(self.uniffiClonePointer(), - FfiConverterTypeAddress.lower(address),$0 + FfiConverterTypeAddress.lower(address), + FfiConverterBool.lower(retainReserve), + FfiConverterOptionTypeFeeRate.lower(feeRate),$0 ) }) } -open func sendToAddress(address: Address, amountSats: UInt64)throws -> Txid { +open func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?)throws -> Txid { return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_onchainpayment_send_to_address(self.uniffiClonePointer(), FfiConverterTypeAddress.lower(address), - FfiConverterUInt64.lower(amountSats),$0 + FfiConverterUInt64.lower(amountSats), + FfiConverterOptionTypeFeeRate.lower(feeRate),$0 ) }) } @@ -1845,6 +2629,8 @@ public protocol SpontaneousPaymentProtocol : AnyObject { func sendProbes(amountMsat: UInt64, nodeId: PublicKey) throws + func sendWithCustomTlvs(amountMsat: UInt64, nodeId: PublicKey, sendingParameters: SendingParameters?, customTlvs: [CustomTlvRecord]) throws -> PaymentId + } open class SpontaneousPayment: @@ -1906,6 +2692,17 @@ open func sendProbes(amountMsat: UInt64, nodeId: PublicKey)throws {try rustCall } } +open func sendWithCustomTlvs(amountMsat: UInt64, nodeId: PublicKey, sendingParameters: SendingParameters?, customTlvs: [CustomTlvRecord])throws -> PaymentId { + return try FfiConverterTypePaymentId.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_spontaneouspayment_send_with_custom_tlvs(self.uniffiClonePointer(), + FfiConverterUInt64.lower(amountMsat), + FfiConverterTypePublicKey.lower(nodeId), + FfiConverterOptionTypeSendingParameters.lower(sendingParameters), + FfiConverterSequenceTypeCustomTlvRecord.lower(customTlvs),$0 + ) +}) +} + } @@ -2236,6 +3033,71 @@ public func FfiConverterTypeAnchorChannelsConfig_lower(_ value: AnchorChannelsCo } +public struct BackgroundSyncConfig { + public var onchainWalletSyncIntervalSecs: UInt64 + public var lightningWalletSyncIntervalSecs: UInt64 + public var feeRateCacheUpdateIntervalSecs: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(onchainWalletSyncIntervalSecs: UInt64, lightningWalletSyncIntervalSecs: UInt64, feeRateCacheUpdateIntervalSecs: UInt64) { + self.onchainWalletSyncIntervalSecs = onchainWalletSyncIntervalSecs + self.lightningWalletSyncIntervalSecs = lightningWalletSyncIntervalSecs + self.feeRateCacheUpdateIntervalSecs = feeRateCacheUpdateIntervalSecs + } +} + + + +extension BackgroundSyncConfig: Equatable, Hashable { + public static func ==(lhs: BackgroundSyncConfig, rhs: BackgroundSyncConfig) -> Bool { + if lhs.onchainWalletSyncIntervalSecs != rhs.onchainWalletSyncIntervalSecs { + return false + } + if lhs.lightningWalletSyncIntervalSecs != rhs.lightningWalletSyncIntervalSecs { + return false + } + if lhs.feeRateCacheUpdateIntervalSecs != rhs.feeRateCacheUpdateIntervalSecs { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(onchainWalletSyncIntervalSecs) + hasher.combine(lightningWalletSyncIntervalSecs) + hasher.combine(feeRateCacheUpdateIntervalSecs) + } +} + + +public struct FfiConverterTypeBackgroundSyncConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BackgroundSyncConfig { + return + try BackgroundSyncConfig( + onchainWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), + lightningWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), + feeRateCacheUpdateIntervalSecs: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: BackgroundSyncConfig, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.onchainWalletSyncIntervalSecs, into: &buf) + FfiConverterUInt64.write(value.lightningWalletSyncIntervalSecs, into: &buf) + FfiConverterUInt64.write(value.feeRateCacheUpdateIntervalSecs, into: &buf) + } +} + + +public func FfiConverterTypeBackgroundSyncConfig_lift(_ buf: RustBuffer) throws -> BackgroundSyncConfig { + return try FfiConverterTypeBackgroundSyncConfig.lift(buf) +} + +public func FfiConverterTypeBackgroundSyncConfig_lower(_ value: BackgroundSyncConfig) -> RustBuffer { + return FfiConverterTypeBackgroundSyncConfig.lower(value) +} + + public struct BalanceDetails { public var totalOnchainBalanceSats: UInt64 public var spendableOnchainBalanceSats: UInt64 @@ -2382,6 +3244,57 @@ public func FfiConverterTypeBestBlock_lower(_ value: BestBlock) -> RustBuffer { } +public struct Bolt11PaymentInfo { + public var state: PaymentState + public var expiresAt: DateTime + public var feeTotalSat: UInt64 + public var orderTotalSat: UInt64 + public var invoice: Bolt11Invoice + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(state: PaymentState, expiresAt: DateTime, feeTotalSat: UInt64, orderTotalSat: UInt64, invoice: Bolt11Invoice) { + self.state = state + self.expiresAt = expiresAt + self.feeTotalSat = feeTotalSat + self.orderTotalSat = orderTotalSat + self.invoice = invoice + } +} + + + +public struct FfiConverterTypeBolt11PaymentInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11PaymentInfo { + return + try Bolt11PaymentInfo( + state: FfiConverterTypePaymentState.read(from: &buf), + expiresAt: FfiConverterTypeDateTime.read(from: &buf), + feeTotalSat: FfiConverterUInt64.read(from: &buf), + orderTotalSat: FfiConverterUInt64.read(from: &buf), + invoice: FfiConverterTypeBolt11Invoice.read(from: &buf) + ) + } + + public static func write(_ value: Bolt11PaymentInfo, into buf: inout [UInt8]) { + FfiConverterTypePaymentState.write(value.state, into: &buf) + FfiConverterTypeDateTime.write(value.expiresAt, into: &buf) + FfiConverterUInt64.write(value.feeTotalSat, into: &buf) + FfiConverterUInt64.write(value.orderTotalSat, into: &buf) + FfiConverterTypeBolt11Invoice.write(value.invoice, into: &buf) + } +} + + +public func FfiConverterTypeBolt11PaymentInfo_lift(_ buf: RustBuffer) throws -> Bolt11PaymentInfo { + return try FfiConverterTypeBolt11PaymentInfo.lift(buf) +} + +public func FfiConverterTypeBolt11PaymentInfo_lower(_ value: Bolt11PaymentInfo) -> RustBuffer { + return FfiConverterTypeBolt11PaymentInfo.lower(value) +} + + public struct ChannelConfig { public var forwardingFeeProportionalMillionths: UInt32 public var forwardingFeeBaseMsat: UInt32 @@ -2475,6 +3388,9 @@ public struct ChannelDetails { public var channelId: ChannelId public var counterpartyNodeId: PublicKey public var fundingTxo: OutPoint? + public var shortChannelId: UInt64? + public var outboundScidAlias: UInt64? + public var inboundScidAlias: UInt64? public var channelValueSats: UInt64 public var unspendablePunishmentReserve: UInt64? public var userChannelId: UserChannelId @@ -2503,10 +3419,13 @@ public struct ChannelDetails { // Default memberwise initializers are never public by default, so we // declare one manually. - public init(channelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint?, channelValueSats: UInt64, unspendablePunishmentReserve: UInt64?, userChannelId: UserChannelId, feerateSatPer1000Weight: UInt32, outboundCapacityMsat: UInt64, inboundCapacityMsat: UInt64, confirmationsRequired: UInt32?, confirmations: UInt32?, isOutbound: Bool, isChannelReady: Bool, isUsable: Bool, isAnnounced: Bool, cltvExpiryDelta: UInt16?, counterpartyUnspendablePunishmentReserve: UInt64, counterpartyOutboundHtlcMinimumMsat: UInt64?, counterpartyOutboundHtlcMaximumMsat: UInt64?, counterpartyForwardingInfoFeeBaseMsat: UInt32?, counterpartyForwardingInfoFeeProportionalMillionths: UInt32?, counterpartyForwardingInfoCltvExpiryDelta: UInt16?, nextOutboundHtlcLimitMsat: UInt64, nextOutboundHtlcMinimumMsat: UInt64, forceCloseSpendDelay: UInt16?, inboundHtlcMinimumMsat: UInt64, inboundHtlcMaximumMsat: UInt64?, config: ChannelConfig) { + public init(channelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint?, shortChannelId: UInt64?, outboundScidAlias: UInt64?, inboundScidAlias: UInt64?, channelValueSats: UInt64, unspendablePunishmentReserve: UInt64?, userChannelId: UserChannelId, feerateSatPer1000Weight: UInt32, outboundCapacityMsat: UInt64, inboundCapacityMsat: UInt64, confirmationsRequired: UInt32?, confirmations: UInt32?, isOutbound: Bool, isChannelReady: Bool, isUsable: Bool, isAnnounced: Bool, cltvExpiryDelta: UInt16?, counterpartyUnspendablePunishmentReserve: UInt64, counterpartyOutboundHtlcMinimumMsat: UInt64?, counterpartyOutboundHtlcMaximumMsat: UInt64?, counterpartyForwardingInfoFeeBaseMsat: UInt32?, counterpartyForwardingInfoFeeProportionalMillionths: UInt32?, counterpartyForwardingInfoCltvExpiryDelta: UInt16?, nextOutboundHtlcLimitMsat: UInt64, nextOutboundHtlcMinimumMsat: UInt64, forceCloseSpendDelay: UInt16?, inboundHtlcMinimumMsat: UInt64, inboundHtlcMaximumMsat: UInt64?, config: ChannelConfig) { self.channelId = channelId self.counterpartyNodeId = counterpartyNodeId self.fundingTxo = fundingTxo + self.shortChannelId = shortChannelId + self.outboundScidAlias = outboundScidAlias + self.inboundScidAlias = inboundScidAlias self.channelValueSats = channelValueSats self.unspendablePunishmentReserve = unspendablePunishmentReserve self.userChannelId = userChannelId @@ -2548,6 +3467,15 @@ extension ChannelDetails: Equatable, Hashable { if lhs.fundingTxo != rhs.fundingTxo { return false } + if lhs.shortChannelId != rhs.shortChannelId { + return false + } + if lhs.outboundScidAlias != rhs.outboundScidAlias { + return false + } + if lhs.inboundScidAlias != rhs.inboundScidAlias { + return false + } if lhs.channelValueSats != rhs.channelValueSats { return false } @@ -2630,6 +3558,9 @@ extension ChannelDetails: Equatable, Hashable { hasher.combine(channelId) hasher.combine(counterpartyNodeId) hasher.combine(fundingTxo) + hasher.combine(shortChannelId) + hasher.combine(outboundScidAlias) + hasher.combine(inboundScidAlias) hasher.combine(channelValueSats) hasher.combine(unspendablePunishmentReserve) hasher.combine(userChannelId) @@ -2666,6 +3597,9 @@ public struct FfiConverterTypeChannelDetails: FfiConverterRustBuffer { channelId: FfiConverterTypeChannelId.read(from: &buf), counterpartyNodeId: FfiConverterTypePublicKey.read(from: &buf), fundingTxo: FfiConverterOptionTypeOutPoint.read(from: &buf), + shortChannelId: FfiConverterOptionUInt64.read(from: &buf), + outboundScidAlias: FfiConverterOptionUInt64.read(from: &buf), + inboundScidAlias: FfiConverterOptionUInt64.read(from: &buf), channelValueSats: FfiConverterUInt64.read(from: &buf), unspendablePunishmentReserve: FfiConverterOptionUInt64.read(from: &buf), userChannelId: FfiConverterTypeUserChannelId.read(from: &buf), @@ -2698,6 +3632,9 @@ public struct FfiConverterTypeChannelDetails: FfiConverterRustBuffer { FfiConverterTypeChannelId.write(value.channelId, into: &buf) FfiConverterTypePublicKey.write(value.counterpartyNodeId, into: &buf) FfiConverterOptionTypeOutPoint.write(value.fundingTxo, into: &buf) + FfiConverterOptionUInt64.write(value.shortChannelId, into: &buf) + FfiConverterOptionUInt64.write(value.outboundScidAlias, into: &buf) + FfiConverterOptionUInt64.write(value.inboundScidAlias, into: &buf) FfiConverterUInt64.write(value.channelValueSats, into: &buf) FfiConverterOptionUInt64.write(value.unspendablePunishmentReserve, into: &buf) FfiConverterTypeUserChannelId.write(value.userChannelId, into: &buf) @@ -2817,6 +3754,71 @@ public func FfiConverterTypeChannelInfo_lower(_ value: ChannelInfo) -> RustBuffe } +public struct ChannelOrderInfo { + public var fundedAt: DateTime + public var fundingOutpoint: OutPoint + public var expiresAt: DateTime + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(fundedAt: DateTime, fundingOutpoint: OutPoint, expiresAt: DateTime) { + self.fundedAt = fundedAt + self.fundingOutpoint = fundingOutpoint + self.expiresAt = expiresAt + } +} + + + +extension ChannelOrderInfo: Equatable, Hashable { + public static func ==(lhs: ChannelOrderInfo, rhs: ChannelOrderInfo) -> Bool { + if lhs.fundedAt != rhs.fundedAt { + return false + } + if lhs.fundingOutpoint != rhs.fundingOutpoint { + return false + } + if lhs.expiresAt != rhs.expiresAt { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(fundedAt) + hasher.combine(fundingOutpoint) + hasher.combine(expiresAt) + } +} + + +public struct FfiConverterTypeChannelOrderInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ChannelOrderInfo { + return + try ChannelOrderInfo( + fundedAt: FfiConverterTypeDateTime.read(from: &buf), + fundingOutpoint: FfiConverterTypeOutPoint.read(from: &buf), + expiresAt: FfiConverterTypeDateTime.read(from: &buf) + ) + } + + public static func write(_ value: ChannelOrderInfo, into buf: inout [UInt8]) { + FfiConverterTypeDateTime.write(value.fundedAt, into: &buf) + FfiConverterTypeOutPoint.write(value.fundingOutpoint, into: &buf) + FfiConverterTypeDateTime.write(value.expiresAt, into: &buf) + } +} + + +public func FfiConverterTypeChannelOrderInfo_lift(_ buf: RustBuffer) throws -> ChannelOrderInfo { + return try FfiConverterTypeChannelOrderInfo.lift(buf) +} + +public func FfiConverterTypeChannelOrderInfo_lower(_ value: ChannelOrderInfo) -> RustBuffer { + return FfiConverterTypeChannelOrderInfo.lower(value) +} + + public struct ChannelUpdateInfo { public var lastUpdate: UInt32 public var enabled: Bool @@ -2908,27 +3910,25 @@ public func FfiConverterTypeChannelUpdateInfo_lower(_ value: ChannelUpdateInfo) public struct Config { public var storageDirPath: String - public var logDirPath: String? public var network: Network public var listeningAddresses: [SocketAddress]? + public var announcementAddresses: [SocketAddress]? public var nodeAlias: NodeAlias? public var trustedPeers0conf: [PublicKey] public var probingLiquidityLimitMultiplier: UInt64 - public var logLevel: LogLevel public var anchorChannelsConfig: AnchorChannelsConfig? public var sendingParameters: SendingParameters? // Default memberwise initializers are never public by default, so we // declare one manually. - public init(storageDirPath: String, logDirPath: String?, network: Network, listeningAddresses: [SocketAddress]?, nodeAlias: NodeAlias?, trustedPeers0conf: [PublicKey], probingLiquidityLimitMultiplier: UInt64, logLevel: LogLevel, anchorChannelsConfig: AnchorChannelsConfig?, sendingParameters: SendingParameters?) { + public init(storageDirPath: String, network: Network, listeningAddresses: [SocketAddress]?, announcementAddresses: [SocketAddress]?, nodeAlias: NodeAlias?, trustedPeers0conf: [PublicKey], probingLiquidityLimitMultiplier: UInt64, anchorChannelsConfig: AnchorChannelsConfig?, sendingParameters: SendingParameters?) { self.storageDirPath = storageDirPath - self.logDirPath = logDirPath self.network = network self.listeningAddresses = listeningAddresses + self.announcementAddresses = announcementAddresses self.nodeAlias = nodeAlias self.trustedPeers0conf = trustedPeers0conf self.probingLiquidityLimitMultiplier = probingLiquidityLimitMultiplier - self.logLevel = logLevel self.anchorChannelsConfig = anchorChannelsConfig self.sendingParameters = sendingParameters } @@ -2941,15 +3941,15 @@ extension Config: Equatable, Hashable { if lhs.storageDirPath != rhs.storageDirPath { return false } - if lhs.logDirPath != rhs.logDirPath { - return false - } if lhs.network != rhs.network { return false } if lhs.listeningAddresses != rhs.listeningAddresses { return false } + if lhs.announcementAddresses != rhs.announcementAddresses { + return false + } if lhs.nodeAlias != rhs.nodeAlias { return false } @@ -2959,9 +3959,6 @@ extension Config: Equatable, Hashable { if lhs.probingLiquidityLimitMultiplier != rhs.probingLiquidityLimitMultiplier { return false } - if lhs.logLevel != rhs.logLevel { - return false - } if lhs.anchorChannelsConfig != rhs.anchorChannelsConfig { return false } @@ -2973,13 +3970,12 @@ extension Config: Equatable, Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(storageDirPath) - hasher.combine(logDirPath) hasher.combine(network) hasher.combine(listeningAddresses) + hasher.combine(announcementAddresses) hasher.combine(nodeAlias) hasher.combine(trustedPeers0conf) hasher.combine(probingLiquidityLimitMultiplier) - hasher.combine(logLevel) hasher.combine(anchorChannelsConfig) hasher.combine(sendingParameters) } @@ -2991,13 +3987,12 @@ public struct FfiConverterTypeConfig: FfiConverterRustBuffer { return try Config( storageDirPath: FfiConverterString.read(from: &buf), - logDirPath: FfiConverterOptionString.read(from: &buf), network: FfiConverterTypeNetwork.read(from: &buf), listeningAddresses: FfiConverterOptionSequenceTypeSocketAddress.read(from: &buf), + announcementAddresses: FfiConverterOptionSequenceTypeSocketAddress.read(from: &buf), nodeAlias: FfiConverterOptionTypeNodeAlias.read(from: &buf), trustedPeers0conf: FfiConverterSequenceTypePublicKey.read(from: &buf), probingLiquidityLimitMultiplier: FfiConverterUInt64.read(from: &buf), - logLevel: FfiConverterTypeLogLevel.read(from: &buf), anchorChannelsConfig: FfiConverterOptionTypeAnchorChannelsConfig.read(from: &buf), sendingParameters: FfiConverterOptionTypeSendingParameters.read(from: &buf) ) @@ -3005,13 +4000,12 @@ public struct FfiConverterTypeConfig: FfiConverterRustBuffer { public static func write(_ value: Config, into buf: inout [UInt8]) { FfiConverterString.write(value.storageDirPath, into: &buf) - FfiConverterOptionString.write(value.logDirPath, into: &buf) FfiConverterTypeNetwork.write(value.network, into: &buf) FfiConverterOptionSequenceTypeSocketAddress.write(value.listeningAddresses, into: &buf) + FfiConverterOptionSequenceTypeSocketAddress.write(value.announcementAddresses, into: &buf) FfiConverterOptionTypeNodeAlias.write(value.nodeAlias, into: &buf) FfiConverterSequenceTypePublicKey.write(value.trustedPeers0conf, into: &buf) FfiConverterUInt64.write(value.probingLiquidityLimitMultiplier, into: &buf) - FfiConverterTypeLogLevel.write(value.logLevel, into: &buf) FfiConverterOptionTypeAnchorChannelsConfig.write(value.anchorChannelsConfig, into: &buf) FfiConverterOptionTypeSendingParameters.write(value.sendingParameters, into: &buf) } @@ -3027,40 +4021,134 @@ public func FfiConverterTypeConfig_lower(_ value: Config) -> RustBuffer { } -public struct EsploraSyncConfig { - public var onchainWalletSyncIntervalSecs: UInt64 - public var lightningWalletSyncIntervalSecs: UInt64 - public var feeRateCacheUpdateIntervalSecs: UInt64 +public struct CustomTlvRecord { + public var typeNum: UInt64 + public var value: [UInt8] // Default memberwise initializers are never public by default, so we // declare one manually. - public init(onchainWalletSyncIntervalSecs: UInt64, lightningWalletSyncIntervalSecs: UInt64, feeRateCacheUpdateIntervalSecs: UInt64) { - self.onchainWalletSyncIntervalSecs = onchainWalletSyncIntervalSecs - self.lightningWalletSyncIntervalSecs = lightningWalletSyncIntervalSecs - self.feeRateCacheUpdateIntervalSecs = feeRateCacheUpdateIntervalSecs + public init(typeNum: UInt64, value: [UInt8]) { + self.typeNum = typeNum + self.value = value } } -extension EsploraSyncConfig: Equatable, Hashable { - public static func ==(lhs: EsploraSyncConfig, rhs: EsploraSyncConfig) -> Bool { - if lhs.onchainWalletSyncIntervalSecs != rhs.onchainWalletSyncIntervalSecs { +extension CustomTlvRecord: Equatable, Hashable { + public static func ==(lhs: CustomTlvRecord, rhs: CustomTlvRecord) -> Bool { + if lhs.typeNum != rhs.typeNum { return false } - if lhs.lightningWalletSyncIntervalSecs != rhs.lightningWalletSyncIntervalSecs { + if lhs.value != rhs.value { return false } - if lhs.feeRateCacheUpdateIntervalSecs != rhs.feeRateCacheUpdateIntervalSecs { + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(typeNum) + hasher.combine(value) + } +} + + +public struct FfiConverterTypeCustomTlvRecord: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CustomTlvRecord { + return + try CustomTlvRecord( + typeNum: FfiConverterUInt64.read(from: &buf), + value: FfiConverterSequenceUInt8.read(from: &buf) + ) + } + + public static func write(_ value: CustomTlvRecord, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.typeNum, into: &buf) + FfiConverterSequenceUInt8.write(value.value, into: &buf) + } +} + + +public func FfiConverterTypeCustomTlvRecord_lift(_ buf: RustBuffer) throws -> CustomTlvRecord { + return try FfiConverterTypeCustomTlvRecord.lift(buf) +} + +public func FfiConverterTypeCustomTlvRecord_lower(_ value: CustomTlvRecord) -> RustBuffer { + return FfiConverterTypeCustomTlvRecord.lower(value) +} + + +public struct ElectrumSyncConfig { + public var backgroundSyncConfig: BackgroundSyncConfig? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(backgroundSyncConfig: BackgroundSyncConfig?) { + self.backgroundSyncConfig = backgroundSyncConfig + } +} + + + +extension ElectrumSyncConfig: Equatable, Hashable { + public static func ==(lhs: ElectrumSyncConfig, rhs: ElectrumSyncConfig) -> Bool { + if lhs.backgroundSyncConfig != rhs.backgroundSyncConfig { return false } return true } public func hash(into hasher: inout Hasher) { - hasher.combine(onchainWalletSyncIntervalSecs) - hasher.combine(lightningWalletSyncIntervalSecs) - hasher.combine(feeRateCacheUpdateIntervalSecs) + hasher.combine(backgroundSyncConfig) + } +} + + +public struct FfiConverterTypeElectrumSyncConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ElectrumSyncConfig { + return + try ElectrumSyncConfig( + backgroundSyncConfig: FfiConverterOptionTypeBackgroundSyncConfig.read(from: &buf) + ) + } + + public static func write(_ value: ElectrumSyncConfig, into buf: inout [UInt8]) { + FfiConverterOptionTypeBackgroundSyncConfig.write(value.backgroundSyncConfig, into: &buf) + } +} + + +public func FfiConverterTypeElectrumSyncConfig_lift(_ buf: RustBuffer) throws -> ElectrumSyncConfig { + return try FfiConverterTypeElectrumSyncConfig.lift(buf) +} + +public func FfiConverterTypeElectrumSyncConfig_lower(_ value: ElectrumSyncConfig) -> RustBuffer { + return FfiConverterTypeElectrumSyncConfig.lower(value) +} + + +public struct EsploraSyncConfig { + public var backgroundSyncConfig: BackgroundSyncConfig? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(backgroundSyncConfig: BackgroundSyncConfig?) { + self.backgroundSyncConfig = backgroundSyncConfig + } +} + + + +extension EsploraSyncConfig: Equatable, Hashable { + public static func ==(lhs: EsploraSyncConfig, rhs: EsploraSyncConfig) -> Bool { + if lhs.backgroundSyncConfig != rhs.backgroundSyncConfig { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(backgroundSyncConfig) } } @@ -3069,16 +4157,12 @@ public struct FfiConverterTypeEsploraSyncConfig: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EsploraSyncConfig { return try EsploraSyncConfig( - onchainWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), - lightningWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), - feeRateCacheUpdateIntervalSecs: FfiConverterUInt64.read(from: &buf) + backgroundSyncConfig: FfiConverterOptionTypeBackgroundSyncConfig.read(from: &buf) ) } public static func write(_ value: EsploraSyncConfig, into buf: inout [UInt8]) { - FfiConverterUInt64.write(value.onchainWalletSyncIntervalSecs, into: &buf) - FfiConverterUInt64.write(value.lightningWalletSyncIntervalSecs, into: &buf) - FfiConverterUInt64.write(value.feeRateCacheUpdateIntervalSecs, into: &buf) + FfiConverterOptionTypeBackgroundSyncConfig.write(value.backgroundSyncConfig, into: &buf) } } @@ -3149,6 +4233,239 @@ public func FfiConverterTypeLSPFeeLimits_lower(_ value: LspFeeLimits) -> RustBuf } +public struct Lsps1OrderStatus { + public var orderId: OrderId + public var orderParams: OrderParameters + public var paymentOptions: PaymentInfo + public var channelState: ChannelOrderInfo? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(orderId: OrderId, orderParams: OrderParameters, paymentOptions: PaymentInfo, channelState: ChannelOrderInfo?) { + self.orderId = orderId + self.orderParams = orderParams + self.paymentOptions = paymentOptions + self.channelState = channelState + } +} + + + +public struct FfiConverterTypeLSPS1OrderStatus: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Lsps1OrderStatus { + return + try Lsps1OrderStatus( + orderId: FfiConverterTypeOrderId.read(from: &buf), + orderParams: FfiConverterTypeOrderParameters.read(from: &buf), + paymentOptions: FfiConverterTypePaymentInfo.read(from: &buf), + channelState: FfiConverterOptionTypeChannelOrderInfo.read(from: &buf) + ) + } + + public static func write(_ value: Lsps1OrderStatus, into buf: inout [UInt8]) { + FfiConverterTypeOrderId.write(value.orderId, into: &buf) + FfiConverterTypeOrderParameters.write(value.orderParams, into: &buf) + FfiConverterTypePaymentInfo.write(value.paymentOptions, into: &buf) + FfiConverterOptionTypeChannelOrderInfo.write(value.channelState, into: &buf) + } +} + + +public func FfiConverterTypeLSPS1OrderStatus_lift(_ buf: RustBuffer) throws -> Lsps1OrderStatus { + return try FfiConverterTypeLSPS1OrderStatus.lift(buf) +} + +public func FfiConverterTypeLSPS1OrderStatus_lower(_ value: Lsps1OrderStatus) -> RustBuffer { + return FfiConverterTypeLSPS1OrderStatus.lower(value) +} + + +public struct Lsps2ServiceConfig { + public var requireToken: String? + public var advertiseService: Bool + public var channelOpeningFeePpm: UInt32 + public var channelOverProvisioningPpm: UInt32 + public var minChannelOpeningFeeMsat: UInt64 + public var minChannelLifetime: UInt32 + public var maxClientToSelfDelay: UInt32 + public var minPaymentSizeMsat: UInt64 + public var maxPaymentSizeMsat: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(requireToken: String?, advertiseService: Bool, channelOpeningFeePpm: UInt32, channelOverProvisioningPpm: UInt32, minChannelOpeningFeeMsat: UInt64, minChannelLifetime: UInt32, maxClientToSelfDelay: UInt32, minPaymentSizeMsat: UInt64, maxPaymentSizeMsat: UInt64) { + self.requireToken = requireToken + self.advertiseService = advertiseService + self.channelOpeningFeePpm = channelOpeningFeePpm + self.channelOverProvisioningPpm = channelOverProvisioningPpm + self.minChannelOpeningFeeMsat = minChannelOpeningFeeMsat + self.minChannelLifetime = minChannelLifetime + self.maxClientToSelfDelay = maxClientToSelfDelay + self.minPaymentSizeMsat = minPaymentSizeMsat + self.maxPaymentSizeMsat = maxPaymentSizeMsat + } +} + + + +extension Lsps2ServiceConfig: Equatable, Hashable { + public static func ==(lhs: Lsps2ServiceConfig, rhs: Lsps2ServiceConfig) -> Bool { + if lhs.requireToken != rhs.requireToken { + return false + } + if lhs.advertiseService != rhs.advertiseService { + return false + } + if lhs.channelOpeningFeePpm != rhs.channelOpeningFeePpm { + return false + } + if lhs.channelOverProvisioningPpm != rhs.channelOverProvisioningPpm { + return false + } + if lhs.minChannelOpeningFeeMsat != rhs.minChannelOpeningFeeMsat { + return false + } + if lhs.minChannelLifetime != rhs.minChannelLifetime { + return false + } + if lhs.maxClientToSelfDelay != rhs.maxClientToSelfDelay { + return false + } + if lhs.minPaymentSizeMsat != rhs.minPaymentSizeMsat { + return false + } + if lhs.maxPaymentSizeMsat != rhs.maxPaymentSizeMsat { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(requireToken) + hasher.combine(advertiseService) + hasher.combine(channelOpeningFeePpm) + hasher.combine(channelOverProvisioningPpm) + hasher.combine(minChannelOpeningFeeMsat) + hasher.combine(minChannelLifetime) + hasher.combine(maxClientToSelfDelay) + hasher.combine(minPaymentSizeMsat) + hasher.combine(maxPaymentSizeMsat) + } +} + + +public struct FfiConverterTypeLSPS2ServiceConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Lsps2ServiceConfig { + return + try Lsps2ServiceConfig( + requireToken: FfiConverterOptionString.read(from: &buf), + advertiseService: FfiConverterBool.read(from: &buf), + channelOpeningFeePpm: FfiConverterUInt32.read(from: &buf), + channelOverProvisioningPpm: FfiConverterUInt32.read(from: &buf), + minChannelOpeningFeeMsat: FfiConverterUInt64.read(from: &buf), + minChannelLifetime: FfiConverterUInt32.read(from: &buf), + maxClientToSelfDelay: FfiConverterUInt32.read(from: &buf), + minPaymentSizeMsat: FfiConverterUInt64.read(from: &buf), + maxPaymentSizeMsat: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: Lsps2ServiceConfig, into buf: inout [UInt8]) { + FfiConverterOptionString.write(value.requireToken, into: &buf) + FfiConverterBool.write(value.advertiseService, into: &buf) + FfiConverterUInt32.write(value.channelOpeningFeePpm, into: &buf) + FfiConverterUInt32.write(value.channelOverProvisioningPpm, into: &buf) + FfiConverterUInt64.write(value.minChannelOpeningFeeMsat, into: &buf) + FfiConverterUInt32.write(value.minChannelLifetime, into: &buf) + FfiConverterUInt32.write(value.maxClientToSelfDelay, into: &buf) + FfiConverterUInt64.write(value.minPaymentSizeMsat, into: &buf) + FfiConverterUInt64.write(value.maxPaymentSizeMsat, into: &buf) + } +} + + +public func FfiConverterTypeLSPS2ServiceConfig_lift(_ buf: RustBuffer) throws -> Lsps2ServiceConfig { + return try FfiConverterTypeLSPS2ServiceConfig.lift(buf) +} + +public func FfiConverterTypeLSPS2ServiceConfig_lower(_ value: Lsps2ServiceConfig) -> RustBuffer { + return FfiConverterTypeLSPS2ServiceConfig.lower(value) +} + + +public struct LogRecord { + public var level: LogLevel + public var args: String + public var modulePath: String + public var line: UInt32 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(level: LogLevel, args: String, modulePath: String, line: UInt32) { + self.level = level + self.args = args + self.modulePath = modulePath + self.line = line + } +} + + + +extension LogRecord: Equatable, Hashable { + public static func ==(lhs: LogRecord, rhs: LogRecord) -> Bool { + if lhs.level != rhs.level { + return false + } + if lhs.args != rhs.args { + return false + } + if lhs.modulePath != rhs.modulePath { + return false + } + if lhs.line != rhs.line { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(level) + hasher.combine(args) + hasher.combine(modulePath) + hasher.combine(line) + } +} + + +public struct FfiConverterTypeLogRecord: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LogRecord { + return + try LogRecord( + level: FfiConverterTypeLogLevel.read(from: &buf), + args: FfiConverterString.read(from: &buf), + modulePath: FfiConverterString.read(from: &buf), + line: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: LogRecord, into buf: inout [UInt8]) { + FfiConverterTypeLogLevel.write(value.level, into: &buf) + FfiConverterString.write(value.args, into: &buf) + FfiConverterString.write(value.modulePath, into: &buf) + FfiConverterUInt32.write(value.line, into: &buf) + } +} + + +public func FfiConverterTypeLogRecord_lift(_ buf: RustBuffer) throws -> LogRecord { + return try FfiConverterTypeLogRecord.lift(buf) +} + +public func FfiConverterTypeLogRecord_lower(_ value: LogRecord) -> RustBuffer { + return FfiConverterTypeLogRecord.lower(value) +} + + public struct NodeAnnouncementInfo { public var lastUpdate: UInt32 public var alias: String @@ -3379,8 +4696,168 @@ public func FfiConverterTypeNodeStatus_lift(_ buf: RustBuffer) throws -> NodeSta return try FfiConverterTypeNodeStatus.lift(buf) } -public func FfiConverterTypeNodeStatus_lower(_ value: NodeStatus) -> RustBuffer { - return FfiConverterTypeNodeStatus.lower(value) +public func FfiConverterTypeNodeStatus_lower(_ value: NodeStatus) -> RustBuffer { + return FfiConverterTypeNodeStatus.lower(value) +} + + +public struct OnchainPaymentInfo { + public var state: PaymentState + public var expiresAt: DateTime + public var feeTotalSat: UInt64 + public var orderTotalSat: UInt64 + public var address: Address + public var minOnchainPaymentConfirmations: UInt16? + public var minFeeFor0conf: FeeRate + public var refundOnchainAddress: Address? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(state: PaymentState, expiresAt: DateTime, feeTotalSat: UInt64, orderTotalSat: UInt64, address: Address, minOnchainPaymentConfirmations: UInt16?, minFeeFor0conf: FeeRate, refundOnchainAddress: Address?) { + self.state = state + self.expiresAt = expiresAt + self.feeTotalSat = feeTotalSat + self.orderTotalSat = orderTotalSat + self.address = address + self.minOnchainPaymentConfirmations = minOnchainPaymentConfirmations + self.minFeeFor0conf = minFeeFor0conf + self.refundOnchainAddress = refundOnchainAddress + } +} + + + +public struct FfiConverterTypeOnchainPaymentInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OnchainPaymentInfo { + return + try OnchainPaymentInfo( + state: FfiConverterTypePaymentState.read(from: &buf), + expiresAt: FfiConverterTypeDateTime.read(from: &buf), + feeTotalSat: FfiConverterUInt64.read(from: &buf), + orderTotalSat: FfiConverterUInt64.read(from: &buf), + address: FfiConverterTypeAddress.read(from: &buf), + minOnchainPaymentConfirmations: FfiConverterOptionUInt16.read(from: &buf), + minFeeFor0conf: FfiConverterTypeFeeRate.read(from: &buf), + refundOnchainAddress: FfiConverterOptionTypeAddress.read(from: &buf) + ) + } + + public static func write(_ value: OnchainPaymentInfo, into buf: inout [UInt8]) { + FfiConverterTypePaymentState.write(value.state, into: &buf) + FfiConverterTypeDateTime.write(value.expiresAt, into: &buf) + FfiConverterUInt64.write(value.feeTotalSat, into: &buf) + FfiConverterUInt64.write(value.orderTotalSat, into: &buf) + FfiConverterTypeAddress.write(value.address, into: &buf) + FfiConverterOptionUInt16.write(value.minOnchainPaymentConfirmations, into: &buf) + FfiConverterTypeFeeRate.write(value.minFeeFor0conf, into: &buf) + FfiConverterOptionTypeAddress.write(value.refundOnchainAddress, into: &buf) + } +} + + +public func FfiConverterTypeOnchainPaymentInfo_lift(_ buf: RustBuffer) throws -> OnchainPaymentInfo { + return try FfiConverterTypeOnchainPaymentInfo.lift(buf) +} + +public func FfiConverterTypeOnchainPaymentInfo_lower(_ value: OnchainPaymentInfo) -> RustBuffer { + return FfiConverterTypeOnchainPaymentInfo.lower(value) +} + + +public struct OrderParameters { + public var lspBalanceSat: UInt64 + public var clientBalanceSat: UInt64 + public var requiredChannelConfirmations: UInt16 + public var fundingConfirmsWithinBlocks: UInt16 + public var channelExpiryBlocks: UInt32 + public var token: String? + public var announceChannel: Bool + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(lspBalanceSat: UInt64, clientBalanceSat: UInt64, requiredChannelConfirmations: UInt16, fundingConfirmsWithinBlocks: UInt16, channelExpiryBlocks: UInt32, token: String?, announceChannel: Bool) { + self.lspBalanceSat = lspBalanceSat + self.clientBalanceSat = clientBalanceSat + self.requiredChannelConfirmations = requiredChannelConfirmations + self.fundingConfirmsWithinBlocks = fundingConfirmsWithinBlocks + self.channelExpiryBlocks = channelExpiryBlocks + self.token = token + self.announceChannel = announceChannel + } +} + + + +extension OrderParameters: Equatable, Hashable { + public static func ==(lhs: OrderParameters, rhs: OrderParameters) -> Bool { + if lhs.lspBalanceSat != rhs.lspBalanceSat { + return false + } + if lhs.clientBalanceSat != rhs.clientBalanceSat { + return false + } + if lhs.requiredChannelConfirmations != rhs.requiredChannelConfirmations { + return false + } + if lhs.fundingConfirmsWithinBlocks != rhs.fundingConfirmsWithinBlocks { + return false + } + if lhs.channelExpiryBlocks != rhs.channelExpiryBlocks { + return false + } + if lhs.token != rhs.token { + return false + } + if lhs.announceChannel != rhs.announceChannel { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(lspBalanceSat) + hasher.combine(clientBalanceSat) + hasher.combine(requiredChannelConfirmations) + hasher.combine(fundingConfirmsWithinBlocks) + hasher.combine(channelExpiryBlocks) + hasher.combine(token) + hasher.combine(announceChannel) + } +} + + +public struct FfiConverterTypeOrderParameters: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OrderParameters { + return + try OrderParameters( + lspBalanceSat: FfiConverterUInt64.read(from: &buf), + clientBalanceSat: FfiConverterUInt64.read(from: &buf), + requiredChannelConfirmations: FfiConverterUInt16.read(from: &buf), + fundingConfirmsWithinBlocks: FfiConverterUInt16.read(from: &buf), + channelExpiryBlocks: FfiConverterUInt32.read(from: &buf), + token: FfiConverterOptionString.read(from: &buf), + announceChannel: FfiConverterBool.read(from: &buf) + ) + } + + public static func write(_ value: OrderParameters, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.lspBalanceSat, into: &buf) + FfiConverterUInt64.write(value.clientBalanceSat, into: &buf) + FfiConverterUInt16.write(value.requiredChannelConfirmations, into: &buf) + FfiConverterUInt16.write(value.fundingConfirmsWithinBlocks, into: &buf) + FfiConverterUInt32.write(value.channelExpiryBlocks, into: &buf) + FfiConverterOptionString.write(value.token, into: &buf) + FfiConverterBool.write(value.announceChannel, into: &buf) + } +} + + +public func FfiConverterTypeOrderParameters_lift(_ buf: RustBuffer) throws -> OrderParameters { + return try FfiConverterTypeOrderParameters.lift(buf) +} + +public func FfiConverterTypeOrderParameters_lower(_ value: OrderParameters) -> RustBuffer { + return FfiConverterTypeOrderParameters.lower(value) } @@ -3445,16 +4922,18 @@ public struct PaymentDetails { public var id: PaymentId public var kind: PaymentKind public var amountMsat: UInt64? + public var feePaidMsat: UInt64? public var direction: PaymentDirection public var status: PaymentStatus public var latestUpdateTimestamp: UInt64 // Default memberwise initializers are never public by default, so we // declare one manually. - public init(id: PaymentId, kind: PaymentKind, amountMsat: UInt64?, direction: PaymentDirection, status: PaymentStatus, latestUpdateTimestamp: UInt64) { + public init(id: PaymentId, kind: PaymentKind, amountMsat: UInt64?, feePaidMsat: UInt64?, direction: PaymentDirection, status: PaymentStatus, latestUpdateTimestamp: UInt64) { self.id = id self.kind = kind self.amountMsat = amountMsat + self.feePaidMsat = feePaidMsat self.direction = direction self.status = status self.latestUpdateTimestamp = latestUpdateTimestamp @@ -3474,6 +4953,9 @@ extension PaymentDetails: Equatable, Hashable { if lhs.amountMsat != rhs.amountMsat { return false } + if lhs.feePaidMsat != rhs.feePaidMsat { + return false + } if lhs.direction != rhs.direction { return false } @@ -3490,6 +4972,7 @@ extension PaymentDetails: Equatable, Hashable { hasher.combine(id) hasher.combine(kind) hasher.combine(amountMsat) + hasher.combine(feePaidMsat) hasher.combine(direction) hasher.combine(status) hasher.combine(latestUpdateTimestamp) @@ -3504,6 +4987,7 @@ public struct FfiConverterTypePaymentDetails: FfiConverterRustBuffer { id: FfiConverterTypePaymentId.read(from: &buf), kind: FfiConverterTypePaymentKind.read(from: &buf), amountMsat: FfiConverterOptionUInt64.read(from: &buf), + feePaidMsat: FfiConverterOptionUInt64.read(from: &buf), direction: FfiConverterTypePaymentDirection.read(from: &buf), status: FfiConverterTypePaymentStatus.read(from: &buf), latestUpdateTimestamp: FfiConverterUInt64.read(from: &buf) @@ -3514,6 +4998,7 @@ public struct FfiConverterTypePaymentDetails: FfiConverterRustBuffer { FfiConverterTypePaymentId.write(value.id, into: &buf) FfiConverterTypePaymentKind.write(value.kind, into: &buf) FfiConverterOptionUInt64.write(value.amountMsat, into: &buf) + FfiConverterOptionUInt64.write(value.feePaidMsat, into: &buf) FfiConverterTypePaymentDirection.write(value.direction, into: &buf) FfiConverterTypePaymentStatus.write(value.status, into: &buf) FfiConverterUInt64.write(value.latestUpdateTimestamp, into: &buf) @@ -3530,6 +5015,45 @@ public func FfiConverterTypePaymentDetails_lower(_ value: PaymentDetails) -> Rus } +public struct PaymentInfo { + public var bolt11: Bolt11PaymentInfo? + public var onchain: OnchainPaymentInfo? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(bolt11: Bolt11PaymentInfo?, onchain: OnchainPaymentInfo?) { + self.bolt11 = bolt11 + self.onchain = onchain + } +} + + + +public struct FfiConverterTypePaymentInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PaymentInfo { + return + try PaymentInfo( + bolt11: FfiConverterOptionTypeBolt11PaymentInfo.read(from: &buf), + onchain: FfiConverterOptionTypeOnchainPaymentInfo.read(from: &buf) + ) + } + + public static func write(_ value: PaymentInfo, into buf: inout [UInt8]) { + FfiConverterOptionTypeBolt11PaymentInfo.write(value.bolt11, into: &buf) + FfiConverterOptionTypeOnchainPaymentInfo.write(value.onchain, into: &buf) + } +} + + +public func FfiConverterTypePaymentInfo_lift(_ buf: RustBuffer) throws -> PaymentInfo { + return try FfiConverterTypePaymentInfo.lift(buf) +} + +public func FfiConverterTypePaymentInfo_lower(_ value: PaymentInfo) -> RustBuffer { + return FfiConverterTypePaymentInfo.lower(value) +} + + public struct PeerDetails { public var nodeId: PublicKey public var address: SocketAddress @@ -3603,6 +5127,95 @@ public func FfiConverterTypePeerDetails_lower(_ value: PeerDetails) -> RustBuffe } +public struct RouteHintHop { + public var srcNodeId: PublicKey + public var shortChannelId: UInt64 + public var cltvExpiryDelta: UInt16 + public var htlcMinimumMsat: UInt64? + public var htlcMaximumMsat: UInt64? + public var fees: RoutingFees + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(srcNodeId: PublicKey, shortChannelId: UInt64, cltvExpiryDelta: UInt16, htlcMinimumMsat: UInt64?, htlcMaximumMsat: UInt64?, fees: RoutingFees) { + self.srcNodeId = srcNodeId + self.shortChannelId = shortChannelId + self.cltvExpiryDelta = cltvExpiryDelta + self.htlcMinimumMsat = htlcMinimumMsat + self.htlcMaximumMsat = htlcMaximumMsat + self.fees = fees + } +} + + + +extension RouteHintHop: Equatable, Hashable { + public static func ==(lhs: RouteHintHop, rhs: RouteHintHop) -> Bool { + if lhs.srcNodeId != rhs.srcNodeId { + return false + } + if lhs.shortChannelId != rhs.shortChannelId { + return false + } + if lhs.cltvExpiryDelta != rhs.cltvExpiryDelta { + return false + } + if lhs.htlcMinimumMsat != rhs.htlcMinimumMsat { + return false + } + if lhs.htlcMaximumMsat != rhs.htlcMaximumMsat { + return false + } + if lhs.fees != rhs.fees { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(srcNodeId) + hasher.combine(shortChannelId) + hasher.combine(cltvExpiryDelta) + hasher.combine(htlcMinimumMsat) + hasher.combine(htlcMaximumMsat) + hasher.combine(fees) + } +} + + +public struct FfiConverterTypeRouteHintHop: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RouteHintHop { + return + try RouteHintHop( + srcNodeId: FfiConverterTypePublicKey.read(from: &buf), + shortChannelId: FfiConverterUInt64.read(from: &buf), + cltvExpiryDelta: FfiConverterUInt16.read(from: &buf), + htlcMinimumMsat: FfiConverterOptionUInt64.read(from: &buf), + htlcMaximumMsat: FfiConverterOptionUInt64.read(from: &buf), + fees: FfiConverterTypeRoutingFees.read(from: &buf) + ) + } + + public static func write(_ value: RouteHintHop, into buf: inout [UInt8]) { + FfiConverterTypePublicKey.write(value.srcNodeId, into: &buf) + FfiConverterUInt64.write(value.shortChannelId, into: &buf) + FfiConverterUInt16.write(value.cltvExpiryDelta, into: &buf) + FfiConverterOptionUInt64.write(value.htlcMinimumMsat, into: &buf) + FfiConverterOptionUInt64.write(value.htlcMaximumMsat, into: &buf) + FfiConverterTypeRoutingFees.write(value.fees, into: &buf) + } +} + + +public func FfiConverterTypeRouteHintHop_lift(_ buf: RustBuffer) throws -> RouteHintHop { + return try FfiConverterTypeRouteHintHop.lift(buf) +} + +public func FfiConverterTypeRouteHintHop_lower(_ value: RouteHintHop) -> RustBuffer { + return FfiConverterTypeRouteHintHop.lower(value) +} + + public struct RoutingFees { public var baseMsat: UInt32 public var proportionalMillionths: UInt32 @@ -3801,6 +5414,67 @@ extension BalanceSource: Equatable, Hashable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum Bolt11InvoiceDescription { + + case hash(hash: String + ) + case direct(description: String + ) +} + + +public struct FfiConverterTypeBolt11InvoiceDescription: FfiConverterRustBuffer { + typealias SwiftType = Bolt11InvoiceDescription + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11InvoiceDescription { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .hash(hash: try FfiConverterString.read(from: &buf) + ) + + case 2: return .direct(description: try FfiConverterString.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: Bolt11InvoiceDescription, into buf: inout [UInt8]) { + switch value { + + + case let .hash(hash): + writeInt(&buf, Int32(1)) + FfiConverterString.write(hash, into: &buf) + + + case let .direct(description): + writeInt(&buf, Int32(2)) + FfiConverterString.write(description, into: &buf) + + } + } +} + + +public func FfiConverterTypeBolt11InvoiceDescription_lift(_ buf: RustBuffer) throws -> Bolt11InvoiceDescription { + return try FfiConverterTypeBolt11InvoiceDescription.lift(buf) +} + +public func FfiConverterTypeBolt11InvoiceDescription_lower(_ value: Bolt11InvoiceDescription) -> RustBuffer { + return FfiConverterTypeBolt11InvoiceDescription.lower(value) +} + + + +extension Bolt11InvoiceDescription: Equatable, Hashable {} + + + public enum BuildError { @@ -3816,6 +5490,8 @@ public enum BuildError { case InvalidListeningAddresses(message: String) + case InvalidAnnouncementAddresses(message: String) + case InvalidNodeAlias(message: String) case ReadFailed(message: String) @@ -3830,6 +5506,8 @@ public enum BuildError { case LoggerSetupFailed(message: String) + case NetworkMismatch(message: String) + } @@ -3863,31 +5541,39 @@ public struct FfiConverterTypeBuildError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) - case 6: return .InvalidNodeAlias( + case 6: return .InvalidAnnouncementAddresses( + message: try FfiConverterString.read(from: &buf) + ) + + case 7: return .InvalidNodeAlias( + message: try FfiConverterString.read(from: &buf) + ) + + case 8: return .ReadFailed( message: try FfiConverterString.read(from: &buf) ) - case 7: return .ReadFailed( + case 9: return .WriteFailed( message: try FfiConverterString.read(from: &buf) ) - case 8: return .WriteFailed( + case 10: return .StoragePathAccessFailed( message: try FfiConverterString.read(from: &buf) ) - case 9: return .StoragePathAccessFailed( + case 11: return .KvStoreSetupFailed( message: try FfiConverterString.read(from: &buf) ) - case 10: return .KvStoreSetupFailed( + case 12: return .WalletSetupFailed( message: try FfiConverterString.read(from: &buf) ) - case 11: return .WalletSetupFailed( + case 13: return .LoggerSetupFailed( message: try FfiConverterString.read(from: &buf) ) - case 12: return .LoggerSetupFailed( + case 14: return .NetworkMismatch( message: try FfiConverterString.read(from: &buf) ) @@ -3912,20 +5598,24 @@ public struct FfiConverterTypeBuildError: FfiConverterRustBuffer { writeInt(&buf, Int32(4)) case .InvalidListeningAddresses(_ /* message is ignored*/): writeInt(&buf, Int32(5)) - case .InvalidNodeAlias(_ /* message is ignored*/): + case .InvalidAnnouncementAddresses(_ /* message is ignored*/): writeInt(&buf, Int32(6)) - case .ReadFailed(_ /* message is ignored*/): + case .InvalidNodeAlias(_ /* message is ignored*/): writeInt(&buf, Int32(7)) - case .WriteFailed(_ /* message is ignored*/): + case .ReadFailed(_ /* message is ignored*/): writeInt(&buf, Int32(8)) - case .StoragePathAccessFailed(_ /* message is ignored*/): + case .WriteFailed(_ /* message is ignored*/): writeInt(&buf, Int32(9)) - case .KvStoreSetupFailed(_ /* message is ignored*/): + case .StoragePathAccessFailed(_ /* message is ignored*/): writeInt(&buf, Int32(10)) - case .WalletSetupFailed(_ /* message is ignored*/): + case .KvStoreSetupFailed(_ /* message is ignored*/): writeInt(&buf, Int32(11)) - case .LoggerSetupFailed(_ /* message is ignored*/): + case .WalletSetupFailed(_ /* message is ignored*/): writeInt(&buf, Int32(12)) + case .LoggerSetupFailed(_ /* message is ignored*/): + writeInt(&buf, Int32(13)) + case .NetworkMismatch(_ /* message is ignored*/): + writeInt(&buf, Int32(14)) } @@ -4089,18 +5779,156 @@ extension ClosureReason: Equatable, Hashable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ConfirmationStatus { + + case confirmed(blockHash: BlockHash, height: UInt32, timestamp: UInt64 + ) + case unconfirmed +} + + +public struct FfiConverterTypeConfirmationStatus: FfiConverterRustBuffer { + typealias SwiftType = ConfirmationStatus + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ConfirmationStatus { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .confirmed(blockHash: try FfiConverterTypeBlockHash.read(from: &buf), height: try FfiConverterUInt32.read(from: &buf), timestamp: try FfiConverterUInt64.read(from: &buf) + ) + + case 2: return .unconfirmed + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ConfirmationStatus, into buf: inout [UInt8]) { + switch value { + + + case let .confirmed(blockHash,height,timestamp): + writeInt(&buf, Int32(1)) + FfiConverterTypeBlockHash.write(blockHash, into: &buf) + FfiConverterUInt32.write(height, into: &buf) + FfiConverterUInt64.write(timestamp, into: &buf) + + + case .unconfirmed: + writeInt(&buf, Int32(2)) + + } + } +} + + +public func FfiConverterTypeConfirmationStatus_lift(_ buf: RustBuffer) throws -> ConfirmationStatus { + return try FfiConverterTypeConfirmationStatus.lift(buf) +} + +public func FfiConverterTypeConfirmationStatus_lower(_ value: ConfirmationStatus) -> RustBuffer { + return FfiConverterTypeConfirmationStatus.lower(value) +} + + + +extension ConfirmationStatus: Equatable, Hashable {} + + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum Currency { + + case bitcoin + case bitcoinTestnet + case regtest + case simnet + case signet +} + + +public struct FfiConverterTypeCurrency: FfiConverterRustBuffer { + typealias SwiftType = Currency + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Currency { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .bitcoin + + case 2: return .bitcoinTestnet + + case 3: return .regtest + + case 4: return .simnet + + case 5: return .signet + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: Currency, into buf: inout [UInt8]) { + switch value { + + + case .bitcoin: + writeInt(&buf, Int32(1)) + + + case .bitcoinTestnet: + writeInt(&buf, Int32(2)) + + + case .regtest: + writeInt(&buf, Int32(3)) + + + case .simnet: + writeInt(&buf, Int32(4)) + + + case .signet: + writeInt(&buf, Int32(5)) + + } + } +} + + +public func FfiConverterTypeCurrency_lift(_ buf: RustBuffer) throws -> Currency { + return try FfiConverterTypeCurrency.lift(buf) +} + +public func FfiConverterTypeCurrency_lower(_ value: Currency) -> RustBuffer { + return FfiConverterTypeCurrency.lower(value) +} + + + +extension Currency: Equatable, Hashable {} + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. public enum Event { - case paymentSuccessful(paymentId: PaymentId?, paymentHash: PaymentHash, feePaidMsat: UInt64? + case paymentSuccessful(paymentId: PaymentId?, paymentHash: PaymentHash, paymentPreimage: PaymentPreimage?, feePaidMsat: UInt64? ) case paymentFailed(paymentId: PaymentId?, paymentHash: PaymentHash?, reason: PaymentFailureReason? ) - case paymentReceived(paymentId: PaymentId?, paymentHash: PaymentHash, amountMsat: UInt64 + case paymentReceived(paymentId: PaymentId?, paymentHash: PaymentHash, amountMsat: UInt64, customRecords: [CustomTlvRecord] + ) + case paymentClaimable(paymentId: PaymentId, paymentHash: PaymentHash, claimableAmountMsat: UInt64, claimDeadline: UInt32?, customRecords: [CustomTlvRecord] ) - case paymentClaimable(paymentId: PaymentId, paymentHash: PaymentHash, claimableAmountMsat: UInt64, claimDeadline: UInt32? + case paymentForwarded(prevChannelId: ChannelId, nextChannelId: ChannelId, prevUserChannelId: UserChannelId?, nextUserChannelId: UserChannelId?, prevNodeId: PublicKey?, nextNodeId: PublicKey?, totalFeeEarnedMsat: UInt64?, skimmedFeeMsat: UInt64?, claimFromOnchainTx: Bool, outboundAmountForwardedMsat: UInt64? ) case channelPending(channelId: ChannelId, userChannelId: UserChannelId, formerTemporaryChannelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint ) @@ -4118,25 +5946,28 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return .paymentSuccessful(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), feePaidMsat: try FfiConverterOptionUInt64.read(from: &buf) + case 1: return .paymentSuccessful(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), paymentPreimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), feePaidMsat: try FfiConverterOptionUInt64.read(from: &buf) ) case 2: return .paymentFailed(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterOptionTypePaymentHash.read(from: &buf), reason: try FfiConverterOptionTypePaymentFailureReason.read(from: &buf) ) - case 3: return .paymentReceived(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), amountMsat: try FfiConverterUInt64.read(from: &buf) + case 3: return .paymentReceived(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), amountMsat: try FfiConverterUInt64.read(from: &buf), customRecords: try FfiConverterSequenceTypeCustomTlvRecord.read(from: &buf) ) - case 4: return .paymentClaimable(paymentId: try FfiConverterTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), claimableAmountMsat: try FfiConverterUInt64.read(from: &buf), claimDeadline: try FfiConverterOptionUInt32.read(from: &buf) + case 4: return .paymentClaimable(paymentId: try FfiConverterTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), claimableAmountMsat: try FfiConverterUInt64.read(from: &buf), claimDeadline: try FfiConverterOptionUInt32.read(from: &buf), customRecords: try FfiConverterSequenceTypeCustomTlvRecord.read(from: &buf) ) - case 5: return .channelPending(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), formerTemporaryChannelId: try FfiConverterTypeChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterTypePublicKey.read(from: &buf), fundingTxo: try FfiConverterTypeOutPoint.read(from: &buf) + case 5: return .paymentForwarded(prevChannelId: try FfiConverterTypeChannelId.read(from: &buf), nextChannelId: try FfiConverterTypeChannelId.read(from: &buf), prevUserChannelId: try FfiConverterOptionTypeUserChannelId.read(from: &buf), nextUserChannelId: try FfiConverterOptionTypeUserChannelId.read(from: &buf), prevNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), nextNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), totalFeeEarnedMsat: try FfiConverterOptionUInt64.read(from: &buf), skimmedFeeMsat: try FfiConverterOptionUInt64.read(from: &buf), claimFromOnchainTx: try FfiConverterBool.read(from: &buf), outboundAmountForwardedMsat: try FfiConverterOptionUInt64.read(from: &buf) ) - case 6: return .channelReady(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf) + case 6: return .channelPending(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), formerTemporaryChannelId: try FfiConverterTypeChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterTypePublicKey.read(from: &buf), fundingTxo: try FfiConverterTypeOutPoint.read(from: &buf) ) - case 7: return .channelClosed(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), reason: try FfiConverterOptionTypeClosureReason.read(from: &buf) + case 7: return .channelReady(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf) + ) + + case 8: return .channelClosed(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), reason: try FfiConverterOptionTypeClosureReason.read(from: &buf) ) default: throw UniffiInternalError.unexpectedEnumCase @@ -4147,10 +5978,11 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { switch value { - case let .paymentSuccessful(paymentId,paymentHash,feePaidMsat): + case let .paymentSuccessful(paymentId,paymentHash,paymentPreimage,feePaidMsat): writeInt(&buf, Int32(1)) FfiConverterOptionTypePaymentId.write(paymentId, into: &buf) FfiConverterTypePaymentHash.write(paymentHash, into: &buf) + FfiConverterOptionTypePaymentPreimage.write(paymentPreimage, into: &buf) FfiConverterOptionUInt64.write(feePaidMsat, into: &buf) @@ -4161,23 +5993,39 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { FfiConverterOptionTypePaymentFailureReason.write(reason, into: &buf) - case let .paymentReceived(paymentId,paymentHash,amountMsat): + case let .paymentReceived(paymentId,paymentHash,amountMsat,customRecords): writeInt(&buf, Int32(3)) FfiConverterOptionTypePaymentId.write(paymentId, into: &buf) FfiConverterTypePaymentHash.write(paymentHash, into: &buf) FfiConverterUInt64.write(amountMsat, into: &buf) + FfiConverterSequenceTypeCustomTlvRecord.write(customRecords, into: &buf) - case let .paymentClaimable(paymentId,paymentHash,claimableAmountMsat,claimDeadline): + case let .paymentClaimable(paymentId,paymentHash,claimableAmountMsat,claimDeadline,customRecords): writeInt(&buf, Int32(4)) FfiConverterTypePaymentId.write(paymentId, into: &buf) FfiConverterTypePaymentHash.write(paymentHash, into: &buf) FfiConverterUInt64.write(claimableAmountMsat, into: &buf) FfiConverterOptionUInt32.write(claimDeadline, into: &buf) + FfiConverterSequenceTypeCustomTlvRecord.write(customRecords, into: &buf) + + + case let .paymentForwarded(prevChannelId,nextChannelId,prevUserChannelId,nextUserChannelId,prevNodeId,nextNodeId,totalFeeEarnedMsat,skimmedFeeMsat,claimFromOnchainTx,outboundAmountForwardedMsat): + writeInt(&buf, Int32(5)) + FfiConverterTypeChannelId.write(prevChannelId, into: &buf) + FfiConverterTypeChannelId.write(nextChannelId, into: &buf) + FfiConverterOptionTypeUserChannelId.write(prevUserChannelId, into: &buf) + FfiConverterOptionTypeUserChannelId.write(nextUserChannelId, into: &buf) + FfiConverterOptionTypePublicKey.write(prevNodeId, into: &buf) + FfiConverterOptionTypePublicKey.write(nextNodeId, into: &buf) + FfiConverterOptionUInt64.write(totalFeeEarnedMsat, into: &buf) + FfiConverterOptionUInt64.write(skimmedFeeMsat, into: &buf) + FfiConverterBool.write(claimFromOnchainTx, into: &buf) + FfiConverterOptionUInt64.write(outboundAmountForwardedMsat, into: &buf) case let .channelPending(channelId,userChannelId,formerTemporaryChannelId,counterpartyNodeId,fundingTxo): - writeInt(&buf, Int32(5)) + writeInt(&buf, Int32(6)) FfiConverterTypeChannelId.write(channelId, into: &buf) FfiConverterTypeUserChannelId.write(userChannelId, into: &buf) FfiConverterTypeChannelId.write(formerTemporaryChannelId, into: &buf) @@ -4186,14 +6034,14 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { case let .channelReady(channelId,userChannelId,counterpartyNodeId): - writeInt(&buf, Int32(6)) + writeInt(&buf, Int32(7)) FfiConverterTypeChannelId.write(channelId, into: &buf) FfiConverterTypeUserChannelId.write(userChannelId, into: &buf) FfiConverterOptionTypePublicKey.write(counterpartyNodeId, into: &buf) case let .channelClosed(channelId,userChannelId,counterpartyNodeId,reason): - writeInt(&buf, Int32(7)) + writeInt(&buf, Int32(8)) FfiConverterTypeChannelId.write(channelId, into: &buf) FfiConverterTypeUserChannelId.write(userChannelId, into: &buf) FfiConverterOptionTypePublicKey.write(counterpartyNodeId, into: &buf) @@ -4640,6 +6488,8 @@ public enum NodeError { case PaymentSendingFailed(message: String) + case InvalidCustomTlvs(message: String) + case ProbeSendingFailed(message: String) case ChannelCreationFailed(message: String) @@ -4710,6 +6560,10 @@ public enum NodeError { case InvalidNodeAlias(message: String) + case InvalidDateTime(message: String) + + case InvalidFeeRate(message: String) + case DuplicatePayment(message: String) case UnsupportedCurrency(message: String) @@ -4769,163 +6623,175 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) - case 10: return .ProbeSendingFailed( + case 10: return .InvalidCustomTlvs( + message: try FfiConverterString.read(from: &buf) + ) + + case 11: return .ProbeSendingFailed( + message: try FfiConverterString.read(from: &buf) + ) + + case 12: return .ChannelCreationFailed( message: try FfiConverterString.read(from: &buf) ) - case 11: return .ChannelCreationFailed( + case 13: return .ChannelClosingFailed( message: try FfiConverterString.read(from: &buf) ) - case 12: return .ChannelClosingFailed( + case 14: return .ChannelConfigUpdateFailed( message: try FfiConverterString.read(from: &buf) ) - case 13: return .ChannelConfigUpdateFailed( + case 15: return .PersistenceFailed( message: try FfiConverterString.read(from: &buf) ) - case 14: return .PersistenceFailed( + case 16: return .FeerateEstimationUpdateFailed( message: try FfiConverterString.read(from: &buf) ) - case 15: return .FeerateEstimationUpdateFailed( + case 17: return .FeerateEstimationUpdateTimeout( message: try FfiConverterString.read(from: &buf) ) - case 16: return .FeerateEstimationUpdateTimeout( + case 18: return .WalletOperationFailed( message: try FfiConverterString.read(from: &buf) ) - case 17: return .WalletOperationFailed( + case 19: return .WalletOperationTimeout( message: try FfiConverterString.read(from: &buf) ) - case 18: return .WalletOperationTimeout( + case 20: return .OnchainTxSigningFailed( message: try FfiConverterString.read(from: &buf) ) - case 19: return .OnchainTxSigningFailed( + case 21: return .TxSyncFailed( message: try FfiConverterString.read(from: &buf) ) - case 20: return .TxSyncFailed( + case 22: return .TxSyncTimeout( message: try FfiConverterString.read(from: &buf) ) - case 21: return .TxSyncTimeout( + case 23: return .GossipUpdateFailed( message: try FfiConverterString.read(from: &buf) ) - case 22: return .GossipUpdateFailed( + case 24: return .GossipUpdateTimeout( message: try FfiConverterString.read(from: &buf) ) - case 23: return .GossipUpdateTimeout( + case 25: return .LiquidityRequestFailed( message: try FfiConverterString.read(from: &buf) ) - case 24: return .LiquidityRequestFailed( + case 26: return .UriParameterParsingFailed( message: try FfiConverterString.read(from: &buf) ) - case 25: return .UriParameterParsingFailed( + case 27: return .InvalidAddress( message: try FfiConverterString.read(from: &buf) ) - case 26: return .InvalidAddress( + case 28: return .InvalidSocketAddress( message: try FfiConverterString.read(from: &buf) ) - case 27: return .InvalidSocketAddress( + case 29: return .InvalidPublicKey( message: try FfiConverterString.read(from: &buf) ) - case 28: return .InvalidPublicKey( + case 30: return .InvalidSecretKey( message: try FfiConverterString.read(from: &buf) ) - case 29: return .InvalidSecretKey( + case 31: return .InvalidOfferId( message: try FfiConverterString.read(from: &buf) ) - case 30: return .InvalidOfferId( + case 32: return .InvalidNodeId( message: try FfiConverterString.read(from: &buf) ) - case 31: return .InvalidNodeId( + case 33: return .InvalidPaymentId( message: try FfiConverterString.read(from: &buf) ) - case 32: return .InvalidPaymentId( + case 34: return .InvalidPaymentHash( message: try FfiConverterString.read(from: &buf) ) - case 33: return .InvalidPaymentHash( + case 35: return .InvalidPaymentPreimage( message: try FfiConverterString.read(from: &buf) ) - case 34: return .InvalidPaymentPreimage( + case 36: return .InvalidPaymentSecret( message: try FfiConverterString.read(from: &buf) ) - case 35: return .InvalidPaymentSecret( + case 37: return .InvalidAmount( message: try FfiConverterString.read(from: &buf) ) - case 36: return .InvalidAmount( + case 38: return .InvalidInvoice( message: try FfiConverterString.read(from: &buf) ) - case 37: return .InvalidInvoice( + case 39: return .InvalidOffer( message: try FfiConverterString.read(from: &buf) ) - case 38: return .InvalidOffer( + case 40: return .InvalidRefund( message: try FfiConverterString.read(from: &buf) ) - case 39: return .InvalidRefund( + case 41: return .InvalidChannelId( message: try FfiConverterString.read(from: &buf) ) - case 40: return .InvalidChannelId( + case 42: return .InvalidNetwork( message: try FfiConverterString.read(from: &buf) ) - case 41: return .InvalidNetwork( + case 43: return .InvalidUri( message: try FfiConverterString.read(from: &buf) ) - case 42: return .InvalidUri( + case 44: return .InvalidQuantity( message: try FfiConverterString.read(from: &buf) ) - case 43: return .InvalidQuantity( + case 45: return .InvalidNodeAlias( message: try FfiConverterString.read(from: &buf) ) - case 44: return .InvalidNodeAlias( + case 46: return .InvalidDateTime( message: try FfiConverterString.read(from: &buf) ) - case 45: return .DuplicatePayment( + case 47: return .InvalidFeeRate( message: try FfiConverterString.read(from: &buf) ) - case 46: return .UnsupportedCurrency( + case 48: return .DuplicatePayment( message: try FfiConverterString.read(from: &buf) ) - case 47: return .InsufficientFunds( + case 49: return .UnsupportedCurrency( message: try FfiConverterString.read(from: &buf) ) - case 48: return .LiquiditySourceUnavailable( + case 50: return .InsufficientFunds( message: try FfiConverterString.read(from: &buf) ) - case 49: return .LiquidityFeeTooHigh( + case 51: return .LiquiditySourceUnavailable( + message: try FfiConverterString.read(from: &buf) + ) + + case 52: return .LiquidityFeeTooHigh( message: try FfiConverterString.read(from: &buf) ) @@ -4958,86 +6824,92 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { writeInt(&buf, Int32(8)) case .PaymentSendingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(9)) - case .ProbeSendingFailed(_ /* message is ignored*/): + case .InvalidCustomTlvs(_ /* message is ignored*/): writeInt(&buf, Int32(10)) - case .ChannelCreationFailed(_ /* message is ignored*/): + case .ProbeSendingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(11)) - case .ChannelClosingFailed(_ /* message is ignored*/): + case .ChannelCreationFailed(_ /* message is ignored*/): writeInt(&buf, Int32(12)) - case .ChannelConfigUpdateFailed(_ /* message is ignored*/): + case .ChannelClosingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(13)) - case .PersistenceFailed(_ /* message is ignored*/): + case .ChannelConfigUpdateFailed(_ /* message is ignored*/): writeInt(&buf, Int32(14)) - case .FeerateEstimationUpdateFailed(_ /* message is ignored*/): + case .PersistenceFailed(_ /* message is ignored*/): writeInt(&buf, Int32(15)) - case .FeerateEstimationUpdateTimeout(_ /* message is ignored*/): + case .FeerateEstimationUpdateFailed(_ /* message is ignored*/): writeInt(&buf, Int32(16)) - case .WalletOperationFailed(_ /* message is ignored*/): + case .FeerateEstimationUpdateTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(17)) - case .WalletOperationTimeout(_ /* message is ignored*/): + case .WalletOperationFailed(_ /* message is ignored*/): writeInt(&buf, Int32(18)) - case .OnchainTxSigningFailed(_ /* message is ignored*/): + case .WalletOperationTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(19)) - case .TxSyncFailed(_ /* message is ignored*/): + case .OnchainTxSigningFailed(_ /* message is ignored*/): writeInt(&buf, Int32(20)) - case .TxSyncTimeout(_ /* message is ignored*/): + case .TxSyncFailed(_ /* message is ignored*/): writeInt(&buf, Int32(21)) - case .GossipUpdateFailed(_ /* message is ignored*/): + case .TxSyncTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(22)) - case .GossipUpdateTimeout(_ /* message is ignored*/): + case .GossipUpdateFailed(_ /* message is ignored*/): writeInt(&buf, Int32(23)) - case .LiquidityRequestFailed(_ /* message is ignored*/): + case .GossipUpdateTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(24)) - case .UriParameterParsingFailed(_ /* message is ignored*/): + case .LiquidityRequestFailed(_ /* message is ignored*/): writeInt(&buf, Int32(25)) - case .InvalidAddress(_ /* message is ignored*/): + case .UriParameterParsingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(26)) - case .InvalidSocketAddress(_ /* message is ignored*/): + case .InvalidAddress(_ /* message is ignored*/): writeInt(&buf, Int32(27)) - case .InvalidPublicKey(_ /* message is ignored*/): + case .InvalidSocketAddress(_ /* message is ignored*/): writeInt(&buf, Int32(28)) - case .InvalidSecretKey(_ /* message is ignored*/): + case .InvalidPublicKey(_ /* message is ignored*/): writeInt(&buf, Int32(29)) - case .InvalidOfferId(_ /* message is ignored*/): + case .InvalidSecretKey(_ /* message is ignored*/): writeInt(&buf, Int32(30)) - case .InvalidNodeId(_ /* message is ignored*/): + case .InvalidOfferId(_ /* message is ignored*/): writeInt(&buf, Int32(31)) - case .InvalidPaymentId(_ /* message is ignored*/): + case .InvalidNodeId(_ /* message is ignored*/): writeInt(&buf, Int32(32)) - case .InvalidPaymentHash(_ /* message is ignored*/): + case .InvalidPaymentId(_ /* message is ignored*/): writeInt(&buf, Int32(33)) - case .InvalidPaymentPreimage(_ /* message is ignored*/): + case .InvalidPaymentHash(_ /* message is ignored*/): writeInt(&buf, Int32(34)) - case .InvalidPaymentSecret(_ /* message is ignored*/): + case .InvalidPaymentPreimage(_ /* message is ignored*/): writeInt(&buf, Int32(35)) - case .InvalidAmount(_ /* message is ignored*/): + case .InvalidPaymentSecret(_ /* message is ignored*/): writeInt(&buf, Int32(36)) - case .InvalidInvoice(_ /* message is ignored*/): + case .InvalidAmount(_ /* message is ignored*/): writeInt(&buf, Int32(37)) - case .InvalidOffer(_ /* message is ignored*/): + case .InvalidInvoice(_ /* message is ignored*/): writeInt(&buf, Int32(38)) - case .InvalidRefund(_ /* message is ignored*/): + case .InvalidOffer(_ /* message is ignored*/): writeInt(&buf, Int32(39)) - case .InvalidChannelId(_ /* message is ignored*/): + case .InvalidRefund(_ /* message is ignored*/): writeInt(&buf, Int32(40)) - case .InvalidNetwork(_ /* message is ignored*/): + case .InvalidChannelId(_ /* message is ignored*/): writeInt(&buf, Int32(41)) - case .InvalidUri(_ /* message is ignored*/): + case .InvalidNetwork(_ /* message is ignored*/): writeInt(&buf, Int32(42)) - case .InvalidQuantity(_ /* message is ignored*/): + case .InvalidUri(_ /* message is ignored*/): writeInt(&buf, Int32(43)) - case .InvalidNodeAlias(_ /* message is ignored*/): + case .InvalidQuantity(_ /* message is ignored*/): writeInt(&buf, Int32(44)) - case .DuplicatePayment(_ /* message is ignored*/): + case .InvalidNodeAlias(_ /* message is ignored*/): writeInt(&buf, Int32(45)) - case .UnsupportedCurrency(_ /* message is ignored*/): + case .InvalidDateTime(_ /* message is ignored*/): writeInt(&buf, Int32(46)) - case .InsufficientFunds(_ /* message is ignored*/): + case .InvalidFeeRate(_ /* message is ignored*/): writeInt(&buf, Int32(47)) - case .LiquiditySourceUnavailable(_ /* message is ignored*/): + case .DuplicatePayment(_ /* message is ignored*/): writeInt(&buf, Int32(48)) - case .LiquidityFeeTooHigh(_ /* message is ignored*/): + case .UnsupportedCurrency(_ /* message is ignored*/): writeInt(&buf, Int32(49)) + case .InsufficientFunds(_ /* message is ignored*/): + writeInt(&buf, Int32(50)) + case .LiquiditySourceUnavailable(_ /* message is ignored*/): + writeInt(&buf, Int32(51)) + case .LiquidityFeeTooHigh(_ /* message is ignored*/): + writeInt(&buf, Int32(52)) } @@ -5118,6 +6990,7 @@ public enum PaymentFailureReason { case unknownRequiredFeatures case invoiceRequestExpired case invoiceRequestRejected + case blindedPathCreationFailed } @@ -5146,6 +7019,8 @@ public struct FfiConverterTypePaymentFailureReason: FfiConverterRustBuffer { case 9: return .invoiceRequestRejected + case 10: return .blindedPathCreationFailed + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -5189,6 +7064,10 @@ public struct FfiConverterTypePaymentFailureReason: FfiConverterRustBuffer { case .invoiceRequestRejected: writeInt(&buf, Int32(9)) + + case .blindedPathCreationFailed: + writeInt(&buf, Int32(10)) + } } } @@ -5213,10 +7092,11 @@ extension PaymentFailureReason: Equatable, Hashable {} public enum PaymentKind { - case onchain + case onchain(txid: Txid, status: ConfirmationStatus + ) case bolt11(hash: PaymentHash, preimage: PaymentPreimage?, secret: PaymentSecret? ) - case bolt11Jit(hash: PaymentHash, preimage: PaymentPreimage?, secret: PaymentSecret?, lspFeeLimits: LspFeeLimits + case bolt11Jit(hash: PaymentHash, preimage: PaymentPreimage?, secret: PaymentSecret?, counterpartySkimmedFeeMsat: UInt64?, lspFeeLimits: LspFeeLimits ) case bolt12Offer(hash: PaymentHash?, preimage: PaymentPreimage?, secret: PaymentSecret?, offerId: OfferId, payerNote: UntrustedString?, quantity: UInt64? ) @@ -5234,12 +7114,13 @@ public struct FfiConverterTypePaymentKind: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return .onchain + case 1: return .onchain(txid: try FfiConverterTypeTxid.read(from: &buf), status: try FfiConverterTypeConfirmationStatus.read(from: &buf) + ) case 2: return .bolt11(hash: try FfiConverterTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf) ) - case 3: return .bolt11Jit(hash: try FfiConverterTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf), lspFeeLimits: try FfiConverterTypeLSPFeeLimits.read(from: &buf) + case 3: return .bolt11Jit(hash: try FfiConverterTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf), counterpartySkimmedFeeMsat: try FfiConverterOptionUInt64.read(from: &buf), lspFeeLimits: try FfiConverterTypeLSPFeeLimits.read(from: &buf) ) case 4: return .bolt12Offer(hash: try FfiConverterOptionTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf), offerId: try FfiConverterTypeOfferId.read(from: &buf), payerNote: try FfiConverterOptionTypeUntrustedString.read(from: &buf), quantity: try FfiConverterOptionUInt64.read(from: &buf) @@ -5259,9 +7140,11 @@ public struct FfiConverterTypePaymentKind: FfiConverterRustBuffer { switch value { - case .onchain: + case let .onchain(txid,status): writeInt(&buf, Int32(1)) - + FfiConverterTypeTxid.write(txid, into: &buf) + FfiConverterTypeConfirmationStatus.write(status, into: &buf) + case let .bolt11(hash,preimage,secret): writeInt(&buf, Int32(2)) @@ -5270,11 +7153,12 @@ public struct FfiConverterTypePaymentKind: FfiConverterRustBuffer { FfiConverterOptionTypePaymentSecret.write(secret, into: &buf) - case let .bolt11Jit(hash,preimage,secret,lspFeeLimits): + case let .bolt11Jit(hash,preimage,secret,counterpartySkimmedFeeMsat,lspFeeLimits): writeInt(&buf, Int32(3)) FfiConverterTypePaymentHash.write(hash, into: &buf) FfiConverterOptionTypePaymentPreimage.write(preimage, into: &buf) FfiConverterOptionTypePaymentSecret.write(secret, into: &buf) + FfiConverterOptionUInt64.write(counterpartySkimmedFeeMsat, into: &buf) FfiConverterTypeLSPFeeLimits.write(lspFeeLimits, into: &buf) @@ -5321,6 +7205,68 @@ extension PaymentKind: Equatable, Hashable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum PaymentState { + + case expectPayment + case paid + case refunded +} + + +public struct FfiConverterTypePaymentState: FfiConverterRustBuffer { + typealias SwiftType = PaymentState + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PaymentState { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .expectPayment + + case 2: return .paid + + case 3: return .refunded + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: PaymentState, into buf: inout [UInt8]) { + switch value { + + + case .expectPayment: + writeInt(&buf, Int32(1)) + + + case .paid: + writeInt(&buf, Int32(2)) + + + case .refunded: + writeInt(&buf, Int32(3)) + + } + } +} + + +public func FfiConverterTypePaymentState_lift(_ buf: RustBuffer) throws -> PaymentState { + return try FfiConverterTypePaymentState.lift(buf) +} + +public func FfiConverterTypePaymentState_lower(_ value: PaymentState) -> RustBuffer { + return FfiConverterTypePaymentState.lower(value) +} + + + +extension PaymentState: Equatable, Hashable {} + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -5731,6 +7677,27 @@ fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionTypeFeeRate: FfiConverterRustBuffer { + typealias SwiftType = FeeRate? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeFeeRate.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeFeeRate.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeAnchorChannelsConfig: FfiConverterRustBuffer { typealias SwiftType = AnchorChannelsConfig? @@ -5752,6 +7719,48 @@ fileprivate struct FfiConverterOptionTypeAnchorChannelsConfig: FfiConverterRustB } } +fileprivate struct FfiConverterOptionTypeBackgroundSyncConfig: FfiConverterRustBuffer { + typealias SwiftType = BackgroundSyncConfig? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeBackgroundSyncConfig.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeBackgroundSyncConfig.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +fileprivate struct FfiConverterOptionTypeBolt11PaymentInfo: FfiConverterRustBuffer { + typealias SwiftType = Bolt11PaymentInfo? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeBolt11PaymentInfo.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeBolt11PaymentInfo.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeChannelConfig: FfiConverterRustBuffer { typealias SwiftType = ChannelConfig? @@ -5794,6 +7803,27 @@ fileprivate struct FfiConverterOptionTypeChannelInfo: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionTypeChannelOrderInfo: FfiConverterRustBuffer { + typealias SwiftType = ChannelOrderInfo? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeChannelOrderInfo.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeChannelOrderInfo.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeChannelUpdateInfo: FfiConverterRustBuffer { typealias SwiftType = ChannelUpdateInfo? @@ -5815,6 +7845,27 @@ fileprivate struct FfiConverterOptionTypeChannelUpdateInfo: FfiConverterRustBuff } } +fileprivate struct FfiConverterOptionTypeElectrumSyncConfig: FfiConverterRustBuffer { + typealias SwiftType = ElectrumSyncConfig? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeElectrumSyncConfig.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeElectrumSyncConfig.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeEsploraSyncConfig: FfiConverterRustBuffer { typealias SwiftType = EsploraSyncConfig? @@ -5851,14 +7902,35 @@ fileprivate struct FfiConverterOptionTypeNodeAnnouncementInfo: FfiConverterRustB public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { switch try readInt(&buf) as Int8 { case 0: return nil - case 1: return try FfiConverterTypeNodeAnnouncementInfo.read(from: &buf) + case 1: return try FfiConverterTypeNodeAnnouncementInfo.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +fileprivate struct FfiConverterOptionTypeNodeInfo: FfiConverterRustBuffer { + typealias SwiftType = NodeInfo? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeNodeInfo.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeNodeInfo.read(from: &buf) default: throw UniffiInternalError.unexpectedOptionalTag } } } -fileprivate struct FfiConverterOptionTypeNodeInfo: FfiConverterRustBuffer { - typealias SwiftType = NodeInfo? +fileprivate struct FfiConverterOptionTypeOnchainPaymentInfo: FfiConverterRustBuffer { + typealias SwiftType = OnchainPaymentInfo? public static func write(_ value: SwiftType, into buf: inout [UInt8]) { guard let value = value else { @@ -5866,13 +7938,13 @@ fileprivate struct FfiConverterOptionTypeNodeInfo: FfiConverterRustBuffer { return } writeInt(&buf, Int8(1)) - FfiConverterTypeNodeInfo.write(value, into: &buf) + FfiConverterTypeOnchainPaymentInfo.write(value, into: &buf) } public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { switch try readInt(&buf) as Int8 { case 0: return nil - case 1: return try FfiConverterTypeNodeInfo.read(from: &buf) + case 1: return try FfiConverterTypeOnchainPaymentInfo.read(from: &buf) default: throw UniffiInternalError.unexpectedOptionalTag } } @@ -5983,6 +8055,27 @@ fileprivate struct FfiConverterOptionTypeEvent: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionTypeLogLevel: FfiConverterRustBuffer { + typealias SwiftType = LogLevel? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeLogLevel.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeLogLevel.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeMaxTotalRoutingFeeLimit: FfiConverterRustBuffer { typealias SwiftType = MaxTotalRoutingFeeLimit? @@ -6046,6 +8139,27 @@ fileprivate struct FfiConverterOptionSequenceTypeSocketAddress: FfiConverterRust } } +fileprivate struct FfiConverterOptionTypeAddress: FfiConverterRustBuffer { + typealias SwiftType = Address? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeAddress.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeAddress.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeChannelId: FfiConverterRustBuffer { typealias SwiftType = ChannelId? @@ -6214,6 +8328,27 @@ fileprivate struct FfiConverterOptionTypeUntrustedString: FfiConverterRustBuffer } } +fileprivate struct FfiConverterOptionTypeUserChannelId: FfiConverterRustBuffer { + typealias SwiftType = UserChannelId? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeUserChannelId.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeUserChannelId.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterSequenceUInt8: FfiConverterRustBuffer { typealias SwiftType = [UInt8] @@ -6280,6 +8415,28 @@ fileprivate struct FfiConverterSequenceTypeChannelDetails: FfiConverterRustBuffe } } +fileprivate struct FfiConverterSequenceTypeCustomTlvRecord: FfiConverterRustBuffer { + typealias SwiftType = [CustomTlvRecord] + + public static func write(_ value: [CustomTlvRecord], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeCustomTlvRecord.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [CustomTlvRecord] { + let len: Int32 = try readInt(&buf) + var seq = [CustomTlvRecord]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeCustomTlvRecord.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypePaymentDetails: FfiConverterRustBuffer { typealias SwiftType = [PaymentDetails] @@ -6324,6 +8481,28 @@ fileprivate struct FfiConverterSequenceTypePeerDetails: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterSequenceTypeRouteHintHop: FfiConverterRustBuffer { + typealias SwiftType = [RouteHintHop] + + public static func write(_ value: [RouteHintHop], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeRouteHintHop.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [RouteHintHop] { + let len: Int32 = try readInt(&buf) + var seq = [RouteHintHop]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeRouteHintHop.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypeLightningBalance: FfiConverterRustBuffer { typealias SwiftType = [LightningBalance] @@ -6368,6 +8547,50 @@ fileprivate struct FfiConverterSequenceTypePendingSweepBalance: FfiConverterRust } } +fileprivate struct FfiConverterSequenceSequenceTypeRouteHintHop: FfiConverterRustBuffer { + typealias SwiftType = [[RouteHintHop]] + + public static func write(_ value: [[RouteHintHop]], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterSequenceTypeRouteHintHop.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [[RouteHintHop]] { + let len: Int32 = try readInt(&buf) + var seq = [[RouteHintHop]]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterSequenceTypeRouteHintHop.read(from: &buf)) + } + return seq + } +} + +fileprivate struct FfiConverterSequenceTypeAddress: FfiConverterRustBuffer { + typealias SwiftType = [Address] + + public static func write(_ value: [Address], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeAddress.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [Address] { + let len: Int32 = try readInt(&buf) + var seq = [Address]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeAddress.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypeNodeId: FfiConverterRustBuffer { typealias SwiftType = [NodeId] @@ -6530,32 +8753,32 @@ public func FfiConverterTypeBlockHash_lower(_ value: BlockHash) -> RustBuffer { * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. */ -public typealias Bolt11Invoice = String -public struct FfiConverterTypeBolt11Invoice: FfiConverter { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11Invoice { +public typealias Bolt12Invoice = String +public struct FfiConverterTypeBolt12Invoice: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt12Invoice { return try FfiConverterString.read(from: &buf) } - public static func write(_ value: Bolt11Invoice, into buf: inout [UInt8]) { + public static func write(_ value: Bolt12Invoice, into buf: inout [UInt8]) { return FfiConverterString.write(value, into: &buf) } - public static func lift(_ value: RustBuffer) throws -> Bolt11Invoice { + public static func lift(_ value: RustBuffer) throws -> Bolt12Invoice { return try FfiConverterString.lift(value) } - public static func lower(_ value: Bolt11Invoice) -> RustBuffer { + public static func lower(_ value: Bolt12Invoice) -> RustBuffer { return FfiConverterString.lower(value) } } -public func FfiConverterTypeBolt11Invoice_lift(_ value: RustBuffer) throws -> Bolt11Invoice { - return try FfiConverterTypeBolt11Invoice.lift(value) +public func FfiConverterTypeBolt12Invoice_lift(_ value: RustBuffer) throws -> Bolt12Invoice { + return try FfiConverterTypeBolt12Invoice.lift(value) } -public func FfiConverterTypeBolt11Invoice_lower(_ value: Bolt11Invoice) -> RustBuffer { - return FfiConverterTypeBolt11Invoice.lower(value) +public func FfiConverterTypeBolt12Invoice_lower(_ value: Bolt12Invoice) -> RustBuffer { + return FfiConverterTypeBolt12Invoice.lower(value) } @@ -6564,32 +8787,32 @@ public func FfiConverterTypeBolt11Invoice_lower(_ value: Bolt11Invoice) -> RustB * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. */ -public typealias Bolt12Invoice = String -public struct FfiConverterTypeBolt12Invoice: FfiConverter { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt12Invoice { +public typealias ChannelId = String +public struct FfiConverterTypeChannelId: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ChannelId { return try FfiConverterString.read(from: &buf) } - public static func write(_ value: Bolt12Invoice, into buf: inout [UInt8]) { + public static func write(_ value: ChannelId, into buf: inout [UInt8]) { return FfiConverterString.write(value, into: &buf) } - public static func lift(_ value: RustBuffer) throws -> Bolt12Invoice { + public static func lift(_ value: RustBuffer) throws -> ChannelId { return try FfiConverterString.lift(value) } - public static func lower(_ value: Bolt12Invoice) -> RustBuffer { + public static func lower(_ value: ChannelId) -> RustBuffer { return FfiConverterString.lower(value) } } -public func FfiConverterTypeBolt12Invoice_lift(_ value: RustBuffer) throws -> Bolt12Invoice { - return try FfiConverterTypeBolt12Invoice.lift(value) +public func FfiConverterTypeChannelId_lift(_ value: RustBuffer) throws -> ChannelId { + return try FfiConverterTypeChannelId.lift(value) } -public func FfiConverterTypeBolt12Invoice_lower(_ value: Bolt12Invoice) -> RustBuffer { - return FfiConverterTypeBolt12Invoice.lower(value) +public func FfiConverterTypeChannelId_lower(_ value: ChannelId) -> RustBuffer { + return FfiConverterTypeChannelId.lower(value) } @@ -6598,32 +8821,32 @@ public func FfiConverterTypeBolt12Invoice_lower(_ value: Bolt12Invoice) -> RustB * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. */ -public typealias ChannelId = String -public struct FfiConverterTypeChannelId: FfiConverter { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ChannelId { +public typealias DateTime = String +public struct FfiConverterTypeDateTime: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DateTime { return try FfiConverterString.read(from: &buf) } - public static func write(_ value: ChannelId, into buf: inout [UInt8]) { + public static func write(_ value: DateTime, into buf: inout [UInt8]) { return FfiConverterString.write(value, into: &buf) } - public static func lift(_ value: RustBuffer) throws -> ChannelId { + public static func lift(_ value: RustBuffer) throws -> DateTime { return try FfiConverterString.lift(value) } - public static func lower(_ value: ChannelId) -> RustBuffer { + public static func lower(_ value: DateTime) -> RustBuffer { return FfiConverterString.lower(value) } } -public func FfiConverterTypeChannelId_lift(_ value: RustBuffer) throws -> ChannelId { - return try FfiConverterTypeChannelId.lift(value) +public func FfiConverterTypeDateTime_lift(_ value: RustBuffer) throws -> DateTime { + return try FfiConverterTypeDateTime.lift(value) } -public func FfiConverterTypeChannelId_lower(_ value: ChannelId) -> RustBuffer { - return FfiConverterTypeChannelId.lower(value) +public func FfiConverterTypeDateTime_lower(_ value: DateTime) -> RustBuffer { + return FfiConverterTypeDateTime.lower(value) } @@ -6798,6 +9021,40 @@ public func FfiConverterTypeOfferId_lower(_ value: OfferId) -> RustBuffer { +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + */ +public typealias OrderId = String +public struct FfiConverterTypeOrderId: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OrderId { + return try FfiConverterString.read(from: &buf) + } + + public static func write(_ value: OrderId, into buf: inout [UInt8]) { + return FfiConverterString.write(value, into: &buf) + } + + public static func lift(_ value: RustBuffer) throws -> OrderId { + return try FfiConverterString.lift(value) + } + + public static func lower(_ value: OrderId) -> RustBuffer { + return FfiConverterString.lower(value) + } +} + + +public func FfiConverterTypeOrderId_lift(_ value: RustBuffer) throws -> OrderId { + return try FfiConverterTypeOrderId.lift(value) +} + +public func FfiConverterTypeOrderId_lower(_ value: OrderId) -> RustBuffer { + return FfiConverterTypeOrderId.lower(value) +} + + + /** * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. @@ -7216,40 +9473,88 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_func_generate_entropy_mnemonic() != 59926) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_bolt11invoice_amount_milli_satoshis() != 50823) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_currency() != 32179) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_expiry_time_seconds() != 23625) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_fallback_addresses() != 55276) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_invoice_description() != 395) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_is_expired() != 15932) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_min_final_cltv_expiry_delta() != 8855) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_network() != 10420) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_payment_hash() != 42571) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_payment_secret() != 26081) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_recover_payee_pub_key() != 18874) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_route_hints() != 63051) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_seconds_since_epoch() != 53979) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_seconds_until_expiry() != 64193) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_signable_hash() != 30910) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_would_expire() != 30331) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_bolt11payment_claim_for_hash() != 52848) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt11payment_fail_for_hash() != 24516) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 28084) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 6073) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 3869) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 27050) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 51453) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 4893) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 21975) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 1402) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 58617) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 24506) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 50555) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 16532) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send() != 39133) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send() != 63952) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes() != 39625) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes() != 969) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes_using_amount() != 25010) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes_using_amount() != 50136) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_using_amount() != 19557) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_using_amount() != 36530) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt12payment_initiate_refund() != 38039) { @@ -7285,12 +9590,21 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_builder_build_with_vss_store_and_header_provider() != 9090) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_announcement_addresses() != 39271) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_chain_source_bitcoind_rpc() != 2111) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_chain_source_electrum() != 55552) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_chain_source_esplora() != 1781) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_custom_logger() != 51232) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_entropy_bip39_mnemonic() != 827) { return InitializationResult.apiChecksumMismatch } @@ -7300,18 +9614,27 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_builder_set_entropy_seed_path() != 64056) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_filesystem_logger() != 10249) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_gossip_source_p2p() != 9279) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_builder_set_gossip_source_rgs() != 64312) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_builder_set_liquidity_source_lsps2() != 2667) { + if (uniffi_ldk_node_checksum_method_builder_set_liquidity_source_lsps1() != 51527) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_builder_set_liquidity_source_lsps2() != 14430) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_builder_set_listening_addresses() != 14051) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_log_facade_logger() != 58410) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_network() != 27539) { return InitializationResult.apiChecksumMismatch } @@ -7321,6 +9644,24 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_builder_set_storage_dir_path() != 59019) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_feerate_to_sat_per_kwu() != 58911) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_feerate_to_sat_per_vb_ceil() != 58575) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_feerate_to_sat_per_vb_floor() != 59617) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_lsps1liquidity_check_order_status() != 64731) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_lsps1liquidity_request_channel() != 18153) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_logwriter_log() != 3299) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_networkgraph_channel() != 38070) { return InitializationResult.apiChecksumMismatch } @@ -7333,6 +9674,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_networkgraph_node() != 48925) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_node_announcement_addresses() != 61426) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_node_bolt11_payment() != 41402) { return InitializationResult.apiChecksumMismatch } @@ -7351,7 +9695,10 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_node_disconnect() != 43538) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_node_event_handled() != 47939) { + if (uniffi_ldk_node_checksum_method_node_event_handled() != 38712) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_node_export_pathfinding_scores() != 62331) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_node_force_close_channel() != 48831) { @@ -7372,6 +9719,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_node_listening_addresses() != 2665) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_node_lsps1_liquidity() != 38201) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_node_network_graph() != 2695) { return InitializationResult.apiChecksumMismatch } @@ -7435,10 +9785,10 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_onchainpayment_new_address() != 37251) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_onchainpayment_send_all_to_address() != 20046) { + if (uniffi_ldk_node_checksum_method_onchainpayment_send_all_to_address() != 37748) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_onchainpayment_send_to_address() != 55731) { + if (uniffi_ldk_node_checksum_method_onchainpayment_send_to_address() != 55646) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_spontaneouspayment_send() != 48210) { @@ -7447,6 +9797,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_spontaneouspayment_send_probes() != 25937) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_spontaneouspayment_send_with_custom_tlvs() != 2376) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_unifiedqrpayment_receive() != 913) { return InitializationResult.apiChecksumMismatch } @@ -7456,13 +9809,23 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_vssheaderprovider_get_headers() != 7788) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_constructor_bolt11invoice_from_str() != 349) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_constructor_builder_from_config() != 994) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_constructor_builder_new() != 40499) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_constructor_feerate_from_sat_per_kwu() != 50548) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_constructor_feerate_from_sat_per_vb_unchecked() != 41808) { + return InitializationResult.apiChecksumMismatch + } + uniffiCallbackInitLogWriter() return InitializationResult.ok } diff --git a/bindings/uniffi-bindgen/Cargo.toml b/bindings/uniffi-bindgen/Cargo.toml index 9a4c9d5daa..a33c0f9ae0 100644 --- a/bindings/uniffi-bindgen/Cargo.toml +++ b/bindings/uniffi-bindgen/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -uniffi = { version = "0.27.3", features = ["cli"] } +uniffi = { version = "0.28.3", features = ["cli"] } diff --git a/docker-compose-cln.yml b/docker-compose-cln.yml index 5fb1f2dcd0..e1fb117e57 100644 --- a/docker-compose-cln.yml +++ b/docker-compose-cln.yml @@ -1,8 +1,6 @@ -version: '3' - services: bitcoin: - image: blockstream/bitcoind:24.1 + image: blockstream/bitcoind:27.2 platform: linux/amd64 command: [ @@ -27,14 +25,13 @@ services: retries: 5 electrs: - image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931 + image: mempool/electrs:v3.2.0 platform: linux/amd64 depends_on: bitcoin: condition: service_healthy command: [ - "/app/electrs_bitcoin/bin/electrs", "-vvvv", "--timestamp", "--jsonrpc-import", diff --git a/docker-compose-lnd.yml b/docker-compose-lnd.yml new file mode 100755 index 0000000000..8b44aba2d2 --- /dev/null +++ b/docker-compose-lnd.yml @@ -0,0 +1,86 @@ +services: + bitcoin: + image: blockstream/bitcoind:27.2 + platform: linux/amd64 + command: + [ + "bitcoind", + "-printtoconsole", + "-regtest=1", + "-rpcallowip=0.0.0.0/0", + "-rpcbind=0.0.0.0", + "-rpcuser=user", + "-rpcpassword=pass", + "-fallbackfee=0.00001", + "-zmqpubrawblock=tcp://0.0.0.0:28332", + "-zmqpubrawtx=tcp://0.0.0.0:28333" + ] + ports: + - "18443:18443" # Regtest RPC port + - "18444:18444" # Regtest P2P port + - "28332:28332" # ZMQ block port + - "28333:28333" # ZMQ tx port + networks: + - bitcoin-electrs + healthcheck: + test: ["CMD", "bitcoin-cli", "-regtest", "-rpcuser=user", "-rpcpassword=pass", "getblockchaininfo"] + interval: 5s + timeout: 10s + retries: 5 + + electrs: + image: mempool/electrs:v3.2.0 + platform: linux/amd64 + depends_on: + bitcoin: + condition: service_healthy + command: + [ + "-vvvv", + "--timestamp", + "--jsonrpc-import", + "--cookie=user:pass", + "--network=regtest", + "--daemon-rpc-addr=bitcoin:18443", + "--http-addr=0.0.0.0:3002", + "--electrum-rpc-addr=0.0.0.0:50001" + ] + ports: + - "3002:3002" + - "50001:50001" + networks: + - bitcoin-electrs + + lnd: + image: lightninglabs/lnd:v0.18.5-beta + container_name: ldk-node-lnd + depends_on: + - bitcoin + volumes: + - ${LND_DATA_DIR}:/root/.lnd + ports: + - "8081:8081" + - "9735:9735" + command: + - "--noseedbackup" + - "--trickledelay=5000" + - "--alias=ldk-node-lnd-test" + - "--externalip=lnd:9735" + - "--bitcoin.active" + - "--bitcoin.regtest" + - "--bitcoin.node=bitcoind" + - "--bitcoind.rpchost=bitcoin:18443" + - "--bitcoind.rpcuser=user" + - "--bitcoind.rpcpass=pass" + - "--bitcoind.zmqpubrawblock=tcp://bitcoin:28332" + - "--bitcoind.zmqpubrawtx=tcp://bitcoin:28333" + - "--accept-keysend" + - "--rpclisten=0.0.0.0:8081" + - "--tlsextradomain=lnd" + - "--tlsextraip=0.0.0.0" + networks: + - bitcoin-electrs + +networks: + bitcoin-electrs: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 3c3233bff6..e71fd70fba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: bitcoin: - image: blockstream/bitcoind:24.1 + image: blockstream/bitcoind:27.2 platform: linux/amd64 command: [ @@ -13,10 +13,11 @@ services: "-rpcbind=0.0.0.0", "-rpcuser=user", "-rpcpassword=pass", - "-fallbackfee=0.00001" + "-fallbackfee=0.00001", + "-rest" ] ports: - - "18443:18443" # Regtest RPC port + - "18443:18443" # Regtest REST and RPC port - "18444:18444" # Regtest P2P port networks: - bitcoin-electrs @@ -27,14 +28,13 @@ services: retries: 5 electrs: - image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931 + image: mempool/electrs:v3.2.0 platform: linux/amd64 depends_on: bitcoin: condition: service_healthy command: [ - "/app/electrs_bitcoin/bin/electrs", "-vvvv", "--timestamp", "--jsonrpc-import", diff --git a/rustfmt.toml b/rustfmt.toml index 4f88472bec..66161555ca 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -10,3 +10,5 @@ match_block_trailing_comma = true # UNSTABLE: format_macro_matchers = true # UNSTABLE: format_strings = true # UNSTABLE: group_imports = "StdExternalCrate" +# UNSTABLE: reorder_imports = true +# UNSTABLE: imports_granularity = "Module" diff --git a/scripts/download_bitcoind_electrs.sh b/scripts/download_bitcoind_electrs.sh index fcdb31891e..47a95332ea 100755 --- a/scripts/download_bitcoind_electrs.sh +++ b/scripts/download_bitcoind_electrs.sh @@ -1,3 +1,6 @@ +#!/bin/bash +set -eox pipefail + # Our Esplora-based tests require `electrs` and `bitcoind` # binaries. Here, we download the binaries, validate them, and export their # location via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the @@ -7,19 +10,20 @@ HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" ELECTRS_DL_ENDPOINT="https://github.com/RCasatta/electrsd/releases/download/electrs_releases" ELECTRS_VERSION="esplora_a33e97e1a1fc63fa9c20a116bb92579bbf43b254" BITCOIND_DL_ENDPOINT="https://bitcoincore.org/bin/" -BITCOIND_VERSION="25.1" +BITCOIND_VERSION="27.2" if [[ "$HOST_PLATFORM" == *linux* ]]; then ELECTRS_DL_FILE_NAME=electrs_linux_"$ELECTRS_VERSION".zip ELECTRS_DL_HASH="865e26a96e8df77df01d96f2f569dcf9622fc87a8d99a9b8fe30861a4db9ddf1" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-linux-gnu.tar.gz - BITCOIND_DL_HASH="a978c407b497a727f0444156e397b50491ce862d1f906fef9b521415b3611c8b" + BITCOIND_DL_HASH="acc223af46c178064c132b235392476f66d486453ddbd6bca6f1f8411547da78" elif [[ "$HOST_PLATFORM" == *darwin* ]]; then ELECTRS_DL_FILE_NAME=electrs_macos_"$ELECTRS_VERSION".zip ELECTRS_DL_HASH="2d5ff149e8a2482d3658e9b386830dfc40c8fbd7c175ca7cbac58240a9505bcd" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-apple-darwin.tar.gz - BITCOIND_DL_HASH="1acfde0ec3128381b83e3e5f54d1c7907871d324549129592144dd12a821eff1" + BITCOIND_DL_HASH="6ebc56ca1397615d5a6df2b5cf6727b768e3dcac320c2d5c2f321dcaabc7efa2" else - echo "\n\nUnsupported platform: $HOST_PLATFORM Exiting.." + printf "\n\n" + echo "Unsupported platform: $HOST_PLATFORM Exiting.." exit 1 fi diff --git a/scripts/generate_checksum_files.sh b/scripts/generate_checksum_files.sh new file mode 100644 index 0000000000..bbfa41a9a2 --- /dev/null +++ b/scripts/generate_checksum_files.sh @@ -0,0 +1,5 @@ +#!/bin/bash +md5sum $1 | cut -d ' ' -f 1 > $1.md5 +sha1sum $1 | cut -d ' ' -f 1 > $1.sha1 +sha256sum $1 | cut -d ' ' -f 1 > $1.sha256 +sha512sum $1 | cut -d ' ' -f 1 > $1.sha512 diff --git a/scripts/uniffi_bindgen_generate_kotlin_android.sh b/scripts/uniffi_bindgen_generate_kotlin_android.sh index 142e5f75d9..c87db59242 100755 --- a/scripts/uniffi_bindgen_generate_kotlin_android.sh +++ b/scripts/uniffi_bindgen_generate_kotlin_android.sh @@ -35,9 +35,9 @@ case "$OSTYPE" in PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$LLVM_ARCH_PATH/bin:$PATH" rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi -CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target x86_64-linux-android || exit 1 -CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --profile release-smaller --features uniffi --target armv7-linux-androideabi || exit 1 -CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target aarch64-linux-android || exit 1 +RUSTFLAGS="-C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target x86_64-linux-android || exit 1 +RUSTFLAGS="-C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --profile release-smaller --features uniffi --target armv7-linux-androideabi || exit 1 +RUSTFLAGS="-C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target aarch64-linux-android || exit 1 $UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language kotlin --config uniffi-android.toml -o "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/kotlin || exit 1 JNI_LIB_DIR="$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/jniLibs/ diff --git a/scripts/uniffi_bindgen_generate_swift.sh b/scripts/uniffi_bindgen_generate_swift.sh index ba2d84d2ab..ce1151a3bd 100755 --- a/scripts/uniffi_bindgen_generate_swift.sh +++ b/scripts/uniffi_bindgen_generate_swift.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -eox pipefail + BINDINGS_DIR="./bindings/swift" UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml" @@ -8,11 +10,11 @@ $UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language swift -o "$BINDING mkdir -p $BINDINGS_DIR # Install rust target toolchains -rustup install 1.73.0 -rustup component add rust-src --toolchain 1.73.0 -rustup target add aarch64-apple-ios x86_64-apple-ios --toolchain 1.73.0 -rustup target add aarch64-apple-ios-sim --toolchain 1.73.0 -rustup target add aarch64-apple-darwin x86_64-apple-darwin --toolchain 1.73.0 +rustup upgrade stable +rustup component add rust-src --toolchain stable +rustup target add aarch64-apple-ios x86_64-apple-ios --toolchain stable +rustup target add aarch64-apple-ios-sim --toolchain stable +rustup target add aarch64-apple-darwin x86_64-apple-darwin --toolchain stable # Build rust target libs cargo build --profile release-smaller --features uniffi || exit 1 @@ -20,7 +22,7 @@ cargo build --profile release-smaller --features uniffi --target x86_64-apple-da cargo build --profile release-smaller --features uniffi --target aarch64-apple-darwin || exit 1 cargo build --profile release-smaller --features uniffi --target x86_64-apple-ios || exit 1 cargo build --profile release-smaller --features uniffi --target aarch64-apple-ios || exit 1 -cargo +1.73.0 build --release --features uniffi --target aarch64-apple-ios-sim || exit 1 +cargo +stable build --release --features uniffi --target aarch64-apple-ios-sim || exit 1 # Combine ios-sim and apple-darwin (macos) libs for x86_64 and aarch64 (m1) mkdir -p target/lipo-ios-sim/release-smaller || exit 1 diff --git a/src/balance.rs b/src/balance.rs index b5e2f5eb71..d96278daeb 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -5,18 +5,14 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::sweep::value_from_descriptor; - -use lightning::chain::channelmonitor::Balance as LdkBalance; -use lightning::chain::channelmonitor::BalanceSource; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Amount, BlockHash, Txid}; +use lightning::chain::channelmonitor::{Balance as LdkBalance, BalanceSource}; use lightning::ln::types::ChannelId; +use lightning::sign::SpendableOutputDescriptor; use lightning::util::sweep::{OutputSpendStatus, TrackedSpendableOutput}; - use lightning_types::payment::{PaymentHash, PaymentPreimage}; -use bitcoin::secp256k1::PublicKey; -use bitcoin::{BlockHash, Txid}; - /// Details of the known available balances returned by [`Node::list_balances`]. /// /// [`Node::list_balances`]: crate::Node::list_balances @@ -74,7 +70,8 @@ pub struct BalanceDetails { pub enum LightningBalance { /// The channel is not yet closed (or the commitment or closing transaction has not yet /// appeared in a block). The given balance is claimable (less on-chain fees) if the channel is - /// force-closed now. + /// force-closed now. Values do not take into account any pending splices and are only based + /// on the confirmed state of the channel. ClaimableOnChannelClose { /// The identifier of the channel this balance belongs to. channel_id: ChannelId, @@ -225,21 +222,26 @@ impl LightningBalance { ) -> Self { match balance { LdkBalance::ClaimableOnChannelClose { - amount_satoshis, - transaction_fee_satoshis, - outbound_payment_htlc_rounded_msat, - outbound_forwarded_htlc_rounded_msat, - inbound_claiming_htlc_rounded_msat, - inbound_htlc_rounded_msat, - } => Self::ClaimableOnChannelClose { - channel_id, - counterparty_node_id, - amount_satoshis, - transaction_fee_satoshis, + balance_candidates, + confirmed_balance_candidate_index, outbound_payment_htlc_rounded_msat, outbound_forwarded_htlc_rounded_msat, inbound_claiming_htlc_rounded_msat, inbound_htlc_rounded_msat, + } => { + // unwrap safety: confirmed_balance_candidate_index is guaranteed to index into balance_candidates + let balance = balance_candidates.get(confirmed_balance_candidate_index).unwrap(); + + Self::ClaimableOnChannelClose { + channel_id, + counterparty_node_id, + amount_satoshis: balance.amount_satoshis, + transaction_fee_satoshis: balance.transaction_fee_satoshis, + outbound_payment_htlc_rounded_msat, + outbound_forwarded_htlc_rounded_msat, + inbound_claiming_htlc_rounded_msat, + inbound_htlc_rounded_msat, + } }, LdkBalance::ClaimableAwaitingConfirmations { amount_satoshis, @@ -385,3 +387,11 @@ impl PendingSweepBalance { } } } + +fn value_from_descriptor(descriptor: &SpendableOutputDescriptor) -> Amount { + match &descriptor { + SpendableOutputDescriptor::StaticOutput { output, .. } => output.value, + SpendableOutputDescriptor::DelayedPaymentOutput(output) => output.output.value, + SpendableOutputDescriptor::StaticPaymentOutput(output) => output.output.value, + } +} diff --git a/src/builder.rs b/src/builder.rs index 72e2ef4584..bd53f8a957 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,35 +5,20 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::chain::{ChainSource, DEFAULT_ESPLORA_SERVER_URL}; -use crate::config::{ - default_user_config, Config, EsploraSyncConfig, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, - WALLET_KEYS_SEED_LEN, -}; - -use crate::connection::ConnectionManager; -use crate::event::EventQueue; -use crate::fee_estimator::OnchainFeeEstimator; -use crate::gossip::GossipSource; -use crate::io::sqlite_store::SqliteStore; -use crate::io::utils::{read_node_metrics, write_node_metrics}; -use crate::io::vss_store::VssStore; -use crate::liquidity::LiquiditySourceBuilder; -use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; -use crate::message_handler::NodeCustomMessageHandler; -use crate::payment::store::PaymentStore; -use crate::peer_store::PeerStore; -use crate::prober::ProbabilisticScoringParameters; -use crate::tx_broadcaster::TransactionBroadcaster; -use crate::types::{ - ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter, - OnionMessenger, PeerManager, -}; -use crate::wallet::persist::KVStoreWalletPersister; -use crate::wallet::Wallet; -use crate::Node; -use crate::{io, NodeMetrics}; +use std::collections::HashMap; +use std::convert::TryInto; +use std::default::Default; +use std::path::PathBuf; +use std::sync::{Arc, Mutex, Once, RwLock}; +use std::time::SystemTime; +use std::{fmt, fs}; +use bdk_wallet::template::Bip84; +use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; +use bip39::Mnemonic; +use bitcoin::bip32::{ChildNumber, Xpriv}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{BlockHash, Network}; use lightning::chain::{chainmonitor, BestBlock, Watch}; use lightning::io::Cursor; use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; @@ -44,42 +29,72 @@ use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ ProbabilisticScorer, ProbabilisticScoringDecayParameters, ProbabilisticScoringFeeParameters, }; -use lightning::sign::EntropySource; - +use lightning::sign::{EntropySource, NodeSigner}; use lightning::util::persist::{ - read_channel_monitors, CHANNEL_MANAGER_PERSISTENCE_KEY, + read_channel_monitors, KVStoreSync, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::ReadableArgs; use lightning::util::sweep::OutputSweeper; - use lightning_persister::fs_store::FilesystemStore; +use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; -use bdk_wallet::template::Bip84; -use bdk_wallet::KeychainKind; -use bdk_wallet::Wallet as BdkWallet; - -use bip39::Mnemonic; - -use bitcoin::secp256k1::PublicKey; -use bitcoin::{BlockHash, Network}; +use crate::chain::ChainSource; +use crate::config::{ + default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, + BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, + DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, +}; +use crate::connection::ConnectionManager; +use crate::event::EventQueue; +use crate::fee_estimator::OnchainFeeEstimator; +use crate::gossip::GossipSource; +use crate::io::sqlite_store::SqliteStore; +use crate::io::utils::{read_node_metrics, write_node_metrics}; +use crate::io::vss_store::VssStore; +use crate::io::{ + self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use crate::liquidity::{ + LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, +}; +use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; +use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +use crate::peer_store::PeerStore; +use crate::prober::ProbabilisticScoringParameters; +use crate::runtime::Runtime; +use crate::tx_broadcaster::TransactionBroadcaster; +use crate::types::{ + ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter, + OnionMessenger, PaymentStore, PeerManager, +}; +use crate::wallet::persist::KVStoreWalletPersister; +use crate::wallet::Wallet; +use crate::{Node, NodeMetrics}; -use bitcoin::bip32::{ChildNumber, Xpriv}; -use std::collections::HashMap; -use std::convert::TryInto; -use std::default::Default; -use std::fmt; -use std::fs; -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::SystemTime; -use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; +const VSS_HARDENED_CHILD_INDEX: u32 = 877; +const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; +const LSPS_HARDENED_CHILD_INDEX: u32 = 577; #[derive(Debug, Clone)] enum ChainDataSourceConfig { - Esplora { server_url: String, sync_config: Option }, - BitcoindRpc { rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String }, + Esplora { + server_url: String, + headers: HashMap, + sync_config: Option, + }, + Electrum { + server_url: String, + sync_config: Option, + }, + Bitcoind { + rpc_host: String, + rpc_port: u16, + rpc_user: String, + rpc_password: String, + rest_client_config: Option, + }, } #[derive(Debug, Clone)] @@ -95,24 +110,20 @@ enum GossipSourceConfig { RapidGossipSync(String), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] struct LiquiditySourceConfig { - // LSPS1 service's (node_id, address, token) - lsps1_service: Option<(PublicKey, SocketAddress, Option)>, - // LSPS2 service's (node_id, address, token) - lsps2_service: Option<(PublicKey, SocketAddress, Option)>, -} - -impl Default for LiquiditySourceConfig { - fn default() -> Self { - Self { lsps1_service: None, lsps2_service: None } - } + // Act as an LSPS1 client connecting to the given service. + lsps1_client: Option, + // Act as an LSPS2 client connecting to the given service. + lsps2_client: Option, + // Act as an LSPS2 service. + lsps2_service: Option, } #[derive(Clone)] enum LogWriterConfig { File { log_file_path: Option, max_log_level: Option }, - Log { max_log_level: Option }, + Log, Custom(Arc), } @@ -124,9 +135,7 @@ impl std::fmt::Debug for LogWriterConfig { .field("max_log_level", max_log_level) .field("log_file_path", log_file_path) .finish(), - LogWriterConfig::Log { max_log_level } => { - f.debug_tuple("Log").field(max_log_level).finish() - }, + LogWriterConfig::Log => write!(f, "LogWriterConfig::Log"), LogWriterConfig::Custom(_) => { f.debug_tuple("Custom").field(&"").finish() }, @@ -149,26 +158,34 @@ pub enum BuildError { InvalidChannelMonitor, /// The given listening addresses are invalid, e.g. too many were passed. InvalidListeningAddresses, + /// The given announcement addresses are invalid, e.g. too many were passed. + InvalidAnnouncementAddresses, /// The provided alias is invalid. InvalidNodeAlias, + /// An attempt to setup a runtime has failed. + RuntimeSetupFailed, /// We failed to read data from the [`KVStore`]. /// - /// [`KVStore`]: lightning::util::persist::KVStore + /// [`KVStore`]: lightning::util::persist::KVStoreSync ReadFailed, /// We failed to write data to the [`KVStore`]. /// - /// [`KVStore`]: lightning::util::persist::KVStore + /// [`KVStore`]: lightning::util::persist::KVStoreSync WriteFailed, /// We failed to access the given `storage_dir_path`. StoragePathAccessFailed, /// We failed to setup our [`KVStore`]. /// - /// [`KVStore`]: lightning::util::persist::KVStore + /// [`KVStore`]: lightning::util::persist::KVStoreSync KVStoreSetupFailed, /// We failed to setup the onchain wallet. WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// The given network does not match the node's previously configured network. + NetworkMismatch, + /// The role of the node in an asynchronous payments context is not compatible with the current configuration. + AsyncPaymentsConfigMismatch, } impl fmt::Display for BuildError { @@ -183,6 +200,10 @@ impl fmt::Display for BuildError { write!(f, "Failed to watch a deserialized ChannelMonitor") }, Self::InvalidListeningAddresses => write!(f, "Given listening addresses are invalid."), + Self::InvalidAnnouncementAddresses => { + write!(f, "Given announcement addresses are invalid.") + }, + Self::RuntimeSetupFailed => write!(f, "Failed to setup a runtime."), Self::ReadFailed => write!(f, "Failed to read from store."), Self::WriteFailed => write!(f, "Failed to write to store."), Self::StoragePathAccessFailed => write!(f, "Failed to access the given storage path."), @@ -190,6 +211,15 @@ impl fmt::Display for BuildError { Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."), Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."), Self::InvalidNodeAlias => write!(f, "Given node alias is invalid."), + Self::NetworkMismatch => { + write!(f, "Given network does not match the node's previously configured network.") + }, + Self::AsyncPaymentsConfigMismatch => { + write!( + f, + "The async payments role is not compatible with the current configuration." + ) + }, } } } @@ -211,6 +241,8 @@ pub struct NodeBuilder { gossip_source_config: Option, liquidity_source_config: Option, log_writer_config: Option, + async_payments_role: Option, + runtime_handle: Option, } impl NodeBuilder { @@ -227,6 +259,7 @@ impl NodeBuilder { let gossip_source_config = None; let liquidity_source_config = None; let log_writer_config = None; + let runtime_handle = None; Self { config, entropy_source_config, @@ -234,9 +267,21 @@ impl NodeBuilder { gossip_source_config, liquidity_source_config, log_writer_config, + runtime_handle, + async_payments_role: None, } } + /// Configures the [`Node`] instance to (re-)use a specific `tokio` runtime. + /// + /// If not provided, the node will spawn its own runtime or reuse any outer runtime context it + /// can detect. + #[cfg_attr(feature = "uniffi", allow(dead_code))] + pub fn set_runtime(&mut self, runtime_handle: tokio::runtime::Handle) -> &mut Self { + self.runtime_handle = Some(runtime_handle); + self + } + /// Configures the [`Node`] instance to source its wallet entropy from a seed file on disk. /// /// If the given file does not exist a new random seed file will be generated and @@ -246,15 +291,11 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to source its wallet entropy from the given 64 seed bytes. - pub fn set_entropy_seed_bytes(&mut self, seed_bytes: Vec) -> Result<&mut Self, BuildError> { - if seed_bytes.len() != WALLET_KEYS_SEED_LEN { - return Err(BuildError::InvalidSeedBytes); - } - let mut bytes = [0u8; WALLET_KEYS_SEED_LEN]; - bytes.copy_from_slice(&seed_bytes); - self.entropy_source_config = Some(EntropySourceConfig::SeedBytes(bytes)); - Ok(self) + /// Configures the [`Node`] instance to source its wallet entropy from the given + /// [`WALLET_KEYS_SEED_LEN`] seed bytes. + pub fn set_entropy_seed_bytes(&mut self, seed_bytes: [u8; WALLET_KEYS_SEED_LEN]) -> &mut Self { + self.entropy_source_config = Some(EntropySourceConfig::SeedBytes(seed_bytes)); + self } /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. @@ -274,19 +315,85 @@ impl NodeBuilder { /// information. pub fn set_chain_source_esplora( &mut self, server_url: String, sync_config: Option, + ) -> &mut Self { + self.chain_data_source_config = Some(ChainDataSourceConfig::Esplora { + server_url, + headers: Default::default(), + sync_config, + }); + self + } + + /// Configures the [`Node`] instance to source its chain data from the given Esplora server. + /// + /// The given `headers` will be included in all requests to the Esplora server, typically used for + /// authentication purposes. + /// + /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more + /// information. + pub fn set_chain_source_esplora_with_headers( + &mut self, server_url: String, headers: HashMap, + sync_config: Option, ) -> &mut Self { self.chain_data_source_config = - Some(ChainDataSourceConfig::Esplora { server_url, sync_config }); + Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }); self } - /// Configures the [`Node`] instance to source its chain data from the given Bitcoin Core RPC - /// endpoint. + /// Configures the [`Node`] instance to source its chain data from the given Electrum server. + /// + /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more + /// information. + pub fn set_chain_source_electrum( + &mut self, server_url: String, sync_config: Option, + ) -> &mut Self { + self.chain_data_source_config = + Some(ChainDataSourceConfig::Electrum { server_url, sync_config }); + self + } + + /// Configures the [`Node`] instance to connect to a Bitcoin Core node via RPC. + /// + /// This method establishes an RPC connection that enables all essential chain operations including + /// transaction broadcasting and chain data synchronization. + /// + /// ## Parameters: + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection. pub fn set_chain_source_bitcoind_rpc( &mut self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, ) -> &mut Self { - self.chain_data_source_config = - Some(ChainDataSourceConfig::BitcoindRpc { rpc_host, rpc_port, rpc_user, rpc_password }); + self.chain_data_source_config = Some(ChainDataSourceConfig::Bitcoind { + rpc_host, + rpc_port, + rpc_user, + rpc_password, + rest_client_config: None, + }); + self + } + + /// Configures the [`Node`] instance to synchronize chain data from a Bitcoin Core REST endpoint. + /// + /// This method enables chain data synchronization via Bitcoin Core's REST interface. We pass + /// additional RPC configuration to non-REST-supported API calls like transaction broadcasting. + /// + /// ## Parameters: + /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection. + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection + pub fn set_chain_source_bitcoind_rest( + &mut self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, + rpc_user: String, rpc_password: String, + ) -> &mut Self { + self.chain_data_source_config = Some(ChainDataSourceConfig::Bitcoind { + rpc_host, + rpc_port, + rpc_user, + rpc_password, + rest_client_config: Some(BitcoindRestClientConfig { rest_host, rest_port }), + }); + self } @@ -320,7 +427,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps1_service = Some((node_id, address, token)); + let lsps1_client_config = LSPS1ClientConfig { node_id, address, token }; + liquidity_source_config.lsps1_client = Some(lsps1_client_config); self } @@ -340,7 +448,23 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some((node_id, address, token)); + let lsps2_client_config = LSPS2ClientConfig { node_id, address, token }; + liquidity_source_config.lsps2_client = Some(lsps2_client_config); + self + } + + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2( + &mut self, service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + liquidity_source_config.lsps2_service = Some(service_config); self } @@ -367,11 +491,8 @@ impl NodeBuilder { } /// Configures the [`Node`] instance to write logs to the [`log`](https://crates.io/crates/log) facade. - /// - /// If set, the `max_log_level` sets the maximum log level. Otherwise, the latter defaults to - /// [`DEFAULT_LOG_LEVEL`]. - pub fn set_log_facade_logger(&mut self, max_log_level: Option) -> &mut Self { - self.log_writer_config = Some(LogWriterConfig::Log { max_log_level }); + pub fn set_log_facade_logger(&mut self) -> &mut Self { + self.log_writer_config = Some(LogWriterConfig::Log); self } @@ -399,6 +520,22 @@ impl NodeBuilder { Ok(self) } + /// Sets the IP address and TCP port which [`Node`] will announce to the gossip network that it accepts connections on. + /// + /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. + /// + /// [`listening_addresses`]: Self::set_listening_addresses + pub fn set_announcement_addresses( + &mut self, announcement_addresses: Vec, + ) -> Result<&mut Self, BuildError> { + if announcement_addresses.len() > 100 { + return Err(BuildError::InvalidAnnouncementAddresses); + } + + self.config.announcement_addresses = Some(announcement_addresses); + Ok(self) + } + /// Sets the node alias that will be used when broadcasting announcements to the gossip /// network. /// @@ -418,6 +555,21 @@ impl NodeBuilder { self } + /// Sets the role of the node in an asynchronous payments context. + /// + /// See for more information about the async payments protocol. + pub fn set_async_payments_role( + &mut self, role: Option, + ) -> Result<&mut Self, BuildError> { + if let Some(AsyncPaymentsRole::Server) = role { + may_announce_channel(&self.config) + .map_err(|_| BuildError::AsyncPaymentsConfigMismatch)?; + } + + self.async_payments_role = role; + Ok(self) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result { @@ -480,10 +632,14 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config, &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = + derive_xprv(config, &seed_bytes, VSS_HARDENED_CHILD_INDEX, Arc::clone(&logger))?; let lnurl_auth_xprv = vss_xprv - .derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 138 }]) + .derive_priv( + &Secp256k1::new(), + &[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }], + ) .map_err(|e| { log_error!(logger, "Failed to derive VSS secret: {}", e); BuildError::KVStoreSetupFailed @@ -537,6 +693,15 @@ impl NodeBuilder { ) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; + let runtime = if let Some(handle) = self.runtime_handle.as_ref() { + Arc::new(Runtime::with_handle(handle.clone(), Arc::clone(&logger))) + } else { + Arc::new(Runtime::new(Arc::clone(&logger)).map_err(|e| { + log_error!(logger, "Failed to setup tokio runtime: {}", e); + BuildError::RuntimeSetupFailed + })?) + }; + let seed_bytes = seed_bytes_from_config( &self.config, self.entropy_source_config.as_ref(), @@ -545,21 +710,25 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config.clone(), &seed_bytes, Arc::clone(&logger))?; + let vss_xprv = derive_xprv( + config.clone(), + &seed_bytes, + VSS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; let vss_seed_bytes: [u8; 32] = vss_xprv.private_key.secret_bytes(); let vss_store = - VssStore::new(vss_url, store_id, vss_seed_bytes, header_provider).map_err(|e| { - log_error!(logger, "Failed to setup VssStore: {}", e); - BuildError::KVStoreSetupFailed - })?; + VssStore::new(vss_url, store_id, vss_seed_bytes, header_provider, Arc::clone(&runtime)); build_with_store_internal( config, self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.async_payments_role, seed_bytes, + runtime, logger, Arc::new(vss_store), ) @@ -569,6 +738,15 @@ impl NodeBuilder { pub fn build_with_store(&self, kv_store: Arc) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; + let runtime = if let Some(handle) = self.runtime_handle.as_ref() { + Arc::new(Runtime::with_handle(handle.clone(), Arc::clone(&logger))) + } else { + Arc::new(Runtime::new(Arc::clone(&logger)).map_err(|e| { + log_error!(logger, "Failed to setup tokio runtime: {}", e); + BuildError::RuntimeSetupFailed + })?) + }; + let seed_bytes = seed_bytes_from_config( &self.config, self.entropy_source_config.as_ref(), @@ -581,7 +759,9 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.async_payments_role, seed_bytes, + runtime, logger, kv_store, ) @@ -623,11 +803,20 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_entropy_seed_path(seed_path); } - /// Configures the [`Node`] instance to source its wallet entropy from the given 64 seed bytes. + /// Configures the [`Node`] instance to source its wallet entropy from the given + /// [`WALLET_KEYS_SEED_LEN`] seed bytes. /// - /// **Note:** Panics if the length of the given `seed_bytes` differs from 64. + /// **Note:** Will return an error if the length of the given `seed_bytes` differs from + /// [`WALLET_KEYS_SEED_LEN`]. pub fn set_entropy_seed_bytes(&self, seed_bytes: Vec) -> Result<(), BuildError> { - self.inner.write().unwrap().set_entropy_seed_bytes(seed_bytes).map(|_| ()) + if seed_bytes.len() != WALLET_KEYS_SEED_LEN { + return Err(BuildError::InvalidSeedBytes); + } + let mut bytes = [0u8; WALLET_KEYS_SEED_LEN]; + bytes.copy_from_slice(&seed_bytes); + + self.inner.write().unwrap().set_entropy_seed_bytes(bytes); + Ok(()) } /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. @@ -647,8 +836,42 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_chain_source_esplora(server_url, sync_config); } - /// Configures the [`Node`] instance to source its chain data from the given Bitcoin Core RPC - /// endpoint. + /// Configures the [`Node`] instance to source its chain data from the given Esplora server. + /// + /// The given `headers` will be included in all requests to the Esplora server, typically used for + /// authentication purposes. + /// + /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more + /// information. + pub fn set_chain_source_esplora_with_headers( + &self, server_url: String, headers: HashMap, + sync_config: Option, + ) { + self.inner.write().unwrap().set_chain_source_esplora_with_headers( + server_url, + headers, + sync_config, + ); + } + + /// Configures the [`Node`] instance to source its chain data from the given Electrum server. + /// + /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more + /// information. + pub fn set_chain_source_electrum( + &self, server_url: String, sync_config: Option, + ) { + self.inner.write().unwrap().set_chain_source_electrum(server_url, sync_config); + } + + /// Configures the [`Node`] instance to connect to a Bitcoin Core node via RPC. + /// + /// This method establishes an RPC connection that enables all essential chain operations including + /// transaction broadcasting and chain data synchronization. + /// + /// ## Parameters: + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection. pub fn set_chain_source_bitcoind_rpc( &self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, ) { @@ -660,6 +883,29 @@ impl ArcedNodeBuilder { ); } + /// Configures the [`Node`] instance to synchronize chain data from a Bitcoin Core REST endpoint. + /// + /// This method enables chain data synchronization via Bitcoin Core's REST interface. We pass + /// additional RPC configuration to non-REST-supported API calls like transaction broadcasting. + /// + /// ## Parameters: + /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection. + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection + pub fn set_chain_source_bitcoind_rest( + &self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, + rpc_user: String, rpc_password: String, + ) { + self.inner.write().unwrap().set_chain_source_bitcoind_rest( + rest_host, + rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + ); + } + /// Configures the [`Node`] instance to source its gossip data from the Lightning peer-to-peer /// network. pub fn set_gossip_source_p2p(&self) { @@ -700,6 +946,16 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token); } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2(&self, service_config: LSPS2ServiceConfig) { + self.inner.write().unwrap().set_liquidity_provider_lsps2(service_config); + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&self, storage_dir_path: String) { self.inner.write().unwrap().set_storage_dir_path(storage_dir_path); @@ -721,11 +977,8 @@ impl ArcedNodeBuilder { } /// Configures the [`Node`] instance to write logs to the [`log`](https://crates.io/crates/log) facade. - /// - /// If set, the `max_log_level` sets the maximum log level. Otherwise, the latter defaults to - /// [`DEFAULT_LOG_LEVEL`]. - pub fn set_log_facade_logger(&self, log_level: Option) { - self.inner.write().unwrap().set_log_facade_logger(log_level); + pub fn set_log_facade_logger(&self) { + self.inner.write().unwrap().set_log_facade_logger(); } /// Configures the [`Node`] instance to write logs to the provided custom [`LogWriter`]. @@ -745,6 +998,17 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_listening_addresses(listening_addresses).map(|_| ()) } + /// Sets the IP address and TCP port which [`Node`] will announce to the gossip network that it accepts connections on. + /// + /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. + /// + /// [`listening_addresses`]: Self::set_listening_addresses + pub fn set_announcement_addresses( + &self, announcement_addresses: Vec, + ) -> Result<(), BuildError> { + self.inner.write().unwrap().set_announcement_addresses(announcement_addresses).map(|_| ()) + } + /// Sets the node alias that will be used when broadcasting announcements to the gossip /// network. /// @@ -753,6 +1017,13 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_node_alias(node_alias).map(|_| ()) } + /// Sets the role of the node in an asynchronous payments context. + pub fn set_async_payments_role( + &self, role: Option, + ) -> Result<(), BuildError> { + self.inner.write().unwrap().set_async_payments_role(role).map(|_| ()) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result, BuildError> { @@ -846,11 +1117,30 @@ impl ArcedNodeBuilder { fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, - liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], + liquidity_source_config: Option<&LiquiditySourceConfig>, + async_payments_role: Option, seed_bytes: [u8; 64], runtime: Arc, logger: Arc, kv_store: Arc, ) -> Result { + optionally_install_rustls_cryptoprovider(); + + if let Err(err) = may_announce_channel(&config) { + if config.announcement_addresses.is_some() { + log_error!(logger, "Announcement addresses were set but some required configuration options for node announcement are missing: {}", err); + let build_error = if matches!(err, AnnounceError::MissingNodeAlias) { + BuildError::InvalidNodeAlias + } else { + BuildError::InvalidListeningAddresses + }; + return Err(build_error); + } + + if config.node_alias.is_some() { + log_error!(logger, "Node alias was set but some required configuration options for node announcement are missing: {}", err); + return Err(BuildError::InvalidListeningAddresses); + } + } + // Initialize the status fields. - let is_listening = Arc::new(AtomicBool::new(false)); let node_metrics = match read_node_metrics(Arc::clone(&kv_store), Arc::clone(&logger)) { Ok(metrics) => Arc::new(RwLock::new(metrics)), Err(e) => { @@ -878,9 +1168,25 @@ fn build_with_store_internal( .extract_keys() .check_network(config.network) .load_wallet(&mut wallet_persister) - .map_err(|e| { - log_error!(logger, "Failed to set up wallet: {}", e); - BuildError::WalletSetupFailed + .map_err(|e| match e { + bdk_wallet::LoadWithPersistError::InvalidChangeSet( + bdk_wallet::LoadError::Mismatch(bdk_wallet::LoadMismatch::Network { + loaded, + expected, + }), + ) => { + log_error!( + logger, + "Failed to setup wallet: Networks do not match. Expected {} but got {}", + expected, + loaded + ); + BuildError::NetworkMismatch + }, + _ => { + log_error!(logger, "Failed to set up wallet: {}", e); + BuildError::WalletSetupFailed + }, })?; let bdk_wallet = match wallet_opt { Some(wallet) => wallet, @@ -897,9 +1203,13 @@ fn build_with_store_internal( let fee_estimator = Arc::new(OnchainFeeEstimator::new()); let payment_store = match io::utils::read_payments(Arc::clone(&kv_store), Arc::clone(&logger)) { - Ok(payments) => { - Arc::new(PaymentStore::new(payments, Arc::clone(&kv_store), Arc::clone(&logger))) - }, + Ok(payments) => Arc::new(PaymentStore::new( + payments, + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), + Arc::clone(&kv_store), + Arc::clone(&logger), + )), Err(_) => { return Err(BuildError::ReadFailed); }, @@ -911,13 +1221,29 @@ fn build_with_store_internal( Arc::clone(&tx_broadcaster), Arc::clone(&fee_estimator), Arc::clone(&payment_store), + Arc::clone(&config), Arc::clone(&logger), )); let chain_source = match chain_data_source_config { - Some(ChainDataSourceConfig::Esplora { server_url, sync_config }) => { + Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => { let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default()); Arc::new(ChainSource::new_esplora( + server_url.clone(), + headers.clone(), + sync_config, + Arc::clone(&wallet), + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + )) + }, + Some(ChainDataSourceConfig::Electrum { server_url, sync_config }) => { + let sync_config = sync_config.unwrap_or(ElectrumSyncConfig::default()); + Arc::new(ChainSource::new_electrum( server_url.clone(), sync_config, Arc::clone(&wallet), @@ -929,8 +1255,14 @@ fn build_with_store_internal( Arc::clone(&node_metrics), )) }, - Some(ChainDataSourceConfig::BitcoindRpc { rpc_host, rpc_port, rpc_user, rpc_password }) => { - Arc::new(ChainSource::new_bitcoind_rpc( + Some(ChainDataSourceConfig::Bitcoind { + rpc_host, + rpc_port, + rpc_user, + rpc_password, + rest_client_config, + }) => match rest_client_config { + Some(rest_client_config) => Arc::new(ChainSource::new_bitcoind_rest( rpc_host.clone(), *rpc_port, rpc_user.clone(), @@ -940,16 +1272,32 @@ fn build_with_store_internal( Arc::clone(&tx_broadcaster), Arc::clone(&kv_store), Arc::clone(&config), + rest_client_config.clone(), Arc::clone(&logger), Arc::clone(&node_metrics), - )) + )), + None => Arc::new(ChainSource::new_bitcoind_rpc( + rpc_host.clone(), + *rpc_port, + rpc_user.clone(), + rpc_password.clone(), + Arc::clone(&wallet), + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + )), }, + None => { // Default to Esplora client. let server_url = DEFAULT_ESPLORA_SERVER_URL.to_string(); let sync_config = EsploraSyncConfig::default(); Arc::new(ChainSource::new_esplora( server_url.clone(), + HashMap::new(), sync_config, Arc::clone(&wallet), Arc::clone(&fee_estimator), @@ -962,17 +1310,6 @@ fn build_with_store_internal( }, }; - let runtime = Arc::new(RwLock::new(None)); - - // Initialize the ChainMonitor - let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( - Some(Arc::clone(&chain_source)), - Arc::clone(&tx_broadcaster), - Arc::clone(&logger), - Arc::clone(&fee_estimator), - Arc::clone(&kv_store), - )); - // Initialize the KeysManager let cur_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_err(|e| { log_error!(logger, "Failed to get current time: {}", e); @@ -988,6 +1325,19 @@ fn build_with_store_internal( Arc::clone(&logger), )); + let peer_storage_key = keys_manager.get_peer_storage_key(); + + // Initialize the ChainMonitor + let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( + Some(Arc::clone(&chain_source)), + Arc::clone(&tx_broadcaster), + Arc::clone(&logger), + Arc::clone(&fee_estimator), + Arc::clone(&kv_store), + Arc::clone(&keys_manager), + peer_storage_key, + )); + // Initialize the network graph, scorer, and router let network_graph = match io::utils::read_network_graph(Arc::clone(&kv_store), Arc::clone(&logger)) { @@ -1048,24 +1398,38 @@ fn build_with_store_internal( }; let mut user_config = default_user_config(&config); + if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { - // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll - // check that they don't take too much before claiming. - user_config.channel_config.accept_underpaying_htlcs = true; + // If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the + // information to the service handler. + user_config.accept_intercept_htlcs = true; + + // If we act as an LSPS2 service, we allow forwarding to unnannounced channels. + user_config.accept_forwards_to_priv_channels = true; - // FIXME: When we're an LSPS2 client, set maximum allowed inbound HTLC value in flight - // to 100%. We should eventually be able to set this on a per-channel basis, but for - // now we just bump the default for all channels. + // If we act as an LSPS2 service, set the HTLC-value-in-flight to 100% of the channel value + // to ensure we can forward the initial payment. user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; } + if let Some(role) = async_payments_role { + match role { + AsyncPaymentsRole::Server => { + user_config.accept_forwards_to_priv_channels = true; + user_config.enable_htlc_hold = true; + }, + AsyncPaymentsRole::Client => user_config.hold_outbound_htlcs_at_next_hop = true, + } + } + let message_router = Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); // Initialize the ChannelManager let channel_manager = { - if let Ok(res) = kv_store.read( + if let Ok(res) = KVStoreSync::read( + &*kv_store, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_KEY, @@ -1122,25 +1486,40 @@ fn build_with_store_internal( // Give ChannelMonitors to ChainMonitor for (_blockhash, channel_monitor) in channel_monitors.into_iter() { - let funding_outpoint = channel_monitor.get_funding_txo().0; - chain_monitor.watch_channel(funding_outpoint, channel_monitor).map_err(|e| { + let channel_id = channel_monitor.channel_id(); + chain_monitor.watch_channel(channel_id, channel_monitor).map_err(|e| { log_error!(logger, "Failed to watch channel monitor: {:?}", e); BuildError::InvalidChannelMonitor })?; } // Initialize the PeerManager - let onion_messenger: Arc = Arc::new(OnionMessenger::new( - Arc::clone(&keys_manager), - Arc::clone(&keys_manager), - Arc::clone(&logger), - Arc::clone(&channel_manager), - message_router, - Arc::clone(&channel_manager), - IgnoringMessageHandler {}, - IgnoringMessageHandler {}, - IgnoringMessageHandler {}, - )); + let onion_messenger: Arc = + if let Some(AsyncPaymentsRole::Server) = async_payments_role { + Arc::new(OnionMessenger::new_with_offline_peer_interception( + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&logger), + Arc::clone(&channel_manager), + message_router, + Arc::clone(&channel_manager), + Arc::clone(&channel_manager), + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + )) + } else { + Arc::new(OnionMessenger::new( + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&logger), + Arc::clone(&channel_manager), + message_router, + Arc::clone(&channel_manager), + Arc::clone(&channel_manager), + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + )) + }; let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); // Initialize the GossipSource @@ -1180,31 +1559,56 @@ fn build_with_store_internal( }, }; - let liquidity_source = liquidity_source_config.as_ref().map(|lsc| { - let mut liquidity_source_builder = LiquiditySourceBuilder::new( - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - Arc::clone(&chain_source), - Arc::clone(&config), - Arc::clone(&logger), - ); - - lsc.lsps1_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone()) - }); + let (liquidity_source, custom_message_handler) = + if let Some(lsc) = liquidity_source_config.as_ref() { + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&wallet), + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&chain_source), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + ); - lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone()) - }); + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - Arc::new(liquidity_source_builder.build()) - }); + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() { - Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))) - } else { - Arc::new(NodeCustomMessageHandler::new_ignoring()) - }; + let promise_secret = { + let lsps_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + LSPS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + lsps_xpriv.private_key.secret_bytes() + }; + lsc.lsps2_service.as_ref().map(|config| { + liquidity_source_builder.lsps2_service(promise_secret, config.clone()) + }); + + let liquidity_source = runtime + .block_on(async move { liquidity_source_builder.build().await.map(Arc::new) })?; + let custom_message_handler = + Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); + (Some(liquidity_source), custom_message_handler) + } else { + (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) + }; let msg_handler = match gossip_source.as_gossip_sync() { GossipSync::P2P(p2p_gossip_sync) => MessageHandler { @@ -1213,6 +1617,7 @@ fn build_with_store_internal( as Arc, onion_message_handler: Arc::clone(&onion_messenger), custom_message_handler, + send_only_message_handler: Arc::clone(&chain_monitor), }, GossipSync::Rapid(_) => MessageHandler { chan_handler: Arc::clone(&channel_manager), @@ -1220,6 +1625,7 @@ fn build_with_store_internal( as Arc, onion_message_handler: Arc::clone(&onion_messenger), custom_message_handler, + send_only_message_handler: Arc::clone(&chain_monitor), }, GossipSync::None => { unreachable!("We must always have a gossip sync!"); @@ -1280,20 +1686,6 @@ fn build_with_store_internal( }, }; - match io::utils::migrate_deprecated_spendable_outputs( - Arc::clone(&output_sweeper), - Arc::clone(&kv_store), - Arc::clone(&logger), - ) { - Ok(()) => { - log_info!(logger, "Successfully migrated OutputSweeper data."); - }, - Err(e) => { - log_error!(logger, "Failed to migrate OutputSweeper data: {}", e); - return Err(BuildError::ReadFailed); - }, - } - let event_queue = match io::utils::read_event_queue(Arc::clone(&kv_store), Arc::clone(&logger)) { Ok(event_queue) => Arc::new(event_queue), @@ -1317,13 +1709,20 @@ fn build_with_store_internal( }, }; + let om_mailbox = if let Some(AsyncPaymentsRole::Server) = async_payments_role { + Some(Arc::new(OnionMessageMailbox::new())) + } else { + None + }; + let (stop_sender, _) = tokio::sync::watch::channel(()); - let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let (background_processor_stop_sender, _) = tokio::sync::watch::channel(()); + let is_running = Arc::new(RwLock::new(false)); Ok(Node { runtime, stop_sender, - event_handling_stopped_sender, + background_processor_stop_sender, config, wallet, chain_source, @@ -1345,11 +1744,32 @@ fn build_with_store_internal( scorer, peer_store, payment_store, - is_listening, + is_running, node_metrics, + om_mailbox, + async_payments_role, }) } +fn optionally_install_rustls_cryptoprovider() { + // Acquire a global Mutex, ensuring that only one process at a time install the provider. This + // is mostly required for running tests concurrently. + static INIT_CRYPTO: Once = Once::new(); + + INIT_CRYPTO.call_once(|| { + // Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now. + if rustls::crypto::CryptoProvider::get_default().is_none() { + let _ = rustls::crypto::ring::default_provider().install_default(); + } + + // Refuse to startup without TLS support. Better to catch it now than even later at runtime. + assert!( + rustls::crypto::CryptoProvider::get_default().is_some(), + "We need to have a CryptoProvider" + ); + }); +} + /// Sets up the node logger. fn setup_logger( log_writer_config: &Option, config: &Config, @@ -1364,10 +1784,7 @@ fn setup_logger( Logger::new_fs_writer(log_file_path, max_log_level) .map_err(|_| BuildError::LoggerSetupFailed)? }, - Some(LogWriterConfig::Log { max_log_level }) => { - let max_log_level = max_log_level.unwrap_or_else(|| DEFAULT_LOG_LEVEL); - Logger::new_log_facade(max_log_level) - }, + Some(LogWriterConfig::Log) => Logger::new_log_facade(), Some(LogWriterConfig::Custom(custom_log_writer)) => { Logger::new_custom_writer(Arc::clone(&custom_log_writer)) @@ -1406,8 +1823,8 @@ fn seed_bytes_from_config( } } -fn derive_vss_xprv( - config: Arc, seed_bytes: &[u8; 64], logger: Arc, +fn derive_xprv( + config: Arc, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc, ) -> Result { use bitcoin::key::Secp256k1; @@ -1416,10 +1833,11 @@ fn derive_vss_xprv( BuildError::InvalidSeedBytes })?; - xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 877 }]).map_err(|e| { - log_error!(logger, "Failed to derive VSS secret: {}", e); - BuildError::KVStoreSetupFailed - }) + xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }]) + .map_err(|e| { + log_error!(logger, "Failed to derive hardened child secret: {}", e); + BuildError::InvalidSeedBytes + }) } /// Sanitize the user-provided node alias to ensure that it is a valid protocol-specified UTF-8 string. diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs new file mode 100644 index 0000000000..e97546e887 --- /dev/null +++ b/src/chain/bitcoind.rs @@ -0,0 +1,1578 @@ +// 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 std::collections::{HashMap, VecDeque}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use bitcoin::{BlockHash, FeeRate, Network, Transaction, Txid}; +use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; +use lightning::chain::Listen; +use lightning::util::ser::Writeable; +use lightning_block_sync::gossip::UtxoSource; +use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; +use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; +use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; +use lightning_block_sync::rest::RestClient; +use lightning_block_sync::rpc::{RpcClient, RpcError}; +use lightning_block_sync::{ + AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceErrorKind, Cache, + SpvClient, +}; +use serde::Serialize; + +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; +use crate::config::{ + BitcoindRestClientConfig, Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, +}; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + ConfirmationTarget, OnchainFeeEstimator, +}; +use crate::io::utils::write_node_metrics; +use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::{Error, NodeMetrics}; + +const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; + +pub(super) struct BitcoindChainSource { + api_client: Arc, + header_cache: tokio::sync::Mutex, + latest_chain_tip: RwLock>, + onchain_wallet: Arc, + wallet_polling_status: Mutex, + fee_estimator: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl BitcoindChainSource { + pub(crate) fn new_rpc( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let api_client = Arc::new(BitcoindClient::new_rpc( + rpc_host.clone(), + rpc_port.clone(), + rpc_user.clone(), + rpc_password.clone(), + )); + + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); + let latest_chain_tip = RwLock::new(None); + let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); + Self { + api_client, + header_cache, + latest_chain_tip, + onchain_wallet, + wallet_polling_status, + fee_estimator, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(crate) fn new_rest( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + kv_store: Arc, config: Arc, rest_client_config: BitcoindRestClientConfig, + logger: Arc, node_metrics: Arc>, + ) -> Self { + let api_client = Arc::new(BitcoindClient::new_rest( + rest_client_config.rest_host, + rest_client_config.rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + )); + + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); + let latest_chain_tip = RwLock::new(None); + let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); + + Self { + api_client, + header_cache, + latest_chain_tip, + wallet_polling_status, + onchain_wallet, + fee_estimator, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(super) fn as_utxo_source(&self) -> Arc { + self.api_client.utxo_source() + } + + pub(super) async fn continuously_sync_wallets( + &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) { + // First register for the wallet polling status to make sure `Node::sync_wallets` calls + // wait on the result before proceeding. + { + let mut status_lock = self.wallet_polling_status.lock().unwrap(); + if status_lock.register_or_subscribe_pending_sync().is_some() { + debug_assert!(false, "Sync already in progress. This should never happen."); + } + } + + log_info!( + self.logger, + "Starting initial synchronization of chain listeners. This might take a while..", + ); + + let mut backoff = CHAIN_POLLING_INTERVAL_SECS; + const MAX_BACKOFF_SECS: u64 = 300; + + loop { + let channel_manager_best_block_hash = channel_manager.current_best_block().block_hash; + let sweeper_best_block_hash = output_sweeper.current_best_block().block_hash; + let onchain_wallet_best_block_hash = + self.onchain_wallet.current_best_block().block_hash; + + let mut chain_listeners = vec![ + ( + onchain_wallet_best_block_hash, + &*self.onchain_wallet as &(dyn Listen + Send + Sync), + ), + (channel_manager_best_block_hash, &*channel_manager as &(dyn Listen + Send + Sync)), + (sweeper_best_block_hash, &*output_sweeper as &(dyn Listen + Send + Sync)), + ]; + + // TODO: Eventually we might want to see if we can synchronize `ChannelMonitor`s + // before giving them to `ChainMonitor` it the first place. However, this isn't + // trivial as we load them on initialization (in the `Builder`) and only gain + // network access during `start`. For now, we just make sure we get the worst known + // block hash and sychronize them via `ChainMonitor`. + if let Some(worst_channel_monitor_block_hash) = chain_monitor + .list_monitors() + .iter() + .flat_map(|channel_id| chain_monitor.get_monitor(*channel_id)) + .map(|m| m.current_best_block()) + .min_by_key(|b| b.height) + .map(|b| b.block_hash) + { + chain_listeners.push(( + worst_channel_monitor_block_hash, + &*chain_monitor as &(dyn Listen + Send + Sync), + )); + } + + let mut locked_header_cache = self.header_cache.lock().await; + let now = SystemTime::now(); + match synchronize_listeners( + self.api_client.as_ref(), + self.config.network, + &mut *locked_header_cache, + chain_listeners.clone(), + ) + .await + { + Ok(chain_tip) => { + { + log_info!( + self.logger, + "Finished synchronizing listeners in {}ms", + now.elapsed().unwrap().as_millis() + ); + *self.latest_chain_tip.write().unwrap() = Some(chain_tip); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + locked_node_metrics.latest_onchain_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to persist node metrics: {}", e); + }); + } + break; + }, + + Err(e) => { + log_error!(self.logger, "Failed to synchronize chain listeners: {:?}", e); + if e.kind() == BlockSourceErrorKind::Transient { + log_info!( + self.logger, + "Transient error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + backoff + ); + tokio::time::sleep(Duration::from_secs(backoff)).await; + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF_SECS); + } else { + log_error!( + self.logger, + "Persistent error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + MAX_BACKOFF_SECS + ); + tokio::time::sleep(Duration::from_secs(MAX_BACKOFF_SECS)).await; + } + }, + } + } + + // Now propagate the initial result to unblock waiting subscribers. + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(Ok(())); + + let mut chain_polling_interval = + tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); + chain_polling_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let mut fee_rate_update_interval = + tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); + // When starting up, we just blocked on updating, so skip the first tick. + fee_rate_update_interval.reset(); + fee_rate_update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + log_info!(self.logger, "Starting continuous polling for chain updates."); + + // Start the polling loop. + let mut last_best_block_hash = None; + loop { + tokio::select! { + _ = stop_sync_receiver.changed() => { + log_trace!( + self.logger, + "Stopping polling for new chain data.", + ); + return; + } + _ = chain_polling_interval.tick() => { + let _ = self.poll_and_update_listeners( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&output_sweeper) + ).await; + } + _ = fee_rate_update_interval.tick() => { + if last_best_block_hash != Some(channel_manager.current_best_block().block_hash) { + let update_res = self.update_fee_rate_estimates().await; + if update_res.is_ok() { + last_best_block_hash = Some(channel_manager.current_best_block().block_hash); + } + } + } + } + } + } + + pub(super) async fn poll_and_update_listeners( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.wallet_polling_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet polling result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet polling result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let res = self + .poll_and_update_listeners_inner(channel_manager, chain_monitor, output_sweeper) + .await; + + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn poll_and_update_listeners_inner( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let latest_chain_tip_opt = self.latest_chain_tip.read().unwrap().clone(); + let chain_tip = if let Some(tip) = latest_chain_tip_opt { + tip + } else { + match validate_best_block_header(self.api_client.as_ref()).await { + Ok(tip) => { + *self.latest_chain_tip.write().unwrap() = Some(tip); + tip + }, + Err(e) => { + log_error!(self.logger, "Failed to poll for chain data: {:?}", e); + return Err(Error::TxSyncFailed); + }, + } + }; + + let mut locked_header_cache = self.header_cache.lock().await; + let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network); + let chain_listener = ChainListener { + onchain_wallet: Arc::clone(&self.onchain_wallet), + channel_manager: Arc::clone(&channel_manager), + chain_monitor: Arc::clone(&chain_monitor), + output_sweeper, + }; + let mut spv_client = + SpvClient::new(chain_tip, chain_poller, &mut *locked_header_cache, &chain_listener); + + let now = SystemTime::now(); + match spv_client.poll_best_tip().await { + Ok((ChainTip::Better(tip), true)) => { + log_trace!( + self.logger, + "Finished polling best tip in {}ms", + now.elapsed().unwrap().as_millis() + ); + *self.latest_chain_tip.write().unwrap() = Some(tip); + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + chain_monitor, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + }, + Ok(_) => {}, + Err(e) => { + log_error!(self.logger, "Failed to poll for chain data: {:?}", e); + return Err(Error::TxSyncFailed); + }, + } + + let cur_height = channel_manager.current_best_block().height; + + let now = SystemTime::now(); + let bdk_unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids(); + match self + .api_client + .get_updated_mempool_transactions(cur_height, bdk_unconfirmed_txids) + .await + { + Ok((unconfirmed_txs, evicted_txids)) => { + log_trace!( + self.logger, + "Finished polling mempool of size {} and {} evicted transactions in {}ms", + unconfirmed_txs.len(), + evicted_txids.len(), + now.elapsed().unwrap().as_millis() + ); + self.onchain_wallet + .apply_mempool_txs(unconfirmed_txs, evicted_txids) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to apply mempool transactions: {:?}", e); + }); + }, + Err(e) => { + log_error!(self.logger, "Failed to poll for mempool transactions: {:?}", e); + return Err(Error::TxSyncFailed); + }, + } + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; + locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; + + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + + Ok(()) + } + + pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + macro_rules! get_fee_rate_update { + ($estimation_fut: expr) => {{ + let update_res = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + $estimation_fut, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })?; + update_res + }}; + } + let confirmation_targets = get_all_conf_targets(); + + let mut new_fee_rate_cache = HashMap::with_capacity(10); + let now = Instant::now(); + for target in confirmation_targets { + let fee_rate_update_res = match target { + ConfirmationTarget::Lightning( + LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee, + ) => { + let estimation_fut = self.api_client.get_mempool_minimum_fee_rate(); + get_fee_rate_update!(estimation_fut) + }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::MaximumFeeEstimate) => { + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Conservative; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::UrgentOnChainSweep) => { + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Conservative; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + _ => { + // Otherwise, we default to economical block-target estimate. + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Economical; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + }; + + let fee_rate = match (fee_rate_update_res, self.config.network) { + (Ok(rate), _) => rate, + (Err(e), Network::Bitcoin) => { + // Strictly fail on mainnet. + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + return Err(Error::FeerateEstimationUpdateFailed); + }, + (Err(e), n) if n == Network::Regtest || n == Network::Signet => { + // On regtest/signet we just fall back to the usual 1 sat/vb == 250 + // sat/kwu default. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: {}. Falling back to default of 1 sat/vb.", + e, + ); + FeeRate::from_sat_per_kwu(250) + }, + (Err(e), _) => { + // On testnet `estimatesmartfee` can be unreliable so we just skip in + // case of a failure, which will have us falling back to defaults. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: {}. Falling back to defaults.", + e, + ); + return Ok(()); + }, + }; + + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); + + new_fee_rate_cache.insert(target, adjusted_fee_rate); + + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } + + if self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache) { + // We only log if the values changed, as it might be very spammy otherwise. + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + } + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + Ok(()) + } + + pub(crate) async fn process_broadcast_package(&self, package: Vec) { + // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 + // features, we should eventually switch to use `submitpackage` via the + // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual + // transactions. + for tx in &package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.api_client.broadcast_transaction(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(id) => { + debug_assert_eq!(id, txid); + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => { + log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + } + } + } +} + +pub enum BitcoindClient { + Rpc { + rpc_client: Arc, + latest_mempool_timestamp: AtomicU64, + mempool_entries_cache: tokio::sync::Mutex>, + mempool_txs_cache: tokio::sync::Mutex>, + }, + Rest { + rest_client: Arc, + rpc_client: Arc, + latest_mempool_timestamp: AtomicU64, + mempool_entries_cache: tokio::sync::Mutex>, + mempool_txs_cache: tokio::sync::Mutex>, + }, +} + +impl BitcoindClient { + /// Creates a new RPC API client for the chain interactions with Bitcoin Core. + pub(crate) fn new_rpc(host: String, port: u16, rpc_user: String, rpc_password: String) -> Self { + let http_endpoint = endpoint(host, port); + let rpc_credentials = rpc_credentials(rpc_user, rpc_password); + + let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, http_endpoint)); + + let latest_mempool_timestamp = AtomicU64::new(0); + + let mempool_entries_cache = tokio::sync::Mutex::new(HashMap::new()); + let mempool_txs_cache = tokio::sync::Mutex::new(HashMap::new()); + Self::Rpc { rpc_client, latest_mempool_timestamp, mempool_entries_cache, mempool_txs_cache } + } + + /// Creates a new, primarily REST API client for the chain interactions + /// with Bitcoin Core. + /// + /// Aside the required REST host and port, we provide RPC configuration + /// options for necessary calls not supported by the REST interface. + pub(crate) fn new_rest( + rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, + rpc_password: String, + ) -> Self { + let rest_endpoint = endpoint(rest_host, rest_port).with_path("/rest".to_string()); + let rest_client = Arc::new(RestClient::new(rest_endpoint)); + + let rpc_endpoint = endpoint(rpc_host, rpc_port); + let rpc_credentials = rpc_credentials(rpc_user, rpc_password); + let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, rpc_endpoint)); + + let latest_mempool_timestamp = AtomicU64::new(0); + + let mempool_entries_cache = tokio::sync::Mutex::new(HashMap::new()); + let mempool_txs_cache = tokio::sync::Mutex::new(HashMap::new()); + + Self::Rest { + rest_client, + rpc_client, + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + } + } + + pub(crate) fn utxo_source(&self) -> Arc { + match self { + BitcoindClient::Rpc { rpc_client, .. } => Arc::clone(rpc_client) as Arc, + BitcoindClient::Rest { rest_client, .. } => { + Arc::clone(rest_client) as Arc + }, + } + } + + /// Broadcasts the provided transaction. + pub(crate) async fn broadcast_transaction(&self, tx: &Transaction) -> std::io::Result { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::broadcast_transaction_inner(Arc::clone(rpc_client), tx).await + }, + BitcoindClient::Rest { rpc_client, .. } => { + // Bitcoin Core's REST interface does not support broadcasting transactions + // so we use the RPC client. + Self::broadcast_transaction_inner(Arc::clone(rpc_client), tx).await + }, + } + } + + async fn broadcast_transaction_inner( + rpc_client: Arc, tx: &Transaction, + ) -> std::io::Result { + let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx); + let tx_json = serde_json::json!(tx_serialized); + rpc_client.call_method::("sendrawtransaction", &[tx_json]).await + } + + /// Retrieve the fee estimate needed for a transaction to begin + /// confirmation within the provided `num_blocks`. + pub(crate) async fn get_fee_estimate_for_target( + &self, num_blocks: usize, estimation_mode: FeeRateEstimationMode, + ) -> std::io::Result { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_fee_estimate_for_target_inner( + Arc::clone(rpc_client), + num_blocks, + estimation_mode, + ) + .await + }, + BitcoindClient::Rest { rpc_client, .. } => { + // We rely on the internal RPC client to make this call, as this + // operation is not supported by Bitcoin Core's REST interface. + Self::get_fee_estimate_for_target_inner( + Arc::clone(rpc_client), + num_blocks, + estimation_mode, + ) + .await + }, + } + } + + /// Estimate the fee rate for the provided target number of blocks. + async fn get_fee_estimate_for_target_inner( + rpc_client: Arc, num_blocks: usize, estimation_mode: FeeRateEstimationMode, + ) -> std::io::Result { + let num_blocks_json = serde_json::json!(num_blocks); + let estimation_mode_json = serde_json::json!(estimation_mode); + rpc_client + .call_method::( + "estimatesmartfee", + &[num_blocks_json, estimation_mode_json], + ) + .await + .map(|resp| resp.0) + } + + /// Gets the mempool minimum fee rate. + pub(crate) async fn get_mempool_minimum_fee_rate(&self) -> std::io::Result { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_mempool_minimum_fee_rate_rpc(Arc::clone(rpc_client)).await + }, + BitcoindClient::Rest { rest_client, .. } => { + Self::get_mempool_minimum_fee_rate_rest(Arc::clone(rest_client)).await + }, + } + } + + /// Get the mempool minimum fee rate via RPC interface. + async fn get_mempool_minimum_fee_rate_rpc( + rpc_client: Arc, + ) -> std::io::Result { + rpc_client + .call_method::("getmempoolinfo", &[]) + .await + .map(|resp| resp.0) + } + + /// Get the mempool minimum fee rate via REST interface. + async fn get_mempool_minimum_fee_rate_rest( + rest_client: Arc, + ) -> std::io::Result { + rest_client + .request_resource::("mempool/info.json") + .await + .map(|resp| resp.0) + } + + /// Gets the raw transaction for the provided transaction ID. Returns `None` if not found. + pub(crate) async fn get_raw_transaction( + &self, txid: &Txid, + ) -> std::io::Result> { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_raw_transaction_rpc(Arc::clone(rpc_client), txid).await + }, + BitcoindClient::Rest { rest_client, .. } => { + Self::get_raw_transaction_rest(Arc::clone(rest_client), txid).await + }, + } + } + + /// Retrieve raw transaction for provided transaction ID via the RPC interface. + async fn get_raw_transaction_rpc( + rpc_client: Arc, txid: &Txid, + ) -> std::io::Result> { + let txid_hex = txid.to_string(); + let txid_json = serde_json::json!(txid_hex); + match rpc_client + .call_method::("getrawtransaction", &[txid_json]) + .await + { + Ok(resp) => Ok(Some(resp.0)), + Err(e) => match e.into_inner() { + Some(inner) => { + let rpc_error_res: Result, _> = inner.downcast(); + + match rpc_error_res { + Ok(rpc_error) => { + // Check if it's the 'not found' error code. + if rpc_error.code == -5 { + Ok(None) + } else { + Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) + } + }, + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to process getrawtransaction response", + )), + } + }, + None => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to process getrawtransaction response", + )), + }, + } + } + + /// Retrieve raw transaction for provided transaction ID via the REST interface. + async fn get_raw_transaction_rest( + rest_client: Arc, txid: &Txid, + ) -> std::io::Result> { + let txid_hex = txid.to_string(); + let tx_path = format!("tx/{}.json", txid_hex); + match rest_client + .request_resource::(&tx_path) + .await + { + Ok(resp) => Ok(Some(resp.0)), + Err(e) => match e.kind() { + std::io::ErrorKind::Other => { + match e.into_inner() { + Some(inner) => { + let http_error_res: Result, _> = inner.downcast(); + match http_error_res { + Ok(http_error) => { + // Check if it's the HTTP NOT_FOUND error code. + if &http_error.status_code == "404" { + Ok(None) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + http_error, + )) + } + }, + Err(_) => { + let error_msg = + format!("Failed to process {} response.", tx_path); + Err(std::io::Error::new( + std::io::ErrorKind::Other, + error_msg.as_str(), + )) + }, + } + }, + None => { + let error_msg = format!("Failed to process {} response.", tx_path); + Err(std::io::Error::new(std::io::ErrorKind::Other, error_msg.as_str())) + }, + } + }, + _ => { + let error_msg = format!("Failed to process {} response.", tx_path); + Err(std::io::Error::new(std::io::ErrorKind::Other, error_msg.as_str())) + }, + }, + } + } + + /// Retrieves the raw mempool. + pub(crate) async fn get_raw_mempool(&self) -> std::io::Result> { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_raw_mempool_rpc(Arc::clone(rpc_client)).await + }, + BitcoindClient::Rest { rest_client, .. } => { + Self::get_raw_mempool_rest(Arc::clone(rest_client)).await + }, + } + } + + /// Retrieves the raw mempool via the RPC interface. + async fn get_raw_mempool_rpc(rpc_client: Arc) -> std::io::Result> { + let verbose_flag_json = serde_json::json!(false); + rpc_client + .call_method::("getrawmempool", &[verbose_flag_json]) + .await + .map(|resp| resp.0) + } + + /// Retrieves the raw mempool via the REST interface. + async fn get_raw_mempool_rest(rest_client: Arc) -> std::io::Result> { + rest_client + .request_resource::( + "mempool/contents.json?verbose=false", + ) + .await + .map(|resp| resp.0) + } + + /// Retrieves an entry from the mempool if it exists, else return `None`. + pub(crate) async fn get_mempool_entry( + &self, txid: Txid, + ) -> std::io::Result> { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_mempool_entry_inner(Arc::clone(rpc_client), txid).await + }, + BitcoindClient::Rest { rpc_client, .. } => { + Self::get_mempool_entry_inner(Arc::clone(rpc_client), txid).await + }, + } + } + + /// Retrieves the mempool entry of the provided transaction ID. + async fn get_mempool_entry_inner( + client: Arc, txid: Txid, + ) -> std::io::Result> { + let txid_hex = txid.to_string(); + let txid_json = serde_json::json!(txid_hex); + + match client.call_method::("getmempoolentry", &[txid_json]).await { + Ok(resp) => Ok(Some(MempoolEntry { txid, time: resp.time, height: resp.height })), + Err(e) => match e.into_inner() { + Some(inner) => { + let rpc_error_res: Result, _> = inner.downcast(); + + match rpc_error_res { + Ok(rpc_error) => { + // Check if it's the 'not found' error code. + if rpc_error.code == -5 { + Ok(None) + } else { + Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) + } + }, + Err(_) => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to process getmempoolentry response", + )), + } + }, + None => Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to process getmempoolentry response", + )), + }, + } + } + + pub(crate) async fn update_mempool_entries_cache(&self) -> std::io::Result<()> { + match self { + BitcoindClient::Rpc { mempool_entries_cache, .. } => { + self.update_mempool_entries_cache_inner(mempool_entries_cache).await + }, + BitcoindClient::Rest { mempool_entries_cache, .. } => { + self.update_mempool_entries_cache_inner(mempool_entries_cache).await + }, + } + } + + async fn update_mempool_entries_cache_inner( + &self, mempool_entries_cache: &tokio::sync::Mutex>, + ) -> std::io::Result<()> { + let mempool_txids = self.get_raw_mempool().await?; + + let mut mempool_entries_cache = mempool_entries_cache.lock().await; + mempool_entries_cache.retain(|txid, _| mempool_txids.contains(txid)); + + if let Some(difference) = mempool_txids.len().checked_sub(mempool_entries_cache.capacity()) + { + mempool_entries_cache.reserve(difference) + } + + for txid in mempool_txids { + if mempool_entries_cache.contains_key(&txid) { + continue; + } + + if let Some(entry) = self.get_mempool_entry(txid).await? { + mempool_entries_cache.insert(txid, entry.clone()); + } + } + + mempool_entries_cache.shrink_to_fit(); + + Ok(()) + } + + /// Returns two `Vec`s: + /// - mempool transactions, alongside their first-seen unix timestamps. + /// - transactions that have been evicted from the mempool, alongside the last time they were seen absent. + pub(crate) async fn get_updated_mempool_transactions( + &self, best_processed_height: u32, bdk_unconfirmed_txids: Vec, + ) -> std::io::Result<(Vec<(Transaction, u64)>, Vec<(Txid, u64)>)> { + let mempool_txs = + self.get_mempool_transactions_and_timestamp_at_height(best_processed_height).await?; + let evicted_txids = + self.get_evicted_mempool_txids_and_timestamp(bdk_unconfirmed_txids).await?; + Ok((mempool_txs, evicted_txids)) + } + + /// Get mempool transactions, alongside their first-seen unix timestamps. + /// + /// This method is an adapted version of `bdk_bitcoind_rpc::Emitter::mempool`. It emits each + /// transaction only once, unless we cannot assume the transaction's ancestors are already + /// emitted. + pub(crate) async fn get_mempool_transactions_and_timestamp_at_height( + &self, best_processed_height: u32, + ) -> std::io::Result> { + match self { + BitcoindClient::Rpc { + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + .. + } => { + self.get_mempool_transactions_and_timestamp_at_height_inner( + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + best_processed_height, + ) + .await + }, + BitcoindClient::Rest { + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + .. + } => { + self.get_mempool_transactions_and_timestamp_at_height_inner( + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + best_processed_height, + ) + .await + }, + } + } + + async fn get_mempool_transactions_and_timestamp_at_height_inner( + &self, latest_mempool_timestamp: &AtomicU64, + mempool_entries_cache: &tokio::sync::Mutex>, + mempool_txs_cache: &tokio::sync::Mutex>, + best_processed_height: u32, + ) -> std::io::Result> { + let prev_mempool_time = latest_mempool_timestamp.load(Ordering::Relaxed); + let mut latest_time = prev_mempool_time; + + self.update_mempool_entries_cache().await?; + + let mempool_entries_cache = mempool_entries_cache.lock().await; + let mut mempool_txs_cache = mempool_txs_cache.lock().await; + mempool_txs_cache.retain(|txid, _| mempool_entries_cache.contains_key(txid)); + + if let Some(difference) = + mempool_entries_cache.len().checked_sub(mempool_txs_cache.capacity()) + { + mempool_txs_cache.reserve(difference) + } + + let mut txs_to_emit = Vec::with_capacity(mempool_entries_cache.len()); + for (txid, entry) in mempool_entries_cache.iter() { + if entry.time > latest_time { + latest_time = entry.time; + } + + // Avoid emitting transactions that are already emitted if we can guarantee + // blocks containing ancestors are already emitted. The bitcoind rpc interface + // provides us with the block height that the tx is introduced to the mempool. + // If we have already emitted the block of height, we can assume that all + // ancestor txs have been processed by the receiver. + let ancestor_within_height = entry.height <= best_processed_height; + let is_already_emitted = entry.time <= prev_mempool_time; + if is_already_emitted && ancestor_within_height { + continue; + } + + if let Some((cached_tx, cached_time)) = mempool_txs_cache.get(txid) { + txs_to_emit.push((cached_tx.clone(), *cached_time)); + continue; + } + + match self.get_raw_transaction(&entry.txid).await { + Ok(Some(tx)) => { + mempool_txs_cache.insert(entry.txid, (tx.clone(), entry.time)); + txs_to_emit.push((tx, entry.time)); + }, + Ok(None) => { + continue; + }, + Err(e) => return Err(e), + }; + } + + if !txs_to_emit.is_empty() { + latest_mempool_timestamp.store(latest_time, Ordering::Release); + } + Ok(txs_to_emit) + } + + // Retrieve a list of Txids that have been evicted from the mempool. + // + // To this end, we first update our local mempool_entries_cache and then return all unconfirmed + // wallet `Txid`s that don't appear in the mempool still. + async fn get_evicted_mempool_txids_and_timestamp( + &self, bdk_unconfirmed_txids: Vec, + ) -> std::io::Result> { + match self { + BitcoindClient::Rpc { latest_mempool_timestamp, mempool_entries_cache, .. } => { + Self::get_evicted_mempool_txids_and_timestamp_inner( + latest_mempool_timestamp, + mempool_entries_cache, + bdk_unconfirmed_txids, + ) + .await + }, + BitcoindClient::Rest { latest_mempool_timestamp, mempool_entries_cache, .. } => { + Self::get_evicted_mempool_txids_and_timestamp_inner( + latest_mempool_timestamp, + mempool_entries_cache, + bdk_unconfirmed_txids, + ) + .await + }, + } + } + + async fn get_evicted_mempool_txids_and_timestamp_inner( + latest_mempool_timestamp: &AtomicU64, + mempool_entries_cache: &tokio::sync::Mutex>, + bdk_unconfirmed_txids: Vec, + ) -> std::io::Result> { + let latest_mempool_timestamp = latest_mempool_timestamp.load(Ordering::Relaxed); + let mempool_entries_cache = mempool_entries_cache.lock().await; + let evicted_txids = bdk_unconfirmed_txids + .into_iter() + .filter(|txid| !mempool_entries_cache.contains_key(txid)) + .map(|txid| (txid, latest_mempool_timestamp)) + .collect(); + Ok(evicted_txids) + } +} + +impl BlockSource for BitcoindClient { + fn get_header<'a>( + &'a self, header_hash: &'a bitcoin::BlockHash, height_hint: Option, + ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Box::pin(async move { rpc_client.get_header(header_hash, height_hint).await }) + }, + BitcoindClient::Rest { rest_client, .. } => { + Box::pin(async move { rest_client.get_header(header_hash, height_hint).await }) + }, + } + } + + fn get_block<'a>( + &'a self, header_hash: &'a bitcoin::BlockHash, + ) -> AsyncBlockSourceResult<'a, BlockData> { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Box::pin(async move { rpc_client.get_block(header_hash).await }) + }, + BitcoindClient::Rest { rest_client, .. } => { + Box::pin(async move { rest_client.get_block(header_hash).await }) + }, + } + } + + fn get_best_block(&self) -> AsyncBlockSourceResult<'_, (bitcoin::BlockHash, Option)> { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Box::pin(async move { rpc_client.get_best_block().await }) + }, + BitcoindClient::Rest { rest_client, .. } => { + Box::pin(async move { rest_client.get_best_block().await }) + }, + } + } +} + +pub(crate) struct FeeResponse(pub FeeRate); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + if !self.0["errors"].is_null() { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + self.0["errors"].to_string(), + )); + } + let fee_rate_btc_per_kvbyte = self.0["feerate"] + .as_f64() + .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?; + // Bitcoin Core gives us a feerate in BTC/KvB. + // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. + let fee_rate = { + let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvbyte * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + Ok(FeeResponse(fee_rate)) + } +} + +pub(crate) struct MempoolMinFeeResponse(pub FeeRate); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let fee_rate_btc_per_kvbyte = self.0["mempoolminfee"] + .as_f64() + .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?; + // Bitcoin Core gives us a feerate in BTC/KvB. + // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. + let fee_rate = { + let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvbyte * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + Ok(MempoolMinFeeResponse(fee_rate)) + } +} + +pub(crate) struct GetRawTransactionResponse(pub Transaction); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let tx = self + .0 + .as_str() + .ok_or(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getrawtransaction response", + )) + .and_then(|s| { + bitcoin::consensus::encode::deserialize_hex(s).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getrawtransaction response", + ) + }) + })?; + + Ok(GetRawTransactionResponse(tx)) + } +} + +pub struct GetRawMempoolResponse(Vec); + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let res = self.0.as_array().ok_or(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getrawmempool response", + ))?; + + let mut mempool_transactions = Vec::with_capacity(res.len()); + + for hex in res { + let txid = if let Some(hex_str) = hex.as_str() { + match hex_str.parse::() { + Ok(txid) => txid, + Err(_) => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getrawmempool response", + )); + }, + } + } else { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getrawmempool response", + )); + }; + + mempool_transactions.push(txid); + } + + Ok(GetRawMempoolResponse(mempool_transactions)) + } +} + +pub struct GetMempoolEntryResponse { + time: u64, + height: u32, +} + +impl TryInto for JsonResponse { + type Error = std::io::Error; + fn try_into(self) -> std::io::Result { + let res = self.0.as_object().ok_or(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getmempoolentry response", + ))?; + + let time = match res["time"].as_u64() { + Some(time) => time, + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getmempoolentry response", + )); + }, + }; + + let height = match res["height"].as_u64().and_then(|h| h.try_into().ok()) { + Some(height) => height, + None => { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + "Failed to parse getmempoolentry response", + )); + }, + }; + + Ok(GetMempoolEntryResponse { time, height }) + } +} + +#[derive(Debug, Clone)] +pub(crate) struct MempoolEntry { + /// The transaction id + txid: Txid, + /// Local time transaction entered pool in seconds since 1 Jan 1970 GMT + time: u64, + /// Block height when transaction entered pool + height: u32, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum FeeRateEstimationMode { + Economical, + Conservative, +} + +const MAX_HEADER_CACHE_ENTRIES: usize = 100; + +pub(crate) struct BoundedHeaderCache { + header_map: HashMap, + recently_seen: VecDeque, +} + +impl BoundedHeaderCache { + pub(crate) fn new() -> Self { + let header_map = HashMap::new(); + let recently_seen = VecDeque::new(); + Self { header_map, recently_seen } + } +} + +impl Cache for BoundedHeaderCache { + fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader> { + self.header_map.get(block_hash) + } + + fn block_connected(&mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader) { + self.recently_seen.push_back(block_hash); + self.header_map.insert(block_hash, block_header); + + if self.header_map.len() >= MAX_HEADER_CACHE_ENTRIES { + // Keep dropping old entries until we've actually removed a header entry. + while let Some(oldest_entry) = self.recently_seen.pop_front() { + if self.header_map.remove(&oldest_entry).is_some() { + break; + } + } + } + } + + fn block_disconnected(&mut self, block_hash: &BlockHash) -> Option { + self.recently_seen.retain(|e| e != block_hash); + self.header_map.remove(block_hash) + } +} + +pub(crate) struct ChainListener { + pub(crate) onchain_wallet: Arc, + pub(crate) channel_manager: Arc, + pub(crate) chain_monitor: Arc, + pub(crate) output_sweeper: Arc, +} + +impl Listen for ChainListener { + fn filtered_block_connected( + &self, header: &bitcoin::block::Header, + txdata: &lightning::chain::transaction::TransactionData, height: u32, + ) { + self.onchain_wallet.filtered_block_connected(header, txdata, height); + self.channel_manager.filtered_block_connected(header, txdata, height); + self.chain_monitor.filtered_block_connected(header, txdata, height); + self.output_sweeper.filtered_block_connected(header, txdata, height); + } + fn block_connected(&self, block: &bitcoin::Block, height: u32) { + self.onchain_wallet.block_connected(block, height); + self.channel_manager.block_connected(block, height); + self.chain_monitor.block_connected(block, height); + self.output_sweeper.block_connected(block, height); + } + + fn blocks_disconnected(&self, fork_point_block: lightning::chain::BestBlock) { + self.onchain_wallet.blocks_disconnected(fork_point_block); + self.channel_manager.blocks_disconnected(fork_point_block); + self.chain_monitor.blocks_disconnected(fork_point_block); + self.output_sweeper.blocks_disconnected(fork_point_block); + } +} + +pub(crate) fn rpc_credentials(rpc_user: String, rpc_password: String) -> String { + BASE64_STANDARD.encode(format!("{}:{}", rpc_user, rpc_password)) +} + +pub(crate) fn endpoint(host: String, port: u16) -> HttpEndpoint { + HttpEndpoint::for_host(host).with_port(port) +} + +#[derive(Debug)] +pub struct HttpError { + pub(crate) status_code: String, + pub(crate) contents: Vec, +} + +impl std::error::Error for HttpError {} + +impl std::fmt::Display for HttpError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let contents = String::from_utf8_lossy(&self.contents); + write!(f, "status_code: {}, contents: {}", self.status_code, contents) + } +} + +#[cfg(test)] +mod tests { + use bitcoin::hashes::Hash; + use bitcoin::{FeeRate, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness}; + use lightning_block_sync::http::JsonResponse; + use proptest::arbitrary::any; + use proptest::collection::vec; + use proptest::{prop_assert_eq, prop_compose, proptest}; + use serde_json::json; + + use crate::chain::bitcoind::{ + FeeResponse, GetMempoolEntryResponse, GetRawMempoolResponse, GetRawTransactionResponse, + MempoolMinFeeResponse, + }; + + prop_compose! { + fn arbitrary_witness()( + witness_elements in vec(vec(any::(), 0..100), 0..20) + ) -> Witness { + let mut witness = Witness::new(); + for element in witness_elements { + witness.push(element); + } + witness + } + } + + prop_compose! { + fn arbitrary_txin()( + outpoint_hash in any::<[u8; 32]>(), + outpoint_vout in any::(), + script_bytes in vec(any::(), 0..100), + witness in arbitrary_witness(), + sequence in any::() + ) -> TxIn { + TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array(outpoint_hash), + vout: outpoint_vout, + }, + script_sig: ScriptBuf::from_bytes(script_bytes), + sequence: bitcoin::Sequence::from_consensus(sequence), + witness, + } + } + } + + prop_compose! { + fn arbitrary_txout()( + value in 0u64..21_000_000_00_000_000u64, + script_bytes in vec(any::(), 0..100) + ) -> TxOut { + TxOut { + value: bitcoin::Amount::from_sat(value), + script_pubkey: ScriptBuf::from_bytes(script_bytes), + } + } + } + + prop_compose! { + fn arbitrary_transaction()( + version in any::(), + inputs in vec(arbitrary_txin(), 1..20), + outputs in vec(arbitrary_txout(), 1..20), + lock_time in any::() + ) -> Transaction { + Transaction { + version: bitcoin::transaction::Version(version), + input: inputs, + output: outputs, + lock_time: bitcoin::absolute::LockTime::from_consensus(lock_time), + } + } + } + + proptest! { + #![proptest_config(proptest::test_runner::Config::with_cases(20))] + + #[test] + fn prop_get_raw_mempool_response_roundtrip(txids in vec(any::<[u8;32]>(), 0..10)) { + let txid_vec: Vec = txids.into_iter().map(Txid::from_byte_array).collect(); + let original = GetRawMempoolResponse(txid_vec.clone()); + + let json_vec: Vec = txid_vec.iter().map(|t| t.to_string()).collect(); + let json_val = serde_json::Value::Array(json_vec.iter().map(|s| json!(s)).collect()); + + let resp = JsonResponse(json_val); + let decoded: GetRawMempoolResponse = resp.try_into().unwrap(); + + prop_assert_eq!(original.0.len(), decoded.0.len()); + + prop_assert_eq!(original.0, decoded.0); + } + + #[test] + fn prop_get_mempool_entry_response_roundtrip( + time in any::(), + height in any::() + ) { + let json_val = json!({ + "time": time, + "height": height + }); + + let resp = JsonResponse(json_val); + let decoded: GetMempoolEntryResponse = resp.try_into().unwrap(); + + prop_assert_eq!(decoded.time, time); + prop_assert_eq!(decoded.height, height); + } + + #[test] + fn prop_get_raw_transaction_response_roundtrip(tx in arbitrary_transaction()) { + let hex = bitcoin::consensus::encode::serialize_hex(&tx); + let json_val = serde_json::Value::String(hex.clone()); + + let resp = JsonResponse(json_val); + let decoded: GetRawTransactionResponse = resp.try_into().unwrap(); + + prop_assert_eq!(decoded.0.compute_txid(), tx.compute_txid()); + prop_assert_eq!(decoded.0.compute_wtxid(), tx.compute_wtxid()); + + prop_assert_eq!(decoded.0, tx); + } + + #[test] + fn prop_fee_response_roundtrip(fee_rate in any::()) { + let fee_rate = fee_rate.abs(); + let json_val = json!({ + "feerate": fee_rate, + "errors": serde_json::Value::Null + }); + + let resp = JsonResponse(json_val); + let decoded: FeeResponse = resp.try_into().unwrap(); + + let expected = { + let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + prop_assert_eq!(decoded.0, expected); + } + + #[test] + fn prop_mempool_min_fee_response_roundtrip(fee_rate in any::()) { + let fee_rate = fee_rate.abs(); + let json_val = json!({ + "mempoolminfee": fee_rate + }); + + let resp = JsonResponse(json_val); + let decoded: MempoolMinFeeResponse = resp.try_into().unwrap(); + + let expected = { + let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + prop_assert_eq!(decoded.0, expected); + } + + } +} diff --git a/src/chain/bitcoind_rpc.rs b/src/chain/bitcoind_rpc.rs deleted file mode 100644 index e05812fa5a..0000000000 --- a/src/chain/bitcoind_rpc.rs +++ /dev/null @@ -1,503 +0,0 @@ -// 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 crate::types::{ChainMonitor, ChannelManager, Sweeper, Wallet}; - -use lightning::chain::Listen; - -use lightning_block_sync::http::HttpEndpoint; -use lightning_block_sync::http::JsonResponse; -use lightning_block_sync::poll::ValidatedBlockHeader; -use lightning_block_sync::rpc::{RpcClient, RpcError}; -use lightning_block_sync::{ - AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, Cache, -}; - -use serde::Serialize; - -use bitcoin::{BlockHash, FeeRate, Transaction, Txid}; - -use base64::prelude::{Engine, BASE64_STANDARD}; - -use std::collections::{HashMap, VecDeque}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; - -pub struct BitcoindRpcClient { - rpc_client: Arc, - latest_mempool_timestamp: AtomicU64, - mempool_entries_cache: tokio::sync::Mutex>, - mempool_txs_cache: tokio::sync::Mutex>, -} - -impl BitcoindRpcClient { - pub(crate) fn new(host: String, port: u16, rpc_user: String, rpc_password: String) -> Self { - let http_endpoint = HttpEndpoint::for_host(host.clone()).with_port(port); - let rpc_credentials = - BASE64_STANDARD.encode(format!("{}:{}", rpc_user.clone(), rpc_password.clone())); - - let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, http_endpoint)); - - let latest_mempool_timestamp = AtomicU64::new(0); - - let mempool_entries_cache = tokio::sync::Mutex::new(HashMap::new()); - let mempool_txs_cache = tokio::sync::Mutex::new(HashMap::new()); - Self { rpc_client, latest_mempool_timestamp, mempool_entries_cache, mempool_txs_cache } - } - - pub(crate) fn rpc_client(&self) -> Arc { - Arc::clone(&self.rpc_client) - } - - pub(crate) async fn broadcast_transaction(&self, tx: &Transaction) -> std::io::Result { - let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx); - let tx_json = serde_json::json!(tx_serialized); - self.rpc_client.call_method::("sendrawtransaction", &[tx_json]).await - } - - pub(crate) async fn get_fee_estimate_for_target( - &self, num_blocks: usize, estimation_mode: FeeRateEstimationMode, - ) -> std::io::Result { - let num_blocks_json = serde_json::json!(num_blocks); - let estimation_mode_json = serde_json::json!(estimation_mode); - self.rpc_client - .call_method::( - "estimatesmartfee", - &[num_blocks_json, estimation_mode_json], - ) - .await - .map(|resp| resp.0) - } - - pub(crate) async fn get_mempool_minimum_fee_rate(&self) -> std::io::Result { - self.rpc_client - .call_method::("getmempoolinfo", &[]) - .await - .map(|resp| resp.0) - } - - pub(crate) async fn get_raw_transaction( - &self, txid: &Txid, - ) -> std::io::Result> { - let txid_hex = bitcoin::consensus::encode::serialize_hex(txid); - let txid_json = serde_json::json!(txid_hex); - match self - .rpc_client - .call_method::("getrawtransaction", &[txid_json]) - .await - { - Ok(resp) => Ok(Some(resp.0)), - Err(e) => match e.into_inner() { - Some(inner) => { - let rpc_error_res: Result, _> = inner.downcast(); - - match rpc_error_res { - Ok(rpc_error) => { - // Check if it's the 'not found' error code. - if rpc_error.code == -5 { - Ok(None) - } else { - Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) - } - }, - Err(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getrawtransaction response", - )), - } - }, - None => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getrawtransaction response", - )), - }, - } - } - - pub(crate) async fn get_raw_mempool(&self) -> std::io::Result> { - let verbose_flag_json = serde_json::json!(false); - self.rpc_client - .call_method::("getrawmempool", &[verbose_flag_json]) - .await - .map(|resp| resp.0) - } - - pub(crate) async fn get_mempool_entry( - &self, txid: Txid, - ) -> std::io::Result> { - let txid_hex = bitcoin::consensus::encode::serialize_hex(&txid); - let txid_json = serde_json::json!(txid_hex); - match self - .rpc_client - .call_method::("getmempoolentry", &[txid_json]) - .await - { - Ok(resp) => Ok(Some(MempoolEntry { txid, height: resp.height, time: resp.time })), - Err(e) => match e.into_inner() { - Some(inner) => { - let rpc_error_res: Result, _> = inner.downcast(); - - match rpc_error_res { - Ok(rpc_error) => { - // Check if it's the 'not found' error code. - if rpc_error.code == -5 { - Ok(None) - } else { - Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)) - } - }, - Err(_) => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getmempoolentry response", - )), - } - }, - None => Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to process getmempoolentry response", - )), - }, - } - } - - pub(crate) async fn update_mempool_entries_cache(&self) -> std::io::Result<()> { - let mempool_txids = self.get_raw_mempool().await?; - - let mut mempool_entries_cache = self.mempool_entries_cache.lock().await; - mempool_entries_cache.retain(|txid, _| mempool_txids.contains(txid)); - - if let Some(difference) = mempool_txids.len().checked_sub(mempool_entries_cache.capacity()) - { - mempool_entries_cache.reserve(difference) - } - - for txid in mempool_txids { - if mempool_entries_cache.contains_key(&txid) { - continue; - } - - if let Some(entry) = self.get_mempool_entry(txid).await? { - mempool_entries_cache.insert(txid, entry.clone()); - } - } - - mempool_entries_cache.shrink_to_fit(); - - Ok(()) - } - - /// Get mempool transactions, alongside their first-seen unix timestamps. - /// - /// This method is an adapted version of `bdk_bitcoind_rpc::Emitter::mempool`. It emits each - /// transaction only once, unless we cannot assume the transaction's ancestors are already - /// emitted. - pub(crate) async fn get_mempool_transactions_and_timestamp_at_height( - &self, best_processed_height: u32, - ) -> std::io::Result> { - let prev_mempool_time = self.latest_mempool_timestamp.load(Ordering::Relaxed); - let mut latest_time = prev_mempool_time; - - self.update_mempool_entries_cache().await?; - - let mempool_entries_cache = self.mempool_entries_cache.lock().await; - let mut mempool_txs_cache = self.mempool_txs_cache.lock().await; - mempool_txs_cache.retain(|txid, _| mempool_entries_cache.contains_key(txid)); - - if let Some(difference) = - mempool_entries_cache.len().checked_sub(mempool_txs_cache.capacity()) - { - mempool_txs_cache.reserve(difference) - } - - let mut txs_to_emit = Vec::with_capacity(mempool_entries_cache.len()); - for (txid, entry) in mempool_entries_cache.iter() { - if entry.time > latest_time { - latest_time = entry.time; - } - - // Avoid emitting transactions that are already emitted if we can guarantee - // blocks containing ancestors are already emitted. The bitcoind rpc interface - // provides us with the block height that the tx is introduced to the mempool. - // If we have already emitted the block of height, we can assume that all - // ancestor txs have been processed by the receiver. - let ancestor_within_height = entry.height <= best_processed_height; - let is_already_emitted = entry.time <= prev_mempool_time; - if is_already_emitted && ancestor_within_height { - continue; - } - - if let Some((cached_tx, cached_time)) = mempool_txs_cache.get(txid) { - txs_to_emit.push((cached_tx.clone(), *cached_time)); - continue; - } - - match self.get_raw_transaction(&entry.txid).await { - Ok(Some(tx)) => { - mempool_txs_cache.insert(entry.txid, (tx.clone(), entry.time)); - txs_to_emit.push((tx, entry.time)); - }, - Ok(None) => { - continue; - }, - Err(e) => return Err(e), - }; - } - - if !txs_to_emit.is_empty() { - self.latest_mempool_timestamp.store(latest_time, Ordering::Release); - } - Ok(txs_to_emit) - } -} - -impl BlockSource for BitcoindRpcClient { - fn get_header<'a>( - &'a self, header_hash: &'a BlockHash, height_hint: Option, - ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { - Box::pin(async move { self.rpc_client.get_header(header_hash, height_hint).await }) - } - - fn get_block<'a>( - &'a self, header_hash: &'a BlockHash, - ) -> AsyncBlockSourceResult<'a, BlockData> { - Box::pin(async move { self.rpc_client.get_block(header_hash).await }) - } - - fn get_best_block(&self) -> AsyncBlockSourceResult<(BlockHash, Option)> { - Box::pin(async move { self.rpc_client.get_best_block().await }) - } -} - -pub(crate) struct FeeResponse(pub FeeRate); - -impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - if !self.0["errors"].is_null() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - self.0["errors"].to_string(), - )); - } - let fee_rate_btc_per_kvbyte = self.0["feerate"] - .as_f64() - .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?; - // Bitcoin Core gives us a feerate in BTC/KvB. - // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. - let fee_rate = { - let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvbyte * 25_000_000.0).round() as u64; - FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) - }; - Ok(FeeResponse(fee_rate)) - } -} - -pub struct MempoolMinFeeResponse(pub FeeRate); - -impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - let fee_rate_btc_per_kvbyte = self.0["mempoolminfee"] - .as_f64() - .ok_or(std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse fee rate"))?; - // Bitcoin Core gives us a feerate in BTC/KvB. - // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. - let fee_rate = { - let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvbyte * 25_000_000.0).round() as u64; - FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) - }; - Ok(MempoolMinFeeResponse(fee_rate)) - } -} - -pub struct GetRawTransactionResponse(pub Transaction); - -impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - let tx = self - .0 - .as_str() - .ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawtransaction response", - )) - .and_then(|s| { - bitcoin::consensus::encode::deserialize_hex(s).map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawtransaction response", - ) - }) - })?; - - Ok(GetRawTransactionResponse(tx)) - } -} - -pub struct GetRawMempoolResponse(Vec); - -impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - let res = self.0.as_array().ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawmempool response", - ))?; - - let mut mempool_transactions = Vec::with_capacity(res.len()); - - for hex in res { - let txid = if let Some(hex_str) = hex.as_str() { - match bitcoin::consensus::encode::deserialize_hex(hex_str) { - Ok(txid) => txid, - Err(_) => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawmempool response", - )); - }, - } - } else { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getrawmempool response", - )); - }; - - mempool_transactions.push(txid); - } - - Ok(GetRawMempoolResponse(mempool_transactions)) - } -} - -pub struct GetMempoolEntryResponse { - time: u64, - height: u32, -} - -impl TryInto for JsonResponse { - type Error = std::io::Error; - fn try_into(self) -> std::io::Result { - let res = self.0.as_object().ok_or(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getmempoolentry response", - ))?; - - let time = match res["time"].as_u64() { - Some(time) => time, - None => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getmempoolentry response", - )); - }, - }; - - let height = match res["height"].as_u64().and_then(|h| h.try_into().ok()) { - Some(height) => height, - None => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to parse getmempoolentry response", - )); - }, - }; - - Ok(GetMempoolEntryResponse { time, height }) - } -} - -#[derive(Debug, Clone)] -pub(crate) struct MempoolEntry { - /// The transaction id - txid: Txid, - /// Local time transaction entered pool in seconds since 1 Jan 1970 GMT - time: u64, - /// Block height when transaction entered pool - height: u32, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "UPPERCASE")] -pub(crate) enum FeeRateEstimationMode { - Economical, - Conservative, -} - -const MAX_HEADER_CACHE_ENTRIES: usize = 100; - -pub(crate) struct BoundedHeaderCache { - header_map: HashMap, - recently_seen: VecDeque, -} - -impl BoundedHeaderCache { - pub(crate) fn new() -> Self { - let header_map = HashMap::new(); - let recently_seen = VecDeque::new(); - Self { header_map, recently_seen } - } -} - -impl Cache for BoundedHeaderCache { - fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader> { - self.header_map.get(block_hash) - } - - fn block_connected(&mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader) { - self.recently_seen.push_back(block_hash); - self.header_map.insert(block_hash, block_header); - - if self.header_map.len() >= MAX_HEADER_CACHE_ENTRIES { - // Keep dropping old entries until we've actually removed a header entry. - while let Some(oldest_entry) = self.recently_seen.pop_front() { - if self.header_map.remove(&oldest_entry).is_some() { - break; - } - } - } - } - - fn block_disconnected(&mut self, block_hash: &BlockHash) -> Option { - self.recently_seen.retain(|e| e != block_hash); - self.header_map.remove(block_hash) - } -} - -pub(crate) struct ChainListener { - pub(crate) onchain_wallet: Arc, - pub(crate) channel_manager: Arc, - pub(crate) chain_monitor: Arc, - pub(crate) output_sweeper: Arc, -} - -impl Listen for ChainListener { - fn filtered_block_connected( - &self, header: &bitcoin::block::Header, - txdata: &lightning::chain::transaction::TransactionData, height: u32, - ) { - self.onchain_wallet.filtered_block_connected(header, txdata, height); - self.channel_manager.filtered_block_connected(header, txdata, height); - self.chain_monitor.filtered_block_connected(header, txdata, height); - self.output_sweeper.filtered_block_connected(header, txdata, height); - } - fn block_connected(&self, block: &bitcoin::Block, height: u32) { - self.onchain_wallet.block_connected(block, height); - self.channel_manager.block_connected(block, height); - self.chain_monitor.block_connected(block, height); - self.output_sweeper.block_connected(block, height); - } - - fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) { - self.onchain_wallet.block_disconnected(header, height); - self.channel_manager.block_disconnected(header, height); - self.chain_monitor.block_disconnected(header, height); - self.output_sweeper.block_disconnected(header, height); - } -} diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs new file mode 100644 index 0000000000..dbd0d9f7f9 --- /dev/null +++ b/src/chain/electrum.rs @@ -0,0 +1,660 @@ +// 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 std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use bdk_chain::bdk_core::spk_client::{ + FullScanRequest as BdkFullScanRequest, FullScanResponse as BdkFullScanResponse, + SyncRequest as BdkSyncRequest, SyncResponse as BdkSyncResponse, +}; +use bdk_electrum::BdkElectrumClient; +use bdk_wallet::{KeychainKind as BdkKeyChainKind, Update as BdkUpdate}; +use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid}; +use electrum_client::{ + Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi, +}; +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::util::ser::Writeable; +use lightning_transaction_sync::ElectrumSyncClient; + +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; +use crate::config::{ + Config, ElectrumSyncConfig, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, + FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, +}; +use crate::error::Error; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + ConfirmationTarget, OnchainFeeEstimator, +}; +use crate::io::utils::write_node_metrics; +use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::runtime::Runtime; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::NodeMetrics; + +const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; +const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; +const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 10; + +pub(super) struct ElectrumChainSource { + server_url: String, + pub(super) sync_config: ElectrumSyncConfig, + electrum_runtime_status: RwLock, + onchain_wallet: Arc, + onchain_wallet_sync_status: Mutex, + lightning_wallet_sync_status: Mutex, + fee_estimator: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl ElectrumChainSource { + pub(super) fn new( + server_url: String, sync_config: ElectrumSyncConfig, onchain_wallet: Arc, + fee_estimator: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, + ) -> Self { + let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + Self { + server_url, + sync_config, + electrum_runtime_status, + onchain_wallet, + onchain_wallet_sync_status, + lightning_wallet_sync_status, + fee_estimator, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { + self.electrum_runtime_status.write().unwrap().start( + self.server_url.clone(), + Arc::clone(&runtime), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) + } + + pub(super) fn stop(&self) { + self.electrum_runtime_status.write().unwrap().stop(); + } + + pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let res = self.sync_onchain_wallet_inner().await; + + self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn sync_onchain_wallet_inner(&self) -> Result<(), Error> { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the onchain wallet" + ); + return Err(Error::FeerateEstimationUpdateFailed); + }; + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + + let apply_wallet_update = + |update_res: Result, now: Instant| match update_res { + Ok(update) => match self.onchain_wallet.apply_update(update) { + Ok(()) => { + log_info!( + self.logger, + "{} of on-chain wallet finished in {}ms.", + if incremental_sync { "Incremental sync" } else { "Sync" }, + now.elapsed().as_millis() + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_onchain_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + Ok(()) + }, + Err(e) => Err(e), + }, + Err(e) => Err(e), + }; + + let cached_txs = self.onchain_wallet.get_cached_txs(); + + let res = if incremental_sync { + let incremental_sync_request = self.onchain_wallet.get_incremental_sync_request(); + let incremental_sync_fut = electrum_client + .get_incremental_sync_wallet_update(incremental_sync_request, cached_txs); + + let now = Instant::now(); + let update_res = incremental_sync_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + } else { + let full_scan_request = self.onchain_wallet.get_full_scan_request(); + let full_scan_fut = + electrum_client.get_full_scan_wallet_update(full_scan_request, cached_txs); + let now = Instant::now(); + let update_res = full_scan_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + }; + + res + } + + pub(crate) async fn sync_lightning_wallet( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::TxSyncFailed + })?; + } + + let res = + self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await; + + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn sync_lightning_wallet_inner( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + sync_cman as Arc, + sync_cmon as Arc, + sync_sweeper as Arc, + ]; + + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the lightning wallet" + ); + return Err(Error::TxSyncFailed); + }; + + let res = electrum_client.sync_confirmables(confirmables).await; + + if let Ok(_) = res { + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + } + + res + } + + pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + let electrum_client: Arc = if let Some(client) = + self.electrum_runtime_status.read().unwrap().client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!(false, "We should have started the chain source before updating fees"); + return Err(Error::FeerateEstimationUpdateFailed); + }; + + let now = Instant::now(); + + let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?; + self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + Ok(()) + } + + pub(crate) async fn process_broadcast_package(&self, package: Vec) { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!(false, "We should have started the chain source before broadcasting"); + return; + }; + + for tx in package { + electrum_client.broadcast(tx).await; + } + } +} + +impl Filter for ElectrumChainSource { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) + } + fn register_output(&self, output: lightning::chain::WatchedOutput) { + self.electrum_runtime_status.write().unwrap().register_output(output) + } +} + +enum ElectrumRuntimeStatus { + Started(Arc), + Stopped { + pending_registered_txs: Vec<(Txid, ScriptBuf)>, + pending_registered_outputs: Vec, + }, +} + +impl ElectrumRuntimeStatus { + fn new() -> Self { + let pending_registered_txs = Vec::new(); + let pending_registered_outputs = Vec::new(); + Self::Stopped { pending_registered_txs, pending_registered_outputs } + } + + pub(super) fn start( + &mut self, server_url: String, runtime: Arc, config: Arc, + logger: Arc, + ) -> Result<(), Error> { + match self { + Self::Stopped { pending_registered_txs, pending_registered_outputs } => { + let client = Arc::new(ElectrumRuntimeClient::new( + server_url.clone(), + runtime, + config, + logger, + )?); + + // Apply any pending `Filter` entries + for (txid, script_pubkey) in pending_registered_txs.drain(..) { + client.register_tx(&txid, &script_pubkey); + } + + for output in pending_registered_outputs.drain(..) { + client.register_output(output) + } + + *self = Self::Started(client); + }, + Self::Started(_) => { + debug_assert!(false, "We shouldn't call start if we're already started") + }, + } + Ok(()) + } + + pub(super) fn stop(&mut self) { + *self = Self::new() + } + + fn client(&self) -> Option> { + match self { + Self::Started(client) => Some(Arc::clone(&client)), + Self::Stopped { .. } => None, + } + } + + fn register_tx(&mut self, txid: &Txid, script_pubkey: &Script) { + match self { + Self::Started(client) => client.register_tx(txid, script_pubkey), + Self::Stopped { pending_registered_txs, .. } => { + pending_registered_txs.push((*txid, script_pubkey.to_owned())) + }, + } + } + + fn register_output(&mut self, output: lightning::chain::WatchedOutput) { + match self { + Self::Started(client) => client.register_output(output), + Self::Stopped { pending_registered_outputs, .. } => { + pending_registered_outputs.push(output) + }, + } + } +} + +struct ElectrumRuntimeClient { + electrum_client: Arc, + bdk_electrum_client: Arc>>, + tx_sync: Arc>>, + runtime: Arc, + config: Arc, + logger: Arc, +} + +impl ElectrumRuntimeClient { + fn new( + server_url: String, runtime: Arc, config: Arc, logger: Arc, + ) -> Result { + let electrum_config = ElectrumConfigBuilder::new() + .retry(ELECTRUM_CLIENT_NUM_RETRIES) + .timeout(Some(ELECTRUM_CLIENT_TIMEOUT_SECS)) + .build(); + + let electrum_client = Arc::new( + ElectrumClient::from_config(&server_url, electrum_config.clone()).map_err(|e| { + log_error!(logger, "Failed to connect to electrum server: {}", e); + Error::ConnectionFailed + })?, + ); + let bdk_electrum_client = Arc::new(BdkElectrumClient::new(Arc::clone(&electrum_client))); + let tx_sync = Arc::new( + ElectrumSyncClient::new(server_url.clone(), Arc::clone(&logger)).map_err(|e| { + log_error!(logger, "Failed to connect to electrum server: {}", e); + Error::ConnectionFailed + })?, + ); + Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, config, logger }) + } + + async fn sync_confirmables( + &self, confirmables: Vec>, + ) -> Result<(), Error> { + let now = Instant::now(); + + let tx_sync = Arc::clone(&self.tx_sync); + let spawn_fut = self.runtime.spawn_blocking(move || tx_sync.sync(confirmables)); + let timeout_fut = + tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), spawn_fut); + + let res = timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Sync of Lightning wallet timed out: {}", e); + Error::TxSyncTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Error::TxSyncFailed + })? + .map_err(|e| { + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Error::TxSyncFailed + })?; + + log_info!( + self.logger, + "Sync of Lightning wallet finished in {}ms.", + now.elapsed().as_millis() + ); + + Ok(res) + } + + async fn get_full_scan_wallet_update( + &self, request: BdkFullScanRequest, + cached_txs: impl IntoIterator>>, + ) -> Result, Error> { + let bdk_electrum_client = Arc::clone(&self.bdk_electrum_client); + bdk_electrum_client.populate_tx_cache(cached_txs); + + let spawn_fut = self.runtime.spawn_blocking(move || { + bdk_electrum_client.full_scan( + request, + BDK_CLIENT_STOP_GAP, + BDK_ELECTRUM_CLIENT_BATCH_SIZE, + true, + ) + }); + let wallet_sync_timeout_fut = + tokio::time::timeout(Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), spawn_fut); + + wallet_sync_timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Sync of on-chain wallet timed out: {}", e); + Error::WalletOperationTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + })? + .map_err(|e| { + log_error!(self.logger, "Sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + }) + } + + async fn get_incremental_sync_wallet_update( + &self, request: BdkSyncRequest<(BdkKeyChainKind, u32)>, + cached_txs: impl IntoIterator>>, + ) -> Result { + let bdk_electrum_client = Arc::clone(&self.bdk_electrum_client); + bdk_electrum_client.populate_tx_cache(cached_txs); + + let spawn_fut = self.runtime.spawn_blocking(move || { + bdk_electrum_client.sync(request, BDK_ELECTRUM_CLIENT_BATCH_SIZE, true) + }); + let wallet_sync_timeout_fut = + tokio::time::timeout(Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), spawn_fut); + + wallet_sync_timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Incremental sync of on-chain wallet timed out: {}", e); + Error::WalletOperationTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Incremental sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + })? + .map_err(|e| { + log_error!(self.logger, "Incremental sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + }) + } + + async fn broadcast(&self, tx: Transaction) { + let electrum_client = Arc::clone(&self.electrum_client); + + let txid = tx.compute_txid(); + let tx_bytes = tx.encode(); + + let spawn_fut = + self.runtime.spawn_blocking(move || electrum_client.transaction_broadcast(&tx)); + let timeout_fut = + tokio::time::timeout(Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), spawn_fut); + + match timeout_fut.await { + Ok(res) => match res { + Ok(_) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => { + log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx_bytes) + ); + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx_bytes) + ); + }, + } + } + + async fn get_fee_rate_cache_update( + &self, + ) -> Result, Error> { + let electrum_client = Arc::clone(&self.electrum_client); + + let mut batch = Batch::default(); + let confirmation_targets = get_all_conf_targets(); + for target in confirmation_targets { + let num_blocks = get_num_block_defaults_for_target(target); + batch.estimate_fee(num_blocks); + } + + let spawn_fut = self.runtime.spawn_blocking(move || electrum_client.batch_call(&batch)); + + let timeout_fut = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + spawn_fut, + ); + + let raw_estimates_btc_kvb = timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })?; + + if raw_estimates_btc_kvb.len() != confirmation_targets.len() + && self.config.network == Network::Bitcoin + { + // Ensure we fail if we didn't receive all estimates. + debug_assert!(false, + "Electrum server didn't return all expected results. This is disallowed on Mainnet." + ); + log_error!(self.logger, + "Failed to retrieve fee rate estimates: Electrum server didn't return all expected results. This is disallowed on Mainnet." + ); + return Err(Error::FeerateEstimationUpdateFailed); + } + + let mut new_fee_rate_cache = HashMap::with_capacity(10); + for (target, raw_fee_rate_btc_per_kvb) in + confirmation_targets.into_iter().zip(raw_estimates_btc_kvb.into_iter()) + { + // Parse the retrieved serde_json::Value and fall back to 1 sat/vb (10^3 / 10^8 = 10^-5 + // = 0.00001 btc/kvb) if we fail or it yields less than that. This is mostly necessary + // to continue on `signet`/`regtest` where we might not get estimates (or bogus + // values). + let fee_rate_btc_per_kvb = raw_fee_rate_btc_per_kvb + .as_f64() + .map_or(0.00001, |converted| converted.max(0.00001)); + + // Electrum, just like Bitcoin Core, gives us a feerate in BTC/KvB. + // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. + let fee_rate = { + let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvb * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); + + new_fee_rate_cache.insert(target, adjusted_fee_rate); + + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } + + Ok(new_fee_rate_cache) + } +} + +impl Filter for ElectrumRuntimeClient { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.tx_sync.register_tx(txid, script_pubkey) + } + fn register_output(&self, output: WatchedOutput) { + self.tx_sync.register_output(output) + } +} diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs new file mode 100644 index 0000000000..be6f2fb86e --- /dev/null +++ b/src/chain/esplora.rs @@ -0,0 +1,446 @@ +// 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 std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use bdk_esplora::EsploraAsyncExt; +use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; +use esplora_client::AsyncClient as EsploraAsyncClient; +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::util::ser::Writeable; +use lightning_transaction_sync::EsploraSyncClient; + +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; +use crate::config::{ + Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, + BDK_WALLET_SYNC_TIMEOUT_SECS, DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS, + FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, +}; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + OnchainFeeEstimator, +}; +use crate::io::utils::write_node_metrics; +use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::{Error, NodeMetrics}; + +pub(super) struct EsploraChainSource { + pub(super) sync_config: EsploraSyncConfig, + esplora_client: EsploraAsyncClient, + onchain_wallet: Arc, + onchain_wallet_sync_status: Mutex, + tx_sync: Arc>>, + lightning_wallet_sync_status: Mutex, + fee_estimator: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl EsploraChainSource { + pub(crate) fn new( + server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, + onchain_wallet: Arc, fee_estimator: Arc, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let mut client_builder = esplora_client::Builder::new(&server_url); + client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + + for (header_name, header_value) in &headers { + client_builder = client_builder.header(header_name, header_value); + } + + let esplora_client = client_builder.build_async().unwrap(); + let tx_sync = + Arc::new(EsploraSyncClient::from_client(esplora_client.clone(), Arc::clone(&logger))); + + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + Self { + sync_config, + esplora_client, + onchain_wallet, + onchain_wallet_sync_status, + tx_sync, + lightning_wallet_sync_status, + fee_estimator, + kv_store, + config, + logger, + node_metrics, + } + } + + pub(super) async fn sync_onchain_wallet(&self) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let res = self.sync_onchain_wallet_inner().await; + + self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn sync_onchain_wallet_inner(&self) -> Result<(), Error> { + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + + macro_rules! get_and_apply_wallet_update { + ($sync_future: expr) => {{ + let now = Instant::now(); + match $sync_future.await { + Ok(res) => match res { + Ok(update) => match self.onchain_wallet.apply_update(update) { + Ok(()) => { + log_info!( + self.logger, + "{} of on-chain wallet finished in {}ms.", + if incremental_sync { "Incremental sync" } else { "Sync" }, + now.elapsed().as_millis() + ); + let unix_time_secs_opt = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger) + )?; + } + Ok(()) + }, + Err(e) => Err(e), + }, + Err(e) => match *e { + esplora_client::Error::Reqwest(he) => { + if let Some(status_code) = he.status() { + log_error!( + self.logger, + "{} of on-chain wallet failed due to HTTP {} error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + status_code, + he, + ); + } else { + log_error!( + self.logger, + "{} of on-chain wallet failed due to HTTP error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + he, + ); + } + Err(Error::WalletOperationFailed) + }, + _ => { + log_error!( + self.logger, + "{} of on-chain wallet failed due to Esplora error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + e + ); + Err(Error::WalletOperationFailed) + }, + }, + }, + Err(e) => { + log_error!( + self.logger, + "{} of on-chain wallet timed out: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + e + ); + Err(Error::WalletOperationTimeout) + }, + } + }} + } + + if incremental_sync { + let sync_request = self.onchain_wallet.get_incremental_sync_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } else { + let full_scan_request = self.onchain_wallet.get_full_scan_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.full_scan( + full_scan_request, + BDK_CLIENT_STOP_GAP, + BDK_CLIENT_CONCURRENCY, + ), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } + } + + pub(super) async fn sync_lightning_wallet( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let res = + self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await; + + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn sync_lightning_wallet_inner( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + &*sync_cman as &(dyn Confirm + Sync + Send), + &*sync_cmon as &(dyn Confirm + Sync + Send), + &*sync_sweeper as &(dyn Confirm + Sync + Send), + ]; + + let timeout_fut = tokio::time::timeout( + Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), + self.tx_sync.sync(confirmables), + ); + let now = Instant::now(); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_info!( + self.logger, + "Sync of Lightning wallet finished in {}ms.", + now.elapsed().as_millis() + ); + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + Ok(()) + }, + Err(e) => { + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Err(e.into()) + }, + }, + Err(e) => { + log_error!(self.logger, "Lightning wallet sync timed out: {}", e); + Err(Error::TxSyncTimeout) + }, + } + } + + pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + let now = Instant::now(); + let estimates = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + self.esplora_client.get_fee_estimates(), + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })?; + + if estimates.is_empty() && self.config.network == Network::Bitcoin { + // Ensure we fail if we didn't receive any estimates. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: empty fee estimates are dissallowed on Mainnet.", + ); + return Err(Error::FeerateEstimationUpdateFailed); + } + + let confirmation_targets = get_all_conf_targets(); + + let mut new_fee_rate_cache = HashMap::with_capacity(10); + for target in confirmation_targets { + let num_blocks = get_num_block_defaults_for_target(target); + + // Convert the retrieved fee rate and fall back to 1 sat/vb if we fail or it + // yields less than that. This is mostly necessary to continue on + // `signet`/`regtest` where we might not get estimates (or bogus values). + let converted_estimate_sat_vb = + esplora_client::convert_fee_rate(num_blocks, estimates.clone()) + .map_or(1.0, |converted| converted.max(1.0)); + + let fee_rate = FeeRate::from_sat_per_kwu((converted_estimate_sat_vb * 250.0) as u64); + + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); + + new_fee_rate_cache.insert(target, adjusted_fee_rate); + + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } + + self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + Ok(()) + } + + pub(crate) async fn process_broadcast_package(&self, package: Vec) { + for tx in &package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.esplora_client.broadcast(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. + log_trace!( + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + message + ); + } else { + log_error!( + self.logger, + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message + ); + } + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + _ => { + log_error!( + self.logger, + "Failed to broadcast transaction {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + } + } + } +} + +impl Filter for EsploraChainSource { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.tx_sync.register_tx(txid, script_pubkey); + } + fn register_output(&self, output: WatchedOutput) { + self.tx_sync.register_output(output); + } +} diff --git a/src/chain/mod.rs b/src/chain/mod.rs index d96ab06efc..309d60eabc 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -5,55 +5,32 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -mod bitcoind_rpc; +mod bitcoind; +mod electrum; +mod esplora; -use crate::chain::bitcoind_rpc::{ - BitcoindRpcClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, -}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::Duration; + +use bitcoin::{Script, Txid}; +use lightning::chain::Filter; +use lightning_block_sync::gossip::UtxoSource; + +use crate::chain::bitcoind::BitcoindChainSource; +use crate::chain::electrum::ElectrumChainSource; +use crate::chain::esplora::EsploraChainSource; use crate::config::{ - Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, - BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, - RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, TX_BROADCAST_TIMEOUT_SECS, - WALLET_SYNC_INTERVAL_MINIMUM_SECS, -}; -use crate::fee_estimator::{ - apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, - ConfirmationTarget, OnchainFeeEstimator, + BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, + RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; +use crate::fee_estimator::OnchainFeeEstimator; use crate::io::utils::write_node_metrics; -use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::logger::{log_debug, log_info, log_trace, LdkLogger, Logger}; +use crate::runtime::Runtime; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; -use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; -use lightning::chain::{Confirm, Filter, Listen}; -use lightning::util::ser::Writeable; - -use lightning_transaction_sync::EsploraSyncClient; - -use lightning_block_sync::gossip::UtxoSource; -use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; -use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; -use lightning_block_sync::SpvClient; - -use bdk_esplora::EsploraAsyncExt; - -use esplora_client::AsyncClient as EsploraAsyncClient; - -use bitcoin::{FeeRate, Network}; - -use std::collections::HashMap; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - -// The default Esplora server we're using. -pub(crate) const DEFAULT_ESPLORA_SERVER_URL: &str = "https://blockstream.info/api"; - -// The default Esplora client timeout we're using. -pub(crate) const DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS: u64 = 10; - -const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; - pub(crate) enum WalletSyncStatus { Completed, InProgress { subscribers: tokio::sync::broadcast::Sender> }, @@ -107,428 +84,278 @@ impl WalletSyncStatus { } } -pub(crate) enum ChainSource { - Esplora { - sync_config: EsploraSyncConfig, - esplora_client: EsploraAsyncClient, - onchain_wallet: Arc, - onchain_wallet_sync_status: Mutex, - tx_sync: Arc>>, - lightning_wallet_sync_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, - }, - BitcoindRpc { - bitcoind_rpc_client: Arc, - header_cache: tokio::sync::Mutex, - latest_chain_tip: RwLock>, - onchain_wallet: Arc, - wallet_polling_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, - }, +pub(crate) struct ChainSource { + kind: ChainSourceKind, + tx_broadcaster: Arc, + logger: Arc, +} + +enum ChainSourceKind { + Esplora(EsploraChainSource), + Electrum(ElectrumChainSource), + Bitcoind(BitcoindChainSource), } impl ChainSource { pub(crate) fn new_esplora( - server_url: String, sync_config: EsploraSyncConfig, onchain_wallet: Arc, + server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, + ) -> Self { + let esplora_chain_source = EsploraChainSource::new( + server_url, + headers, + sync_config, + onchain_wallet, + fee_estimator, + kv_store, + config, + Arc::clone(&logger), + node_metrics, + ); + let kind = ChainSourceKind::Esplora(esplora_chain_source); + Self { kind, tx_broadcaster, logger } + } + + pub(crate) fn new_electrum( + server_url: String, sync_config: ElectrumSyncConfig, onchain_wallet: Arc, fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { - let mut client_builder = esplora_client::Builder::new(&server_url); - client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); - let esplora_client = client_builder.build_async().unwrap(); - let tx_sync = - Arc::new(EsploraSyncClient::from_client(esplora_client.clone(), Arc::clone(&logger))); - let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - Self::Esplora { + let electrum_chain_source = ElectrumChainSource::new( + server_url, sync_config, - esplora_client, onchain_wallet, - onchain_wallet_sync_status, - tx_sync, - lightning_wallet_sync_status, fee_estimator, - tx_broadcaster, kv_store, config, - logger, + Arc::clone(&logger), node_metrics, - } + ); + let kind = ChainSourceKind::Electrum(electrum_chain_source); + Self { kind, tx_broadcaster, logger } } pub(crate) fn new_bitcoind_rpc( - host: String, port: u16, rpc_user: String, rpc_password: String, + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, onchain_wallet: Arc, fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { - let bitcoind_rpc_client = - Arc::new(BitcoindRpcClient::new(host, port, rpc_user, rpc_password)); - let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); - let latest_chain_tip = RwLock::new(None); - let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - Self::BitcoindRpc { - bitcoind_rpc_client, - header_cache, - latest_chain_tip, + let bitcoind_chain_source = BitcoindChainSource::new_rpc( + rpc_host, + rpc_port, + rpc_user, + rpc_password, + onchain_wallet, + fee_estimator, + kv_store, + config, + Arc::clone(&logger), + node_metrics, + ); + let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); + Self { kind, tx_broadcaster, logger } + } + + pub(crate) fn new_bitcoind_rest( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + rest_client_config: BitcoindRestClientConfig, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let bitcoind_chain_source = BitcoindChainSource::new_rest( + rpc_host, + rpc_port, + rpc_user, + rpc_password, onchain_wallet, - wallet_polling_status, fee_estimator, - tx_broadcaster, kv_store, config, - logger, + rest_client_config, + Arc::clone(&logger), node_metrics, + ); + let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); + Self { kind, tx_broadcaster, logger } + } + + pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { + match &self.kind { + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.start(runtime)? + }, + _ => { + // Nothing to do for other chain sources. + }, + } + Ok(()) + } + + pub(crate) fn stop(&self) { + match &self.kind { + ChainSourceKind::Electrum(electrum_chain_source) => electrum_chain_source.stop(), + _ => { + // Nothing to do for other chain sources. + }, } } pub(crate) fn as_utxo_source(&self) -> Option> { - match self { - Self::BitcoindRpc { bitcoind_rpc_client, .. } => Some(bitcoind_rpc_client.rpc_client()), + match &self.kind { + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + Some(bitcoind_chain_source.as_utxo_source()) + }, _ => None, } } + pub(crate) fn is_transaction_based(&self) -> bool { + match &self.kind { + ChainSourceKind::Esplora(_) => true, + ChainSourceKind::Electrum { .. } => true, + ChainSourceKind::Bitcoind { .. } => false, + } + } + pub(crate) async fn continuously_sync_wallets( - &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + &self, stop_sync_receiver: tokio::sync::watch::Receiver<()>, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) { - match self { - Self::Esplora { sync_config, logger, .. } => { - // Setup syncing intervals - let onchain_wallet_sync_interval_secs = sync_config - .onchain_wallet_sync_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut onchain_wallet_sync_interval = - tokio::time::interval(Duration::from_secs(onchain_wallet_sync_interval_secs)); - onchain_wallet_sync_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let fee_rate_cache_update_interval_secs = sync_config - .fee_rate_cache_update_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut fee_rate_update_interval = - tokio::time::interval(Duration::from_secs(fee_rate_cache_update_interval_secs)); - // When starting up, we just blocked on updating, so skip the first tick. - fee_rate_update_interval.reset(); - fee_rate_update_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let lightning_wallet_sync_interval_secs = sync_config - .lightning_wallet_sync_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut lightning_wallet_sync_interval = - tokio::time::interval(Duration::from_secs(lightning_wallet_sync_interval_secs)); - lightning_wallet_sync_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - // Start the syncing loop. - loop { - tokio::select! { - _ = stop_sync_receiver.changed() => { - log_trace!( - logger, - "Stopping background syncing on-chain wallet.", - ); - return; - } - _ = onchain_wallet_sync_interval.tick() => { - let _ = self.sync_onchain_wallet().await; - } - _ = fee_rate_update_interval.tick() => { - let _ = self.update_fee_rate_estimates().await; - } - _ = lightning_wallet_sync_interval.tick() => { - let _ = self.sync_lightning_wallet( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&output_sweeper), - ).await; - } - } + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + if let Some(background_sync_config) = + esplora_chain_source.sync_config.background_sync_config.as_ref() + { + self.start_tx_based_sync_loop( + stop_sync_receiver, + channel_manager, + chain_monitor, + output_sweeper, + background_sync_config, + Arc::clone(&self.logger), + ) + .await + } else { + // Background syncing is disabled + log_info!( + self.logger, + "Background syncing is disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", + ); + return; } }, - Self::BitcoindRpc { - bitcoind_rpc_client, - header_cache, - latest_chain_tip, - onchain_wallet, - wallet_polling_status, - kv_store, - config, - logger, - node_metrics, - .. - } => { - // First register for the wallet polling status to make sure `Node::sync_wallets` calls - // wait on the result before proceeding. + ChainSourceKind::Electrum(electrum_chain_source) => { + if let Some(background_sync_config) = + electrum_chain_source.sync_config.background_sync_config.as_ref() { - let mut status_lock = wallet_polling_status.lock().unwrap(); - if status_lock.register_or_subscribe_pending_sync().is_some() { - debug_assert!(false, "Sync already in progress. This should never happen."); - } + self.start_tx_based_sync_loop( + stop_sync_receiver, + channel_manager, + chain_monitor, + output_sweeper, + background_sync_config, + Arc::clone(&self.logger), + ) + .await + } else { + // Background syncing is disabled + log_info!( + self.logger, + "Background syncing is disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", + ); + return; } - - log_info!( - logger, - "Starting initial synchronization of chain listeners. This might take a while..", - ); - - loop { - let channel_manager_best_block_hash = - channel_manager.current_best_block().block_hash; - let sweeper_best_block_hash = output_sweeper.current_best_block().block_hash; - let onchain_wallet_best_block_hash = - onchain_wallet.current_best_block().block_hash; - - let mut chain_listeners = vec![ - ( - onchain_wallet_best_block_hash, - &**onchain_wallet as &(dyn Listen + Send + Sync), - ), - ( - channel_manager_best_block_hash, - &*channel_manager as &(dyn Listen + Send + Sync), - ), - (sweeper_best_block_hash, &*output_sweeper as &(dyn Listen + Send + Sync)), - ]; - - // TODO: Eventually we might want to see if we can synchronize `ChannelMonitor`s - // before giving them to `ChainMonitor` it the first place. However, this isn't - // trivial as we load them on initialization (in the `Builder`) and only gain - // network access during `start`. For now, we just make sure we get the worst known - // block hash and sychronize them via `ChainMonitor`. - if let Some(worst_channel_monitor_block_hash) = chain_monitor - .list_monitors() - .iter() - .flat_map(|(txo, _)| chain_monitor.get_monitor(*txo)) - .map(|m| m.current_best_block()) - .min_by_key(|b| b.height) - .map(|b| b.block_hash) - { - chain_listeners.push(( - worst_channel_monitor_block_hash, - &*chain_monitor as &(dyn Listen + Send + Sync), - )); - } - - let mut locked_header_cache = header_cache.lock().await; - let now = SystemTime::now(); - match synchronize_listeners( - bitcoind_rpc_client.as_ref(), - config.network, - &mut *locked_header_cache, - chain_listeners.clone(), + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source + .continuously_sync_wallets( + stop_sync_receiver, + channel_manager, + chain_monitor, + output_sweeper, ) .await - { - Ok(chain_tip) => { - { - log_info!( - logger, - "Finished synchronizing listeners in {}ms", - now.elapsed().unwrap().as_millis() - ); - *latest_chain_tip.write().unwrap() = Some(chain_tip); - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - locked_node_metrics.latest_onchain_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - ) - .unwrap_or_else(|e| { - log_error!(logger, "Failed to persist node metrics: {}", e); - }); - } - break; - }, + }, + } + } - Err(e) => { - log_error!(logger, "Failed to synchronize chain listeners: {:?}", e); - tokio::time::sleep(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)) - .await; - }, - } + async fn start_tx_based_sync_loop( + &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, background_sync_config: &BackgroundSyncConfig, + logger: Arc, + ) { + // Setup syncing intervals + let onchain_wallet_sync_interval_secs = background_sync_config + .onchain_wallet_sync_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut onchain_wallet_sync_interval = + tokio::time::interval(Duration::from_secs(onchain_wallet_sync_interval_secs)); + onchain_wallet_sync_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let fee_rate_cache_update_interval_secs = background_sync_config + .fee_rate_cache_update_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut fee_rate_update_interval = + tokio::time::interval(Duration::from_secs(fee_rate_cache_update_interval_secs)); + // When starting up, we just blocked on updating, so skip the first tick. + fee_rate_update_interval.reset(); + fee_rate_update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let lightning_wallet_sync_interval_secs = background_sync_config + .lightning_wallet_sync_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut lightning_wallet_sync_interval = + tokio::time::interval(Duration::from_secs(lightning_wallet_sync_interval_secs)); + lightning_wallet_sync_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + // Start the syncing loop. + loop { + tokio::select! { + _ = stop_sync_receiver.changed() => { + log_trace!( + logger, + "Stopping background syncing on-chain wallet.", + ); + return; } - - // Now propagate the initial result to unblock waiting subscribers. - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(Ok(())); - - let mut chain_polling_interval = - tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); - chain_polling_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let mut fee_rate_update_interval = - tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); - // When starting up, we just blocked on updating, so skip the first tick. - fee_rate_update_interval.reset(); - fee_rate_update_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - log_info!(logger, "Starting continuous polling for chain updates."); - - // Start the polling loop. - loop { - tokio::select! { - _ = stop_sync_receiver.changed() => { - log_trace!( - logger, - "Stopping polling for new chain data.", - ); - return; - } - _ = chain_polling_interval.tick() => { - let _ = self.poll_and_update_listeners(Arc::clone(&channel_manager), Arc::clone(&chain_monitor), Arc::clone(&output_sweeper)).await; - } - _ = fee_rate_update_interval.tick() => { - let _ = self.update_fee_rate_estimates().await; - } - } + _ = onchain_wallet_sync_interval.tick() => { + let _ = self.sync_onchain_wallet().await; } - }, + _ = fee_rate_update_interval.tick() => { + let _ = self.update_fee_rate_estimates().await; + } + _ = lightning_wallet_sync_interval.tick() => { + let _ = self.sync_lightning_wallet( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&output_sweeper), + ).await; + } + } } } // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { - match self { - Self::Esplora { - esplora_client, - onchain_wallet, - onchain_wallet_sync_status, - kv_store, - logger, - node_metrics, - .. - } => { - let receiver_res = { - let mut status_lock = onchain_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - let res = { - // If this is our first sync, do a full scan with the configured gap limit. - // Otherwise just do an incremental sync. - let incremental_sync = - node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); - - macro_rules! get_and_apply_wallet_update { - ($sync_future: expr) => {{ - let now = Instant::now(); - match $sync_future.await { - Ok(res) => match res { - Ok(update) => match onchain_wallet.apply_update(update) { - Ok(()) => { - log_info!( - logger, - "{} of on-chain wallet finished in {}ms.", - if incremental_sync { "Incremental sync" } else { "Sync" }, - now.elapsed().as_millis() - ); - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; - write_node_metrics(&*locked_node_metrics, Arc::clone(&kv_store), Arc::clone(&logger))?; - } - Ok(()) - }, - Err(e) => Err(e), - }, - Err(e) => match *e { - esplora_client::Error::Reqwest(he) => { - log_error!( - logger, - "{} of on-chain wallet failed due to HTTP connection error: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - he - ); - Err(Error::WalletOperationFailed) - }, - _ => { - log_error!( - logger, - "{} of on-chain wallet failed due to Esplora error: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - e - ); - Err(Error::WalletOperationFailed) - }, - }, - }, - Err(e) => { - log_error!( - logger, - "{} of on-chain wallet timed out: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - e - ); - Err(Error::WalletOperationTimeout) - }, - } - }} - } - - if incremental_sync { - let sync_request = onchain_wallet.get_incremental_sync_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } else { - let full_scan_request = onchain_wallet.get_full_scan_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - esplora_client.full_scan( - full_scan_request, - BDK_CLIENT_STOP_GAP, - BDK_CLIENT_CONCURRENCY, - ), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } - }; - - onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.sync_onchain_wallet().await + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.sync_onchain_wallet().await }, - Self::BitcoindRpc { .. } => { - // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via + ChainSourceKind::Bitcoind { .. } => { + // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. unreachable!("Onchain wallet will be synced via chain polling") }, @@ -541,93 +368,19 @@ impl ChainSource { &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { - match self { - Self::Esplora { - tx_sync, - lightning_wallet_sync_status, - kv_store, - logger, - node_metrics, - .. - } => { - let sync_cman = Arc::clone(&channel_manager); - let sync_cmon = Arc::clone(&chain_monitor); - let sync_sweeper = Arc::clone(&output_sweeper); - let confirmables = vec![ - &*sync_cman as &(dyn Confirm + Sync + Send), - &*sync_cmon as &(dyn Confirm + Sync + Send), - &*sync_sweeper as &(dyn Confirm + Sync + Send), - ]; - - let receiver_res = { - let mut status_lock = lightning_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - let res = { - let timeout_fut = tokio::time::timeout( - Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), - tx_sync.sync(confirmables), - ); - let now = Instant::now(); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_info!( - logger, - "Sync of Lightning wallet finished in {}ms.", - now.elapsed().as_millis() - ); - - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } - - periodically_archive_fully_resolved_monitors( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&kv_store), - Arc::clone(&logger), - Arc::clone(&node_metrics), - )?; - Ok(()) - }, - Err(e) => { - log_error!(logger, "Sync of Lightning wallet failed: {}", e); - Err(e.into()) - }, - }, - Err(e) => { - log_error!(logger, "Lightning wallet sync timed out: {}", e); - Err(Error::TxSyncTimeout) - }, - } - }; - - lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source + .sync_lightning_wallet(channel_manager, chain_monitor, output_sweeper) + .await }, - Self::BitcoindRpc { .. } => { - // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source + .sync_lightning_wallet(channel_manager, chain_monitor, output_sweeper) + .await + }, + ChainSourceKind::Bitcoind { .. } => { + // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. unreachable!("Lightning wallet will be synced via chain polling") }, @@ -638,511 +391,92 @@ impl ChainSource { &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { - match self { - Self::Esplora { .. } => { + match &self.kind { + ChainSourceKind::Esplora { .. } => { // In Esplora mode we sync lightning and onchain wallets via // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, - Self::BitcoindRpc { - bitcoind_rpc_client, - header_cache, - latest_chain_tip, - onchain_wallet, - wallet_polling_status, - kv_store, - config, - logger, - node_metrics, - .. - } => { - let receiver_res = { - let mut status_lock = wallet_polling_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet polling result: {:?}", e); - log_error!(logger, "Failed to receive wallet polling result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - let latest_chain_tip_opt = latest_chain_tip.read().unwrap().clone(); - let chain_tip = if let Some(tip) = latest_chain_tip_opt { - tip - } else { - match validate_best_block_header(bitcoind_rpc_client.as_ref()).await { - Ok(tip) => { - *latest_chain_tip.write().unwrap() = Some(tip); - tip - }, - Err(e) => { - log_error!(logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - wallet_polling_status - .lock() - .unwrap() - .propagate_result_to_subscribers(res); - return res; - }, - } - }; - - let mut locked_header_cache = header_cache.lock().await; - let chain_poller = - ChainPoller::new(Arc::clone(&bitcoind_rpc_client), config.network); - let chain_listener = ChainListener { - onchain_wallet: Arc::clone(&onchain_wallet), - channel_manager: Arc::clone(&channel_manager), - chain_monitor, - output_sweeper, - }; - let mut spv_client = SpvClient::new( - chain_tip, - chain_poller, - &mut *locked_header_cache, - &chain_listener, - ); - - let now = SystemTime::now(); - match spv_client.poll_best_tip().await { - Ok((ChainTip::Better(tip), true)) => { - log_trace!( - logger, - "Finished polling best tip in {}ms", - now.elapsed().unwrap().as_millis() - ); - *latest_chain_tip.write().unwrap() = Some(tip); - }, - Ok(_) => {}, - Err(e) => { - log_error!(logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } - - let cur_height = channel_manager.current_best_block().height; - - let now = SystemTime::now(); - match bitcoind_rpc_client - .get_mempool_transactions_and_timestamp_at_height(cur_height) + ChainSourceKind::Electrum { .. } => { + // In Electrum mode we sync lightning and onchain wallets via + // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. + unreachable!("Listeners will be synced via transction-based syncing") + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source + .poll_and_update_listeners(channel_manager, chain_monitor, output_sweeper) .await - { - Ok(unconfirmed_txs) => { - log_trace!( - logger, - "Finished polling mempool of size {} in {}ms", - unconfirmed_txs.len(), - now.elapsed().unwrap().as_millis() - ); - let _ = onchain_wallet.apply_unconfirmed_txs(unconfirmed_txs); - }, - Err(e) => { - log_error!(logger, "Failed to poll for mempool transactions: {:?}", e); - let res = Err(Error::TxSyncFailed); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } - - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; - locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; - - let write_res = write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - ); - match write_res { - Ok(()) => (), - Err(e) => { - log_error!(logger, "Failed to persist node metrics: {}", e); - let res = Err(Error::PersistenceFailed); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } - - let res = Ok(()); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - res }, } } pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { - match self { - Self::Esplora { - esplora_client, - fee_estimator, - config, - kv_store, - logger, - node_metrics, - .. - } => { - let now = Instant::now(); - let estimates = tokio::time::timeout( - Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), - esplora_client.get_fee_estimates(), - ) - .await - .map_err(|e| { - log_error!(logger, "Updating fee rate estimates timed out: {}", e); - Error::FeerateEstimationUpdateTimeout - })? - .map_err(|e| { - log_error!(logger, "Failed to retrieve fee rate estimates: {}", e); - Error::FeerateEstimationUpdateFailed - })?; - - if estimates.is_empty() && config.network == Network::Bitcoin { - // Ensure we fail if we didn't receive any estimates. - log_error!( - logger, - "Failed to retrieve fee rate estimates: empty fee estimates are dissallowed on Mainnet.", - ); - return Err(Error::FeerateEstimationUpdateFailed); - } - - let confirmation_targets = get_all_conf_targets(); - - let mut new_fee_rate_cache = HashMap::with_capacity(10); - for target in confirmation_targets { - let num_blocks = get_num_block_defaults_for_target(target); - - // Convert the retrieved fee rate and fall back to 1 sat/vb if we fail or it - // yields less than that. This is mostly necessary to continue on - // `signet`/`regtest` where we might not get estimates (or bogus values). - let converted_estimate_sat_vb = - esplora_client::convert_fee_rate(num_blocks, estimates.clone()) - .map_or(1.0, |converted| converted.max(1.0)); - - let fee_rate = - FeeRate::from_sat_per_kwu((converted_estimate_sat_vb * 250.0) as u64); - - // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that - // require some post-estimation adjustments to the fee rates, which we do here. - let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); - - new_fee_rate_cache.insert(target, adjusted_fee_rate); - - log_trace!( - logger, - "Fee rate estimation updated for {:?}: {} sats/kwu", - target, - adjusted_fee_rate.to_sat_per_kwu(), - ); - } - - fee_estimator.set_fee_rate_cache(new_fee_rate_cache); - - log_info!( - logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } - - Ok(()) + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.update_fee_rate_estimates().await }, - Self::BitcoindRpc { - bitcoind_rpc_client, - fee_estimator, - config, - kv_store, - logger, - node_metrics, - .. - } => { - macro_rules! get_fee_rate_update { - ($estimation_fut: expr) => {{ - let update_res = tokio::time::timeout( - Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), - $estimation_fut, - ) - .await - .map_err(|e| { - log_error!(logger, "Updating fee rate estimates timed out: {}", e); - Error::FeerateEstimationUpdateTimeout - })?; - update_res - }}; - } - let confirmation_targets = get_all_conf_targets(); - - let mut new_fee_rate_cache = HashMap::with_capacity(10); - let now = Instant::now(); - for target in confirmation_targets { - let fee_rate_update_res = match target { - ConfirmationTarget::Lightning( - LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee, - ) => { - let estimation_fut = bitcoind_rpc_client.get_mempool_minimum_fee_rate(); - get_fee_rate_update!(estimation_fut) - }, - ConfirmationTarget::Lightning( - LdkConfirmationTarget::MaximumFeeEstimate, - ) => { - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = bitcoind_rpc_client - .get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - ConfirmationTarget::Lightning( - LdkConfirmationTarget::UrgentOnChainSweep, - ) => { - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = bitcoind_rpc_client - .get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - _ => { - // Otherwise, we default to economical block-target estimate. - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Economical; - let estimation_fut = bitcoind_rpc_client - .get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - }; - - let fee_rate = match (fee_rate_update_res, config.network) { - (Ok(rate), _) => rate, - (Err(e), Network::Bitcoin) => { - // Strictly fail on mainnet. - log_error!(logger, "Failed to retrieve fee rate estimates: {}", e); - return Err(Error::FeerateEstimationUpdateFailed); - }, - (Err(e), n) if n == Network::Regtest || n == Network::Signet => { - // On regtest/signet we just fall back to the usual 1 sat/vb == 250 - // sat/kwu default. - log_error!( - logger, - "Failed to retrieve fee rate estimates: {}. Falling back to default of 1 sat/vb.", - e, - ); - FeeRate::from_sat_per_kwu(250) - }, - (Err(e), _) => { - // On testnet `estimatesmartfee` can be unreliable so we just skip in - // case of a failure, which will have us falling back to defaults. - log_error!( - logger, - "Failed to retrieve fee rate estimates: {}. Falling back to defaults.", - e, - ); - return Ok(()); - }, - }; - - // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that - // require some post-estimation adjustments to the fee rates, which we do here. - let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); - - new_fee_rate_cache.insert(target, adjusted_fee_rate); - - log_trace!( - logger, - "Fee rate estimation updated for {:?}: {} sats/kwu", - target, - adjusted_fee_rate.to_sat_per_kwu(), - ); - } - - if fee_estimator.set_fee_rate_cache(new_fee_rate_cache) { - // We only log if the values changed, as it might be very spammy otherwise. - log_info!( - logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); - } - - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } - - Ok(()) + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.update_fee_rate_estimates().await + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.update_fee_rate_estimates().await }, } } - pub(crate) async fn process_broadcast_queue(&self) { - match self { - Self::Esplora { esplora_client, tx_broadcaster, logger, .. } => { - let mut receiver = tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - esplora_client.broadcast(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_trace!( - logger, - "Successfully broadcast transaction {}", - txid - ); - }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. - log_trace!( - logger, - "Failed to broadcast due to HTTP connection error: {}", - message - ); - } else { - log_error!( - logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, message - ); - } - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - _ => { - log_error!( - logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - }, - Err(e) => { - log_error!( - logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } - } + pub(crate) async fn continuously_process_broadcast_queue( + &self, mut stop_tx_bcast_receiver: tokio::sync::watch::Receiver<()>, + ) { + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + loop { + let tx_bcast_logger = Arc::clone(&self.logger); + tokio::select! { + _ = stop_tx_bcast_receiver.changed() => { + log_debug!( + tx_bcast_logger, + "Stopping broadcasting transactions.", + ); + return; } - }, - Self::BitcoindRpc { bitcoind_rpc_client, tx_broadcaster, logger, .. } => { - // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 - // features, we should eventually switch to use `submitpackage` via the - // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual - // transactions. - let mut receiver = tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - bitcoind_rpc_client.broadcast_transaction(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(id) => { - debug_assert_eq!(id, txid); - log_trace!( - logger, - "Successfully broadcast transaction {}", - txid - ); - }, - Err(e) => { - log_error!( - logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - Err(e) => { - log_error!( - logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } + Some(next_package) = receiver.recv() => { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.process_broadcast_package(next_package).await + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.process_broadcast_package(next_package).await + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.process_broadcast_package(next_package).await + }, } } - }, + } } } } impl Filter for ChainSource { - fn register_tx(&self, txid: &bitcoin::Txid, script_pubkey: &bitcoin::Script) { - match self { - Self::Esplora { tx_sync, .. } => tx_sync.register_tx(txid, script_pubkey), - Self::BitcoindRpc { .. } => (), + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.register_tx(txid, script_pubkey) + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.register_tx(txid, script_pubkey) + }, + ChainSourceKind::Bitcoind { .. } => (), } } fn register_output(&self, output: lightning::chain::WatchedOutput) { - match self { - Self::Esplora { tx_sync, .. } => tx_sync.register_output(output), - Self::BitcoindRpc { .. } => (), + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.register_output(output) + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.register_output(output) + }, + ChainSourceKind::Bitcoind { .. } => (), } } } diff --git a/src/config.rs b/src/config.rs index e7dd2a6181..e82e8d3943 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,20 +7,20 @@ //! Objects for configuring the node. -use crate::logger::LogLevel; -use crate::payment::SendingParameters; -use crate::prober::ProbabilisticScoringParameters; - -use lightning::ln::msgs::SocketAddress; -use lightning::routing::gossip::NodeAlias; -use lightning::util::config::ChannelConfig as LdkChannelConfig; -use lightning::util::config::MaxDustHTLCExposure as LdkMaxDustHTLCExposure; -use lightning::util::config::UserConfig; +use std::fmt; +use std::time::Duration; use bitcoin::secp256k1::PublicKey; use bitcoin::Network; +use lightning::ln::msgs::SocketAddress; +use lightning::routing::gossip::NodeAlias; +use lightning::routing::router::RouteParametersConfig; +use lightning::util::config::{ + ChannelConfig as LdkChannelConfig, MaxDustHTLCExposure as LdkMaxDustHTLCExposure, UserConfig, +}; -use std::time::Duration; +use crate::logger::LogLevel; +use crate::prober::ProbabilisticScoringParameters; // Config defaults const DEFAULT_NETWORK: Network = Network::Bitcoin; @@ -39,6 +39,12 @@ pub const DEFAULT_LOG_FILENAME: &'static str = "ldk_node.log"; /// The default storage directory. pub const DEFAULT_STORAGE_DIR_PATH: &str = "/tmp/ldk_node"; +// The default Esplora server we're using. +pub(crate) const DEFAULT_ESPLORA_SERVER_URL: &str = "https://blockstream.info/api"; + +// The default Esplora client timeout we're using. +pub(crate) const DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS: u64 = 10; + // The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold // number of derivation indexes after which BDK stops looking for new scripts belonging to the wallet. pub(crate) const BDK_CLIENT_STOP_GAP: usize = 20; @@ -65,10 +71,16 @@ pub(crate) const NODE_ANN_BCAST_INTERVAL: Duration = Duration::from_secs(60 * 60 pub(crate) const WALLET_SYNC_INTERVAL_MINIMUM_SECS: u64 = 10; // The timeout after which we abort a wallet syncing operation. -pub(crate) const BDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 90; +pub(crate) const BDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 20; // The timeout after which we abort a wallet syncing operation. -pub(crate) const LDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 30; +pub(crate) const LDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 10; + +// The timeout after which we give up waiting on LDK's event handler to exit on shutdown. +pub(crate) const LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS: u64 = 30; + +// The timeout after which we give up waiting on a background task to exit on shutdown. +pub(crate) const BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS: u64 = 5; // The timeout after which we abort a fee rate cache update operation. pub(crate) const FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS: u64 = 5; @@ -79,8 +91,8 @@ pub(crate) const TX_BROADCAST_TIMEOUT_SECS: u64 = 5; // The timeout after which we abort a RGS sync operation. pub(crate) const RGS_SYNC_TIMEOUT_SECS: u64 = 5; -// The length in bytes of our wallets' keys seed. -pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64; +/// The length in bytes of our wallets' keys seed. +pub const WALLET_KEYS_SEED_LEN: usize = 64; #[derive(Debug, Clone)] /// Represents the configuration of an [`Node`] instance. @@ -102,9 +114,9 @@ pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64; /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | -/// | `sending_parameters` | None | +/// | `route_parameters` | None | /// -/// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their +/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. /// /// [`Node`]: crate::Node @@ -118,6 +130,12 @@ pub struct Config { /// **Note**: We will only allow opening and accepting public channels if the `node_alias` and the /// `listening_addresses` are set. pub listening_addresses: Option>, + /// The addresses which the node will announce to the gossip network that it accepts connections on. + /// + /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. + /// + /// [`listening_addresses`]: Config::listening_addresses + pub announcement_addresses: Option>, /// The node alias that will be used when broadcasting announcements to the gossip network. /// /// The provided alias must be a valid UTF-8 string and no longer than 32 bytes in total. @@ -155,13 +173,12 @@ pub struct Config { pub anchor_channels_config: Option, /// Configuration options for payment routing and pathfinding. /// - /// Setting the `SendingParameters` provides flexibility to customize how payments are routed, + /// Setting the [`RouteParametersConfig`] provides flexibility to customize how payments are routed, /// including setting limits on routing fees, CLTV expiry, and channel utilization. /// /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. - pub sending_parameters: Option, - + pub route_parameters: Option, /// The parameters used to configure the [`lightning::routing::scoring::ProbabilisticScorer`] /// used by the node. pub scoring_parameters: ProbabilisticScoringParameters, @@ -173,10 +190,11 @@ impl Default for Config { storage_dir_path: DEFAULT_STORAGE_DIR_PATH.to_string(), network: DEFAULT_NETWORK, listening_addresses: None, + announcement_addresses: None, trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, anchor_channels_config: Some(AnchorChannelsConfig::default()), - sending_parameters: None, + route_parameters: None, node_alias: None, scoring_parameters: ProbabilisticScoringParameters::default(), } @@ -262,9 +280,37 @@ pub fn default_config() -> Config { Config::default() } -pub(crate) fn may_announce_channel(config: &Config) -> bool { - config.node_alias.is_some() - && config.listening_addresses.as_ref().map_or(false, |addrs| !addrs.is_empty()) +#[derive(Debug, PartialEq)] +pub(crate) enum AnnounceError { + MissingNodeAlias, + MissingListeningAddresses, + MissingAliasAndAddresses, +} + +impl fmt::Display for AnnounceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnnounceError::MissingNodeAlias => write!(f, "Node alias is not configured"), + AnnounceError::MissingListeningAddresses => { + write!(f, "Listening addresses are not configured") + }, + AnnounceError::MissingAliasAndAddresses => { + write!(f, "Node alias and listening addresses are not configured") + }, + } + } +} + +pub(crate) fn may_announce_channel(config: &Config) -> Result<(), AnnounceError> { + let has_listening_addresses = + config.listening_addresses.as_ref().map_or(false, |addrs| !addrs.is_empty()); + + match (config.node_alias.is_some(), has_listening_addresses) { + (true, true) => Ok(()), + (true, false) => Err(AnnounceError::MissingListeningAddresses), + (false, true) => Err(AnnounceError::MissingNodeAlias), + (false, false) => Err(AnnounceError::MissingAliasAndAddresses), + } } pub(crate) fn default_user_config(config: &Config) -> UserConfig { @@ -279,7 +325,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = config.anchor_channels_config.is_some(); - if !may_announce_channel(config) { + if may_announce_channel(config).is_err() { user_config.accept_forwards_to_priv_channels = false; user_config.channel_handshake_config.announce_for_forwarding = false; user_config.channel_handshake_limits.force_announced_channel_preference = true; @@ -288,7 +334,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config } -/// Options related to syncing the Lightning and on-chain wallets via an Esplora backend. +/// Options related to background syncing the Lightning and on-chain wallets. /// /// ### Defaults /// @@ -298,22 +344,24 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { /// | `lightning_wallet_sync_interval_secs` | 30 | /// | `fee_rate_cache_update_interval_secs` | 600 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct EsploraSyncConfig { +pub struct BackgroundSyncConfig { /// The time in-between background sync attempts of the onchain wallet, in seconds. /// - /// **Note:** A minimum of 10 seconds is always enforced. + /// **Note:** A minimum of 10 seconds is enforced when background syncing is enabled. pub onchain_wallet_sync_interval_secs: u64, + /// The time in-between background sync attempts of the LDK wallet, in seconds. /// - /// **Note:** A minimum of 10 seconds is always enforced. + /// **Note:** A minimum of 10 seconds is enforced when background syncing is enabled. pub lightning_wallet_sync_interval_secs: u64, + /// The time in-between background update attempts to our fee rate cache, in seconds. /// - /// **Note:** A minimum of 10 seconds is always enforced. + /// **Note:** A minimum of 10 seconds is enforced when background syncing is enabled. pub fee_rate_cache_update_interval_secs: u64, } -impl Default for EsploraSyncConfig { +impl Default for BackgroundSyncConfig { fn default() -> Self { Self { onchain_wallet_sync_interval_secs: DEFAULT_BDK_WALLET_SYNC_INTERVAL_SECS, @@ -323,6 +371,57 @@ impl Default for EsploraSyncConfig { } } +/// Configuration for syncing with an Esplora backend. +/// +/// Background syncing is enabled by default, using the default values specified in +/// [`BackgroundSyncConfig`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct EsploraSyncConfig { + /// Background sync configuration. + /// + /// If set to `None`, background syncing will be disabled. Users will need to manually + /// sync via [`Node::sync_wallets`] for the wallets and fee rate updates. + /// + /// [`Node::sync_wallets`]: crate::Node::sync_wallets + pub background_sync_config: Option, +} + +impl Default for EsploraSyncConfig { + fn default() -> Self { + Self { background_sync_config: Some(BackgroundSyncConfig::default()) } + } +} + +/// Configuration for syncing with an Electrum backend. +/// +/// Background syncing is enabled by default, using the default values specified in +/// [`BackgroundSyncConfig`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ElectrumSyncConfig { + /// Background sync configuration. + /// + /// If set to `None`, background syncing will be disabled. Users will need to manually + /// sync via [`Node::sync_wallets`] for the wallets and fee rate updates. + /// + /// [`Node::sync_wallets`]: crate::Node::sync_wallets + pub background_sync_config: Option, +} + +impl Default for ElectrumSyncConfig { + fn default() -> Self { + Self { background_sync_config: Some(BackgroundSyncConfig::default()) } + } +} + +/// Configuration for syncing with Bitcoin Core backend via REST. +#[derive(Debug, Clone)] +pub struct BitcoindRestClientConfig { + /// Host URL. + pub rest_host: String, + /// Host port. + pub rest_port: u16, +} + /// Options which apply on a per-channel basis and may change at runtime or based on negotiation /// with our counterparty. #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -439,20 +538,33 @@ impl From for LdkMaxDustHTLCExposure { } } +#[derive(Debug, Clone, Copy)] +/// The role of the node in an asynchronous payments context. +/// +/// See for more information about the async payments protocol. +pub enum AsyncPaymentsRole { + /// Node acts a client in an async payments context. This means that if possible, it will instruct its peers to hold + /// HTLCs for it, so that it can go offline. + Client, + /// Node acts as a server in an async payments context. This means that it will hold async payments HTLCs and onion + /// messages for its peers. + Server, +} + #[cfg(test)] mod tests { use std::str::FromStr; - use super::may_announce_channel; - use super::Config; - use super::NodeAlias; - use super::SocketAddress; + use super::{may_announce_channel, AnnounceError, Config, NodeAlias, SocketAddress}; #[test] fn node_announce_channel() { // Default configuration with node alias and listening addresses unset let mut node_config = Config::default(); - assert!(!may_announce_channel(&node_config)); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingAliasAndAddresses) + ); // Set node alias with listening addresses unset let alias_frm_str = |alias: &str| { @@ -461,11 +573,26 @@ mod tests { NodeAlias(bytes) }; node_config.node_alias = Some(alias_frm_str("LDK_Node")); - assert!(!may_announce_channel(&node_config)); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingListeningAddresses) + ); + + // Set announcement addresses with listening addresses unset + let announcement_address = SocketAddress::from_str("123.45.67.89:9735") + .expect("Socket address conversion failed."); + node_config.announcement_addresses = Some(vec![announcement_address]); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingListeningAddresses) + ); // Set node alias with an empty list of listening addresses node_config.listening_addresses = Some(vec![]); - assert!(!may_announce_channel(&node_config)); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingListeningAddresses) + ); // Set node alias with a non-empty list of listening addresses let socket_address = @@ -473,6 +600,6 @@ mod tests { if let Some(ref mut addresses) = node_config.listening_addresses { addresses.push(socket_address); } - assert!(may_announce_channel(&node_config)); + assert!(may_announce_channel(&node_config).is_ok()); } } diff --git a/src/connection.rs b/src/connection.rs index c4cde717af..e3a25f3577 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -5,20 +5,19 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::logger::{log_error, log_info, LdkLogger}; -use crate::types::PeerManager; -use crate::Error; - -use lightning::ln::msgs::SocketAddress; - -use bitcoin::secp256k1::PublicKey; - use std::collections::hash_map::{self, HashMap}; use std::net::ToSocketAddrs; use std::ops::Deref; use std::sync::{Arc, Mutex}; use std::time::Duration; +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::SocketAddress; + +use crate::logger::{log_error, log_info, LdkLogger}; +use crate::types::PeerManager; +use crate::Error; + pub(crate) struct ConnectionManager where L::Target: LdkLogger, diff --git a/src/data_store.rs b/src/data_store.rs new file mode 100644 index 0000000000..ce4b294e0a --- /dev/null +++ b/src/data_store.rs @@ -0,0 +1,297 @@ +// 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 std::collections::{hash_map, HashMap}; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; + +use lightning::util::persist::KVStoreSync; +use lightning::util::ser::{Readable, Writeable}; + +use crate::logger::{log_error, LdkLogger}; +use crate::types::DynStore; +use crate::Error; + +pub(crate) trait StorableObject: Clone + Readable + Writeable { + type Id: StorableObjectId; + type Update: StorableObjectUpdate; + + fn id(&self) -> Self::Id; + fn update(&mut self, update: &Self::Update) -> bool; + fn to_update(&self) -> Self::Update; +} + +pub(crate) trait StorableObjectId: std::hash::Hash + PartialEq + Eq { + fn encode_to_hex_str(&self) -> String; +} + +pub(crate) trait StorableObjectUpdate { + fn id(&self) -> SO::Id; +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub(crate) enum DataStoreUpdateResult { + Updated, + Unchanged, + NotFound, +} + +pub(crate) struct DataStore +where + L::Target: LdkLogger, +{ + objects: Mutex>, + primary_namespace: String, + secondary_namespace: String, + kv_store: Arc, + logger: L, +} + +impl DataStore +where + L::Target: LdkLogger, +{ + pub(crate) fn new( + objects: Vec, primary_namespace: String, secondary_namespace: String, + kv_store: Arc, logger: L, + ) -> Self { + let objects = + Mutex::new(HashMap::from_iter(objects.into_iter().map(|obj| (obj.id(), obj)))); + Self { objects, primary_namespace, secondary_namespace, kv_store, logger } + } + + pub(crate) fn insert(&self, object: SO) -> Result { + let mut locked_objects = self.objects.lock().unwrap(); + + self.persist(&object)?; + let updated = locked_objects.insert(object.id(), object).is_some(); + Ok(updated) + } + + pub(crate) fn insert_or_update(&self, object: SO) -> Result { + let mut locked_objects = self.objects.lock().unwrap(); + + let updated; + match locked_objects.entry(object.id()) { + hash_map::Entry::Occupied(mut e) => { + let update = object.to_update(); + updated = e.get_mut().update(&update); + if updated { + self.persist(&e.get())?; + } + }, + hash_map::Entry::Vacant(e) => { + e.insert(object.clone()); + self.persist(&object)?; + updated = true; + }, + } + + Ok(updated) + } + + pub(crate) fn remove(&self, id: &SO::Id) -> Result<(), Error> { + let removed = self.objects.lock().unwrap().remove(id).is_some(); + if removed { + let store_key = id.encode_to_hex_str(); + KVStoreSync::remove( + &*self.kv_store, + &self.primary_namespace, + &self.secondary_namespace, + &store_key, + ) + .map_err(|e| { + log_error!( + self.logger, + "Removing object data for key {}/{}/{} failed due to: {}", + &self.primary_namespace, + &self.secondary_namespace, + store_key, + e + ); + Error::PersistenceFailed + })?; + } + Ok(()) + } + + pub(crate) fn get(&self, id: &SO::Id) -> Option { + self.objects.lock().unwrap().get(id).cloned() + } + + pub(crate) fn update(&self, update: &SO::Update) -> Result { + let mut locked_objects = self.objects.lock().unwrap(); + + if let Some(object) = locked_objects.get_mut(&update.id()) { + let updated = object.update(update); + if updated { + self.persist(&object)?; + Ok(DataStoreUpdateResult::Updated) + } else { + Ok(DataStoreUpdateResult::Unchanged) + } + } else { + Ok(DataStoreUpdateResult::NotFound) + } + } + + pub(crate) fn list_filter bool>(&self, f: F) -> Vec { + self.objects.lock().unwrap().values().filter(f).cloned().collect::>() + } + + fn persist(&self, object: &SO) -> Result<(), Error> { + let store_key = object.id().encode_to_hex_str(); + let data = object.encode(); + KVStoreSync::write( + &*self.kv_store, + &self.primary_namespace, + &self.secondary_namespace, + &store_key, + data, + ) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", + &self.primary_namespace, + &self.secondary_namespace, + store_key, + e + ); + Error::PersistenceFailed + })?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use lightning::impl_writeable_tlv_based; + use lightning::util::test_utils::{TestLogger, TestStore}; + + use super::*; + use crate::hex_utils; + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + struct TestObjectId { + id: [u8; 4], + } + + impl StorableObjectId for TestObjectId { + fn encode_to_hex_str(&self) -> String { + hex_utils::to_string(&self.id) + } + } + impl_writeable_tlv_based!(TestObjectId, { (0, id, required) }); + + struct TestObjectUpdate { + id: TestObjectId, + data: [u8; 3], + } + impl StorableObjectUpdate for TestObjectUpdate { + fn id(&self) -> TestObjectId { + self.id + } + } + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + struct TestObject { + id: TestObjectId, + data: [u8; 3], + } + + impl StorableObject for TestObject { + type Id = TestObjectId; + type Update = TestObjectUpdate; + + fn id(&self) -> Self::Id { + self.id + } + + fn update(&mut self, update: &Self::Update) -> bool { + if self.data != update.data { + self.data = update.data; + true + } else { + false + } + } + + fn to_update(&self) -> Self::Update { + Self::Update { id: self.id, data: self.data } + } + } + + impl_writeable_tlv_based!(TestObject, { + (0, id, required), + (2, data, required), + }); + + #[test] + fn data_is_persisted() { + let store: Arc = Arc::new(TestStore::new(false)); + let logger = Arc::new(TestLogger::new()); + let primary_namespace = "datastore_test_primary".to_string(); + let secondary_namespace = "datastore_test_secondary".to_string(); + let data_store: DataStore> = DataStore::new( + Vec::new(), + primary_namespace.clone(), + secondary_namespace.clone(), + Arc::clone(&store), + logger, + ); + + let id = TestObjectId { id: [42u8; 4] }; + assert!(data_store.get(&id).is_none()); + + let store_key = id.encode_to_hex_str(); + + // Check we start empty. + assert!(KVStoreSync::read(&*store, &primary_namespace, &secondary_namespace, &store_key) + .is_err()); + + // Check we successfully store an object and return `false` + let object = TestObject { id, data: [23u8; 3] }; + assert_eq!(Ok(false), data_store.insert(object.clone())); + assert_eq!(Some(object), data_store.get(&id)); + assert!(KVStoreSync::read(&*store, &primary_namespace, &secondary_namespace, &store_key) + .is_ok()); + + // Test re-insertion returns `true` + let mut override_object = object.clone(); + override_object.data = [24u8; 3]; + assert_eq!(Ok(true), data_store.insert(override_object)); + assert_eq!(Some(override_object), data_store.get(&id)); + + // Check update returns `Updated` + let update = TestObjectUpdate { id, data: [25u8; 3] }; + assert_eq!(Ok(DataStoreUpdateResult::Updated), data_store.update(&update)); + assert_eq!(data_store.get(&id).unwrap().data, [25u8; 3]); + + // Check no-op update yields `Unchanged` + let update = TestObjectUpdate { id, data: [25u8; 3] }; + assert_eq!(Ok(DataStoreUpdateResult::Unchanged), data_store.update(&update)); + + // Check bogus update yields `NotFound` + let bogus_id = TestObjectId { id: [84u8; 4] }; + let update = TestObjectUpdate { id: bogus_id, data: [12u8; 3] }; + assert_eq!(Ok(DataStoreUpdateResult::NotFound), data_store.update(&update)); + + // Check `insert_or_update` inserts unknown objects + let iou_id = TestObjectId { id: [55u8; 4] }; + let iou_object = TestObject { id: iou_id, data: [34u8; 3] }; + assert_eq!(Ok(true), data_store.insert_or_update(iou_object.clone())); + + // Check `insert_or_update` doesn't update the same object + assert_eq!(Ok(false), data_store.insert_or_update(iou_object.clone())); + + // Check `insert_or_update` updates if object changed + let mut new_iou_object = iou_object; + new_iou_object.data[0] += 1; + assert_eq!(Ok(true), data_store.insert_or_update(new_iou_object)); + } +} diff --git a/src/error.rs b/src/error.rs index babe3258e9..d0f6aec7ae 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,14 +5,15 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::fmt; + use bdk_chain::bitcoin::psbt::ExtractTxError as BdkExtractTxError; use bdk_chain::local_chain::CannotConnectError as BdkChainConnectionError; use bdk_chain::tx_graph::CalculateFeeError as BdkChainCalculateFeeError; use bdk_wallet::error::CreateTxError as BdkCreateTxError; +#[allow(deprecated)] use bdk_wallet::signer::SignerError as BdkSignerError; -use std::fmt; - #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// An error that possibly needs to be handled by the user. pub enum Error { @@ -122,6 +123,10 @@ pub enum Error { LiquidityFeeTooHigh, /// The router failed to find a route to the given destination. RouteNotFound, + /// The given blinded paths are invalid. + InvalidBlindedPaths, + /// Asynchronous payment services are disabled. + AsyncPaymentServicesDisabled, } impl fmt::Display for Error { @@ -195,6 +200,10 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::InvalidBlindedPaths => write!(f, "The given blinded paths are invalid."), + Self::AsyncPaymentServicesDisabled => { + write!(f, "Asynchronous payment services are disabled.") + }, Self::RouteNotFound => { write!(f, "The router failed to find a route to the given destination.") }, @@ -204,6 +213,7 @@ impl fmt::Display for Error { impl std::error::Error for Error {} +#[allow(deprecated)] impl From for Error { fn from(_: BdkSignerError) -> Self { Self::OnchainTxSigningFailed diff --git a/src/event.rs b/src/event.rs index 54103f5b99..318dcccf41 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,55 +5,55 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::types::{CustomTlvRecord, DynStore, Sweeper, Wallet}; - -use crate::{ - hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, - UserChannelId, -}; - -use crate::config::{may_announce_channel, Config}; -use crate::connection::ConnectionManager; -use crate::fee_estimator::ConfirmationTarget; - -use crate::payment::store::{ - PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, - PaymentStore, -}; - -use crate::io::{ - EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, -}; -use crate::logger::{log_debug, log_error, log_info, LdkLogger}; +use core::future::Future; +use core::task::{Poll, Waker}; +use std::collections::VecDeque; +use std::ops::Deref; +use std::sync::{Arc, Condvar, Mutex}; +use bitcoin::blockdata::locktime::absolute::LockTime; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Amount, OutPoint}; use lightning::events::bump_transaction::BumpTransactionEvent; -use lightning::events::{ClosureReason, PaymentPurpose, ReplayEvent}; -use lightning::events::{Event as LdkEvent, PaymentFailureReason}; +use lightning::events::{ + ClosureReason, Event as LdkEvent, PaymentFailureReason, PaymentPurpose, ReplayEvent, +}; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; use lightning::ln::types::ChannelId; use lightning::routing::gossip::NodeId; use lightning::routing::router::RouteHop; +use lightning::util::config::{ + ChannelConfigOverrides, ChannelConfigUpdate, ChannelHandshakeConfigUpdate, +}; use lightning::util::errors::APIError; +use lightning::util::persist::KVStoreSync; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; - -use lightning_types::payment::{PaymentHash, PaymentPreimage}; - use lightning_liquidity::lsps2::utils::compute_opening_fee; - -use bitcoin::blockdata::locktime::absolute::LockTime; -use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint}; - +use lightning_types::payment::{PaymentHash, PaymentPreimage}; use rand::{thread_rng, Rng}; -use core::future::Future; -use core::task::{Poll, Waker}; -use std::collections::VecDeque; -use std::ops::Deref; -use std::sync::{Arc, Condvar, Mutex, RwLock}; -use std::time::Duration; +use crate::config::{may_announce_channel, Config}; +use crate::connection::ConnectionManager; +use crate::data_store::DataStoreUpdateResult; +use crate::fee_estimator::ConfirmationTarget; +use crate::io::{ + EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use crate::liquidity::LiquiditySource; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; +use crate::payment::store::{ + PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, +}; +use crate::runtime::Runtime; +use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet}; +use crate::{ + hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, + UserChannelId, +}; /// An event emitted by [`Node`], which should be handled by the user. /// @@ -386,24 +386,24 @@ where fn persist_queue(&self, locked_queue: &VecDeque) -> Result<(), Error> { let data = EventQueueSerWrapper(locked_queue).encode(); - self.kv_store - .write( + KVStoreSync::write( + &*self.kv_store, + EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_KEY, + data, + ) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_KEY, - &data, - ) - .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_KEY, - e - ); - Error::PersistenceFailed - })?; + e + ); + Error::PersistenceFailed + })?; Ok(()) } } @@ -483,11 +483,15 @@ where connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, - payment_store: Arc>, + liquidity_source: Option>>>, + payment_store: Arc, peer_store: Arc>, - runtime: Arc>>>, + runtime: Arc, logger: L, config: Arc, + static_invoice_store: Option, + onion_messenger: Arc, + om_mailbox: Option>, } impl EventHandler @@ -499,8 +503,11 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, - payment_store: Arc>, peer_store: Arc>, - runtime: Arc>>>, logger: L, config: Arc, + liquidity_source: Option>>>, + payment_store: Arc, peer_store: Arc>, + static_invoice_store: Option, onion_messenger: Arc, + om_mailbox: Option>, runtime: Arc, logger: L, + config: Arc, ) -> Self { Self { event_queue, @@ -510,11 +517,15 @@ where connection_manager, output_sweeper, network_graph, + liquidity_source, payment_store, peer_store, logger, runtime, config, + static_invoice_store, + onion_messenger, + om_mailbox, } } @@ -574,7 +585,7 @@ where Err(err) => { log_error!(self.logger, "Failed to create funding transaction: {}", err); self.channel_manager - .force_close_without_broadcasting_txn( + .force_close_broadcasting_latest_txn( &temporary_channel_id, &counterparty_node_id, "Failed to create funding transaction".to_string(), @@ -595,13 +606,10 @@ where payment_hash, purpose, amount_msat, - receiver_node_id: _, - via_channel_id: _, - via_user_channel_id: _, claim_deadline, onion_fields, counterparty_skimmed_fee_msat, - payment_id: _, + .. } => { let payment_id = PaymentId(payment_hash.0); log_info!( @@ -699,11 +707,32 @@ where }; } + // If the LSP skimmed anything, update our stored payment. + if counterparty_skimmed_fee_msat > 0 { + match info.kind { + PaymentKind::Bolt11Jit { .. } => { + let update = PaymentDetailsUpdate { + counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)), + ..PaymentDetailsUpdate::new(payment_id) + }; + match self.payment_store.update(&update) { + Ok(_) => (), + Err(e) => { + log_error!(self.logger, "Failed to access payment store: {}", e); + return Err(ReplayEvent()); + }, + }; + } + _ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments."), + } + } + // If this is known by the store but ChannelManager doesn't know the preimage, // the payment has been registered via `_for_hash` variants and needs to be manually claimed via // user interaction. match info.kind { - PaymentKind::Bolt11 { preimage, .. } => { + PaymentKind::Bolt11 { preimage, .. } + | PaymentKind::Bolt11Jit { preimage, .. } => { if purpose.preimage().is_none() { debug_assert!( preimage.is_none(), @@ -771,6 +800,7 @@ where payment_id, kind, Some(amount_msat), + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -811,6 +841,7 @@ where payment_id, kind, Some(amount_msat), + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -924,14 +955,16 @@ where }; match self.payment_store.update(&update) { - Ok(true) => (), - Ok(false) => { + Ok(DataStoreUpdateResult::Updated) | Ok(DataStoreUpdateResult::Unchanged) => ( + // No need to do anything if the idempotent update was applied, which might + // be the result of a replayed event. + ), + Ok(DataStoreUpdateResult::NotFound) => { log_error!( self.logger, - "Payment with ID {} couldn't be found in store", + "Claimed payment with ID {} couldn't be found in store", payment_id, ); - debug_assert!(false); }, Err(e) => { log_error!( @@ -977,6 +1010,7 @@ where let update = PaymentDetailsUpdate { hash: Some(Some(payment_hash)), preimage: Some(Some(payment_preimage)), + fee_paid_msat: Some(fee_paid_msat), status: Some(PaymentStatus::Succeeded), ..PaymentDetailsUpdate::new(payment_id) }; @@ -1074,25 +1108,17 @@ where panic!("Failed to push to event queue"); }); }, - LdkEvent::HTLCHandlingFailed { .. } => {}, - LdkEvent::PendingHTLCsForwardable { time_forwardable } => { - let forwarding_channel_manager = self.channel_manager.clone(); - let min = time_forwardable.as_millis() as u64; - - let runtime_lock = self.runtime.read().unwrap(); - debug_assert!(runtime_lock.is_some()); - - if let Some(runtime) = runtime_lock.as_ref() { - runtime.spawn(async move { - let millis_to_sleep = thread_rng().gen_range(min..min * 5) as u64; - tokio::time::sleep(Duration::from_millis(millis_to_sleep)).await; - - forwarding_channel_manager.process_pending_htlc_forwards(); - }); + LdkEvent::HTLCHandlingFailed { failure_type, .. } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_handling_failed(failure_type).await; } }, LdkEvent::SpendableOutputs { outputs, channel_id } => { - match self.output_sweeper.track_spendable_outputs(outputs, channel_id, true, None) { + match self + .output_sweeper + .track_spendable_outputs(outputs, channel_id, true, None) + .await + { Ok(_) => return Ok(()), Err(_) => { log_error!(self.logger, "Failed to track spendable outputs"); @@ -1109,23 +1135,21 @@ where is_announced, params: _, } => { - if is_announced && !may_announce_channel(&*self.config) { - log_error!( - self.logger, - "Rejecting inbound announced channel from peer {} as not all required details are set. Please ensure node alias and listening addresses have been configured.", - counterparty_node_id, - ); + if is_announced { + if let Err(err) = may_announce_channel(&*self.config) { + log_error!(self.logger, "Rejecting inbound announced channel from peer {} due to missing configuration: {}", counterparty_node_id, err); - self.channel_manager - .force_close_without_broadcasting_txn( - &temporary_channel_id, - &counterparty_node_id, - "Channel request rejected".to_string(), - ) - .unwrap_or_else(|e| { - log_error!(self.logger, "Failed to reject channel: {:?}", e) - }); - return Ok(()); + self.channel_manager + .force_close_broadcasting_latest_txn( + &temporary_channel_id, + &counterparty_node_id, + "Channel request rejected".to_string(), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return Ok(()); + } } let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); @@ -1154,11 +1178,13 @@ where if spendable_amount_sats < required_amount_sats { log_error!( self.logger, - "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves.", + "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves. Available: {}/{}sats", counterparty_node_id, + spendable_amount_sats, + required_amount_sats, ); self.channel_manager - .force_close_without_broadcasting_txn( + .force_close_broadcasting_latest_txn( &temporary_channel_id, &counterparty_node_id, "Channel request rejected".to_string(), @@ -1175,7 +1201,7 @@ where counterparty_node_id, ); self.channel_manager - .force_close_without_broadcasting_txn( + .force_close_broadcasting_latest_txn( &temporary_channel_id, &counterparty_node_id, "Channel request rejected".to_string(), @@ -1187,19 +1213,46 @@ where } } - let user_channel_id: u128 = rand::thread_rng().gen::(); + let user_channel_id: u128 = thread_rng().gen::(); let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id); + let mut channel_override_config = None; + if let Some((lsp_node_id, _)) = self + .liquidity_source + .as_ref() + .and_then(|ls| ls.as_ref().get_lsps2_lsp_details()) + { + if lsp_node_id == counterparty_node_id { + // When we're an LSPS2 client, allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll + // check that they don't take too much before claiming. + // + // We also set maximum allowed inbound HTLC value in flight + // to 100%. We should eventually be able to set this on a per-channel basis, but for + // now we just bump the default for all channels. + channel_override_config = Some(ChannelConfigOverrides { + handshake_overrides: Some(ChannelHandshakeConfigUpdate { + max_inbound_htlc_value_in_flight_percent_of_channel: Some(100), + ..Default::default() + }), + update_overrides: Some(ChannelConfigUpdate { + accept_underpaying_htlcs: Some(true), + ..Default::default() + }), + }); + } + } let res = if allow_0conf { self.channel_manager.accept_inbound_channel_from_trusted_peer_0conf( &temporary_channel_id, &counterparty_node_id, user_channel_id, + channel_override_config, ) } else { self.channel_manager.accept_inbound_channel( &temporary_channel_id, &counterparty_node_id, user_channel_id, + channel_override_config, ) }; @@ -1240,6 +1293,78 @@ where claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { + { + let read_only_network_graph = self.network_graph.read_only(); + let nodes = read_only_network_graph.nodes(); + let channels = self.channel_manager.list_channels(); + + let node_str = |channel_id: &Option| { + channel_id + .and_then(|channel_id| { + channels.iter().find(|c| c.channel_id == channel_id) + }) + .and_then(|channel| { + nodes.get(&NodeId::from_pubkey(&channel.counterparty.node_id)) + }) + .map_or("private_node".to_string(), |node| { + node.announcement_info + .as_ref() + .map_or("unnamed node".to_string(), |ann| { + format!("node {}", ann.alias()) + }) + }) + }; + let channel_str = |channel_id: &Option| { + channel_id + .map(|channel_id| format!(" with channel {}", channel_id)) + .unwrap_or_default() + }; + let from_prev_str = format!( + " from {}{}", + node_str(&prev_channel_id), + channel_str(&prev_channel_id) + ); + let to_next_str = format!( + " to {}{}", + node_str(&next_channel_id), + channel_str(&next_channel_id) + ); + + let fee_earned = total_fee_earned_msat.unwrap_or(0); + if claim_from_onchain_tx { + log_info!( + self.logger, + "Forwarded payment{}{} of {}msat, earning {}msat in fees from claiming onchain.", + from_prev_str, + to_next_str, + outbound_amount_forwarded_msat.unwrap_or(0), + fee_earned, + ); + } else { + log_info!( + self.logger, + "Forwarded payment{}{} of {}msat, earning {}msat in fees.", + from_prev_str, + to_next_str, + outbound_amount_forwarded_msat.unwrap_or(0), + fee_earned, + ); + } + } + + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + if let Some(skimmed_fee_msat) = skimmed_fee_msat { + liquidity_source + .handle_payment_forwarded(next_channel_id, skimmed_fee_msat) + .await; + } else { + debug_assert!( + false, + "We expect skimmed_fee_msat to be set since LDK 0.0.122" + ); + } + } + let event = Event::PaymentForwarded { prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), @@ -1256,59 +1381,6 @@ where log_error!(self.logger, "Failed to push to event queue: {}", e); ReplayEvent() })?; - - let read_only_network_graph = self.network_graph.read_only(); - let nodes = read_only_network_graph.nodes(); - let channels = self.channel_manager.list_channels(); - - let node_str = |channel_id: &Option| { - channel_id - .and_then(|channel_id| channels.iter().find(|c| c.channel_id == channel_id)) - .and_then(|channel| { - nodes.get(&NodeId::from_pubkey(&channel.counterparty.node_id)) - }) - .map_or("private_node".to_string(), |node| { - node.announcement_info - .as_ref() - .map_or("unnamed node".to_string(), |ann| { - format!("node {}", ann.alias()) - }) - }) - }; - let channel_str = |channel_id: &Option| { - channel_id - .map(|channel_id| format!(" with channel {}", channel_id)) - .unwrap_or_default() - }; - let from_prev_str = format!( - " from {}{}", - node_str(&prev_channel_id), - channel_str(&prev_channel_id) - ); - let to_next_str = - format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); - - let fee_earned = total_fee_earned_msat.unwrap_or(0); - let outbound_amount_forwarded_msat = outbound_amount_forwarded_msat.unwrap_or(0); - if claim_from_onchain_tx { - log_info!( - self.logger, - "Forwarded payment{}{} of {}msat, earning {}msat in fees from claiming onchain.", - from_prev_str, - to_next_str, - outbound_amount_forwarded_msat, - fee_earned, - ); - } else { - log_info!( - self.logger, - "Forwarded payment{}{} of {}msat, earning {}msat in fees.", - from_prev_str, - to_next_str, - outbound_amount_forwarded_msat, - fee_earned, - ); - } }, LdkEvent::ChannelPending { channel_id, @@ -1382,6 +1454,12 @@ where counterparty_node_id, ); + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source + .handle_channel_ready(user_channel_id, &channel_id, &counterparty_node_id) + .await; + } + let event = Event::ChannelReady { channel_id, user_channel_id: UserChannelId(user_channel_id), @@ -1420,36 +1498,49 @@ where }; }, LdkEvent::DiscardFunding { .. } => {}, - LdkEvent::HTLCIntercepted { .. } => {}, + LdkEvent::HTLCIntercepted { + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + .. + } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source + .handle_htlc_intercepted( + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) + .await; + } + }, LdkEvent::InvoiceReceived { .. } => { debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); }, LdkEvent::ConnectionNeeded { node_id, addresses } => { - let runtime_lock = self.runtime.read().unwrap(); - debug_assert!(runtime_lock.is_some()); - - if let Some(runtime) = runtime_lock.as_ref() { - let spawn_logger = self.logger.clone(); - let spawn_cm = Arc::clone(&self.connection_manager); - runtime.spawn(async move { - for addr in &addresses { - match spawn_cm.connect_peer_if_necessary(node_id, addr.clone()).await { - Ok(()) => { - return; - }, - Err(e) => { - log_error!( - spawn_logger, - "Failed to establish connection to peer {}@{}: {}", - node_id, - addr, - e - ); - }, - } + let spawn_logger = self.logger.clone(); + let spawn_cm = Arc::clone(&self.connection_manager); + let future = async move { + for addr in &addresses { + match spawn_cm.connect_peer_if_necessary(node_id, addr.clone()).await { + Ok(()) => { + return; + }, + Err(e) => { + log_error!( + spawn_logger, + "Failed to establish connection to peer {}@{}: {}", + node_id, + addr, + e + ); + }, } - }); - } + } + }; + self.runtime.spawn_cancellable_background_task(future); }, LdkEvent::BumpTransaction(bte) => { match bte { @@ -1477,13 +1568,114 @@ where BumpTransactionEvent::HTLCResolution { .. } => {}, } - self.bump_tx_event_handler.handle_event(&bte); + self.bump_tx_event_handler.handle_event(&bte).await; }, - LdkEvent::OnionMessageIntercepted { .. } => { - debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); + LdkEvent::OnionMessageIntercepted { peer_node_id, message } => { + if let Some(om_mailbox) = self.om_mailbox.as_ref() { + om_mailbox.onion_message_intercepted(peer_node_id, message); + } else { + log_trace!( + self.logger, + "Onion message intercepted, but no onion message mailbox available" + ); + } + }, + LdkEvent::OnionMessagePeerConnected { peer_node_id } => { + if let Some(om_mailbox) = self.om_mailbox.as_ref() { + let messages = om_mailbox.onion_message_peer_connected(peer_node_id); + + for message in messages { + if let Err(e) = + self.onion_messenger.forward_onion_message(message, &peer_node_id) + { + log_trace!( + self.logger, + "Failed to forward onion message to peer {}: {:?}", + peer_node_id, + e + ); + } + } + } }, - LdkEvent::OnionMessagePeerConnected { .. } => { - debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); + + LdkEvent::PersistStaticInvoice { + invoice, + invoice_request_path, + invoice_slot, + recipient_id, + invoice_persisted_path, + } => { + if let Some(store) = self.static_invoice_store.as_ref() { + match store + .handle_persist_static_invoice( + invoice, + invoice_request_path, + invoice_slot, + recipient_id, + ) + .await + { + Ok(_) => { + self.channel_manager.static_invoice_persisted(invoice_persisted_path); + }, + Err(e) => { + log_error!(self.logger, "Failed to persist static invoice: {}", e); + return Err(ReplayEvent()); + }, + }; + } + }, + LdkEvent::StaticInvoiceRequested { + recipient_id, + invoice_slot, + reply_path, + invoice_request, + } => { + if let Some(store) = self.static_invoice_store.as_ref() { + let invoice = + store.handle_static_invoice_requested(&recipient_id, invoice_slot).await; + + match invoice { + Ok(Some((invoice, invoice_request_path))) => { + if let Err(e) = self.channel_manager.respond_to_static_invoice_request( + invoice, + reply_path, + invoice_request, + invoice_request_path, + ) { + log_error!(self.logger, "Failed to send static invoice: {:?}", e); + } + }, + Ok(None) => { + log_trace!( + self.logger, + "No static invoice found for recipient {} and slot {}", + hex_utils::to_string(&recipient_id), + invoice_slot + ); + }, + Err(e) => { + log_error!(self.logger, "Failed to retrieve static invoice: {}", e); + return Err(ReplayEvent()); + }, + } + } + }, + LdkEvent::FundingTransactionReadyForSigning { .. } => { + debug_assert!(false, "We currently don't support interactive-tx, so this event should never be emitted."); + }, + LdkEvent::SplicePending { .. } => { + debug_assert!( + false, + "We currently don't support splicing, so this event should never be emitted." + ); + }, + LdkEvent::SpliceFailed { .. } => { + debug_assert!( + false, + "We currently don't support splicing, so this event should never be emitted." + ); }, } Ok(()) @@ -1492,11 +1684,13 @@ where #[cfg(test)] mod tests { - use super::*; - use lightning::util::test_utils::{TestLogger, TestStore}; use std::sync::atomic::{AtomicU16, Ordering}; use std::time::Duration; + use lightning::util::test_utils::{TestLogger, TestStore}; + + use super::*; + #[tokio::test] async fn event_queue_persistence() { let store: Arc = Arc::new(TestStore::new(false)); @@ -1519,13 +1713,13 @@ mod tests { } // Check we can read back what we persisted. - let persisted_bytes = store - .read( - EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_KEY, - ) - .unwrap(); + let persisted_bytes = KVStoreSync::read( + &*store, + EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_KEY, + ) + .unwrap(); let deser_event_queue = EventQueue::read(&mut &persisted_bytes[..], (Arc::clone(&store), logger)).unwrap(); assert_eq!(deser_event_queue.wait_next_event(), expected_event); diff --git a/src/fee_estimator.rs b/src/fee_estimator.rs index f000245aa9..b787ecd336 100644 --- a/src/fee_estimator.rs +++ b/src/fee_estimator.rs @@ -5,15 +5,15 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; -use lightning::chain::chaininterface::FeeEstimator as LdkFeeEstimator; -use lightning::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; - -use bitcoin::FeeRate; - use std::collections::HashMap; use std::sync::RwLock; +use bitcoin::FeeRate; +use lightning::chain::chaininterface::{ + ConfirmationTarget as LdkConfirmationTarget, FeeEstimator as LdkFeeEstimator, + FEERATE_FLOOR_SATS_PER_KW, +}; + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub(crate) enum ConfirmationTarget { /// The default target for onchain payments. @@ -150,6 +150,17 @@ pub(crate) fn apply_post_estimation_adjustments( .max(FEERATE_FLOOR_SATS_PER_KW as u64); FeeRate::from_sat_per_kwu(slightly_less_than_background) }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::MaximumFeeEstimate) => { + // MaximumFeeEstimate is mostly used for protection against fee-inflation attacks. As + // users were previously impacted by this limit being too restrictive (read: too low), + // we bump it here a bit to give them some leeway. + let slightly_bump = estimated_rate + .to_sat_per_kwu() + .saturating_mul(11) + .saturating_div(10) + .saturating_add(2500); + FeeRate::from_sat_per_kwu(slightly_bump) + }, _ => estimated_rate, } } diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs new file mode 100644 index 0000000000..32464d0445 --- /dev/null +++ b/src/ffi/mod.rs @@ -0,0 +1,47 @@ +// 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 + +#[cfg(feature = "uniffi")] +mod types; + +#[cfg(feature = "uniffi")] +pub use types::*; + +#[cfg(feature = "uniffi")] +pub fn maybe_deref(wrapped_type: &std::sync::Arc) -> &R +where + T: AsRef, +{ + wrapped_type.as_ref().as_ref() +} + +#[cfg(feature = "uniffi")] +pub fn maybe_try_convert_enum(wrapped_type: &T) -> Result +where + for<'a> R: TryFrom<&'a T, Error = crate::error::Error>, +{ + R::try_from(wrapped_type) +} + +#[cfg(feature = "uniffi")] +pub fn maybe_wrap(ldk_type: impl Into) -> std::sync::Arc { + std::sync::Arc::new(ldk_type.into()) +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_deref(value: &T) -> &T { + value +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_try_convert_enum(value: &T) -> Result<&T, crate::error::Error> { + Ok(value) +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_wrap(value: T) -> T { + value +} diff --git a/src/ffi/types.rs b/src/ffi/types.rs new file mode 100644 index 0000000000..b64bd730eb --- /dev/null +++ b/src/ffi/types.rs @@ -0,0 +1,1668 @@ +// 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. + +// Importing these items ensures they are accessible in the uniffi bindings +// without introducing unused import warnings in lib.rs. +// +// Make sure to add any re-exported items that need to be used in uniffi below. + +use std::convert::TryInto; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; + +pub use bip39::Mnemonic; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; +pub use lightning::chain::channelmonitor::BalanceSource; +pub use lightning::events::{ClosureReason, PaymentFailureReason}; +use lightning::ln::channelmanager::PaymentId; +pub use lightning::ln::types::ChannelId; +use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; +pub use lightning::offers::offer::OfferId; +use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; +use lightning::offers::refund::Refund as LdkRefund; +pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; +pub use lightning::routing::router::RouteParametersConfig; +use lightning::util::ser::Writeable; +use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; +pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; +pub use lightning_liquidity::lsps0::ser::LSPSDateTime; +pub use lightning_liquidity::lsps1::msgs::{ + LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState, +}; +pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +pub use lightning_types::string::UntrustedString; +pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; + +use crate::builder::sanitize_alias; +pub use crate::config::{ + default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, + EsploraSyncConfig, MaxDustHTLCExposure, +}; +use crate::error::Error; +pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; +pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig}; +pub use crate::logger::{LogLevel, LogRecord, LogWriter}; +pub use crate::payment::store::{ + ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, +}; +pub use crate::payment::QrPaymentResult; +use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId}; + +impl UniffiCustomTypeConverter for PublicKey { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(key) = PublicKey::from_str(&val) { + return Ok(key); + } + + Err(Error::InvalidPublicKey.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for NodeId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(key) = NodeId::from_str(&val) { + return Ok(key); + } + + Err(Error::InvalidNodeId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Address { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(addr) = Address::from_str(&val) { + return Ok(addr.assume_checked()); + } + + Err(Error::InvalidAddress.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OfferAmount { + Bitcoin { amount_msats: u64 }, + Currency { iso4217_code: String, amount: u64 }, +} + +impl From for OfferAmount { + fn from(ldk_amount: LdkAmount) -> Self { + match ldk_amount { + LdkAmount::Bitcoin { amount_msats } => OfferAmount::Bitcoin { amount_msats }, + LdkAmount::Currency { iso4217_code, amount } => { + OfferAmount::Currency { iso4217_code: iso4217_code.as_str().to_owned(), amount } + }, + } + } +} + +/// An `Offer` is a potentially long-lived proposal for payment of a good or service. +/// +/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a +/// customer may request an [`Bolt12Invoice`] for a specific quantity and using an amount sufficient +/// to cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`]. +/// +/// Offers may be denominated in currency other than bitcoin but are ultimately paid using the +/// latter. +/// +/// Through the use of [`BlindedMessagePath`]s, offers provide recipient privacy. +/// +/// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest +/// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice +/// [`Offer`]: lightning::offers::Offer:amount +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Offer { + pub(crate) inner: LdkOffer, +} + +impl Offer { + pub fn from_str(offer_str: &str) -> Result { + offer_str.parse() + } + + /// Returns the id of the offer. + pub fn id(&self) -> OfferId { + OfferId(self.inner.id().0) + } + + /// Whether the offer has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// A complete description of the purpose of the payment. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn description(&self) -> Option { + self.inner.description().map(|printable| printable.to_string()) + } + + /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn issuer(&self) -> Option { + self.inner.issuer().map(|printable| printable.to_string()) + } + + /// The minimum amount required for a successful payment of a single item. + pub fn amount(&self) -> Option { + self.inner.amount().map(|amount| amount.into()) + } + + /// Returns whether the given quantity is valid for the offer. + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + self.inner.is_valid_quantity(quantity) + } + + /// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer. + /// + /// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest + pub fn expects_quantity(&self) -> bool { + self.inner.expects_quantity() + } + + /// Returns whether the given chain is supported by the offer. + pub fn supports_chain(&self, chain: Network) -> bool { + self.inner.supports_chain(chain.chain_hash()) + } + + /// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet). + /// + /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats) + /// for the selected chain. + pub fn chains(&self) -> Vec { + self.inner.chains().into_iter().filter_map(Network::from_chain_hash).collect() + } + + /// Opaque bytes set by the originator. + /// + /// Useful for authentication and validating fields since it is reflected in `invoice_request` + /// messages along with all the other fields from the `offer`. + pub fn metadata(&self) -> Option> { + self.inner.metadata().cloned() + } + + /// Seconds since the Unix epoch when an invoice should no longer be requested. + /// + /// If `None`, the offer does not expire. + pub fn absolute_expiry_seconds(&self) -> Option { + self.inner.absolute_expiry().map(|duration| duration.as_secs()) + } + + /// The public key corresponding to the key used by the recipient to sign invoices. + /// - If [`Offer::paths`] is empty, MUST be `Some` and contain the recipient's node id for + /// sending an [`InvoiceRequest`]. + /// - If [`Offer::paths`] is not empty, MAY be `Some` and contain a transient id. + /// - If `None`, the signing pubkey will be the final blinded node id from the + /// [`BlindedMessagePath`] in [`Offer::paths`] used to send the [`InvoiceRequest`]. + /// + /// See also [`Bolt12Invoice::signing_pubkey`]. + /// + /// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest + /// [`Bolt12Invoice::signing_pubkey`]: lightning::offers::invoice::Bolt12Invoice::signing_pubkey + pub fn issuer_signing_pubkey(&self) -> Option { + self.inner.issuer_signing_pubkey() + } +} + +impl std::str::FromStr for Offer { + type Err = Error; + + fn from_str(offer_str: &str) -> Result { + offer_str + .parse::() + .map(|offer| Offer { inner: offer }) + .map_err(|_| Error::InvalidOffer) + } +} + +impl From for Offer { + fn from(offer: LdkOffer) -> Self { + Offer { inner: offer } + } +} + +impl Deref for Offer { + type Target = LdkOffer; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Offer { + fn as_ref(&self) -> &LdkOffer { + self.deref() + } +} + +impl std::fmt::Display for Offer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +/// A `Refund` is a request to send an [`Bolt12Invoice`] without a preceding [`Offer`]. +/// +/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to +/// recoup their funds. A refund may be used more generally as an "offer for money", such as with a +/// bitcoin ATM. +/// +/// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice +/// [`Offer`]: lightning::offers::offer::Offer +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Refund { + pub(crate) inner: LdkRefund, +} + +impl Refund { + pub fn from_str(refund_str: &str) -> Result { + refund_str.parse() + } + + /// A complete description of the purpose of the refund. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn description(&self) -> String { + self.inner.description().to_string() + } + + /// Seconds since the Unix epoch when an invoice should no longer be sent. + /// + /// If `None`, the refund does not expire. + pub fn absolute_expiry_seconds(&self) -> Option { + self.inner.absolute_expiry().map(|duration| duration.as_secs()) + } + + /// Whether the refund has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn issuer(&self) -> Option { + self.inner.issuer().map(|printable| printable.to_string()) + } + + /// An unpredictable series of bytes, typically containing information about the derivation of + /// [`payer_signing_pubkey`]. + /// + /// [`payer_signing_pubkey`]: Self::payer_signing_pubkey + pub fn payer_metadata(&self) -> Vec { + self.inner.payer_metadata().to_vec() + } + + /// A chain that the refund is valid for. + pub fn chain(&self) -> Option { + Network::try_from(self.inner.chain()).ok() + } + + /// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]). + /// + /// [`chain`]: Self::chain + pub fn amount_msats(&self) -> u64 { + self.inner.amount_msats() + } + + /// The quantity of an item that refund is for. + pub fn quantity(&self) -> Option { + self.inner.quantity() + } + + /// A public node id to send to in the case where there are no [`paths`]. + /// + /// Otherwise, a possibly transient pubkey. + /// + /// [`paths`]: lightning::offers::refund::Refund::paths + pub fn payer_signing_pubkey(&self) -> PublicKey { + self.inner.payer_signing_pubkey() + } + + /// Payer provided note to include in the invoice. + pub fn payer_note(&self) -> Option { + self.inner.payer_note().map(|printable| printable.to_string()) + } +} + +impl std::str::FromStr for Refund { + type Err = Error; + + fn from_str(refund_str: &str) -> Result { + refund_str + .parse::() + .map(|refund| Refund { inner: refund }) + .map_err(|_| Error::InvalidRefund) + } +} + +impl From for Refund { + fn from(refund: LdkRefund) -> Self { + Refund { inner: refund } + } +} + +impl Deref for Refund { + type Target = LdkRefund; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Refund { + fn as_ref(&self) -> &LdkRefund { + self.deref() + } +} + +impl std::fmt::Display for Refund { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bolt12Invoice { + pub(crate) inner: LdkBolt12Invoice, +} + +impl Bolt12Invoice { + pub fn from_str(invoice_str: &str) -> Result { + invoice_str.parse() + } + + /// SHA256 hash of the payment preimage that will be given in return for paying the invoice. + pub fn payment_hash(&self) -> PaymentHash { + PaymentHash(self.inner.payment_hash().0) + } + + /// The minimum amount required for a successful payment of the invoice. + pub fn amount_msats(&self) -> u64 { + self.inner.amount_msats() + } + + /// The minimum amount required for a successful payment of a single item. + /// + /// From [`Offer::amount`]; `None` if the invoice was created in response to a [`Refund`] or if + /// the [`Offer`] did not set it. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`Offer::amount`]: lightning::offers::offer::Offer::amount + /// [`Refund`]: lightning::offers::refund::Refund + pub fn amount(&self) -> Option { + self.inner.amount().map(|amount| amount.into()) + } + + /// A typically transient public key corresponding to the key used to sign the invoice. + /// + /// If the invoices was created in response to an [`Offer`], then this will be: + /// - [`Offer::issuer_signing_pubkey`] if it's `Some`, otherwise + /// - the final blinded node id from a [`BlindedMessagePath`] in [`Offer::paths`] if `None`. + /// + /// If the invoice was created in response to a [`Refund`], then it is a valid pubkey chosen by + /// the recipient. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`Offer::issuer_signing_pubkey`]: lightning::offers::offer::Offer::issuer_signing_pubkey + /// [`Offer::paths`]: lightning::offers::offer::Offer::paths + /// [`Refund`]: lightning::offers::refund::Refund + pub fn signing_pubkey(&self) -> PublicKey { + self.inner.signing_pubkey() + } + + /// Duration since the Unix epoch when the invoice was created. + pub fn created_at(&self) -> u64 { + self.inner.created_at().as_secs() + } + + /// Seconds since the Unix epoch when an invoice should no longer be requested. + /// + /// From [`Offer::absolute_expiry`] or [`Refund::absolute_expiry`]. + /// + /// [`Offer::absolute_expiry`]: lightning::offers::offer::Offer::absolute_expiry + pub fn absolute_expiry_seconds(&self) -> Option { + self.inner.absolute_expiry().map(|duration| duration.as_secs()) + } + + /// When the invoice has expired and therefore should no longer be paid. + pub fn relative_expiry(&self) -> u64 { + self.inner.relative_expiry().as_secs() + } + + /// Whether the invoice has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// A complete description of the purpose of the originating offer or refund. + /// + /// From [`Offer::description`] or [`Refund::description`]. + /// + /// [`Offer::description`]: lightning::offers::offer::Offer::description + /// [`Refund::description`]: lightning::offers::refund::Refund::description + pub fn description(&self) -> Option { + self.inner.description().map(|printable| printable.to_string()) + } + + /// The issuer of the offer or refund. + /// + /// From [`Offer::issuer`] or [`Refund::issuer`]. + /// + /// [`Offer::issuer`]: lightning::offers::offer::Offer::issuer + /// [`Refund::issuer`]: lightning::offers::refund::Refund::issuer + pub fn issuer(&self) -> Option { + self.inner.issuer().map(|printable| printable.to_string()) + } + + /// A payer-provided note reflected back in the invoice. + /// + /// From [`InvoiceRequest::payer_note`] or [`Refund::payer_note`]. + /// + /// [`Refund::payer_note`]: lightning::offers::refund::Refund::payer_note + pub fn payer_note(&self) -> Option { + self.inner.payer_note().map(|note| note.to_string()) + } + + /// Opaque bytes set by the originating [`Offer`]. + /// + /// From [`Offer::metadata`]; `None` if the invoice was created in response to a [`Refund`] or + /// if the [`Offer`] did not set it. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`Offer::metadata`]: lightning::offers::offer::Offer::metadata + /// [`Refund`]: lightning::offers::refund::Refund + pub fn metadata(&self) -> Option> { + self.inner.metadata().cloned() + } + + /// The quantity of items requested or refunded for. + /// + /// From [`InvoiceRequest::quantity`] or [`Refund::quantity`]. + /// + /// [`Refund::quantity`]: lightning::offers::refund::Refund::quantity + pub fn quantity(&self) -> Option { + self.inner.quantity() + } + + /// Hash that was used for signing the invoice. + pub fn signable_hash(&self) -> Vec { + self.inner.signable_hash().to_vec() + } + + /// A possibly transient pubkey used to sign the invoice request or to send an invoice for a + /// refund in case there are no [`message_paths`]. + /// + /// [`message_paths`]: lightning::offers::invoice::Bolt12Invoice + pub fn payer_signing_pubkey(&self) -> PublicKey { + self.inner.payer_signing_pubkey() + } + + /// The public key used by the recipient to sign invoices. + /// + /// From [`Offer::issuer_signing_pubkey`] and may be `None`; also `None` if the invoice was + /// created in response to a [`Refund`]. + /// + /// [`Offer::issuer_signing_pubkey`]: lightning::offers::offer::Offer::issuer_signing_pubkey + /// [`Refund`]: lightning::offers::refund::Refund + pub fn issuer_signing_pubkey(&self) -> Option { + self.inner.issuer_signing_pubkey() + } + + /// The chain that must be used when paying the invoice; selected from [`offer_chains`] if the + /// invoice originated from an offer. + /// + /// From [`InvoiceRequest::chain`] or [`Refund::chain`]. + /// + /// [`offer_chains`]: lightning::offers::invoice::Bolt12Invoice::offer_chains + /// [`InvoiceRequest::chain`]: lightning::offers::invoice_request::InvoiceRequest::chain + /// [`Refund::chain`]: lightning::offers::refund::Refund::chain + pub fn chain(&self) -> Vec { + self.inner.chain().to_bytes().to_vec() + } + + /// The chains that may be used when paying a requested invoice. + /// + /// From [`Offer::chains`]; `None` if the invoice was created in response to a [`Refund`]. + /// + /// [`Offer::chains`]: lightning::offers::offer::Offer::chains + /// [`Refund`]: lightning::offers::refund::Refund + pub fn offer_chains(&self) -> Option>> { + self.inner + .offer_chains() + .map(|chains| chains.iter().map(|chain| chain.to_bytes().to_vec()).collect()) + } + + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to + /// least-preferred. + pub fn fallback_addresses(&self) -> Vec
{ + self.inner.fallbacks() + } + + /// Writes `self` out to a `Vec`. + pub fn encode(&self) -> Vec { + self.inner.encode() + } +} + +impl std::str::FromStr for Bolt12Invoice { + type Err = Error; + + fn from_str(invoice_str: &str) -> Result { + if let Some(bytes_vec) = hex_utils::to_vec(invoice_str) { + if let Ok(invoice) = LdkBolt12Invoice::try_from(bytes_vec) { + return Ok(Bolt12Invoice { inner: invoice }); + } + } + Err(Error::InvalidInvoice) + } +} + +impl From for Bolt12Invoice { + fn from(invoice: LdkBolt12Invoice) -> Self { + Bolt12Invoice { inner: invoice } + } +} + +impl Deref for Bolt12Invoice { + type Target = LdkBolt12Invoice; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Bolt12Invoice { + fn as_ref(&self) -> &LdkBolt12Invoice { + self.deref() + } +} + +impl UniffiCustomTypeConverter for OfferId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(OfferId(bytes)); + } + } + Err(Error::InvalidOfferId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for PaymentId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentId(bytes)); + } + } + Err(Error::InvalidPaymentId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for PaymentHash { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Ok(hash) = Sha256::from_str(&val) { + Ok(PaymentHash(hash.to_byte_array())) + } else { + Err(Error::InvalidPaymentHash.into()) + } + } + + fn from_custom(obj: Self) -> Self::Builtin { + Sha256::from_slice(&obj.0).unwrap().to_string() + } +} + +impl UniffiCustomTypeConverter for PaymentPreimage { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentPreimage(bytes)); + } + } + Err(Error::InvalidPaymentPreimage.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for PaymentSecret { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(bytes_vec) = hex_utils::to_vec(&val) { + let bytes_res = bytes_vec.try_into(); + if let Ok(bytes) = bytes_res { + return Ok(PaymentSecret(bytes)); + } + } + Err(Error::InvalidPaymentSecret.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for ChannelId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + if let Some(hex_vec) = hex_utils::to_vec(&val) { + if hex_vec.len() == 32 { + let mut channel_id = [0u8; 32]; + channel_id.copy_from_slice(&hex_vec[..]); + return Ok(Self(channel_id)); + } + } + Err(Error::InvalidChannelId.into()) + } + + fn from_custom(obj: Self) -> Self::Builtin { + hex_utils::to_string(&obj.0) + } +} + +impl UniffiCustomTypeConverter for UserChannelId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(UserChannelId(u128::from_str(&val).map_err(|_| Error::InvalidChannelId)?)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.0.to_string() + } +} + +impl UniffiCustomTypeConverter for Txid { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Txid::from_str(&val)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for BlockHash { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(BlockHash::from_str(&val)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for Mnemonic { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Mnemonic::from_str(&val).map_err(|_| Error::InvalidSecretKey)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for SocketAddress { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(SocketAddress::from_str(&val).map_err(|_| Error::InvalidSocketAddress)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for UntrustedString { + type Builtin = String; + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(UntrustedString(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +impl UniffiCustomTypeConverter for NodeAlias { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(sanitize_alias(&val).map_err(|_| Error::InvalidNodeAlias)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_string() + } +} + +/// Represents the description of an invoice which has to be either a directly included string or +/// a hash of a description provided out of band. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Bolt11InvoiceDescription { + /// Contains a full description. + Direct { + /// Description of what the invoice is for + description: String, + }, + /// Contains a hash. + Hash { + /// Hash of the description of what the invoice is for + hash: String, + }, +} + +impl TryFrom<&Bolt11InvoiceDescription> for lightning_invoice::Bolt11InvoiceDescription { + type Error = Error; + + fn try_from(value: &Bolt11InvoiceDescription) -> Result { + match value { + Bolt11InvoiceDescription::Direct { description } => { + Description::new(description.clone()) + .map(lightning_invoice::Bolt11InvoiceDescription::Direct) + .map_err(|_| Error::InvoiceCreationFailed) + }, + Bolt11InvoiceDescription::Hash { hash } => Sha256::from_str(&hash) + .map(lightning_invoice::Sha256) + .map(lightning_invoice::Bolt11InvoiceDescription::Hash) + .map_err(|_| Error::InvoiceCreationFailed), + } + } +} + +impl From for Bolt11InvoiceDescription { + fn from(value: lightning_invoice::Bolt11InvoiceDescription) -> Self { + match value { + lightning_invoice::Bolt11InvoiceDescription::Direct(description) => { + Bolt11InvoiceDescription::Direct { description: description.to_string() } + }, + lightning_invoice::Bolt11InvoiceDescription::Hash(hash) => { + Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) } + }, + } + } +} + +impl<'a> From> for Bolt11InvoiceDescription { + fn from(value: Bolt11InvoiceDescriptionRef<'a>) -> Self { + match value { + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(description) => { + Bolt11InvoiceDescription::Direct { description: description.to_string() } + }, + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(hash) => { + Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) } + }, + } + } +} + +/// Enum representing the crypto currencies (or networks) supported by this library +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Currency { + /// Bitcoin mainnet + Bitcoin, + + /// Bitcoin testnet + BitcoinTestnet, + + /// Bitcoin regtest + Regtest, + + /// Bitcoin simnet + Simnet, + + /// Bitcoin signet + Signet, +} + +impl From for Currency { + fn from(currency: lightning_invoice::Currency) -> Self { + match currency { + lightning_invoice::Currency::Bitcoin => Currency::Bitcoin, + lightning_invoice::Currency::BitcoinTestnet => Currency::BitcoinTestnet, + lightning_invoice::Currency::Regtest => Currency::Regtest, + lightning_invoice::Currency::Simnet => Currency::Simnet, + lightning_invoice::Currency::Signet => Currency::Signet, + } + } +} + +/// A channel descriptor for a hop along a payment path. +/// +/// While this generally comes from BOLT 11's `r` field, this struct includes more fields than are +/// available in BOLT 11. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RouteHintHop { + /// The node_id of the non-target end of the route + pub src_node_id: PublicKey, + /// The short_channel_id of this channel + pub short_channel_id: u64, + /// The fees which must be paid to use this channel + pub fees: RoutingFees, + /// The difference in CLTV values between this node and the next node. + pub cltv_expiry_delta: u16, + /// The minimum value, in msat, which must be relayed to the next hop. + pub htlc_minimum_msat: Option, + /// The maximum value in msat available for routing with a single HTLC. + pub htlc_maximum_msat: Option, +} + +impl From for RouteHintHop { + fn from(hop: lightning::routing::router::RouteHintHop) -> Self { + Self { + src_node_id: hop.src_node_id, + short_channel_id: hop.short_channel_id, + cltv_expiry_delta: hop.cltv_expiry_delta, + htlc_minimum_msat: hop.htlc_minimum_msat, + htlc_maximum_msat: hop.htlc_maximum_msat, + fees: hop.fees, + } + } +} + +/// Represents a syntactically and semantically correct lightning BOLT11 invoice. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bolt11Invoice { + pub(crate) inner: LdkBolt11Invoice, +} + +impl Bolt11Invoice { + pub fn from_str(invoice_str: &str) -> Result { + invoice_str.parse() + } + + /// The hash of the [`RawBolt11Invoice`] that was signed. + /// + /// [`RawBolt11Invoice`]: lightning_invoice::RawBolt11Invoice + pub fn signable_hash(&self) -> Vec { + self.inner.signable_hash().to_vec() + } + + /// Returns the hash to which we will receive the preimage on completion of the payment + pub fn payment_hash(&self) -> PaymentHash { + PaymentHash(self.inner.payment_hash().to_byte_array()) + } + + /// Get the payment secret if one was included in the invoice + pub fn payment_secret(&self) -> PaymentSecret { + PaymentSecret(self.inner.payment_secret().0) + } + + /// Returns the amount if specified in the invoice as millisatoshis. + pub fn amount_milli_satoshis(&self) -> Option { + self.inner.amount_milli_satoshis() + } + + /// Returns the invoice's expiry time (in seconds), if present, otherwise [`DEFAULT_EXPIRY_TIME`]. + /// + /// [`DEFAULT_EXPIRY_TIME`]: lightning_invoice::DEFAULT_EXPIRY_TIME + pub fn expiry_time_seconds(&self) -> u64 { + self.inner.expiry_time().as_secs() + } + + /// Returns the `Bolt11Invoice`'s timestamp as seconds since the Unix epoch + pub fn seconds_since_epoch(&self) -> u64 { + self.inner.duration_since_epoch().as_secs() + } + + /// Returns the seconds remaining until the invoice expires. + pub fn seconds_until_expiry(&self) -> u64 { + self.inner.duration_until_expiry().as_secs() + } + + /// Returns whether the invoice has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// Returns whether the expiry time would pass at the given point in time. + /// `at_time_seconds` is the timestamp as seconds since the Unix epoch. + pub fn would_expire(&self, at_time_seconds: u64) -> bool { + self.inner.would_expire(Duration::from_secs(at_time_seconds)) + } + + /// Return the description or a hash of it for longer ones + pub fn invoice_description(&self) -> Bolt11InvoiceDescription { + self.inner.description().into() + } + + /// Returns the invoice's `min_final_cltv_expiry_delta` time, if present, otherwise + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]. + /// + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]: lightning_invoice::DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA + pub fn min_final_cltv_expiry_delta(&self) -> u64 { + self.inner.min_final_cltv_expiry_delta() + } + + /// Returns the network for which the invoice was issued + pub fn network(&self) -> Network { + self.inner.network() + } + + /// Returns the currency for which the invoice was issued + pub fn currency(&self) -> Currency { + self.inner.currency().into() + } + + /// Returns a list of all fallback addresses as [`Address`]es + pub fn fallback_addresses(&self) -> Vec
{ + self.inner.fallback_addresses() + } + + /// Returns a list of all routes included in the invoice as the underlying hints + pub fn route_hints(&self) -> Vec> { + self.inner + .route_hints() + .iter() + .map(|route| route.0.iter().map(|hop| RouteHintHop::from(hop.clone())).collect()) + .collect() + } + + /// Recover the payee's public key (only to be used if none was included in the invoice) + pub fn recover_payee_pub_key(&self) -> PublicKey { + self.inner.recover_payee_pub_key() + } +} + +impl std::str::FromStr for Bolt11Invoice { + type Err = Error; + + fn from_str(invoice_str: &str) -> Result { + match invoice_str.parse::() { + Ok(signed) => match LdkBolt11Invoice::from_signed(signed) { + Ok(invoice) => Ok(Bolt11Invoice { inner: invoice }), + Err(_) => Err(Error::InvalidInvoice), + }, + Err(_) => Err(Error::InvalidInvoice), + } + } +} + +impl From for Bolt11Invoice { + fn from(invoice: LdkBolt11Invoice) -> Self { + Bolt11Invoice { inner: invoice } + } +} + +impl Deref for Bolt11Invoice { + type Target = LdkBolt11Invoice; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Bolt11Invoice { + fn as_ref(&self) -> &LdkBolt11Invoice { + self.deref() + } +} + +impl std::fmt::Display for Bolt11Invoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1PaymentInfo { + /// A Lightning payment using BOLT 11. + pub bolt11: Option, + /// An onchain payment. + pub onchain: Option, +} + +#[cfg(feature = "uniffi")] +impl From for LSPS1PaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo) -> Self { + LSPS1PaymentInfo { + bolt11: value.bolt11.map(|b| b.into()), + onchain: value.onchain.map(|o| o.into()), + } + } +} + +/// An onchain payment. +#[cfg(feature = "uniffi")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1OnchainPaymentInfo { + /// Indicates the current state of the payment. + pub state: lightning_liquidity::lsps1::msgs::LSPS1PaymentState, + /// The datetime when the payment option expires. + pub expires_at: LSPSDateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel + /// opened. + pub address: bitcoin::Address, + /// The minimum number of block confirmations that are required for the on-chain payment to be + /// considered confirmed. + pub min_onchain_payment_confirmations: Option, + /// The minimum fee rate for the on-chain payment in case the client wants the payment to be + /// confirmed without a confirmation. + pub min_fee_for_0conf: Arc, + /// The address where the LSP will send the funds if the order fails. + pub refund_onchain_address: Option, +} + +#[cfg(feature = "uniffi")] +impl From for LSPS1OnchainPaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::LSPS1OnchainPaymentInfo) -> Self { + Self { + state: value.state, + expires_at: value.expires_at, + fee_total_sat: value.fee_total_sat, + order_total_sat: value.order_total_sat, + address: value.address, + min_onchain_payment_confirmations: value.min_onchain_payment_confirmations, + min_fee_for_0conf: Arc::new(value.min_fee_for_0conf), + refund_onchain_address: value.refund_onchain_address, + } + } +} +/// A Lightning payment using BOLT 11. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1Bolt11PaymentInfo { + /// Indicates the current state of the payment. + pub state: LSPS1PaymentState, + /// The datetime when the payment option expires. + pub expires_at: LSPSDateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// A BOLT11 invoice the client can pay to have to channel opened. + pub invoice: Arc, +} + +impl From for LSPS1Bolt11PaymentInfo { + fn from(info: lightning_liquidity::lsps1::msgs::LSPS1Bolt11PaymentInfo) -> Self { + Self { + state: info.state, + expires_at: info.expires_at, + fee_total_sat: info.fee_total_sat, + order_total_sat: info.order_total_sat, + invoice: Arc::new(info.invoice.into()), + } + } +} + +impl UniffiCustomTypeConverter for LSPS1OrderId { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(Self(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.0 + } +} + +impl UniffiCustomTypeConverter for LSPSDateTime { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(LSPSDateTime::from_str(&val).map_err(|_| Error::InvalidDateTime)?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.to_rfc3339() + } +} + +#[cfg(test)] +mod tests { + use std::num::NonZeroU64; + use std::time::{SystemTime, UNIX_EPOCH}; + + use lightning::offers::offer::{OfferBuilder, Quantity}; + use lightning::offers::refund::RefundBuilder; + + use super::*; + + fn create_test_bolt11_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { + let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; + let ldk_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap(); + let wrapped_invoice = Bolt11Invoice::from(ldk_invoice.clone()); + (ldk_invoice, wrapped_invoice) + } + + fn create_test_offer() -> (LdkOffer, Offer) { + let pubkey = bitcoin::secp256k1::PublicKey::from_str( + "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + ) + .unwrap(); + + let expiry = + (SystemTime::now() + Duration::from_secs(3600)).duration_since(UNIX_EPOCH).unwrap(); + + let quantity = NonZeroU64::new(10_000).unwrap(); + + let builder = OfferBuilder::new(pubkey) + .description("Test offer description".to_string()) + .amount_msats(100_000) + .issuer("Offer issuer".to_string()) + .absolute_expiry(expiry) + .chain(Network::Bitcoin) + .supported_quantity(Quantity::Bounded(quantity)) + .metadata(vec![ + 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, + ]) + .unwrap(); + + let ldk_offer = builder.build().unwrap(); + let wrapped_offer = Offer::from(ldk_offer.clone()); + + (ldk_offer, wrapped_offer) + } + + fn create_test_refund() -> (LdkRefund, Refund) { + let payer_key = bitcoin::secp256k1::PublicKey::from_str( + "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + ) + .unwrap(); + + let expiry = + (SystemTime::now() + Duration::from_secs(3600)).duration_since(UNIX_EPOCH).unwrap(); + + let builder = RefundBuilder::new("Test refund".to_string().into(), payer_key, 100_000) + .unwrap() + .description("Test refund description".to_string()) + .absolute_expiry(expiry) + .quantity(3) + .issuer("test_issuer".to_string()); + + let ldk_refund = builder.build().unwrap(); + let wrapped_refund = Refund::from(ldk_refund.clone()); + + (ldk_refund, wrapped_refund) + } + + fn create_test_bolt12_invoice() -> (LdkBolt12Invoice, Bolt12Invoice) { + let invoice_hex = "0020a5b7104b95f17442d6638143ded62b02c2fda98cdf35841713fd0f44b59286560a000e04682cb028502006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f520227105601015821034b4f0765a115caeff6787a8fb2d976c02467a36aea32901539d76473817937c65904546573745a9c00000068000001000003e75203dee4b3e5d48650caf1faadda53ac6e0dc3f509cc5e9c46defb8aeeec14010348e84fab39b226b1e0696cb6fb40bdb293952c184cf02007fa6e983cd311d189004e7bd75ff9ef069642f2abfa5916099e5a16144e1a6d9b4f246624d3b57d2895d5d2e46fe8661e49717d1663ad2c07b023738370a3e44a960f683040b1862fe36e22347c2dbe429c51af377bdbe01ca0e103f295d1678c68b628957a53a820afcc25763cc67b38aca82067bdf52dc68c061a02575d91c01beca64cc09735c395e91d034841d3e61b58948da631192ce556b85b01028e2284ead4ce184981f4d0f387f8d47295d4fa1dab6a6ae3a417550ac1c8b1aa007b38c926212fbf23154c6ff707621d6eedafc4298b133111d90934bb9d5a2103f0c8e4a3f3daa992334aad300677f23b4285db2ee5caf0a0ecc39c6596c3c4e42318040bec46add3626501f6e422be9c791adc81ea5c83ff0bfa91b7d42bcac0ed128a640fe970da584cff80fd5c12a8ea9b546a2d63515343a933daa21c0000000000000000001800000000000000011d24b2dfac5200000000a404682ca218a820a4a878fb352e63673c05eb07e53563fc8022ff039ad4c66e65848a7cde7ee780aa022710ae03020000b02103800fd75bf6b1e7c5f3fab33a372f6599730e0fae7a30fa4e5c8fbc69c3a87981f0403c9a40e6c9d08e12b0a155101d23a170b4f5b38051b0a0a09a794ce49e820f65d50c8fad7518200d3a28331aa5c668a8f7d70206aaf8bea2e8f05f0904b6e033"; + + let invoice_bytes = hex_utils::to_vec(invoice_hex).expect("Valid hex string"); + + let ldk_invoice = + LdkBolt12Invoice::try_from(invoice_bytes).expect("Valid Bolt12Invoice bytes"); + + let wrapped_invoice = Bolt12Invoice { inner: ldk_invoice.clone() }; + + (ldk_invoice, wrapped_invoice) + } + + #[test] + fn test_invoice_description_conversion() { + let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); + let description = Bolt11InvoiceDescription::Hash { hash }; + let converted_description = + lightning_invoice::Bolt11InvoiceDescription::try_from(&description).unwrap(); + let reconverted_description: Bolt11InvoiceDescription = converted_description.into(); + assert_eq!(description, reconverted_description); + } + + #[test] + fn test_bolt11_invoice_basic_properties() { + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); + + assert_eq!( + ldk_invoice.payment_hash().to_string(), + wrapped_invoice.payment_hash().to_string() + ); + assert_eq!(ldk_invoice.amount_milli_satoshis(), wrapped_invoice.amount_milli_satoshis()); + + assert_eq!( + ldk_invoice.min_final_cltv_expiry_delta(), + wrapped_invoice.min_final_cltv_expiry_delta() + ); + assert_eq!( + ldk_invoice.payment_secret().0.to_vec(), + wrapped_invoice.payment_secret().0.to_vec() + ); + + assert_eq!(ldk_invoice.network(), wrapped_invoice.network()); + assert_eq!( + format!("{:?}", ldk_invoice.currency()), + format!("{:?}", wrapped_invoice.currency()) + ); + } + + #[test] + fn test_bolt11_invoice_time_related_fields() { + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); + + assert_eq!(ldk_invoice.expiry_time().as_secs(), wrapped_invoice.expiry_time_seconds()); + assert_eq!( + ldk_invoice.duration_until_expiry().as_secs(), + wrapped_invoice.seconds_until_expiry() + ); + assert_eq!( + ldk_invoice.duration_since_epoch().as_secs(), + wrapped_invoice.seconds_since_epoch() + ); + + let future_time = Duration::from_secs(wrapped_invoice.seconds_since_epoch() + 10000); + assert!(!ldk_invoice.would_expire(future_time)); + assert!(!wrapped_invoice.would_expire(future_time.as_secs())); + } + + #[test] + fn test_bolt11_invoice_description() { + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); + + let ldk_description = ldk_invoice.description(); + let wrapped_description = wrapped_invoice.invoice_description(); + + match (ldk_description, &wrapped_description) { + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(ldk_description), + Bolt11InvoiceDescription::Direct { description }, + ) => { + assert_eq!(ldk_description.to_string(), *description) + }, + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(ldk_hash), + Bolt11InvoiceDescription::Hash { hash }, + ) => { + assert_eq!(hex_utils::to_string(ldk_hash.0.as_ref()), *hash) + }, + _ => panic!("Description types don't match"), + } + } + + #[test] + fn test_bolt11_invoice_route_hints() { + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); + + let wrapped_route_hints = wrapped_invoice.route_hints(); + let ldk_route_hints = ldk_invoice.route_hints(); + assert_eq!(ldk_route_hints.len(), wrapped_route_hints.len()); + + let ldk_hop = &ldk_route_hints[0].0[0]; + let wrapped_hop = &wrapped_route_hints[0][0]; + assert_eq!(ldk_hop.src_node_id, wrapped_hop.src_node_id); + assert_eq!(ldk_hop.short_channel_id, wrapped_hop.short_channel_id); + assert_eq!(ldk_hop.cltv_expiry_delta, wrapped_hop.cltv_expiry_delta); + assert_eq!(ldk_hop.htlc_minimum_msat, wrapped_hop.htlc_minimum_msat); + assert_eq!(ldk_hop.htlc_maximum_msat, wrapped_hop.htlc_maximum_msat); + assert_eq!(ldk_hop.fees.base_msat, wrapped_hop.fees.base_msat); + assert_eq!(ldk_hop.fees.proportional_millionths, wrapped_hop.fees.proportional_millionths); + } + + #[test] + fn test_bolt11_invoice_roundtrip() { + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); + + let invoice_str = wrapped_invoice.to_string(); + let parsed_invoice: LdkBolt11Invoice = invoice_str.parse().unwrap(); + assert_eq!( + ldk_invoice.payment_hash().to_byte_array().to_vec(), + parsed_invoice.payment_hash().to_byte_array().to_vec() + ); + } + + #[test] + fn test_offer() { + let (ldk_offer, wrapped_offer) = create_test_offer(); + match (ldk_offer.description(), wrapped_offer.description()) { + (Some(ldk_desc), Some(wrapped_desc)) => { + assert_eq!(ldk_desc.to_string(), wrapped_desc); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had a description but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had a description but LDK offer did not!"); + }, + } + + match (ldk_offer.amount(), wrapped_offer.amount()) { + (Some(ldk_amount), Some(wrapped_amount)) => { + let ldk_amount: OfferAmount = ldk_amount.into(); + assert_eq!(ldk_amount, wrapped_amount); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an amount but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an amount but LDK offer did not!"); + }, + } + + match (ldk_offer.issuer(), wrapped_offer.issuer()) { + (Some(ldk_issuer), Some(wrapped_issuer)) => { + assert_eq!(ldk_issuer.to_string(), wrapped_issuer); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an issuer but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an issuer but LDK offer did not!"); + }, + } + + assert_eq!(ldk_offer.is_expired(), wrapped_offer.is_expired()); + assert_eq!(ldk_offer.id(), wrapped_offer.id()); + assert_eq!(ldk_offer.is_valid_quantity(10_000), wrapped_offer.is_valid_quantity(10_000)); + assert_eq!(ldk_offer.expects_quantity(), wrapped_offer.expects_quantity()); + assert_eq!( + ldk_offer.supports_chain(Network::Bitcoin.chain_hash()), + wrapped_offer.supports_chain(Network::Bitcoin) + ); + assert_eq!( + ldk_offer.chains(), + wrapped_offer.chains().iter().map(|c| c.chain_hash()).collect::>() + ); + match (ldk_offer.metadata(), wrapped_offer.metadata()) { + (Some(ldk_metadata), Some(wrapped_metadata)) => { + assert_eq!(ldk_metadata.clone(), wrapped_metadata); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had metadata but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had metadata but LDK offer did not!"); + }, + } + + match (ldk_offer.absolute_expiry(), wrapped_offer.absolute_expiry_seconds()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry.as_secs(), wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an absolute expiry but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an absolute expiry but LDK offer did not!"); + }, + } + + match (ldk_offer.issuer_signing_pubkey(), wrapped_offer.issuer_signing_pubkey()) { + (Some(ldk_expiry_signing_pubkey), Some(wrapped_issuer_signing_pubkey)) => { + assert_eq!(ldk_expiry_signing_pubkey, wrapped_issuer_signing_pubkey); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an issuer signing pubkey but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an issuer signing pubkey but LDK offer did not!"); + }, + } + } + + #[test] + fn test_refund_roundtrip() { + let (ldk_refund, _) = create_test_refund(); + + let refund_str = ldk_refund.to_string(); + + let parsed_refund = Refund::from_str(&refund_str); + assert!(parsed_refund.is_ok(), "Failed to parse refund from string!"); + + let invalid_result = Refund::from_str("invalid_refund_string"); + assert!(invalid_result.is_err()); + assert!(matches!(invalid_result.err().unwrap(), Error::InvalidRefund)); + } + + #[test] + fn test_refund_properties() { + let (ldk_refund, wrapped_refund) = create_test_refund(); + + assert_eq!(ldk_refund.description().to_string(), wrapped_refund.description()); + assert_eq!(ldk_refund.amount_msats(), wrapped_refund.amount_msats()); + assert_eq!(ldk_refund.is_expired(), wrapped_refund.is_expired()); + + match (ldk_refund.absolute_expiry(), wrapped_refund.absolute_expiry_seconds()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry.as_secs(), wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK refund had an expiry but wrapped refund did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped refund had an expiry but LDK refund did not!"); + }, + } + + match (ldk_refund.quantity(), wrapped_refund.quantity()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry, wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK refund had an quantity but wrapped refund did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped refund had an quantity but LDK refund did not!"); + }, + } + + match (ldk_refund.issuer(), wrapped_refund.issuer()) { + (Some(ldk_issuer), Some(wrapped_issuer)) => { + assert_eq!(ldk_issuer.to_string(), wrapped_issuer); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK refund had an issuer but wrapped refund did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped refund had an issuer but LDK refund did not!"); + }, + } + + assert_eq!(ldk_refund.payer_metadata().to_vec(), wrapped_refund.payer_metadata()); + assert_eq!(ldk_refund.payer_signing_pubkey(), wrapped_refund.payer_signing_pubkey()); + + if let Ok(network) = Network::try_from(ldk_refund.chain()) { + assert_eq!(wrapped_refund.chain(), Some(network)); + } + + assert_eq!(ldk_refund.payer_note().map(|p| p.to_string()), wrapped_refund.payer_note()); + } + + #[test] + fn test_bolt12_invoice_properties() { + let (ldk_invoice, wrapped_invoice) = create_test_bolt12_invoice(); + + assert_eq!( + ldk_invoice.payment_hash().0.to_vec(), + wrapped_invoice.payment_hash().0.to_vec() + ); + assert_eq!(ldk_invoice.amount_msats(), wrapped_invoice.amount_msats()); + assert_eq!(ldk_invoice.is_expired(), wrapped_invoice.is_expired()); + + assert_eq!(ldk_invoice.signing_pubkey(), wrapped_invoice.signing_pubkey()); + + assert_eq!(ldk_invoice.created_at().as_secs(), wrapped_invoice.created_at()); + + match (ldk_invoice.absolute_expiry(), wrapped_invoice.absolute_expiry_seconds()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry.as_secs(), wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had an absolute expiry but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had an absolute expiry but LDK invoice did not!"); + }, + } + + assert_eq!(ldk_invoice.relative_expiry().as_secs(), wrapped_invoice.relative_expiry()); + + match (ldk_invoice.description(), wrapped_invoice.description()) { + (Some(ldk_desc), Some(wrapped_desc)) => { + assert_eq!(ldk_desc.to_string(), wrapped_desc); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had a description but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had a description but LDK invoice did not!"); + }, + } + + match (ldk_invoice.issuer(), wrapped_invoice.issuer()) { + (Some(ldk_issuer), Some(wrapped_issuer)) => { + assert_eq!(ldk_issuer.to_string(), wrapped_issuer); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had an issuer but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had an issuer but LDK invoice did not!"); + }, + } + + match (ldk_invoice.payer_note(), wrapped_invoice.payer_note()) { + (Some(ldk_note), Some(wrapped_note)) => { + assert_eq!(ldk_note.to_string(), wrapped_note); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had a payer note but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had a payer note but LDK invoice did not!"); + }, + } + + match (ldk_invoice.metadata(), wrapped_invoice.metadata()) { + (Some(ldk_metadata), Some(wrapped_metadata)) => { + assert_eq!(ldk_metadata.as_slice(), wrapped_metadata.as_slice()); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had metadata but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had metadata but LDK invoice did not!"); + }, + } + + assert_eq!(ldk_invoice.quantity(), wrapped_invoice.quantity()); + + assert_eq!(ldk_invoice.chain().to_bytes().to_vec(), wrapped_invoice.chain()); + + match (ldk_invoice.offer_chains(), wrapped_invoice.offer_chains()) { + (Some(ldk_chains), Some(wrapped_chains)) => { + assert_eq!(ldk_chains.len(), wrapped_chains.len()); + for (i, ldk_chain) in ldk_chains.iter().enumerate() { + assert_eq!(ldk_chain.to_bytes().to_vec(), wrapped_chains[i]); + } + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had offer chains but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had offer chains but LDK invoice did not!"); + }, + } + + let ldk_fallbacks = ldk_invoice.fallbacks(); + let wrapped_fallbacks = wrapped_invoice.fallback_addresses(); + assert_eq!(ldk_fallbacks.len(), wrapped_fallbacks.len()); + for (i, ldk_fallback) in ldk_fallbacks.iter().enumerate() { + assert_eq!(*ldk_fallback, wrapped_fallbacks[i]); + } + + assert_eq!(ldk_invoice.encode(), wrapped_invoice.encode()); + + assert_eq!(ldk_invoice.signable_hash().to_vec(), wrapped_invoice.signable_hash()); + } +} diff --git a/src/gossip.rs b/src/gossip.rs index 8e2f59b318..499d164128 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -5,23 +5,24 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::future::Future; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use lightning::util::native_async::FutureSpawner; +use lightning_block_sync::gossip::GossipVerifier; + use crate::chain::ChainSource; use crate::config::RGS_SYNC_TIMEOUT_SECS; -use crate::logger::{log_error, log_trace, LdkLogger, Logger}; +use crate::logger::{log_trace, LdkLogger, Logger}; +use crate::runtime::Runtime; use crate::types::{GossipSync, Graph, P2PGossipSync, PeerManager, RapidGossipSync, UtxoLookup}; use crate::Error; -use lightning_block_sync::gossip::{FutureSpawner, GossipVerifier}; - -use std::future::Future; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - pub(crate) enum GossipSource { P2PNetwork { gossip_sync: Arc, - logger: Arc, }, RapidGossipSync { gossip_sync: Arc, @@ -38,7 +39,7 @@ impl GossipSource { None::>, Arc::clone(&logger), )); - Self::P2PNetwork { gossip_sync, logger } + Self::P2PNetwork { gossip_sync } } pub fn new_rgs( @@ -63,12 +64,12 @@ impl GossipSource { pub(crate) fn set_gossip_verifier( &self, chain_source: Arc, peer_manager: Arc, - runtime: Arc>>>, + runtime: Arc, ) { match self { - Self::P2PNetwork { gossip_sync, logger } => { + Self::P2PNetwork { gossip_sync } => { if let Some(utxo_source) = chain_source.as_utxo_source() { - let spawner = RuntimeSpawner::new(Arc::clone(&runtime), Arc::clone(&logger)); + let spawner = RuntimeSpawner::new(Arc::clone(&runtime)); let gossip_verifier = Arc::new(GossipVerifier::new( utxo_source, spawner, @@ -134,28 +135,17 @@ impl GossipSource { } pub(crate) struct RuntimeSpawner { - runtime: Arc>>>, - logger: Arc, + runtime: Arc, } impl RuntimeSpawner { - pub(crate) fn new( - runtime: Arc>>>, logger: Arc, - ) -> Self { - Self { runtime, logger } + pub(crate) fn new(runtime: Arc) -> Self { + Self { runtime } } } impl FutureSpawner for RuntimeSpawner { fn spawn + Send + 'static>(&self, future: T) { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - log_error!(self.logger, "Tried spawing a future while the runtime wasn't available. This should never happen."); - debug_assert!(false, "Tried spawing a future while the runtime wasn't available. This should never happen."); - return; - } - - let runtime = rt_lock.as_ref().unwrap(); - runtime.spawn(future); + self.runtime.spawn_cancellable_background_task(future); } } diff --git a/src/graph.rs b/src/graph.rs index 769b65284c..049112e387 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -7,19 +7,17 @@ //! Objects for querying the network graph. -use crate::types::Graph; - -use lightning::{routing::gossip::NodeId, util::ser::Writeable}; +use std::sync::Arc; #[cfg(feature = "uniffi")] use lightning::ln::msgs::SocketAddress; +use lightning::{routing::gossip::NodeId, util::ser::Writeable as _}; #[cfg(feature = "uniffi")] use lightning::routing::gossip::RoutingFees; - #[cfg(not(feature = "uniffi"))] use lightning::routing::gossip::{ChannelInfo, NodeInfo}; -use std::sync::Arc; +use crate::types::Graph; /// Represents the network as nodes and channels between them. pub struct NetworkGraph { diff --git a/src/io/mod.rs b/src/io/mod.rs index 3192dbb863..38fba5114f 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -27,11 +27,6 @@ pub(crate) const PEER_INFO_PERSISTENCE_KEY: &str = "peers"; pub(crate) const PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payments"; pub(crate) const PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; -/// The spendable output information used to persisted under this prefix until LDK Node v0.3.0. -pub(crate) const DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = - "spendable_outputs"; -pub(crate) const DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; - /// The node metrics will be persisted under this key. pub(crate) const NODE_METRICS_PRIMARY_NAMESPACE: &str = ""; pub(crate) const NODE_METRICS_SECONDARY_NAMESPACE: &str = ""; @@ -78,3 +73,8 @@ pub(crate) const BDK_WALLET_TX_GRAPH_KEY: &str = "tx_graph"; pub(crate) const BDK_WALLET_INDEXER_PRIMARY_NAMESPACE: &str = "bdk_wallet"; pub(crate) const BDK_WALLET_INDEXER_SECONDARY_NAMESPACE: &str = ""; pub(crate) const BDK_WALLET_INDEXER_KEY: &str = "indexer"; + +/// [`StaticInvoice`]s will be persisted under this key. +/// +/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice +pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices"; diff --git a/src/io/sqlite_store/migrations.rs b/src/io/sqlite_store/migrations.rs index 0486b8a4f9..abfbdf6ef0 100644 --- a/src/io/sqlite_store/migrations.rs +++ b/src/io/sqlite_store/migrations.rs @@ -5,9 +5,8 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use rusqlite::Connection; - use lightning::io; +use rusqlite::Connection; pub(super) fn migrate_schema( connection: &mut Connection, kv_table_name: &str, from_version: u16, to_version: u16, @@ -75,14 +74,13 @@ pub(super) fn migrate_schema( #[cfg(test)] mod tests { - use crate::io::sqlite_store::SqliteStore; - use crate::io::test_utils::{do_read_write_remove_list_persist, random_storage_path}; - - use lightning::util::persist::KVStore; + use std::fs; + use lightning::util::persist::KVStoreSync; use rusqlite::{named_params, Connection}; - use std::fs; + use crate::io::sqlite_store::SqliteStore; + use crate::io::test_utils::{do_read_write_remove_list_persist, random_storage_path}; #[test] fn rwrl_post_schema_1_migration() { diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index b72db5a2ba..c41df8ea04 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -6,17 +6,21 @@ // accordance with one or both of these licenses. //! Objects related to [`SqliteStore`] live here. -use crate::io::utils::check_namespace_key_validity; +use std::boxed::Box; +use std::collections::HashMap; +use std::fs; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; use lightning::io; -use lightning::util::persist::KVStore; -use lightning::util::string::PrintableString; - +use lightning::util::persist::{KVStore, KVStoreSync}; +use lightning_types::string::PrintableString; use rusqlite::{named_params, Connection}; -use std::fs; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use crate::io::utils::check_namespace_key_validity; mod migrations; @@ -34,13 +38,15 @@ pub const DEFAULT_KV_TABLE_NAME: &str = "ldk_data"; // The current SQLite `user_version`, which we can use if we'd ever need to do a schema migration. const SCHEMA_USER_VERSION: u16 = 2; -/// A [`KVStore`] implementation that writes to and reads from an [SQLite] database. +/// A [`KVStoreSync`] implementation that writes to and reads from an [SQLite] database. /// /// [SQLite]: https://sqlite.org pub struct SqliteStore { - connection: Arc>, - data_dir: PathBuf, - kv_table_name: String, + inner: Arc, + + // Version counter to ensure that writes are applied in the correct order. It is assumed that read and list + // operations aren't sensitive to the order of execution. + next_write_version: AtomicU64, } impl SqliteStore { @@ -52,6 +58,182 @@ impl SqliteStore { /// Similarly, the given `kv_table_name` will be used or default to [`DEFAULT_KV_TABLE_NAME`]. pub fn new( data_dir: PathBuf, db_file_name: Option, kv_table_name: Option, + ) -> io::Result { + let inner = Arc::new(SqliteStoreInner::new(data_dir, db_file_name, kv_table_name)?); + let next_write_version = AtomicU64::new(1); + Ok(Self { inner, next_write_version }) + } + + fn build_locking_key( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> String { + format!("{}#{}#{}", primary_namespace, secondary_namespace, key) + } + + fn get_new_version_and_lock_ref(&self, locking_key: String) -> (Arc>, u64) { + let version = self.next_write_version.fetch_add(1, Ordering::Relaxed); + if version == u64::MAX { + panic!("SqliteStore version counter overflowed"); + } + + // Get a reference to the inner lock. We do this early so that the arc can double as an in-flight counter for + // cleaning up unused locks. + let inner_lock_ref = self.inner.get_inner_lock_ref(locking_key); + + (inner_lock_ref, version) + } + + /// Returns the data directory. + pub fn get_data_dir(&self) -> PathBuf { + self.inner.data_dir.clone() + } +} + +impl KVStore for SqliteStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.read_internal(&primary_namespace, &secondary_namespace, &key) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.write_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + buf, + ) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.remove_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + ) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.list_internal(&primary_namespace, &secondary_namespace) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } +} + +impl KVStoreSync for SqliteStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result> { + self.inner.read_internal(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> io::Result<()> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + self.inner.write_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + buf, + ) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result<()> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + self.inner.remove_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + ) + } + + fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + self.inner.list_internal(primary_namespace, secondary_namespace) + } +} + +struct SqliteStoreInner { + connection: Arc>, + data_dir: PathBuf, + kv_table_name: String, + write_version_locks: Mutex>>>, +} + +impl SqliteStoreInner { + fn new( + data_dir: PathBuf, db_file_name: Option, kv_table_name: Option, ) -> io::Result { let db_file_name = db_file_name.unwrap_or(DEFAULT_SQLITE_DB_FILE_NAME.to_string()); let kv_table_name = kv_table_name.unwrap_or(DEFAULT_KV_TABLE_NAME.to_string()); @@ -120,17 +302,16 @@ impl SqliteStore { })?; let connection = Arc::new(Mutex::new(connection)); - Ok(Self { connection, data_dir, kv_table_name }) + let write_version_locks = Mutex::new(HashMap::new()); + Ok(Self { connection, data_dir, kv_table_name, write_version_locks }) } - /// Returns the data directory. - pub fn get_data_dir(&self) -> PathBuf { - self.data_dir.clone() + fn get_inner_lock_ref(&self, locking_key: String) -> Arc> { + let mut outer_lock = self.write_version_locks.lock().unwrap(); + Arc::clone(&outer_lock.entry(locking_key).or_default()) } -} -impl KVStore for SqliteStore { - fn read( + fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; @@ -178,75 +359,83 @@ impl KVStore for SqliteStore { Ok(res) } - fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], + fn write_internal( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; - let locked_conn = self.connection.lock().unwrap(); + self.execute_locked_write(inner_lock_ref, locking_key, version, || { + let locked_conn = self.connection.lock().unwrap(); - let sql = format!( - "INSERT OR REPLACE INTO {} (primary_namespace, secondary_namespace, key, value) VALUES (:primary_namespace, :secondary_namespace, :key, :value);", - self.kv_table_name - ); + let sql = format!( + "INSERT OR REPLACE INTO {} (primary_namespace, secondary_namespace, key, value) VALUES (:primary_namespace, :secondary_namespace, :key, :value);", + self.kv_table_name + ); - let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { - let msg = format!("Failed to prepare statement: {}", e); - io::Error::new(io::ErrorKind::Other, msg) - })?; + let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { + let msg = format!("Failed to prepare statement: {}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; - stmt.execute(named_params! { - ":primary_namespace": primary_namespace, - ":secondary_namespace": secondary_namespace, - ":key": key, - ":value": buf, - }) - .map(|_| ()) - .map_err(|e| { - let msg = format!( - "Failed to write to key {}/{}/{}: {}", - PrintableString(primary_namespace), - PrintableString(secondary_namespace), - PrintableString(key), - e - ); - io::Error::new(io::ErrorKind::Other, msg) + stmt.execute(named_params! { + ":primary_namespace": primary_namespace, + ":secondary_namespace": secondary_namespace, + ":key": key, + ":value": buf, + }) + .map(|_| ()) + .map_err(|e| { + let msg = format!( + "Failed to write to key {}/{}/{}: {}", + PrintableString(primary_namespace), + PrintableString(secondary_namespace), + PrintableString(key), + e + ); + io::Error::new(io::ErrorKind::Other, msg) + }) }) } - fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, + fn remove_internal( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; - let locked_conn = self.connection.lock().unwrap(); + self.execute_locked_write(inner_lock_ref, locking_key, version, || { + let locked_conn = self.connection.lock().unwrap(); - let sql = format!("DELETE FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", self.kv_table_name); + let sql = format!("DELETE FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", self.kv_table_name); - let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { - let msg = format!("Failed to prepare statement: {}", e); - io::Error::new(io::ErrorKind::Other, msg) - })?; + let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { + let msg = format!("Failed to prepare statement: {}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; - stmt.execute(named_params! { - ":primary_namespace": primary_namespace, - ":secondary_namespace": secondary_namespace, - ":key": key, + stmt.execute(named_params! { + ":primary_namespace": primary_namespace, + ":secondary_namespace": secondary_namespace, + ":key": key, + }) + .map_err(|e| { + let msg = format!( + "Failed to delete key {}/{}/{}: {}", + PrintableString(primary_namespace), + PrintableString(secondary_namespace), + PrintableString(key), + e + ); + io::Error::new(io::ErrorKind::Other, msg) + })?; + Ok(()) }) - .map_err(|e| { - let msg = format!( - "Failed to delete key {}/{}/{}: {}", - PrintableString(primary_namespace), - PrintableString(secondary_namespace), - PrintableString(key), - e - ); - io::Error::new(io::ErrorKind::Other, msg) - })?; - Ok(()) } - fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + fn list_internal( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?; let locked_conn = self.connection.lock().unwrap(); @@ -284,6 +473,46 @@ impl KVStore for SqliteStore { Ok(keys) } + + fn execute_locked_write Result<(), lightning::io::Error>>( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, callback: F, + ) -> Result<(), lightning::io::Error> { + let res = { + let mut last_written_version = inner_lock_ref.lock().unwrap(); + + // Check if we already have a newer version written/removed. This is used in async contexts to realize eventual + // consistency. + let is_stale_version = version <= *last_written_version; + + // If the version is not stale, we execute the callback. Otherwise we can and must skip writing. + if is_stale_version { + Ok(()) + } else { + callback().map(|_| { + *last_written_version = version; + }) + } + }; + + self.clean_locks(&inner_lock_ref, locking_key); + + res + } + + fn clean_locks(&self, inner_lock_ref: &Arc>, locking_key: String) { + // If there no arcs in use elsewhere, this means that there are no in-flight writes. We can remove the map entry + // to prevent leaking memory. The two arcs that are expected are the one in the map and the one held here in + // inner_lock_ref. The outer lock is obtained first, to avoid a new arc being cloned after we've already + // counted. + let mut outer_lock = self.write_version_locks.lock().unwrap(); + + let strong_count = Arc::strong_count(&inner_lock_ref); + debug_assert!(strong_count >= 2, "Unexpected SqliteStore strong count"); + + if strong_count == 2 { + outer_lock.remove(&locking_key); + } + } } #[cfg(test)] @@ -295,7 +524,7 @@ mod tests { impl Drop for SqliteStore { fn drop(&mut self) { - match fs::remove_dir_all(&self.data_dir) { + match fs::remove_dir_all(&self.inner.data_dir) { Err(e) => println!("Failed to remove test store directory: {}", e), _ => {}, } diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index aed03b6fc7..22f1a4ea5a 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -5,22 +5,20 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::panic::RefUnwindSafe; +use std::path::PathBuf; + +use lightning::events::ClosureReason; use lightning::ln::functional_test_utils::{ connect_block, create_announced_chan_between_nodes, create_chanmon_cfgs, create_dummy_block, create_network, create_node_cfgs, create_node_chanmgrs, send_payment, }; -use lightning::util::persist::{read_channel_monitors, KVStore, KVSTORE_NAMESPACE_KEY_MAX_LEN}; - -use lightning::events::ClosureReason; +use lightning::util::persist::{read_channel_monitors, KVStoreSync, KVSTORE_NAMESPACE_KEY_MAX_LEN}; use lightning::util::test_utils; use lightning::{check_added_monitors, check_closed_broadcast, check_closed_event}; - use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use std::panic::RefUnwindSafe; -use std::path::PathBuf; - pub(crate) fn random_storage_path() -> PathBuf { let mut temp_path = std::env::temp_dir(); let mut rng = thread_rng(); @@ -29,23 +27,24 @@ pub(crate) fn random_storage_path() -> PathBuf { temp_path } -pub(crate) fn do_read_write_remove_list_persist(kv_store: &K) { - let data = [42u8; 32]; +pub(crate) fn do_read_write_remove_list_persist(kv_store: &K) { + let data = vec![42u8; 32]; let primary_namespace = "testspace"; let secondary_namespace = "testsubspace"; let key = "testkey"; // Test the basic KVStore operations. - kv_store.write(primary_namespace, secondary_namespace, key, &data).unwrap(); + kv_store.write(primary_namespace, secondary_namespace, key, data.clone()).unwrap(); // Test empty primary/secondary namespaces are allowed, but not empty primary namespace and non-empty // secondary primary_namespace, and not empty key. - kv_store.write("", "", key, &data).unwrap(); - let res = std::panic::catch_unwind(|| kv_store.write("", secondary_namespace, key, &data)); + kv_store.write("", "", key, data.clone()).unwrap(); + let res = + std::panic::catch_unwind(|| kv_store.write("", secondary_namespace, key, data.clone())); assert!(res.is_err()); let res = std::panic::catch_unwind(|| { - kv_store.write(primary_namespace, secondary_namespace, "", &data) + kv_store.write(primary_namespace, secondary_namespace, "", data.clone()) }); assert!(res.is_err()); @@ -56,14 +55,14 @@ pub(crate) fn do_read_write_remove_list_persist(kv_s let read_data = kv_store.read(primary_namespace, secondary_namespace, key).unwrap(); assert_eq!(data, &*read_data); - kv_store.remove(primary_namespace, secondary_namespace, key, false).unwrap(); + kv_store.remove(primary_namespace, secondary_namespace, key).unwrap(); let listed_keys = kv_store.list(primary_namespace, secondary_namespace).unwrap(); assert_eq!(listed_keys.len(), 0); // Ensure we have no issue operating with primary_namespace/secondary_namespace/key being KVSTORE_NAMESPACE_KEY_MAX_LEN let max_chars: String = std::iter::repeat('A').take(KVSTORE_NAMESPACE_KEY_MAX_LEN).collect(); - kv_store.write(&max_chars, &max_chars, &max_chars, &data).unwrap(); + kv_store.write(&max_chars, &max_chars, &max_chars, data.clone()).unwrap(); let listed_keys = kv_store.list(&max_chars, &max_chars).unwrap(); assert_eq!(listed_keys.len(), 1); @@ -72,7 +71,7 @@ pub(crate) fn do_read_write_remove_list_persist(kv_s let read_data = kv_store.read(&max_chars, &max_chars, &max_chars).unwrap(); assert_eq!(data, &*read_data); - kv_store.remove(&max_chars, &max_chars, &max_chars, false).unwrap(); + kv_store.remove(&max_chars, &max_chars, &max_chars).unwrap(); let listed_keys = kv_store.list(&max_chars, &max_chars).unwrap(); assert_eq!(listed_keys.len(), 0); @@ -80,7 +79,7 @@ pub(crate) fn do_read_write_remove_list_persist(kv_s // Integration-test the given KVStore implementation. Test relaying a few payments and check that // the persisted data is updated the appropriate number of times. -pub(crate) fn do_test_store(store_0: &K, store_1: &K) { +pub(crate) fn do_test_store(store_0: &K, store_1: &K) { let chanmon_cfgs = create_chanmon_cfgs(2); let mut node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let chain_mon_0 = test_utils::TestChainMonitor::new( @@ -145,18 +144,19 @@ pub(crate) fn do_test_store(store_0: &K, store_1: &K) { // Force close because cooperative close doesn't result in any persisted // updates. + let message = "Channel force-closed".to_owned(); nodes[0] .node .force_close_broadcasting_latest_txn( &nodes[0].node.list_channels()[0].channel_id, &nodes[1].node.get_our_node_id(), - "whoops".to_string(), + message.clone(), ) .unwrap(); check_closed_event!( nodes[0], 1, - ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true) }, + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }, [nodes[1].node.get_our_node_id()], 100000 ); diff --git a/src/io/utils.rs b/src/io/utils.rs index b5537ed7d0..1556314c4d 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -5,35 +5,11 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use super::*; -use crate::config::WALLET_KEYS_SEED_LEN; - -use crate::chain::ChainSource; -use crate::fee_estimator::OnchainFeeEstimator; -use crate::io::{ - NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, -}; -use crate::logger::{log_error, LdkLogger, Logger}; -use crate::peer_store::PeerStore; -use crate::sweep::DeprecatedSpendableOutputInfo; -use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; -use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; -use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; - -use lightning::io::Cursor; -use lightning::ln::msgs::DecodeError; -use lightning::routing::gossip::NetworkGraph; -use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringDecayParameters}; -use lightning::util::persist::{ - KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, NETWORK_GRAPH_PERSISTENCE_KEY, - NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, - OUTPUT_SWEEPER_PERSISTENCE_KEY, OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, - OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY, - SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, -}; -use lightning::util::ser::{Readable, ReadableArgs, Writeable}; -use lightning::util::string::PrintableString; -use lightning::util::sweep::{OutputSpendStatus, OutputSweeper}; +use std::fs; +use std::io::Write; +use std::ops::Deref; +use std::path::Path; +use std::sync::Arc; use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet; @@ -41,16 +17,36 @@ use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey}; use bdk_chain::tx_graph::ChangeSet as BdkTxGraphChangeSet; use bdk_chain::ConfirmationBlockTime; use bdk_wallet::ChangeSet as BdkWalletChangeSet; - use bip39::Mnemonic; use bitcoin::Network; +use lightning::io::Cursor; +use lightning::ln::msgs::DecodeError; +use lightning::routing::gossip::NetworkGraph; +use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringDecayParameters}; +use lightning::util::persist::{ + KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, + NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, + NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_KEY, + OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, + SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, + SCORER_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use lightning::util::ser::{Readable, ReadableArgs, Writeable}; +use lightning_types::string::PrintableString; use rand::{thread_rng, RngCore}; -use std::fs; -use std::io::Write; -use std::ops::Deref; -use std::path::Path; -use std::sync::Arc; +use super::*; +use crate::chain::ChainSource; +use crate::config::WALLET_KEYS_SEED_LEN; +use crate::fee_estimator::OnchainFeeEstimator; +use crate::io::{ + NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, +}; +use crate::logger::{log_error, LdkLogger, Logger}; +use crate::peer_store::PeerStore; +use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; +use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; +use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; /// Generates a random [BIP 39] mnemonic. /// @@ -135,7 +131,8 @@ pub(crate) fn read_network_graph( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_KEY, @@ -154,7 +151,8 @@ where L::Target: LdkLogger, { let params = ProbabilisticScoringDecayParameters::default(); - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY, @@ -173,7 +171,8 @@ pub(crate) fn read_event_queue( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_KEY, @@ -191,7 +190,8 @@ pub(crate) fn read_peer_info( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PEER_INFO_PERSISTENCE_KEY, @@ -211,11 +211,13 @@ where { let mut res = Vec::new(); - for stored_key in kv_store.list( + for stored_key in KVStoreSync::list( + &*kv_store, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, )? { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, &stored_key, @@ -238,7 +240,8 @@ pub(crate) fn read_output_sweeper( chain_data_source: Arc, keys_manager: Arc, kv_store: Arc, logger: Arc, ) -> Result { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_KEY, @@ -252,107 +255,11 @@ pub(crate) fn read_output_sweeper( kv_store, logger.clone(), ); - OutputSweeper::read(&mut reader, args).map_err(|e| { + let (_, sweeper) = <(_, Sweeper)>::read(&mut reader, args).map_err(|e| { log_error!(logger, "Failed to deserialize OutputSweeper: {}", e); std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to deserialize OutputSweeper") - }) -} - -/// Read previously persisted spendable output information from the store and migrate to the -/// upstreamed `OutputSweeper`. -/// -/// We first iterate all `DeprecatedSpendableOutputInfo`s and have them tracked by the new -/// `OutputSweeper`. In order to be certain the initial output spends will happen in a single -/// transaction (and safe on-chain fees), we batch them to happen at current height plus two -/// blocks. Lastly, we remove the previously persisted data once we checked they are tracked and -/// awaiting their initial spend at the correct height. -/// -/// Note that this migration will be run in the `Builder`, i.e., at the time when the migration is -/// happening no background sync is ongoing, so we shouldn't have a risk of interleaving block -/// connections during the migration. -pub(crate) fn migrate_deprecated_spendable_outputs( - sweeper: Arc, kv_store: Arc, logger: L, -) -> Result<(), std::io::Error> -where - L::Target: LdkLogger, -{ - let best_block = sweeper.current_best_block(); - - for stored_key in kv_store.list( - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - )? { - let mut reader = Cursor::new(kv_store.read( - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &stored_key, - )?); - let output = DeprecatedSpendableOutputInfo::read(&mut reader).map_err(|e| { - log_error!(logger, "Failed to deserialize SpendableOutputInfo: {}", e); - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Failed to deserialize SpendableOutputInfo", - ) - })?; - let descriptors = vec![output.descriptor.clone()]; - let spend_delay = Some(best_block.height + 2); - sweeper - .track_spendable_outputs(descriptors, output.channel_id, true, spend_delay) - .map_err(|_| { - log_error!(logger, "Failed to track spendable outputs. Aborting migration, will retry in the future."); - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Failed to track spendable outputs. Aborting migration, will retry in the future.", - ) - })?; - - if let Some(tracked_spendable_output) = - sweeper.tracked_spendable_outputs().iter().find(|o| o.descriptor == output.descriptor) - { - match tracked_spendable_output.status { - OutputSpendStatus::PendingInitialBroadcast { delayed_until_height } => { - if delayed_until_height == spend_delay { - kv_store.remove( - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &stored_key, - false, - )?; - } else { - debug_assert!(false, "Unexpected status in OutputSweeper migration."); - log_error!(logger, "Unexpected status in OutputSweeper migration."); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to migrate OutputSweeper state.", - )); - } - }, - _ => { - debug_assert!(false, "Unexpected status in OutputSweeper migration."); - log_error!(logger, "Unexpected status in OutputSweeper migration."); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to migrate OutputSweeper state.", - )); - }, - } - } else { - debug_assert!( - false, - "OutputSweeper failed to track and persist outputs during migration." - ); - log_error!( - logger, - "OutputSweeper failed to track and persist outputs during migration." - ); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to migrate OutputSweeper state.", - )); - } - } - - Ok(()) + })?; + Ok(sweeper) } pub(crate) fn read_node_metrics( @@ -361,7 +268,8 @@ pub(crate) fn read_node_metrics( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, NODE_METRICS_KEY, @@ -379,24 +287,24 @@ where L::Target: LdkLogger, { let data = node_metrics.encode(); - kv_store - .write( + KVStoreSync::write( + &*kv_store, + NODE_METRICS_PRIMARY_NAMESPACE, + NODE_METRICS_SECONDARY_NAMESPACE, + NODE_METRICS_KEY, + data, + ) + .map_err(|e| { + log_error!( + logger, + "Writing data to key {}/{}/{} failed due to: {}", NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, NODE_METRICS_KEY, - &data, - ) - .map_err(|e| { - log_error!( - logger, - "Writing data to key {}/{}/{} failed due to: {}", - NODE_METRICS_PRIMARY_NAMESPACE, - NODE_METRICS_SECONDARY_NAMESPACE, - NODE_METRICS_KEY, - e - ); - Error::PersistenceFailed - }) + e + ); + Error::PersistenceFailed + }) } pub(crate) fn is_valid_kvstore_str(key: &str) -> bool { @@ -498,24 +406,26 @@ macro_rules! impl_read_write_change_set_type { where L::Target: LdkLogger, { - let bytes = match kv_store.read($primary_namespace, $secondary_namespace, $key) { - Ok(bytes) => bytes, - Err(e) => { - if e.kind() == lightning::io::ErrorKind::NotFound { - return Ok(None); - } else { - log_error!( - logger, - "Reading data from key {}/{}/{} failed due to: {}", - $primary_namespace, - $secondary_namespace, - $key, - e - ); - return Err(e.into()); - } - }, - }; + let bytes = + match KVStoreSync::read(&*kv_store, $primary_namespace, $secondary_namespace, $key) + { + Ok(bytes) => bytes, + Err(e) => { + if e.kind() == lightning::io::ErrorKind::NotFound { + return Ok(None); + } else { + log_error!( + logger, + "Reading data from key {}/{}/{} failed due to: {}", + $primary_namespace, + $secondary_namespace, + $key, + e + ); + return Err(e.into()); + } + }, + }; let mut reader = Cursor::new(bytes); let res: Result, DecodeError> = @@ -539,17 +449,18 @@ macro_rules! impl_read_write_change_set_type { L::Target: LdkLogger, { let data = ChangeSetSerWrapper(value).encode(); - kv_store.write($primary_namespace, $secondary_namespace, $key, &data).map_err(|e| { - log_error!( - logger, - "Writing data to key {}/{}/{} failed due to: {}", - $primary_namespace, - $secondary_namespace, - $key, - e - ); - e.into() - }) + KVStoreSync::write(&*kv_store, $primary_namespace, $secondary_namespace, $key, data) + .map_err(|e| { + log_error!( + logger, + "Writing data to key {}/{}/{} failed due to: {}", + $primary_namespace, + $secondary_namespace, + $key, + e + ); + e.into() + }) } }; } diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 296eaabe32..134ff7af2f 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -5,17 +5,21 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::io::utils::check_namespace_key_validity; +use std::boxed::Box; +use std::collections::HashMap; +use std::future::Future; +#[cfg(test)] +use std::panic::RefUnwindSafe; +use std::pin::Pin; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use lightning::io::{self, Error, ErrorKind}; -use lightning::util::persist::KVStore; +use lightning::util::persist::{KVStore, KVStoreSync}; use prost::Message; use rand::RngCore; -#[cfg(test)] -use std::panic::RefUnwindSafe; -use std::sync::Arc; -use std::time::Duration; -use tokio::runtime::Runtime; use vss_client::client::VssClient; use vss_client::error::VssError; use vss_client::headers::VssHeaderProvider; @@ -30,6 +34,9 @@ use vss_client::util::retry::{ }; use vss_client::util::storable_builder::{EntropySource, StorableBuilder}; +use crate::io::utils::check_namespace_key_validity; +use crate::runtime::Runtime; + type CustomRetryPolicy = FilteredRetryPolicy< JitteredRetryPolicy< MaxTotalDelayRetryPolicy>>, @@ -37,21 +44,181 @@ type CustomRetryPolicy = FilteredRetryPolicy< Box bool + 'static + Send + Sync>, >; -/// A [`KVStore`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend. +/// A [`KVStoreSync`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend. pub struct VssStore { + inner: Arc, + // Version counter to ensure that writes are applied in the correct order. It is assumed that read and list + // operations aren't sensitive to the order of execution. + next_version: AtomicU64, + runtime: Arc, +} + +impl VssStore { + pub(crate) fn new( + base_url: String, store_id: String, vss_seed: [u8; 32], + header_provider: Arc, runtime: Arc, + ) -> Self { + let inner = Arc::new(VssStoreInner::new(base_url, store_id, vss_seed, header_provider)); + let next_version = AtomicU64::new(1); + Self { inner, next_version, runtime } + } + + // Same logic as for the obfuscated keys below, but just for locking, using the plaintext keys + fn build_locking_key( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> String { + if primary_namespace.is_empty() { + key.to_owned() + } else { + format!("{}#{}#{}", primary_namespace, secondary_namespace, key) + } + } + + fn get_new_version_and_lock_ref( + &self, locking_key: String, + ) -> (Arc>, u64) { + let version = self.next_version.fetch_add(1, Ordering::Relaxed); + if version == u64::MAX { + panic!("VssStore version counter overflowed"); + } + + // Get a reference to the inner lock. We do this early so that the arc can double as an in-flight counter for + // cleaning up unused locks. + let inner_lock_ref = self.inner.get_inner_lock_ref(locking_key); + + (inner_lock_ref, version) + } +} + +impl KVStoreSync for VssStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result> { + let fut = self.inner.read_internal(primary_namespace, secondary_namespace, key); + self.runtime.block_on(fut) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> io::Result<()> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let fut = self.inner.write_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + buf, + ); + self.runtime.block_on(fut) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result<()> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let fut = self.inner.remove_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + ); + self.runtime.block_on(fut) + } + + fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + let fut = self.inner.list_internal(primary_namespace, secondary_namespace); + self.runtime.block_on(fut) + } +} + +impl KVStore for VssStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { + inner.read_internal(&primary_namespace, &secondary_namespace, &key).await + }) + } + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { + inner + .write_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + buf, + ) + .await + }) + } + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { + inner + .remove_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + ) + .await + }) + } + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { inner.list_internal(&primary_namespace, &secondary_namespace).await }) + } +} + +struct VssStoreInner { client: VssClient, store_id: String, - runtime: Runtime, storable_builder: StorableBuilder, key_obfuscator: KeyObfuscator, + // Per-key locks that ensures that we don't have concurrent writes to the same namespace/key. + // The lock also encapsulates the latest written version per key. + locks: Mutex>>>, } -impl VssStore { +impl VssStoreInner { pub(crate) fn new( base_url: String, store_id: String, vss_seed: [u8; 32], header_provider: Arc, - ) -> io::Result { - let runtime = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; + ) -> Self { let (data_encryption_key, obfuscation_master_key) = derive_data_encryption_and_obfuscation_keys(&vss_seed); let key_obfuscator = KeyObfuscator::new(obfuscation_master_key); @@ -70,17 +237,23 @@ impl VssStore { }) as _); let client = VssClient::new_with_headers(base_url, retry_policy, header_provider); - Ok(Self { client, store_id, runtime, storable_builder, key_obfuscator }) + let locks = Mutex::new(HashMap::new()); + Self { client, store_id, storable_builder, key_obfuscator, locks } + } + + fn get_inner_lock_ref(&self, locking_key: String) -> Arc> { + let mut outer_lock = self.locks.lock().unwrap(); + Arc::clone(&outer_lock.entry(locking_key).or_default()) } - fn build_key( + fn build_obfuscated_key( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> io::Result { + ) -> String { let obfuscated_key = self.key_obfuscator.obfuscate(key); if primary_namespace.is_empty() { - Ok(obfuscated_key) + obfuscated_key } else { - Ok(format!("{}#{}#{}", primary_namespace, secondary_namespace, obfuscated_key)) + format!("{}#{}#{}", primary_namespace, secondary_namespace, obfuscated_key) } } @@ -125,30 +298,25 @@ impl VssStore { } Ok(keys) } -} -impl KVStore for VssStore { - fn read( + async fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; - let request = GetObjectRequest { - store_id: self.store_id.clone(), - key: self.build_key(primary_namespace, secondary_namespace, key)?, - }; - let resp = - tokio::task::block_in_place(|| self.runtime.block_on(self.client.get_object(&request))) - .map_err(|e| { - let msg = format!( - "Failed to read from key {}/{}/{}: {}", - primary_namespace, secondary_namespace, key, e - ); - match e { - VssError::NoSuchKeyError(..) => Error::new(ErrorKind::NotFound, msg), - _ => Error::new(ErrorKind::Other, msg), - } - })?; + let obfuscated_key = self.build_obfuscated_key(primary_namespace, secondary_namespace, key); + let request = GetObjectRequest { store_id: self.store_id.clone(), key: obfuscated_key }; + let resp = self.client.get_object(&request).await.map_err(|e| { + let msg = format!( + "Failed to read from key {}/{}/{}: {}", + primary_namespace, secondary_namespace, key, e + ); + match e { + VssError::NoSuchKeyError(..) => Error::new(ErrorKind::NotFound, msg), + _ => Error::new(ErrorKind::Other, msg), + } + })?; + // unwrap safety: resp.value must be always present for a non-erroneous VSS response, otherwise // it is an API-violation which is converted to [`VssError::InternalServerError`] in [`VssClient`] let storable = Storable::decode(&resp.value.unwrap().value[..]).map_err(|e| { @@ -162,25 +330,29 @@ impl KVStore for VssStore { Ok(self.storable_builder.deconstruct(storable)?.0) } - fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], + async fn write_internal( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; - let version = -1; - let storable = self.storable_builder.build(buf.to_vec(), version); - let request = PutObjectRequest { - store_id: self.store_id.clone(), - global_version: None, - transaction_items: vec![KeyValue { - key: self.build_key(primary_namespace, secondary_namespace, key)?, - version, - value: storable.encode_to_vec(), - }], - delete_items: vec![], - }; - tokio::task::block_in_place(|| self.runtime.block_on(self.client.put_object(&request))) - .map_err(|e| { + self.execute_locked_write(inner_lock_ref, locking_key, version, async move || { + let obfuscated_key = + self.build_obfuscated_key(primary_namespace, secondary_namespace, key); + let vss_version = -1; + let storable = self.storable_builder.build(buf, vss_version); + let request = PutObjectRequest { + store_id: self.store_id.clone(), + global_version: None, + transaction_items: vec![KeyValue { + key: obfuscated_key, + version: vss_version, + value: storable.encode_to_vec(), + }], + delete_items: vec![], + }; + + self.client.put_object(&request).await.map_err(|e| { let msg = format!( "Failed to write to key {}/{}/{}: {}", primary_namespace, secondary_namespace, key, e @@ -188,49 +360,98 @@ impl KVStore for VssStore { Error::new(ErrorKind::Other, msg) })?; - Ok(()) + Ok(()) + }) + .await } - fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, + async fn remove_internal( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; - let request = DeleteObjectRequest { - store_id: self.store_id.clone(), - key_value: Some(KeyValue { - key: self.build_key(primary_namespace, secondary_namespace, key)?, - version: -1, - value: vec![], - }), - }; - tokio::task::block_in_place(|| self.runtime.block_on(self.client.delete_object(&request))) - .map_err(|e| { + self.execute_locked_write(inner_lock_ref, locking_key, version, async move || { + let obfuscated_key = + self.build_obfuscated_key(primary_namespace, secondary_namespace, key); + let request = DeleteObjectRequest { + store_id: self.store_id.clone(), + key_value: Some(KeyValue { key: obfuscated_key, version: -1, value: vec![] }), + }; + + self.client.delete_object(&request).await.map_err(|e| { let msg = format!( "Failed to delete key {}/{}/{}: {}", primary_namespace, secondary_namespace, key, e ); Error::new(ErrorKind::Other, msg) })?; - Ok(()) + + Ok(()) + }) + .await } - fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + async fn list_internal( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?; - let keys = tokio::task::block_in_place(|| { - self.runtime.block_on(self.list_all_keys(primary_namespace, secondary_namespace)) - }) - .map_err(|e| { - let msg = format!( - "Failed to retrieve keys in namespace: {}/{} : {}", - primary_namespace, secondary_namespace, e - ); - Error::new(ErrorKind::Other, msg) - })?; + let keys = + self.list_all_keys(primary_namespace, secondary_namespace).await.map_err(|e| { + let msg = format!( + "Failed to retrieve keys in namespace: {}/{} : {}", + primary_namespace, secondary_namespace, e + ); + Error::new(ErrorKind::Other, msg) + })?; Ok(keys) } + + async fn execute_locked_write< + F: Future>, + FN: FnOnce() -> F, + >( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + callback: FN, + ) -> Result<(), lightning::io::Error> { + let res = { + let mut last_written_version = inner_lock_ref.lock().await; + + // Check if we already have a newer version written/removed. This is used in async contexts to realize eventual + // consistency. + let is_stale_version = version <= *last_written_version; + + // If the version is not stale, we execute the callback. Otherwise we can and must skip writing. + if is_stale_version { + Ok(()) + } else { + callback().await.map(|_| { + *last_written_version = version; + }) + } + }; + + self.clean_locks(&inner_lock_ref, locking_key); + + res + } + + fn clean_locks(&self, inner_lock_ref: &Arc>, locking_key: String) { + // If there no arcs in use elsewhere, this means that there are no in-flight writes. We can remove the map entry + // to prevent leaking memory. The two arcs that are expected are the one in the map and the one held here in + // inner_lock_ref. The outer lock is obtained first, to avoid a new arc being cloned after we've already + // counted. + let mut outer_lock = self.locks.lock().unwrap(); + + let strong_count = Arc::strong_count(&inner_lock_ref); + debug_assert!(strong_count >= 2, "Unexpected VssStore strong count"); + + if strong_count == 2 { + outer_lock.remove(&locking_key); + } + } } fn derive_data_encryption_and_obfuscation_keys(vss_seed: &[u8; 32]) -> ([u8; 32], [u8; 32]) { @@ -261,15 +482,34 @@ impl RefUnwindSafe for VssStore {} #[cfg(test)] #[cfg(vss_test)] mod tests { - use super::*; - use crate::io::test_utils::do_read_write_remove_list_persist; + use std::collections::HashMap; + use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng, RngCore}; - use std::collections::HashMap; + use tokio::runtime; use vss_client::headers::FixedHeaders; + use super::*; + use crate::io::test_utils::do_read_write_remove_list_persist; + #[test] - fn read_write_remove_list_persist() { + fn vss_read_write_remove_list_persist() { + let runtime = Arc::new(Runtime::new().unwrap()); + let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); + let mut rng = thread_rng(); + let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); + let mut vss_seed = [0u8; 32]; + rng.fill_bytes(&mut vss_seed); + let header_provider = Arc::new(FixedHeaders::new(HashMap::new())); + let vss_store = + VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider, runtime).unwrap(); + + do_read_write_remove_list_persist(&vss_store); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn vss_read_write_remove_list_persist_in_runtime_context() { + let runtime = Arc::new(Runtime::new().unwrap()); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let mut rng = thread_rng(); let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); @@ -277,8 +517,9 @@ mod tests { rng.fill_bytes(&mut vss_seed); let header_provider = Arc::new(FixedHeaders::new(HashMap::new())); let vss_store = - VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider).unwrap(); + VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider, runtime).unwrap(); do_read_write_remove_list_persist(&vss_store); + drop(vss_store) } } diff --git a/src/lib.rs b/src/lib.rs index 18bd814f83..f4c7b5c84a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ //! controlled via commands such as [`start`], [`stop`], [`open_channel`], [`send`], etc.: //! //! ```no_run +//! # #[cfg(not(feature = "uniffi"))] +//! # { //! use ldk_node::Builder; //! use ldk_node::lightning_invoice::Bolt11Invoice; //! use ldk_node::lightning::ln::msgs::SocketAddress; @@ -57,6 +59,7 @@ //! //! node.stop().unwrap(); //! } +//! # } //! ``` //! //! [`build`]: Builder::build @@ -77,9 +80,11 @@ mod builder; mod chain; pub mod config; mod connection; +mod data_store; mod error; mod event; mod fee_estimator; +mod ffi; mod gossip; pub mod graph; mod hex_utils; @@ -90,65 +95,39 @@ mod message_handler; pub mod payment; mod peer_store; pub mod prober; -mod sweep; +mod runtime; mod tx_broadcaster; mod types; -#[cfg(feature = "uniffi")] -mod uniffi_types; mod wallet; -pub use bip39; -pub use bitcoin; -pub use lightning; -pub use lightning_invoice; -pub use lightning_liquidity; -pub use lightning_types; -pub use vss_client; +use std::default::Default; +use std::net::ToSocketAddrs; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; -pub use error::Error as NodeError; -use error::Error; - -pub use event::Event; - -pub use io::utils::generate_entropy_mnemonic; - -#[cfg(feature = "uniffi")] -use uniffi_types::*; - +use bitcoin::secp256k1::PublicKey; #[cfg(feature = "uniffi")] pub use builder::ArcedNodeBuilder as Builder; pub use builder::BuildError; #[cfg(not(feature = "uniffi"))] pub use builder::NodeBuilder as Builder; - use chain::ChainSource; use config::{ - default_user_config, may_announce_channel, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, - PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, + default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config, + NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; +pub use error::Error as NodeError; +use error::Error; +pub use event::Event; use event::{EventHandler, EventQueue}; +#[cfg(feature = "uniffi")] +use ffi::*; use gossip::GossipSource; use graph::NetworkGraph; +pub use io::utils::generate_entropy_mnemonic; use io::utils::write_node_metrics; -use liquidity::{LSPS1Liquidity, LiquiditySource}; -use payment::store::PaymentStore; -use payment::{ - Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, - UnifiedQrPayment, -}; -use peer_store::{PeerInfo, PeerStore}; -use prober::Prober; -use types::{ - Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - KeysManager, OnionMessenger, PeerManager, Router, Scorer, Sweeper, Wallet, -}; - -pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; - -use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; - use lightning::chain::BestBlock; use lightning::events::bump_transaction::Wallet as LdkWallet; use lightning::impl_writeable_tlv_based; @@ -156,18 +135,32 @@ use lightning::ln::channel_state::ChannelShutdownState; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; - +use lightning::util::persist::KVStoreSync; use lightning_background_processor::process_events_async; - -use bitcoin::secp256k1::PublicKey; - +use liquidity::{LSPS1Liquidity, LiquiditySource}; +use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use payment::asynchronous::om_mailbox::OnionMessageMailbox; +use payment::asynchronous::static_invoice_store::StaticInvoiceStore; +use payment::{ + Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, + UnifiedQrPayment, +}; +use peer_store::{PeerInfo, PeerStore}; use rand::Rng; +use runtime::Runtime; +use types::{ + Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, Graph, KeysManager, + OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, +}; +pub use types::{ + ChannelDetails, CustomTlvRecord, DynStore, PeerDetails, SyncAndAsyncKVStore, UserChannelId, +}; +use crate::prober::Prober; -use std::default::Default; -use std::net::ToSocketAddrs; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +pub use { + bip39, bitcoin, lightning, lightning_invoice, lightning_liquidity, lightning_types, tokio, + vss_client, +}; #[cfg(feature = "uniffi")] uniffi::include_scaffolding!("ldk_node"); @@ -176,9 +169,9 @@ uniffi::include_scaffolding!("ldk_node"); /// /// Needs to be initialized and instantiated through [`Builder::build`]. pub struct Node { - runtime: Arc>>>, + runtime: Arc, stop_sender: tokio::sync::watch::Sender<()>, - event_handling_stopped_sender: tokio::sync::watch::Sender<()>, + background_processor_stop_sender: tokio::sync::watch::Sender<()>, config: Arc, wallet: Arc, chain_source: Arc, @@ -199,36 +192,26 @@ pub struct Node { router: Arc, scorer: Arc>, peer_store: Arc>>, - payment_store: Arc>>, - is_listening: Arc, + payment_store: Arc, + is_running: Arc>, node_metrics: Arc>, + om_mailbox: Option>, + async_payments_role: Option, } impl Node { /// Starts the necessary background tasks, such as handling events coming from user input, /// LDK/BDK, and the peer-to-peer network. /// - /// After this returns, the [`Node`] instance can be controlled via the provided API methods in - /// a thread-safe manner. - pub fn start(&self) -> Result<(), Error> { - let runtime = - Arc::new(tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()); - self.start_with_runtime(runtime) - } - - /// Starts the necessary background tasks (such as handling events coming from user input, - /// LDK/BDK, and the peer-to-peer network) on the the given `runtime`. - /// - /// This allows to have LDK Node reuse an outer pre-existing runtime, e.g., to avoid stacking Tokio - /// runtime contexts. + /// This will try to auto-detect an outer pre-existing runtime, e.g., to avoid stacking Tokio + /// runtime contexts. Note we require the outer runtime to be of the `multithreaded` flavor. /// /// After this returns, the [`Node`] instance can be controlled via the provided API methods in /// a thread-safe manner. - pub fn start_with_runtime(&self, runtime: Arc) -> Result<(), Error> { + pub fn start(&self) -> Result<(), Error> { // Acquire a run lock and hold it until we're setup. - let mut runtime_lock = self.runtime.write().unwrap(); - if runtime_lock.is_some() { - // We're already running. + let mut is_running_lock = self.is_running.write().unwrap(); + if *is_running_lock { return Err(Error::AlreadyRunning); } @@ -239,12 +222,15 @@ impl Node { self.config.network ); + // Start up any runtime-dependant chain sources (e.g. Electrum) + self.chain_source.start(Arc::clone(&self.runtime)).map_err(|e| { + log_error!(self.logger, "Failed to start chain syncing: {}", e); + e + })?; + // Block to ensure we update our fee rate cache once on startup let chain_source = Arc::clone(&self.chain_source); - let runtime_ref = &runtime; - tokio::task::block_in_place(move || { - runtime_ref.block_on(async move { chain_source.update_fee_rate_estimates().await }) - })?; + self.runtime.block_on(async move { chain_source.update_fee_rate_estimates().await })?; // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); @@ -252,7 +238,7 @@ impl Node { let sync_cman = Arc::clone(&self.channel_manager); let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); - runtime.spawn(async move { + self.runtime.spawn_background_task(async move { chain_source .continuously_sync_wallets(stop_sync_receiver, sync_cman, sync_cmon, sync_sweeper) .await; @@ -264,7 +250,7 @@ impl Node { let gossip_sync_logger = Arc::clone(&self.logger); let gossip_node_metrics = Arc::clone(&self.node_metrics); let mut stop_gossip_sync = self.stop_sender.subscribe(); - runtime.spawn(async move { + self.runtime.spawn_cancellable_background_task(async move { let mut interval = tokio::time::interval(RGS_SYNC_INTERVAL); loop { tokio::select! { @@ -280,7 +266,7 @@ impl Node { let now = Instant::now(); match gossip_source.update_rgs_snapshot(false).await { Ok(updated_timestamp) => { - log_trace!( + log_info!( gossip_sync_logger, "Background sync of RGS gossip data finished in {}ms.", now.elapsed().as_millis() @@ -311,9 +297,7 @@ impl Node { if let Some(listening_addresses) = &self.config.listening_addresses { // Setup networking let peer_manager_connection_handler = Arc::clone(&self.peer_manager); - let mut stop_listen = self.stop_sender.subscribe(); let listening_logger = Arc::clone(&self.logger); - let listening_indicator = Arc::clone(&self.is_listening); let mut bind_addrs = Vec::with_capacity(listening_addresses.len()); @@ -331,45 +315,62 @@ impl Node { bind_addrs.extend(resolved_address); } - runtime.spawn(async move { - { - let listener = - tokio::net::TcpListener::bind(&*bind_addrs).await - .unwrap_or_else(|e| { - log_error!(listening_logger, "Failed to bind to listen addresses/ports - is something else already listening on it?: {}", e); - panic!( - "Failed to bind to listen address/port - is something else already listening on it?", - ); - }); - - listening_indicator.store(true, Ordering::Release); + let logger = Arc::clone(&listening_logger); + let listeners = self.runtime.block_on(async move { + let mut listeners = Vec::new(); - loop { - let peer_mgr = Arc::clone(&peer_manager_connection_handler); - tokio::select! { - _ = stop_listen.changed() => { - log_debug!( - listening_logger, - "Stopping listening to inbound connections.", + // Try to bind to all addresses + for addr in &*bind_addrs { + match tokio::net::TcpListener::bind(addr).await { + Ok(listener) => { + log_trace!(logger, "Listener bound to {}", addr); + listeners.push(listener); + }, + Err(e) => { + log_error!( + logger, + "Failed to bind to {}: {} - is something else already listening?", + addr, + e ); - break; - } - res = listener.accept() => { - let tcp_stream = res.unwrap().0; - tokio::spawn(async move { - lightning_net_tokio::setup_inbound( - Arc::clone(&peer_mgr), - tcp_stream.into_std().unwrap(), - ) - .await; - }); - } + return Err(Error::InvalidSocketAddress); + }, } } - } - listening_indicator.store(false, Ordering::Release); - }); + Ok(listeners) + })?; + + for listener in listeners { + let logger = Arc::clone(&listening_logger); + let peer_mgr = Arc::clone(&peer_manager_connection_handler); + let mut stop_listen = self.stop_sender.subscribe(); + let runtime = Arc::clone(&self.runtime); + self.runtime.spawn_cancellable_background_task(async move { + loop { + tokio::select! { + _ = stop_listen.changed() => { + log_debug!( + logger, + "Stopping listening to inbound connections." + ); + break; + } + res = listener.accept() => { + let tcp_stream = res.unwrap().0; + let peer_mgr = Arc::clone(&peer_mgr); + runtime.spawn_cancellable_background_task(async move { + lightning_net_tokio::setup_inbound( + Arc::clone(&peer_mgr), + tcp_stream.into_std().unwrap(), + ) + .await; + }); + } + } + } + }); + } } // Regularly reconnect to persisted peers. @@ -378,7 +379,7 @@ impl Node { let connect_logger = Arc::clone(&self.logger); let connect_peer_store = Arc::clone(&self.peer_store); let mut stop_connect = self.stop_sender.subscribe(); - runtime.spawn(async move { + self.runtime.spawn_cancellable_background_task(async move { let mut interval = tokio::time::interval(PEER_RECONNECTION_INTERVAL); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { @@ -386,7 +387,7 @@ impl Node { _ = stop_connect.changed() => { log_debug!( connect_logger, - "Stopping reconnecting known peers.", + "Stopping reconnecting known peers." ); return; } @@ -417,8 +418,8 @@ impl Node { let bcast_node_metrics = Arc::clone(&self.node_metrics); let mut stop_bcast = self.stop_sender.subscribe(); let node_alias = self.config.node_alias.clone(); - if may_announce_channel(&self.config) { - runtime.spawn(async move { + if may_announce_channel(&self.config).is_ok() { + self.runtime.spawn_cancellable_background_task(async move { // We check every 30 secs whether our last broadcast is NODE_ANN_BCAST_INTERVAL away. #[cfg(not(test))] let mut interval = tokio::time::interval(Duration::from_secs(30)); @@ -460,8 +461,10 @@ impl Node { continue; } - let addresses = if let Some(addresses) = bcast_config.listening_addresses.clone() { - addresses + let addresses = if let Some(announcement_addresses) = bcast_config.announcement_addresses.clone() { + announcement_addresses + } else if let Some(listening_addresses) = bcast_config.listening_addresses.clone() { + listening_addresses } else { debug_assert!(false, "We checked whether the node may announce, so listening addresses should always be set"); continue; @@ -490,27 +493,10 @@ impl Node { }); } - let mut stop_tx_bcast = self.stop_sender.subscribe(); + let stop_tx_bcast = self.stop_sender.subscribe(); let chain_source = Arc::clone(&self.chain_source); - let tx_bcast_logger = Arc::clone(&self.logger); - runtime.spawn(async move { - // Every second we try to clear our broadcasting queue. - let mut interval = tokio::time::interval(Duration::from_secs(1)); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = stop_tx_bcast.changed() => { - log_debug!( - tx_bcast_logger, - "Stopping broadcasting transactions.", - ); - return; - } - _ = interval.tick() => { - chain_source.process_broadcast_queue().await; - } - } - } + self.runtime.spawn_cancellable_background_task(async move { + chain_source.continuously_process_broadcast_queue(stop_tx_bcast).await }); let bump_tx_event_handler = Arc::new(BumpTransactionEventHandler::new( @@ -520,6 +506,13 @@ impl Node { Arc::clone(&self.logger), )); + let static_invoice_store = if let Some(AsyncPaymentsRole::Server) = self.async_payments_role + { + Some(StaticInvoiceStore::new(Arc::clone(&self.kv_store))) + } else { + None + }; + let event_handler = Arc::new(EventHandler::new( Arc::clone(&self.event_queue), Arc::clone(&self.wallet), @@ -528,8 +521,12 @@ impl Node { Arc::clone(&self.connection_manager), Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), + self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), + static_invoice_store, + Arc::clone(&self.onion_messenger), + self.om_mailbox.clone(), Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), @@ -542,11 +539,14 @@ impl Node { let background_chan_man = Arc::clone(&self.channel_manager); let background_gossip_sync = self.gossip_source.as_gossip_sync(); let background_peer_man = Arc::clone(&self.peer_manager); + let background_liquidity_man_opt = + self.liquidity_source.as_ref().map(|ls| ls.liquidity_manager()); + let background_sweeper = Arc::clone(&self.output_sweeper); let background_onion_messenger = Arc::clone(&self.onion_messenger); let background_logger = Arc::clone(&self.logger); let background_error_logger = Arc::clone(&self.logger); let background_scorer = Arc::clone(&self.scorer); - let stop_bp = self.stop_sender.subscribe(); + let stop_bp = self.background_processor_stop_sender.subscribe(); let sleeper_logger = Arc::clone(&self.logger); let sleeper = move |d| { let mut stop = stop_bp.clone(); @@ -567,9 +567,7 @@ impl Node { }) }; - let background_stop_logger = Arc::clone(&self.logger); - let event_handling_stopped_sender = self.event_handling_stopped_sender.clone(); - runtime.spawn(async move { + self.runtime.spawn_background_processor_task(async move { process_events_async( background_persister, |e| background_event_handler.handle_event(e), @@ -578,6 +576,8 @@ impl Node { Some(background_onion_messenger), background_gossip_sync, background_peer_man, + background_liquidity_man_opt, + Some(background_sweeper), background_logger, Some(background_scorer), sleeper, @@ -589,26 +589,13 @@ impl Node { log_error!(background_error_logger, "Failed to process events: {}", e); panic!("Failed to process events"); }); - log_debug!(background_stop_logger, "Events processing stopped.",); - - match event_handling_stopped_sender.send(()) { - Ok(_) => (), - Err(e) => { - log_error!( - background_stop_logger, - "Failed to send 'events handling stopped' signal. This should never happen: {}", - e - ); - debug_assert!(false); - }, - } }); if let Some(liquidity_source) = self.liquidity_source.as_ref() { let mut stop_liquidity_handler = self.stop_sender.subscribe(); let liquidity_handler = Arc::clone(&liquidity_source); let liquidity_logger = Arc::clone(&self.logger); - runtime.spawn(async move { + self.runtime.spawn_background_task(async move { loop { tokio::select! { _ = stop_liquidity_handler.changed() => { @@ -624,9 +611,8 @@ impl Node { }); } - *runtime_lock = Some(runtime); - log_info!(self.logger, "Startup complete."); + *is_running_lock = true; Ok(()) } @@ -634,83 +620,71 @@ impl Node { /// /// After this returns most API methods will return [`Error::NotRunning`]. pub fn stop(&self) -> Result<(), Error> { - let runtime = self.runtime.write().unwrap().take().ok_or(Error::NotRunning)?; - #[cfg(tokio_unstable)] - let metrics_runtime = Arc::clone(&runtime); + let mut is_running_lock = self.is_running.write().unwrap(); + if !*is_running_lock { + return Err(Error::NotRunning); + } log_info!(self.logger, "Shutting down LDK Node with node ID {}...", self.node_id()); - // Stop the runtime. - match self.stop_sender.send(()) { - Ok(_) => (), - Err(e) => { + // Stop background tasks. + self.stop_sender + .send(()) + .map(|_| { + log_trace!(self.logger, "Sent shutdown signal to background tasks."); + }) + .unwrap_or_else(|e| { log_error!( self.logger, "Failed to send shutdown signal. This should never happen: {}", e ); debug_assert!(false); - }, - } + }); + + // Cancel cancellable background tasks + self.runtime.abort_cancellable_background_tasks(); // Disconnect all peers. self.peer_manager.disconnect_all_peers(); + log_debug!(self.logger, "Disconnected all network peers."); - // Wait until event handling stopped, at least until a timeout is reached. - let event_handling_stopped_logger = Arc::clone(&self.logger); - let mut event_handling_stopped_receiver = self.event_handling_stopped_sender.subscribe(); - - // FIXME: For now, we wait up to 100 secs (BDK_WALLET_SYNC_TIMEOUT_SECS + 10) to allow - // event handling to exit gracefully even if it was blocked on the BDK wallet syncing. We - // should drop this considerably post upgrading to BDK 1.0. - let timeout_res = tokio::task::block_in_place(move || { - runtime.block_on(async { - tokio::time::timeout( - Duration::from_secs(100), - event_handling_stopped_receiver.changed(), - ) - .await - }) - }); + // Wait until non-cancellable background tasks (mod LDK's background processor) are done. + self.runtime.wait_on_background_tasks(); - match timeout_res { - Ok(stop_res) => match stop_res { - Ok(()) => {}, - Err(e) => { - log_error!( - event_handling_stopped_logger, - "Stopping event handling failed. This should never happen: {}", - e - ); - panic!("Stopping event handling failed. This should never happen."); - }, - }, - Err(e) => { + // Stop any runtime-dependant chain sources. + self.chain_source.stop(); + log_debug!(self.logger, "Stopped chain sources."); + + // Stop the background processor. + self.background_processor_stop_sender + .send(()) + .map(|_| { + log_trace!(self.logger, "Sent shutdown signal to background processor."); + }) + .unwrap_or_else(|e| { log_error!( - event_handling_stopped_logger, - "Stopping event handling timed out: {}", + self.logger, + "Failed to send shutdown signal. This should never happen: {}", e ); - }, - } + debug_assert!(false); + }); + + // Finally, wait until background processing stopped, at least until a timeout is reached. + self.runtime.wait_on_background_processor_task(); #[cfg(tokio_unstable)] - { - log_trace!( - self.logger, - "Active runtime tasks left prior to shutdown: {}", - metrics_runtime.metrics().active_tasks_count() - ); - } + self.runtime.log_metrics(); log_info!(self.logger, "Shutdown complete."); + *is_running_lock = false; Ok(()) } /// Returns the status of the [`Node`]. pub fn status(&self) -> NodeStatus { - let is_running = self.runtime.read().unwrap().is_some(); - let is_listening = self.is_listening.load(Ordering::Acquire); + let is_running = *self.is_running.read().unwrap(); let current_best_block = self.channel_manager.current_best_block().into(); let locked_node_metrics = self.node_metrics.read().unwrap(); let latest_lightning_wallet_sync_timestamp = @@ -728,7 +702,6 @@ impl Node { NodeStatus { is_running, - is_listening, current_best_block, latest_lightning_wallet_sync_timestamp, latest_onchain_wallet_sync_timestamp, @@ -783,15 +756,15 @@ impl Node { /// Confirm the last retrieved event handled. /// /// **Note:** This **MUST** be called after each event has been handled. - pub fn event_handled(&self) { - self.event_queue.event_handled().unwrap_or_else(|e| { + pub fn event_handled(&self) -> Result<(), Error> { + self.event_queue.event_handled().map_err(|e| { log_error!( self.logger, "Couldn't mark event handled due to persistence failure: {}", e ); - panic!("Couldn't mark event handled due to persistence failure"); - }); + e + }) } /// Returns our own node id @@ -804,6 +777,14 @@ impl Node { self.config.listening_addresses.clone() } + /// Returns the addresses that the node will announce to the network. + pub fn announcement_addresses(&self) -> Option> { + self.config + .announcement_addresses + .clone() + .or_else(|| self.config.listening_addresses.clone()) + } + /// Returns our node alias. pub fn node_alias(&self) -> Option { self.config.node_alias @@ -822,6 +803,7 @@ impl Node { Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -839,6 +821,7 @@ impl Node { Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -849,10 +832,11 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn bolt12_payment(&self) -> Bolt12Payment { Bolt12Payment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.is_running), Arc::clone(&self.logger), + self.async_payments_role, ) } @@ -862,10 +846,11 @@ impl Node { #[cfg(feature = "uniffi")] pub fn bolt12_payment(&self) -> Arc { Arc::new(Bolt12Payment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.is_running), Arc::clone(&self.logger), + self.async_payments_role, )) } @@ -873,11 +858,11 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn spontaneous_payment(&self) -> SpontaneousPayment { SpontaneousPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -886,11 +871,11 @@ impl Node { #[cfg(feature = "uniffi")] pub fn spontaneous_payment(&self) -> Arc { Arc::new(SpontaneousPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -899,10 +884,10 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn onchain_payment(&self) -> OnchainPayment { OnchainPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.wallet), Arc::clone(&self.channel_manager), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -911,10 +896,10 @@ impl Node { #[cfg(feature = "uniffi")] pub fn onchain_payment(&self) -> Arc { Arc::new(OnchainPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.wallet), Arc::clone(&self.channel_manager), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -1005,11 +990,9 @@ impl Node { pub fn connect( &self, node_id: PublicKey, address: SocketAddress, persist: bool, ) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let runtime = rt_lock.as_ref().unwrap(); let peer_info = PeerInfo { node_id, address }; @@ -1019,10 +1002,8 @@ impl Node { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; log_info!(self.logger, "Connected to peer {}@{}. ", peer_info.node_id, peer_info.address); @@ -1039,8 +1020,7 @@ impl Node { /// Will also remove the peer from the peer store, i.e., after this has been called we won't /// try to reconnect on restart. pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } @@ -1062,11 +1042,9 @@ impl Node { push_to_counterparty_msat: Option, channel_config: Option, announce_for_forwarding: bool, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let runtime = rt_lock.as_ref().unwrap(); let peer_info = PeerInfo { node_id, address }; @@ -1090,10 +1068,8 @@ impl Node { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; // Fail if we have less than the channel value + anchor reserve available (if applicable). @@ -1216,35 +1192,33 @@ impl Node { &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, push_to_counterparty_msat: Option, channel_config: Option, ) -> Result { - if may_announce_channel(&self.config) { - self.open_channel_inner( - node_id, - address, - channel_amount_sats, - push_to_counterparty_msat, - channel_config, - true, - ) - } else { - log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node. Please make sure to configure listening addreesses and node alias"); + if let Err(err) = may_announce_channel(&self.config) { + log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node: {}", err); return Err(Error::ChannelCreationFailed); } + + self.open_channel_inner( + node_id, + address, + channel_amount_sats, + push_to_counterparty_msat, + channel_config, + true, + ) } /// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate /// cache. /// - /// **Note:** The wallets are regularly synced in the background, which is configurable via the - /// respective config object, e.g., via - /// [`EsploraSyncConfig::onchain_wallet_sync_interval_secs`] and - /// [`EsploraSyncConfig::lightning_wallet_sync_interval_secs`]. Therefore, using this blocking - /// sync method is almost always redundant and should be avoided where possible. + /// **Note:** The wallets are regularly synced in the background if background syncing is enabled + /// via [`EsploraSyncConfig::background_sync_config`]. Therefore, using this blocking sync method + /// is almost always redundant when background syncing is enabled and should be avoided where possible. + /// However, if background syncing is disabled (i.e., `background_sync_config` is set to `None`), + /// this method must be called manually to keep wallets in sync with the chain state. /// - /// [`EsploraSyncConfig::onchain_wallet_sync_interval_secs`]: crate::config::EsploraSyncConfig::onchain_wallet_sync_interval_secs - /// [`EsploraSyncConfig::lightning_wallet_sync_interval_secs`]: crate::config::EsploraSyncConfig::lightning_wallet_sync_interval_secs + /// [`EsploraSyncConfig::background_sync_config`]: crate::config::EsploraSyncConfig::background_sync_config pub fn sync_wallets(&self) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } @@ -1252,27 +1226,21 @@ impl Node { let sync_cman = Arc::clone(&self.channel_manager); let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); - tokio::task::block_in_place(move || { - tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( - async move { - match chain_source.as_ref() { - ChainSource::Esplora { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) - .await?; - chain_source.sync_onchain_wallet().await?; - }, - ChainSource::BitcoindRpc { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper) - .await?; - }, - } - Ok(()) - }, - ) + self.runtime.block_on(async move { + if chain_source.is_transaction_based() { + chain_source.update_fee_rate_estimates().await?; + chain_source + .sync_lightning_wallet(sync_cman, sync_cmon, Arc::clone(&sync_sweeper)) + .await?; + chain_source.sync_onchain_wallet().await?; + } else { + chain_source.update_fee_rate_estimates().await?; + chain_source + .poll_and_update_listeners(sync_cman, sync_cmon, Arc::clone(&sync_sweeper)) + .await?; + } + let _ = sync_sweeper.regenerate_and_broadcast_spend_if_necessary().await; + Ok(()) }) } @@ -1328,35 +1296,16 @@ impl Node { open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0) { if force { - if self.config.anchor_channels_config.as_ref().map_or(false, |acc| { - acc.trusted_peers_no_reserve.contains(&counterparty_node_id) - }) { - self.channel_manager - .force_close_without_broadcasting_txn( - &channel_details.channel_id, - &counterparty_node_id, - force_close_reason.unwrap_or_default(), - ) - .map_err(|e| { - log_error!( - self.logger, - "Failed to force-close channel to trusted peer: {:?}", - e - ); - Error::ChannelClosingFailed - })?; - } else { - self.channel_manager - .force_close_broadcasting_latest_txn( - &channel_details.channel_id, - &counterparty_node_id, - force_close_reason.unwrap_or_default(), - ) - .map_err(|e| { - log_error!(self.logger, "Failed to force-close channel: {:?}", e); - Error::ChannelClosingFailed - })?; - } + self.channel_manager + .force_close_broadcasting_latest_txn( + &channel_details.channel_id, + &counterparty_node_id, + force_close_reason.unwrap_or_default(), + ) + .map_err(|e| { + log_error!(self.logger, "Failed to force-close channel: {:?}", e); + Error::ChannelClosingFailed + })?; } else { self.channel_manager .close_channel(&channel_details.channel_id, &counterparty_node_id) @@ -1421,12 +1370,10 @@ impl Node { let mut total_lightning_balance_sats = 0; let mut lightning_balances = Vec::new(); - for (funding_txo, channel_id) in self.chain_monitor.list_monitors() { - match self.chain_monitor.get_monitor(funding_txo) { + for channel_id in self.chain_monitor.list_monitors() { + match self.chain_monitor.get_monitor(channel_id) { Ok(monitor) => { - // unwrap safety: `get_counterparty_node_id` will always be `Some` after 0.0.110 and - // LDK Node 0.1 depended on 0.0.115 already. - let counterparty_node_id = monitor.get_counterparty_node_id().unwrap(); + let counterparty_node_id = monitor.get_counterparty_node_id(); for ldk_balance in monitor.get_claimable_balances() { total_lightning_balance_sats += ldk_balance.claimable_amount_satoshis(); lightning_balances.push(LightningBalance::from_ldk_balance( @@ -1558,20 +1505,20 @@ impl Node { /// Exports the current state of the scorer. The result can be shared with and merged by light nodes that only have /// a limited view of the network. pub fn export_pathfinding_scores(&self) -> Result, Error> { - self.kv_store - .read( - lightning::util::persist::SCORER_PERSISTENCE_PRIMARY_NAMESPACE, - lightning::util::persist::SCORER_PERSISTENCE_SECONDARY_NAMESPACE, - lightning::util::persist::SCORER_PERSISTENCE_KEY, - ) - .map_err(|e| { - log_error!( - self.logger, - "Failed to access store while exporting pathfinding scores: {}", - e - ); - Error::PersistenceFailed - }) + KVStoreSync::read( + &*self.kv_store, + lightning::util::persist::SCORER_PERSISTENCE_PRIMARY_NAMESPACE, + lightning::util::persist::SCORER_PERSISTENCE_SECONDARY_NAMESPACE, + lightning::util::persist::SCORER_PERSISTENCE_KEY, + ) + .map_err(|e| { + log_error!( + self.logger, + "Failed to access store while exporting pathfinding scores: {}", + e + ); + Error::PersistenceFailed + }) } } @@ -1586,9 +1533,6 @@ impl Drop for Node { pub struct NodeStatus { /// Indicates whether the [`Node`] is running. pub is_running: bool, - /// Indicates whether the [`Node`] is listening for incoming connections on the addresses - /// configured via [`Config::listening_addresses`]. - pub is_listening: bool, /// The best block to which our Lightning wallet is currently synced. pub current_best_block: BestBlock, /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced diff --git a/src/liquidity.rs b/src/liquidity.rs index cbc19954fc..81d48e530f 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -7,69 +7,144 @@ //! Objects related to liquidity management. -use crate::chain::ChainSource; -use crate::connection::ConnectionManager; -use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; -use crate::{Config, Error}; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::Duration; -use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::{PublicKey, Secp256k1}; +use chrono::Utc; +use lightning::events::HTLCHandlingFailureType; +use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; +use lightning::ln::types::ChannelId; use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; -use lightning_liquidity::events::Event; -use lightning_liquidity::lsps0::ser::RequestId; -use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::events::LiquidityEvent; +use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; +use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; -use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, OrderParameters}; -use lightning_liquidity::lsps2::client::LSPS2ClientConfig; -use lightning_liquidity::lsps2::event::LSPS2ClientEvent; -use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps1::msgs::{ + LSPS1ChannelInfo, LSPS1Options, LSPS1OrderId, LSPS1OrderParams, +}; +use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; +use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::msgs::{LSPS2OpeningFeeParams, LSPS2RawOpeningFeeParams}; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; -use lightning_liquidity::LiquidityClientConfig; - -use bitcoin::hashes::{sha256, Hash}; -use bitcoin::secp256k1::{PublicKey, Secp256k1}; - +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; +use lightning_types::payment::PaymentHash; +use rand::Rng; use tokio::sync::oneshot; -use std::collections::HashMap; -use std::ops::Deref; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; +use crate::builder::BuildError; +use crate::chain::ChainSource; +use crate::connection::ConnectionManager; +use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; +use crate::runtime::Runtime; +use crate::types::{ + Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet, +}; +use crate::{total_anchor_channels_reserve_sats, Config, Error}; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; -struct LSPS1Service { - node_id: PublicKey, - address: SocketAddress, +const LSPS2_GETINFO_REQUEST_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); +const LSPS2_CLIENT_TRUSTS_LSP_MODE: bool = true; +const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; + +struct LSPS1Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, - client_config: LSPS1ClientConfig, + ldk_client_config: LdkLSPS1ClientConfig, pending_opening_params_requests: - Mutex>>, - pending_create_order_requests: Mutex>>, + Mutex>>, + pending_create_order_requests: Mutex>>, pending_check_order_status_requests: - Mutex>>, + Mutex>>, } -struct LSPS2Service { - node_id: PublicKey, - address: SocketAddress, +#[derive(Debug, Clone)] +pub(crate) struct LSPS1ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + +struct LSPS2Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, - client_config: LSPS2ClientConfig, - pending_fee_requests: Mutex>>, - pending_buy_requests: Mutex>>, + ldk_client_config: LdkLSPS2ClientConfig, + pending_fee_requests: Mutex>>, + pending_buy_requests: Mutex>>, +} + +#[derive(Debug, Clone)] +pub(crate) struct LSPS2ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + +struct LSPS2Service { + service_config: LSPS2ServiceConfig, + ldk_service_config: LdkLSPS2ServiceConfig, +} + +/// Represents the configuration of the LSPS2 service. +/// +/// See [bLIP-52 / LSPS2] for more information. +/// +/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md +#[derive(Debug, Clone)] +pub struct LSPS2ServiceConfig { + /// A token we may require to be sent by the clients. + /// + /// If set, only requests matching this token will be accepted. + pub require_token: Option, + /// Indicates whether the LSPS service will be announced via the gossip network. + pub advertise_service: bool, + /// The fee we withhold for the channel open from the initial payment. + /// + /// This fee is proportional to the client-requested amount, in parts-per-million. + pub channel_opening_fee_ppm: u32, + /// The proportional overprovisioning for the channel. + /// + /// This determines, in parts-per-million, how much value we'll provision on top of the amount + /// we need to forward the payment to the client. + /// + /// For example, setting this to `100_000` will result in a channel being opened that is 10% + /// larger than then the to-be-forwarded amount (i.e., client-requested amount minus the + /// channel opening fee fee). + pub channel_over_provisioning_ppm: u32, + /// The minimum fee required for opening a channel. + pub min_channel_opening_fee_msat: u64, + /// The minimum number of blocks after confirmation we promise to keep the channel open. + pub min_channel_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The minimum payment size that we will accept when opening a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size that we will accept when opening a channel. + pub max_payment_size_msat: u64, } pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, { - lsps1_service: Option, + lsps1_client: Option, + lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, + tx_broadcaster: Arc, + kv_store: Arc, config: Arc, logger: L, } @@ -79,35 +154,41 @@ where L::Target: LdkLogger, { pub(crate) fn new( - channel_manager: Arc, keys_manager: Arc, - chain_source: Arc, config: Arc, logger: L, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, + chain_source: Arc, tx_broadcaster: Arc, kv_store: Arc, + config: Arc, logger: L, ) -> Self { - let lsps1_service = None; + let lsps1_client = None; + let lsps2_client = None; let lsps2_service = None; Self { - lsps1_service, + lsps1_client, + lsps2_client, lsps2_service, + wallet, channel_manager, keys_manager, chain_source, + tx_broadcaster, + kv_store, config, logger, } } - pub(crate) fn lsps1_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps1_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { // TODO: allow to set max_channel_fees_msat - let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let ldk_client_config = LdkLSPS1ClientConfig { max_channel_fees_msat: None }; let pending_opening_params_requests = Mutex::new(HashMap::new()); let pending_create_order_requests = Mutex::new(HashMap::new()); let pending_check_order_status_requests = Mutex::new(HashMap::new()); - self.lsps1_service = Some(LSPS1Service { - node_id, - address, + self.lsps1_client = Some(LSPS1Client { + lsp_node_id, + lsp_address, token, - client_config, + ldk_client_config, pending_opening_params_requests, pending_create_order_requests, pending_check_order_status_requests, @@ -115,47 +196,76 @@ where self } - pub(crate) fn lsps2_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps2_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { - let client_config = LSPS2ClientConfig {}; + let ldk_client_config = LdkLSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); - self.lsps2_service = Some(LSPS2Service { - node_id, - address, + self.lsps2_client = Some(LSPS2Client { + lsp_node_id, + lsp_address, token, - client_config, + ldk_client_config, pending_fee_requests, pending_buy_requests, }); self } - pub(crate) fn build(self) -> LiquiditySource { - let lsps1_client_config = self.lsps1_service.as_ref().map(|s| s.client_config.clone()); - let lsps2_client_config = self.lsps2_service.as_ref().map(|s| s.client_config.clone()); - let liquidity_client_config = - Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); + pub(crate) fn lsps2_service( + &mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let ldk_service_config = LdkLSPS2ServiceConfig { promise_secret }; + self.lsps2_service = Some(LSPS2Service { service_config, ldk_service_config }); + self + } - let liquidity_manager = Arc::new(LiquidityManager::new( - Arc::clone(&self.keys_manager), - Arc::clone(&self.channel_manager), - Some(Arc::clone(&self.chain_source)), - None, - None, - liquidity_client_config, - )); + pub(crate) async fn build(self) -> Result, BuildError> { + let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { + let lsps2_service_config = Some(s.ldk_service_config.clone()); + let lsps5_service_config = None; + let advertise_service = s.service_config.advertise_service; + LiquidityServiceConfig { lsps2_service_config, lsps5_service_config, advertise_service } + }); - LiquiditySource { - lsps1_service: self.lsps1_service, + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone()); + let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.ldk_client_config.clone()); + let lsps5_client_config = None; + let liquidity_client_config = Some(LiquidityClientConfig { + lsps1_client_config, + lsps2_client_config, + lsps5_client_config, + }); + + let liquidity_manager = Arc::new( + LiquidityManager::new( + Arc::clone(&self.keys_manager), + Arc::clone(&self.keys_manager), + Arc::clone(&self.channel_manager), + Some(Arc::clone(&self.chain_source)), + None, + Arc::clone(&self.kv_store), + Arc::clone(&self.tx_broadcaster), + liquidity_service_config, + liquidity_client_config, + ) + .await + .map_err(|_| BuildError::ReadFailed)?, + ); + + Ok(LiquiditySource { + lsps1_client: self.lsps1_client, + lsps2_client: self.lsps2_client, lsps2_service: self.lsps2_service, + wallet: self.wallet, channel_manager: self.channel_manager, + peer_manager: RwLock::new(None), keys_manager: self.keys_manager, liquidity_manager, config: self.config, logger: self.logger, - } + }) } } @@ -163,9 +273,12 @@ pub(crate) struct LiquiditySource where L::Target: LdkLogger, { - lsps1_service: Option, + lsps1_client: Option, + lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, + peer_manager: RwLock>>, keys_manager: Arc, liquidity_manager: Arc, config: Arc, @@ -177,31 +290,30 @@ where L::Target: LdkLogger, { pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { - let process_msgs_callback = move || peer_manager.process_events(); - self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); + *self.peer_manager.write().unwrap() = Some(peer_manager); } - pub(crate) fn liquidity_manager(&self) -> &LiquidityManager { - self.liquidity_manager.as_ref() + pub(crate) fn liquidity_manager(&self) -> Arc { + Arc::clone(&self.liquidity_manager) } - pub(crate) fn get_lsps1_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps1_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps1_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } - pub(crate) fn get_lsps2_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps2_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps2_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { - Event::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { request_id, counterparty_node_id, supported_options, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -213,7 +325,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_opening_params_requests .lock() .unwrap() @@ -248,7 +360,7 @@ where ); } }, - Event::LSPS1Client(LSPS1ClientEvent::OrderCreated { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { request_id, counterparty_node_id, order_id, @@ -256,8 +368,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -269,7 +381,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_create_order_requests .lock() .unwrap() @@ -306,7 +418,7 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderCreated event!"); } }, - Event::LSPS1Client(LSPS1ClientEvent::OrderStatus { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { request_id, counterparty_node_id, order_id, @@ -314,8 +426,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -327,7 +439,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_check_order_status_requests .lock() .unwrap() @@ -364,13 +476,265 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); } }, - Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + if let Some(required) = service_config.require_token { + if token != Some(required) { + log_error!( + self.logger, + "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", + request_id, + counterparty_node_id + ); + lsps2_service_handler.invalid_token_provided(&counterparty_node_id, request_id.clone()).unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS2 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS2 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + return; + } + } + + let valid_until = LSPSDateTime(Utc::now() + LSPS2_GETINFO_REQUEST_EXPIRY); + let opening_fee_params = LSPS2RawOpeningFeeParams { + min_fee_msat: service_config.min_channel_opening_fee_msat, + proportional: service_config.channel_opening_fee_ppm, + valid_until, + min_lifetime: service_config.min_channel_lifetime, + max_client_to_self_delay: service_config.max_client_to_self_delay, + min_payment_size_msat: service_config.min_payment_size_msat, + max_payment_size_msat: service_config.max_payment_size_msat, + }; + + let opening_fee_params_menu = vec![opening_fee_params]; + + if let Err(e) = lsps2_service_handler.opening_fee_params_generated( + &counterparty_node_id, + request_id, + opening_fee_params_menu, + ) { + log_error!( + self.logger, + "Failed to handle generated opening fee params: {:?}", + e + ); + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: _, + payment_size_msat, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let user_channel_id: u128 = rand::thread_rng().gen::(); + let intercept_scid = self.channel_manager.get_intercept_scid(); + + if let Some(payment_size_msat) = payment_size_msat { + // We already check this in `lightning-liquidity`, but better safe than + // sorry. + // + // TODO: We might want to eventually send back an error here, but we + // currently can't and have to trust `lightning-liquidity` is doing the + // right thing. + // + // TODO: Eventually we also might want to make sure that we have sufficient + // liquidity for the channel opening here. + if payment_size_msat > service_config.max_payment_size_msat + || payment_size_msat < service_config.min_payment_size_msat + { + log_error!( + self.logger, + "Rejecting to handle LSPS2 buy request {:?} from counterparty {} as the client requested an invalid payment size.", + request_id, + counterparty_node_id + ); + return; + } + } + + match lsps2_service_handler + .invoice_parameters_generated( + &counterparty_node_id, + request_id, + intercept_scid, + LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, + LSPS2_CLIENT_TRUSTS_LSP_MODE, + user_channel_id, + ) + .await + { + Ok(()) => {}, + Err(e) => { + log_error!( + self.logger, + "Failed to provide invoice parameters: {:?}", + e + ); + return; + }, + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + their_network_key, + amt_to_forward_msat, + opening_fee_msat: _, + user_channel_id, + intercept_scid: _, + }) => { + if self.liquidity_manager.lsps2_service_handler().is_none() { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let init_features = if let Some(peer_manager) = + self.peer_manager.read().unwrap().as_ref() + { + // Fail if we're not connected to the prospective channel partner. + if let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) { + peer.init_features + } else { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {} due to peer not being not connected.", + their_network_key, + ); + return; + } + } else { + debug_assert!(false, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + return; + }; + + // Fail if we have insufficient onchain funds available. + let over_provisioning_msat = (amt_to_forward_msat + * service_config.channel_over_provisioning_ppm as u64) + / 1_000_000; + let channel_amount_sats = (amt_to_forward_msat + over_provisioning_msat) / 1000; + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let required_funds_sats = channel_amount_sats + + self.config.anchor_channels_config.as_ref().map_or(0, |c| { + if init_features.requires_anchors_zero_fee_htlc_tx() + && !c.trusted_peers_no_reserve.contains(&their_network_key) + { + c.per_channel_reserve_sats + } else { + 0 + } + }); + if spendable_amount_sats < required_funds_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, channel_amount_sats + ); + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + return; + } + + let mut config = self.channel_manager.get_current_config().clone(); + + // We set these LSP-specific values during Node building, here we're making sure it's actually set. + debug_assert_eq!( + config + .channel_handshake_config + .max_inbound_htlc_value_in_flight_percent_of_channel, + 100 + ); + debug_assert!(config.accept_forwards_to_priv_channels); + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + // + // TODO: revisit this decision eventually. + config.channel_config.forwarding_fee_base_msat = 0; + config.channel_config.forwarding_fee_proportional_millionths = 0; + + match self.channel_manager.create_channel( + their_network_key, + channel_amount_sats, + 0, + user_channel_id, + None, + Some(config), + ) { + Ok(_) => {}, + Err(e) => { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {}: {:?}", + their_network_key, + e + ); + return; + }, + } + }, + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, opening_fee_params_menu, }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -383,7 +747,7 @@ where } if let Some(sender) = - lsps2_service.pending_fee_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_fee_requests.lock().unwrap().remove(&request_id) { let response = LSPS2FeeResponse { opening_fee_params_menu }; @@ -414,15 +778,15 @@ where ); } }, - Event::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { request_id, counterparty_node_id, intercept_scid, cltv_expiry_delta, .. }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -435,7 +799,7 @@ where } if let Some(sender) = - lsps2_service.pending_buy_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_buy_requests.lock().unwrap().remove(&request_id) { let response = LSPS2BuyResponse { intercept_scid, cltv_expiry_delta }; @@ -475,7 +839,7 @@ where pub(crate) async fn lsps1_request_opening_params( &self, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); @@ -485,8 +849,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_opening_params_requests_lock = - lsps1_service.pending_opening_params_requests.lock().unwrap(); - let request_id = client_handler.request_supported_options(lsps1_service.node_id); + lsps1_client.pending_opening_params_requests.lock().unwrap(); + let request_id = client_handler.request_supported_options(lsps1_client.lsp_node_id); pending_opening_params_requests_lock.insert(request_id, request_sender); } @@ -506,7 +870,7 @@ where &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, announce_channel: bool, refund_address: bitcoin::Address, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -554,13 +918,13 @@ where return Err(Error::LiquidityRequestFailed); } - let order_params = OrderParameters { + let order_params = LSPS1OrderParams { lsp_balance_sat, client_balance_sat, required_channel_confirmations: lsp_limits.min_required_channel_confirmations, funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks, channel_expiry_blocks, - token: lsps1_service.token.clone(), + token: lsps1_client.token.clone(), announce_channel, }; @@ -568,9 +932,9 @@ where let request_id; { let mut pending_create_order_requests_lock = - lsps1_service.pending_create_order_requests.lock().unwrap(); + lsps1_client.pending_create_order_requests.lock().unwrap(); request_id = client_handler.create_order( - &lsps1_service.node_id, + &lsps1_client.lsp_node_id, order_params.clone(), Some(refund_address), ); @@ -603,9 +967,9 @@ where } pub(crate) async fn lsps1_check_order_status( - &self, order_id: OrderId, + &self, order_id: LSPS1OrderId, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -614,8 +978,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_check_order_status_requests_lock = - lsps1_service.pending_check_order_status_requests.lock().unwrap(); - let request_id = client_handler.check_order_status(&lsps1_service.node_id, order_id); + lsps1_client.pending_check_order_status_requests.lock().unwrap(); + let request_id = client_handler.check_order_status(&lsps1_client.lsp_node_id, order_id); pending_check_order_status_requests_lock.insert(request_id, request_sender); } @@ -638,7 +1002,7 @@ where pub(crate) async fn lsps2_receive_to_jit_channel( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, - max_total_lsp_fee_limit_msat: Option, + max_total_lsp_fee_limit_msat: Option, payment_hash: Option, ) -> Result<(Bolt11Invoice, u64), Error> { let fee_response = self.lsps2_request_opening_fee_params().await?; @@ -690,6 +1054,7 @@ where Some(amount_msat), description, expiry_secs, + payment_hash, )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); @@ -698,7 +1063,7 @@ where pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, - max_proportional_lsp_fee_limit_ppm_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result<(Bolt11Invoice, u64), Error> { let fee_response = self.lsps2_request_opening_fee_params().await?; @@ -732,15 +1097,20 @@ where ); let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?; - let invoice = - self.lsps2_create_jit_invoice(buy_response, None, description, expiry_secs)?; + let invoice = self.lsps2_create_jit_invoice( + buy_response, + None, + description, + expiry_secs, + payment_hash, + )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); Ok((invoice, min_prop_fee_ppm_msat)) } async fn lsps2_request_opening_fee_params(&self) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -749,9 +1119,9 @@ where let (fee_request_sender, fee_request_receiver) = oneshot::channel(); { - let mut pending_fee_requests_lock = lsps2_service.pending_fee_requests.lock().unwrap(); + let mut pending_fee_requests_lock = lsps2_client.pending_fee_requests.lock().unwrap(); let request_id = client_handler - .request_opening_params(lsps2_service.node_id, lsps2_service.token.clone()); + .request_opening_params(lsps2_client.lsp_node_id, lsps2_client.token.clone()); pending_fee_requests_lock.insert(request_id, fee_request_sender); } @@ -771,9 +1141,9 @@ where } async fn lsps2_send_buy_request( - &self, amount_msat: Option, opening_fee_params: OpeningFeeParams, + &self, amount_msat: Option, opening_fee_params: LSPS2OpeningFeeParams, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -782,9 +1152,9 @@ where let (buy_request_sender, buy_request_receiver) = oneshot::channel(); { - let mut pending_buy_requests_lock = lsps2_service.pending_buy_requests.lock().unwrap(); + let mut pending_buy_requests_lock = lsps2_client.pending_buy_requests.lock().unwrap(); let request_id = client_handler - .select_opening_params(lsps2_service.node_id, amount_msat, opening_fee_params) + .select_opening_params(lsps2_client.lsp_node_id, amount_msat, opening_fee_params) .map_err(|e| { log_error!( self.logger, @@ -816,21 +1186,39 @@ where fn lsps2_create_jit_invoice( &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, + payment_hash: Option, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; - let (payment_hash, payment_secret) = self - .channel_manager - .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) - .map_err(|e| { - log_error!(self.logger, "Failed to register inbound payment: {:?}", e); - Error::InvoiceCreationFailed - })?; + let (payment_hash, payment_secret) = match payment_hash { + Some(payment_hash) => { + let payment_secret = self + .channel_manager + .create_inbound_payment_for_hash( + payment_hash, + None, + expiry_secs, + Some(min_final_cltv_expiry_delta), + ) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?; + (payment_hash, payment_secret) + }, + None => self + .channel_manager + .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?, + }; let route_hint = RouteHint(vec![RouteHintHop { - src_node_id: lsps2_service.node_id, + src_node_id: lsps2_client.lsp_node_id, short_channel_id: buy_response.intercept_scid, fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, cltv_expiry_delta: buy_response.cltv_expiry_delta as u16, @@ -867,6 +1255,76 @@ where Error::InvoiceCreationFailed }) } + + pub(crate) async fn handle_channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler + .channel_ready(user_channel_id, channel_id, counterparty_node_id) + .await + { + log_error!( + self.logger, + "LSPS2 service failed to handle ChannelReady event: {:?}", + e + ); + } + } + } + + pub(crate) async fn handle_htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler + .htlc_intercepted( + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) + .await + { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCIntercepted event: {:?}", + e + ); + } + } + } + + pub(crate) async fn handle_htlc_handling_failed(&self, failure_type: HTLCHandlingFailureType) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failure_type).await { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", + e + ); + } + } + } + + pub(crate) async fn handle_payment_forwarded( + &self, next_channel_id: Option, skimmed_fee_msat: u64, + ) { + if let Some(next_channel_id) = next_channel_id { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = + lsps2_service_handler.payment_forwarded(next_channel_id, skimmed_fee_msat).await + { + log_error!( + self.logger, + "LSPS2 service failed to handle PaymentForwarded: {:?}", + e + ); + } + } + } + } } #[derive(Debug, Clone)] @@ -878,79 +1336,24 @@ pub(crate) struct LSPS1OpeningParamsResponse { #[derive(Debug, Clone)] pub struct LSPS1OrderStatus { /// The id of the channel order. - pub order_id: OrderId, + pub order_id: LSPS1OrderId, /// The parameters of channel order. - pub order_params: OrderParameters, + pub order_params: LSPS1OrderParams, /// Contains details about how to pay for the order. - pub payment_options: PaymentInfo, + pub payment_options: LSPS1PaymentInfo, /// Contains information about the channel state. - pub channel_state: Option, + pub channel_state: Option, } #[cfg(not(feature = "uniffi"))] -type PaymentInfo = lightning_liquidity::lsps1::msgs::PaymentInfo; +type LSPS1PaymentInfo = lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo; -/// Details regarding how to pay for an order. #[cfg(feature = "uniffi")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PaymentInfo { - /// A Lightning payment using BOLT 11. - pub bolt11: Option, - /// An onchain payment. - pub onchain: Option, -} - -#[cfg(feature = "uniffi")] -impl From for PaymentInfo { - fn from(value: lightning_liquidity::lsps1::msgs::PaymentInfo) -> Self { - PaymentInfo { bolt11: value.bolt11, onchain: value.onchain.map(|o| o.into()) } - } -} - -/// An onchain payment. -#[cfg(feature = "uniffi")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OnchainPaymentInfo { - /// Indicates the current state of the payment. - pub state: lightning_liquidity::lsps1::msgs::PaymentState, - /// The datetime when the payment option expires. - pub expires_at: chrono::DateTime, - /// The total fee the LSP will charge to open this channel in satoshi. - pub fee_total_sat: u64, - /// The amount the client needs to pay to have the requested channel openend. - pub order_total_sat: u64, - /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel - /// opened. - pub address: bitcoin::Address, - /// The minimum number of block confirmations that are required for the on-chain payment to be - /// considered confirmed. - pub min_onchain_payment_confirmations: Option, - /// The minimum fee rate for the on-chain payment in case the client wants the payment to be - /// confirmed without a confirmation. - pub min_fee_for_0conf: Arc, - /// The address where the LSP will send the funds if the order fails. - pub refund_onchain_address: Option, -} - -#[cfg(feature = "uniffi")] -impl From for OnchainPaymentInfo { - fn from(value: lightning_liquidity::lsps1::msgs::OnchainPaymentInfo) -> Self { - Self { - state: value.state, - expires_at: value.expires_at, - fee_total_sat: value.fee_total_sat, - order_total_sat: value.order_total_sat, - address: value.address, - min_onchain_payment_confirmations: value.min_onchain_payment_confirmations, - min_fee_for_0conf: Arc::new(value.min_fee_for_0conf), - refund_onchain_address: value.refund_onchain_address, - } - } -} +type LSPS1PaymentInfo = crate::ffi::LSPS1PaymentInfo; #[derive(Debug, Clone)] pub(crate) struct LSPS2FeeResponse { - opening_fee_params_menu: Vec, + opening_fee_params_menu: Vec, } #[derive(Debug, Clone)] @@ -972,7 +1375,7 @@ pub(crate) struct LSPS2BuyResponse { /// [`Bolt11Payment::receive_via_jit_channel`]: crate::payment::Bolt11Payment::receive_via_jit_channel #[derive(Clone)] pub struct LSPS1Liquidity { - runtime: Arc>>>, + runtime: Arc, wallet: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, @@ -981,7 +1384,7 @@ pub struct LSPS1Liquidity { impl LSPS1Liquidity { pub(crate) fn new( - runtime: Arc>>>, wallet: Arc, + runtime: Arc, wallet: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, logger: Arc, ) -> Self { @@ -999,12 +1402,8 @@ impl LSPS1Liquidity { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; - - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let con_node_id = lsp_node_id; let con_addr = lsp_address.clone(); @@ -1012,10 +1411,8 @@ impl LSPS1Liquidity { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; log_info!(self.logger, "Connected to LSP {}@{}. ", lsp_node_id, lsp_address); @@ -1023,34 +1420,28 @@ impl LSPS1Liquidity { let refund_address = self.wallet.get_new_address()?; let liquidity_source = Arc::clone(&liquidity_source); - let response = tokio::task::block_in_place(move || { - runtime.block_on(async move { - liquidity_source - .lsps1_request_channel( - lsp_balance_sat, - client_balance_sat, - channel_expiry_blocks, - announce_channel, - refund_address, - ) - .await - }) + let response = self.runtime.block_on(async move { + liquidity_source + .lsps1_request_channel( + lsp_balance_sat, + client_balance_sat, + channel_expiry_blocks, + announce_channel, + refund_address, + ) + .await })?; Ok(response) } /// Connects to the configured LSP and checks for the status of a previously-placed order. - pub fn check_order_status(&self, order_id: OrderId) -> Result { + pub fn check_order_status(&self, order_id: LSPS1OrderId) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; - - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let con_node_id = lsp_node_id; let con_addr = lsp_address.clone(); @@ -1058,18 +1449,14 @@ impl LSPS1Liquidity { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; let liquidity_source = Arc::clone(&liquidity_source); - let response = tokio::task::block_in_place(move || { - runtime - .block_on(async move { liquidity_source.lsps1_check_order_status(order_id).await }) - })?; - + let response = self + .runtime + .block_on(async move { liquidity_source.lsps1_check_order_status(order_id).await })?; Ok(response) } } diff --git a/src/logger.rs b/src/logger.rs index 7a329fae64..4eaefad746 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -7,14 +7,6 @@ //! Logging-related objects. -pub(crate) use lightning::util::logger::{Logger as LdkLogger, Record as LdkRecord}; -pub(crate) use lightning::{log_bytes, log_debug, log_error, log_info, log_trace}; - -pub use lightning::util::logger::Level as LogLevel; - -use chrono::Utc; -use log::{debug, error, info, trace, warn}; - #[cfg(not(feature = "uniffi"))] use core::fmt; use std::fs; @@ -22,6 +14,12 @@ use std::io::Write; use std::path::Path; use std::sync::Arc; +use chrono::Utc; +pub use lightning::util::logger::Level as LogLevel; +pub(crate) use lightning::util::logger::{Logger as LdkLogger, Record as LdkRecord}; +pub(crate) use lightning::{log_bytes, log_debug, log_error, log_info, log_trace}; +use log::{Level as LogFacadeLevel, Record as LogFacadeRecord}; + /// A unit of logging output with metadata to enable filtering `module_path`, /// `file`, and `line` to inform on log's source. #[cfg(not(feature = "uniffi"))] @@ -108,7 +106,7 @@ pub(crate) enum Writer { /// Writes logs to the file system. FileWriter { file_path: String, max_log_level: LogLevel }, /// Forwards logs to the `log` facade. - LogFacadeWriter { max_log_level: LogLevel }, + LogFacadeWriter, /// Forwards logs to a custom writer. CustomWriter(Arc), } @@ -123,7 +121,7 @@ impl LogWriter for Writer { let log = format!( "{} {:<5} [{}:{}] {}\n", - Utc::now().format("%Y-%m-%d %H:%M:%S"), + Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"), record.level.to_string(), record.module_path, record.line, @@ -138,24 +136,35 @@ impl LogWriter for Writer { .write_all(log.as_bytes()) .expect("Failed to write to log file") }, - Writer::LogFacadeWriter { max_log_level } => { - if record.level < *max_log_level { - return; - } - macro_rules! log_with_level { - ($log_level:expr, $target: expr, $($args:tt)*) => { - match $log_level { - LogLevel::Gossip | LogLevel::Trace => trace!(target: $target, $($args)*), - LogLevel::Debug => debug!(target: $target, $($args)*), - LogLevel::Info => info!(target: $target, $($args)*), - LogLevel::Warn => warn!(target: $target, $($args)*), - LogLevel::Error => error!(target: $target, $($args)*), - } - }; - } - - let target = format!("[{}:{}]", record.module_path, record.line); - log_with_level!(record.level, &target, " {}", record.args) + Writer::LogFacadeWriter => { + let mut builder = LogFacadeRecord::builder(); + + match record.level { + LogLevel::Gossip | LogLevel::Trace => builder.level(LogFacadeLevel::Trace), + LogLevel::Debug => builder.level(LogFacadeLevel::Debug), + LogLevel::Info => builder.level(LogFacadeLevel::Info), + LogLevel::Warn => builder.level(LogFacadeLevel::Warn), + LogLevel::Error => builder.level(LogFacadeLevel::Error), + }; + + #[cfg(not(feature = "uniffi"))] + log::logger().log( + &builder + .target(record.module_path) + .module_path(Some(record.module_path)) + .line(Some(record.line)) + .args(format_args!("{}", record.args)) + .build(), + ); + #[cfg(feature = "uniffi")] + log::logger().log( + &builder + .target(&record.module_path) + .module_path(Some(&record.module_path)) + .line(Some(record.line)) + .args(format_args!("{}", record.args)) + .build(), + ); }, Writer::CustomWriter(custom_logger) => custom_logger.log(record), } @@ -186,8 +195,8 @@ impl Logger { Ok(Self { writer: Writer::FileWriter { file_path, max_log_level } }) } - pub fn new_log_facade(max_log_level: LogLevel) -> Self { - Self { writer: Writer::LogFacadeWriter { max_log_level } } + pub fn new_log_facade() -> Self { + Self { writer: Writer::LogFacadeWriter } } pub fn new_custom_writer(log_writer: Arc) -> Self { @@ -204,10 +213,7 @@ impl LdkLogger for Logger { } self.writer.log(record.into()); }, - Writer::LogFacadeWriter { max_log_level } => { - if record.level < *max_log_level { - return; - } + Writer::LogFacadeWriter => { self.writer.log(record.into()); }, Writer::CustomWriter(_arc) => { diff --git a/src/message_handler.rs b/src/message_handler.rs index cebd1ea07c..fc206ec4da 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -5,20 +5,18 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::liquidity::LiquiditySource; +use std::ops::Deref; +use std::sync::Arc; +use bitcoin::secp256k1::PublicKey; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; use lightning::util::logger::Logger; - -use lightning_types::features::{InitFeatures, NodeFeatures}; - +use lightning::util::ser::LengthLimitedRead; use lightning_liquidity::lsps0::ser::RawLSPSMessage; +use lightning_types::features::{InitFeatures, NodeFeatures}; -use bitcoin::secp256k1::PublicKey; - -use std::ops::Deref; -use std::sync::Arc; +use crate::liquidity::LiquiditySource; pub(crate) enum NodeCustomMessageHandler where @@ -47,7 +45,7 @@ where { type CustomMessage = RawLSPSMessage; - fn read( + fn read( &self, message_type: u16, buffer: &mut RD, ) -> Result, lightning::ln::msgs::DecodeError> { match self { diff --git a/src/payment/asynchronous/mod.rs b/src/payment/asynchronous/mod.rs new file mode 100644 index 0000000000..c28f6e2433 --- /dev/null +++ b/src/payment/asynchronous/mod.rs @@ -0,0 +1,10 @@ +// 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. + +pub(crate) mod om_mailbox; +mod rate_limiter; +pub(crate) mod static_invoice_store; diff --git a/src/payment/asynchronous/om_mailbox.rs b/src/payment/asynchronous/om_mailbox.rs new file mode 100644 index 0000000000..9a7478706d --- /dev/null +++ b/src/payment/asynchronous/om_mailbox.rs @@ -0,0 +1,99 @@ +use std::collections::{HashMap, VecDeque}; +use std::sync::Mutex; + +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::OnionMessage; + +pub(crate) struct OnionMessageMailbox { + map: Mutex>>, +} + +impl OnionMessageMailbox { + const MAX_MESSAGES_PER_PEER: usize = 30; + const MAX_PEERS: usize = 300; + + pub fn new() -> Self { + Self { map: Mutex::new(HashMap::with_capacity(Self::MAX_PEERS)) } + } + + pub(crate) fn onion_message_intercepted(&self, peer_node_id: PublicKey, message: OnionMessage) { + let mut map = self.map.lock().unwrap(); + + let queue = map.entry(peer_node_id).or_insert_with(VecDeque::new); + if queue.len() >= Self::MAX_MESSAGES_PER_PEER { + queue.pop_front(); + } + queue.push_back(message); + + // Enforce a peers limit. If exceeded, evict the peer with the longest queue. + if map.len() > Self::MAX_PEERS { + let peer_to_remove = + map.iter().max_by_key(|(_, queue)| queue.len()).map(|(peer, _)| *peer).unwrap(); + + map.remove(&peer_to_remove); + } + } + + pub(crate) fn onion_message_peer_connected( + &self, peer_node_id: PublicKey, + ) -> Vec { + let mut map = self.map.lock().unwrap(); + + if let Some(queue) = map.remove(&peer_node_id) { + queue.into() + } else { + Vec::new() + } + } + + #[cfg(test)] + pub(crate) fn is_empty(&self) -> bool { + let map = self.map.lock().unwrap(); + map.is_empty() + } +} + +#[cfg(test)] +mod tests { + use bitcoin::key::Secp256k1; + use bitcoin::secp256k1::{PublicKey, SecretKey}; + use lightning::onion_message; + + use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; + + #[test] + fn onion_message_mailbox() { + let mailbox = OnionMessageMailbox::new(); + + let secp = Secp256k1::new(); + let sk_bytes = [12; 32]; + let sk = SecretKey::from_slice(&sk_bytes).unwrap(); + let peer_node_id = PublicKey::from_secret_key(&secp, &sk); + + let blinding_sk = SecretKey::from_slice(&[13; 32]).unwrap(); + let blinding_point = PublicKey::from_secret_key(&secp, &blinding_sk); + + let message_sk = SecretKey::from_slice(&[13; 32]).unwrap(); + let message_point = PublicKey::from_secret_key(&secp, &message_sk); + + let message = lightning::ln::msgs::OnionMessage { + blinding_point, + onion_routing_packet: onion_message::packet::Packet { + version: 0, + public_key: message_point, + hop_data: vec![1, 2, 3], + hmac: [0; 32], + }, + }; + mailbox.onion_message_intercepted(peer_node_id, message.clone()); + + let messages = mailbox.onion_message_peer_connected(peer_node_id); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0], message); + + assert!(mailbox.is_empty()); + + let messages = mailbox.onion_message_peer_connected(peer_node_id); + assert_eq!(messages.len(), 0); + } +} diff --git a/src/payment/asynchronous/rate_limiter.rs b/src/payment/asynchronous/rate_limiter.rs new file mode 100644 index 0000000000..671b1dc72a --- /dev/null +++ b/src/payment/asynchronous/rate_limiter.rs @@ -0,0 +1,96 @@ +// 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. + +//! [`RateLimiter`] to control the rate of requests from users. + +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +/// Implements a leaky-bucket style rate limiter parameterized by the max capacity of the bucket, the refill interval, +/// and the max idle duration. +/// +/// For every passing of the refill interval, one token is added to the bucket, up to the maximum capacity. When the +/// bucket has remained at the maximum capacity for longer than the max idle duration, it is removed to prevent memory +/// leakage. +pub(crate) struct RateLimiter { + users: HashMap, Bucket>, + capacity: u32, + refill_interval: Duration, + max_idle: Duration, +} + +struct Bucket { + tokens: u32, + last_refill: Instant, +} + +impl RateLimiter { + pub(crate) fn new(capacity: u32, refill_interval: Duration, max_idle: Duration) -> Self { + Self { users: HashMap::new(), capacity, refill_interval, max_idle } + } + + pub(crate) fn allow(&mut self, user_id: &[u8]) -> bool { + let now = Instant::now(); + + let entry = self.users.entry(user_id.to_vec()); + let is_new_user = matches!(entry, std::collections::hash_map::Entry::Vacant(_)); + + let bucket = entry.or_insert(Bucket { tokens: self.capacity, last_refill: now }); + + let elapsed = now.duration_since(bucket.last_refill); + let tokens_to_add = (elapsed.as_secs_f64() / self.refill_interval.as_secs_f64()) as u32; + + if tokens_to_add > 0 { + bucket.tokens = (bucket.tokens + tokens_to_add).min(self.capacity); + bucket.last_refill = now; + } + + let allow = if bucket.tokens > 0 { + bucket.tokens -= 1; + true + } else { + false + }; + + // Each time a new user is added, we take the opportunity to clean up old rate limits. + if is_new_user { + self.garbage_collect(self.max_idle); + } + + allow + } + + fn garbage_collect(&mut self, max_idle: Duration) { + let now = Instant::now(); + self.users.retain(|_, bucket| now.duration_since(bucket.last_refill) < max_idle); + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::payment::asynchronous::rate_limiter::RateLimiter; + + #[test] + fn rate_limiter_test() { + // Test + let mut rate_limiter = + RateLimiter::new(3, Duration::from_millis(100), Duration::from_secs(1)); + + assert!(rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user1")); + assert!(!rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user2")); + + std::thread::sleep(Duration::from_millis(150)); + + assert!(rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user2")); + } +} diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs new file mode 100644 index 0000000000..a7e2d2f9e7 --- /dev/null +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -0,0 +1,309 @@ +// 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. + +//! Store implementation for [`StaticInvoice`]s. + +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use lightning::blinded_path::message::BlindedMessagePath; +use lightning::impl_writeable_tlv_based; +use lightning::offers::static_invoice::StaticInvoice; +use lightning::util::persist::KVStoreSync; +use lightning::util::ser::{Readable, Writeable}; + +use crate::hex_utils; +use crate::io::STATIC_INVOICE_STORE_PRIMARY_NAMESPACE; +use crate::payment::asynchronous::rate_limiter::RateLimiter; +use crate::types::DynStore; + +struct PersistedStaticInvoice { + invoice: StaticInvoice, + request_path: BlindedMessagePath, +} + +impl_writeable_tlv_based!(PersistedStaticInvoice, { + (0, invoice, required), + (2, request_path, required) +}); + +pub(crate) struct StaticInvoiceStore { + kv_store: Arc, + request_rate_limiter: Mutex, + persist_rate_limiter: Mutex, +} + +impl StaticInvoiceStore { + const RATE_LIMITER_BUCKET_CAPACITY: u32 = 5; + const RATE_LIMITER_REFILL_INTERVAL: Duration = Duration::from_millis(100); + const RATE_LIMITER_MAX_IDLE: Duration = Duration::from_secs(600); + + pub(crate) fn new(kv_store: Arc) -> Self { + Self { + kv_store, + request_rate_limiter: Mutex::new(RateLimiter::new( + Self::RATE_LIMITER_BUCKET_CAPACITY, + Self::RATE_LIMITER_REFILL_INTERVAL, + Self::RATE_LIMITER_MAX_IDLE, + )), + persist_rate_limiter: Mutex::new(RateLimiter::new( + Self::RATE_LIMITER_BUCKET_CAPACITY, + Self::RATE_LIMITER_REFILL_INTERVAL, + Self::RATE_LIMITER_MAX_IDLE, + )), + } + } + + fn check_rate_limit( + limiter: &Mutex, recipient_id: &[u8], + ) -> Result<(), lightning::io::Error> { + let mut limiter = limiter.lock().unwrap(); + if !limiter.allow(recipient_id) { + Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, "Rate limit exceeded")) + } else { + Ok(()) + } + } + + pub(crate) async fn handle_static_invoice_requested( + &self, recipient_id: &[u8], invoice_slot: u16, + ) -> Result, lightning::io::Error> { + Self::check_rate_limit(&self.request_rate_limiter, &recipient_id)?; + + let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, recipient_id); + + KVStoreSync::read( + &*self.kv_store, + STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, + &secondary_namespace, + &key, + ) + .and_then(|data| { + PersistedStaticInvoice::read(&mut &*data) + .map(|persisted_invoice| { + Some((persisted_invoice.invoice, persisted_invoice.request_path)) + }) + .map_err(|e| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + format!("Failed to parse static invoice: {:?}", e), + ) + }) + }) + .or_else( + |e| { + if e.kind() == lightning::io::ErrorKind::NotFound { + Ok(None) + } else { + Err(e) + } + }, + ) + } + + pub(crate) async fn handle_persist_static_invoice( + &self, invoice: StaticInvoice, invoice_request_path: BlindedMessagePath, invoice_slot: u16, + recipient_id: Vec, + ) -> Result<(), lightning::io::Error> { + Self::check_rate_limit(&self.persist_rate_limiter, &recipient_id)?; + + let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, &recipient_id); + + let persisted_invoice = + PersistedStaticInvoice { invoice, request_path: invoice_request_path }; + + let mut buf = Vec::new(); + persisted_invoice.write(&mut buf)?; + + // Static invoices will be persisted at "static_invoices//". + // + // Example: static_invoices/039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81/00001 + KVStoreSync::write( + &*self.kv_store, + STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, + &secondary_namespace, + &key, + buf, + ) + } + + fn get_storage_location(invoice_slot: u16, recipient_id: &[u8]) -> (String, String) { + let hash = Sha256::hash(recipient_id).to_byte_array(); + let secondary_namespace = hex_utils::to_string(&hash); + + let key = format!("{:05}", invoice_slot); + (secondary_namespace, key) + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::time::Duration; + + use bitcoin::key::{Keypair, Secp256k1}; + use bitcoin::secp256k1::{PublicKey, SecretKey}; + use lightning::blinded_path::message::BlindedMessagePath; + use lightning::blinded_path::payment::{BlindedPayInfo, BlindedPaymentPath}; + use lightning::blinded_path::BlindedHop; + use lightning::ln::inbound_payment::ExpandedKey; + use lightning::offers::nonce::Nonce; + use lightning::offers::offer::OfferBuilder; + use lightning::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}; + use lightning::sign::EntropySource; + use lightning::util::test_utils::TestStore; + use lightning_types::features::BlindedHopFeatures; + + use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; + use crate::types::DynStore; + + #[tokio::test] + async fn static_invoice_store_test() { + let store: Arc = Arc::new(TestStore::new(false)); + let static_invoice_store = StaticInvoiceStore::new(Arc::clone(&store)); + + let static_invoice = invoice(); + let recipient_id = vec![1, 1, 1]; + let invoice_request_path = blinded_path(); + assert!(static_invoice_store + .handle_persist_static_invoice( + static_invoice.clone(), + invoice_request_path.clone(), + 0, + recipient_id.clone() + ) + .await + .is_ok()); + + let requested_invoice = + static_invoice_store.handle_static_invoice_requested(&recipient_id, 0).await.unwrap(); + + assert_eq!(requested_invoice.unwrap(), (static_invoice, invoice_request_path)); + + assert!(static_invoice_store + .handle_static_invoice_requested(&recipient_id, 1) + .await + .unwrap() + .is_none()); + + assert!(static_invoice_store + .handle_static_invoice_requested(&[2, 2, 2], 0) + .await + .unwrap() + .is_none()); + } + + fn invoice() -> StaticInvoice { + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = now(); + 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(); + + StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap() + } + + fn now() -> Duration { + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH") + } + + fn payment_paths() -> Vec { + vec![ + BlindedPaymentPath::from_blinded_path_and_payinfo( + pubkey(40), + pubkey(41), + vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ), + BlindedPaymentPath::from_blinded_path_and_payinfo( + pubkey(40), + pubkey(41), + vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ), + ] + } + + fn blinded_path() -> BlindedMessagePath { + BlindedMessagePath::from_blinded_path( + pubkey(40), + pubkey(41), + vec![ + BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 44] }, + ], + ) + } + + fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) + } + + fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() + } + + fn recipient_keys() -> Keypair { + let secp_ctx = Secp256k1::new(); + Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) + } + + fn recipient_pubkey() -> PublicKey { + recipient_keys().public_key() + } + + struct FixedEntropy; + + impl EntropySource for FixedEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + [42; 32] + } + } +} diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 2c1b19143c..60c313381a 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -9,52 +9,43 @@ //! //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +use std::sync::{Arc, RwLock}; + +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use lightning::ln::channelmanager::{ + Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, +}; +use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; +use lightning_invoice::{ + Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription, +}; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; + use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::connection::ConnectionManager; +use crate::data_store::DataStoreUpdateResult; use crate::error::Error; +use crate::ffi::{maybe_deref, maybe_try_convert_enum, maybe_wrap}; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{ LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, - PaymentStatus, PaymentStore, + PaymentStatus, }; -use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; -use crate::types::ChannelManager; +use crate::runtime::Runtime; +use crate::types::{ChannelManager, PaymentStore}; -use lightning::ln::bolt11_payment; -use lightning::ln::channelmanager::{ - Bolt11InvoiceParameters, PaymentId, RecipientOnionFields, Retry, RetryableSendFailure, -}; -use lightning::routing::router::{PaymentParameters, RouteParameters}; - -use lightning_types::payment::{PaymentHash, PaymentPreimage}; - -use lightning_invoice::Bolt11Invoice; -use lightning_invoice::Bolt11InvoiceDescription as LdkBolt11InvoiceDescription; - -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; - -use std::sync::{Arc, RwLock}; +#[cfg(not(feature = "uniffi"))] +type Bolt11Invoice = LdkBolt11Invoice; +#[cfg(feature = "uniffi")] +type Bolt11Invoice = Arc; #[cfg(not(feature = "uniffi"))] type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; #[cfg(feature = "uniffi")] -type Bolt11InvoiceDescription = crate::uniffi_types::Bolt11InvoiceDescription; - -macro_rules! maybe_convert_description { - ($description: expr) => {{ - #[cfg(not(feature = "uniffi"))] - { - $description - } - #[cfg(feature = "uniffi")] - { - &LdkBolt11InvoiceDescription::try_from($description)? - } - }}; -} +type Bolt11InvoiceDescription = crate::ffi::Bolt11InvoiceDescription; /// A payment handler allowing to create and pay [BOLT 11] invoices. /// @@ -63,24 +54,24 @@ macro_rules! maybe_convert_description { /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md /// [`Node::bolt11_payment`]: crate::Node::bolt11_payment pub struct Bolt11Payment { - runtime: Arc>>>, + runtime: Arc, channel_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, - payment_store: Arc>>, + payment_store: Arc, peer_store: Arc>>, config: Arc, + is_running: Arc>, logger: Arc, } impl Bolt11Payment { pub(crate) fn new( - runtime: Arc>>>, - channel_manager: Arc, + runtime: Arc, channel_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, - payment_store: Arc>>, peer_store: Arc>>, - config: Arc, logger: Arc, + payment_store: Arc, peer_store: Arc>>, + config: Arc, is_running: Arc>, logger: Arc, ) -> Self { Self { runtime, @@ -90,27 +81,24 @@ impl Bolt11Payment { payment_store, peer_store, config, + is_running, logger, } } /// Send a payment given an invoice. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send( - &self, invoice: &Bolt11Invoice, sending_parameters: Option, + &self, invoice: &Bolt11Invoice, route_parameters: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let (payment_hash, recipient_onion, mut route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead."); - Error::InvalidInvoice - })?; - + let invoice = maybe_deref(invoice); + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); if let Some(payment) = self.payment_store.get(&payment_id) { if payment.status == PaymentStatus::Pending @@ -121,29 +109,16 @@ impl Bolt11Payment { } } - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; - - let payment_secret = Some(*invoice.payment_secret()); + let route_parameters = + route_parameters.or(self.config.route_parameters).unwrap_or_default(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let payment_secret = Some(*invoice.payment_secret()); - match self.channel_manager.send_payment( - payment_hash, - recipient_onion, + match self.channel_manager.pay_for_bolt11_invoice( + invoice, payment_id, - route_params, + None, + route_parameters, retry_strategy, ) { Ok(()) => { @@ -160,6 +135,7 @@ impl Bolt11Payment { payment_id, kind, invoice.amount_milli_satoshis(), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -168,7 +144,13 @@ impl Bolt11Payment { Ok(payment_id) }, - Err(e) => { + Err(Bolt11PaymentError::InvalidAmount) => { + log_error!(self.logger, + "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead." + ); + return Err(Error::InvalidInvoice); + }, + Err(Bolt11PaymentError::SendingFailed(e)) => { log_error!(self.logger, "Failed to send payment: {:?}", e); match e { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), @@ -182,6 +164,7 @@ impl Bolt11Payment { payment_id, kind, invoice.amount_milli_satoshis(), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -201,17 +184,17 @@ impl Bolt11Payment { /// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the /// amount paid to be determined by the user. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, - sending_parameters: Option, + route_parameters: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + let invoice = maybe_deref(invoice); if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { if amount_msat < invoice_amount_msat { log_error!( @@ -232,46 +215,16 @@ impl Bolt11Payment { } } - let payment_secret = invoice.payment_secret(); - let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); - let mut payment_params = PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), - invoice.min_final_cltv_expiry_delta() as u32, - ) - .with_expiry_time(expiry_time.as_secs()) - .with_route_hints(invoice.route_hints()) - .map_err(|_| Error::InvalidInvoice)?; - if let Some(features) = invoice.features() { - payment_params = payment_params - .with_bolt11_features(features.clone()) - .map_err(|_| Error::InvalidInvoice)?; - } - let mut route_params = - RouteParameters::from_payment_params_and_value(payment_params, amount_msat); - - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; - + let route_parameters = + route_parameters.or(self.config.route_parameters).unwrap_or_default(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let recipient_fields = RecipientOnionFields::secret_only(*payment_secret); + let payment_secret = Some(*invoice.payment_secret()); - match self.channel_manager.send_payment( - payment_hash, - recipient_fields, + match self.channel_manager.pay_for_bolt11_invoice( + invoice, payment_id, - route_params, + Some(amount_msat), + route_parameters, retry_strategy, ) { Ok(()) => { @@ -286,13 +239,14 @@ impl Bolt11Payment { let kind = PaymentKind::Bolt11 { hash: payment_hash, preimage: None, - secret: Some(*payment_secret), + secret: payment_secret, }; let payment = PaymentDetails::new( payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -300,26 +254,33 @@ impl Bolt11Payment { Ok(payment_id) }, - Err(e) => { + Err(Bolt11PaymentError::InvalidAmount) => { + log_error!( + self.logger, + "Failed to send payment due to amount given being insufficient." + ); + return Err(Error::InvalidInvoice); + }, + Err(Bolt11PaymentError::SendingFailed(e)) => { log_error!(self.logger, "Failed to send payment: {:?}", e); - match e { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { let kind = PaymentKind::Bolt11 { hash: payment_hash, preimage: None, - secret: Some(*payment_secret), + secret: payment_secret, }; let payment = PaymentDetails::new( payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); - self.payment_store.insert(payment)?; + self.payment_store.insert(payment)?; Err(Error::PaymentSendingFailed) }, } @@ -360,8 +321,17 @@ impl Bolt11Payment { } if let Some(details) = self.payment_store.get(&payment_id) { - if let Some(expected_amount_msat) = details.amount_msat { - if claimable_amount_msat < expected_amount_msat { + // For payments requested via `receive*_via_jit_channel_for_hash()` + // `skimmed_fee_msat` held by LSP must be taken into account. + let skimmed_fee_msat = match details.kind { + PaymentKind::Bolt11Jit { + counterparty_skimmed_fee_msat: Some(skimmed_fee_msat), + .. + } => skimmed_fee_msat, + _ => 0, + }; + if let Some(invoice_amount_msat) = details.amount_msat { + if claimable_amount_msat < invoice_amount_msat - skimmed_fee_msat { log_error!( self.logger, "Failed to manually claim payment {} as the claimable amount is less than expected", @@ -404,13 +374,25 @@ impl Bolt11Payment { ..PaymentDetailsUpdate::new(payment_id) }; - if !self.payment_store.update(&update)? { - log_error!( - self.logger, - "Failed to manually fail unknown payment with hash: {}", - payment_hash - ); - return Err(Error::InvalidPaymentHash); + match self.payment_store.update(&update) { + Ok(DataStoreUpdateResult::Updated) | Ok(DataStoreUpdateResult::Unchanged) => (), + Ok(DataStoreUpdateResult::NotFound) => { + log_error!( + self.logger, + "Failed to manually fail unknown payment with hash {}", + payment_hash, + ); + return Err(Error::InvalidPaymentHash); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to manually fail payment with hash {}: {}", + payment_hash, + e + ); + return Err(e); + }, } self.channel_manager.fail_htlc_backwards(&payment_hash); @@ -424,8 +406,9 @@ impl Bolt11Payment { pub fn receive( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let description = maybe_convert_description!(description); - self.receive_inner(Some(amount_msat), description, expiry_secs, None) + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner(Some(amount_msat), &description, expiry_secs, None)?; + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request a payment of the amount @@ -446,8 +429,10 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { - let description = maybe_convert_description!(description); - self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash)) + let description = maybe_try_convert_enum(description)?; + let invoice = + self.receive_inner(Some(amount_msat), &description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request and receive a payment for which the @@ -457,8 +442,9 @@ impl Bolt11Payment { pub fn receive_variable_amount( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let description = maybe_convert_description!(description); - self.receive_inner(None, description, expiry_secs, None) + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner(None, &description, expiry_secs, None)?; + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request a payment for the given payment hash @@ -478,14 +464,15 @@ impl Bolt11Payment { pub fn receive_variable_amount_for_hash( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { - let description = maybe_convert_description!(description); - self.receive_inner(None, description, expiry_secs, Some(payment_hash)) + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner(None, &description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap(invoice)) } pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, - ) -> Result { + ) -> Result { let invoice = { let invoice_params = Bolt11InvoiceParameters { amount_msats: amount_msat, @@ -531,6 +518,7 @@ impl Bolt11Payment { id, kind, amount_msat, + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -553,14 +541,55 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, ) -> Result { - let description = maybe_convert_description!(description); - self.receive_via_jit_channel_inner( + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( Some(amount_msat), - description, + &description, expiry_secs, max_total_lsp_fee_limit_msat, None, - ) + None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a payment of the amount given and + /// receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the + /// channel to us. We'll use its cheapest offer otherwise. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits + /// is performed *before* emitting the event. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + pub fn receive_via_jit_channel_for_hash( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, payment_hash: PaymentHash, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( + Some(amount_msat), + &description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + Some(payment_hash), + )?; + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request a variable amount payment (also known @@ -578,30 +607,68 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { - let description = maybe_convert_description!(description); - self.receive_via_jit_channel_inner( + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( + None, + &description, + expiry_secs, + None, + max_proportional_lsp_fee_limit_ppm_msat, + None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a variable amount payment (also known + /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in + /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. + /// We'll use its cheapest offer otherwise. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits + /// is performed *before* emitting the event. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + pub fn receive_variable_amount_via_jit_channel_for_hash( + &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: PaymentHash, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( None, - description, + &description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, - ) + Some(payment_hash), + )?; + Ok(maybe_wrap(invoice)) } fn receive_via_jit_channel_inner( &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, - max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, + ) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (node_id, address) = liquidity_source - .get_lsps2_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; - - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let peer_info = PeerInfo { node_id, address }; @@ -611,39 +678,37 @@ impl Bolt11Payment { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); let liquidity_source = Arc::clone(&liquidity_source); let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = - tokio::task::block_in_place(move || { - runtime.block_on(async move { - if let Some(amount_msat) = amount_msat { - liquidity_source - .lsps2_receive_to_jit_channel( - amount_msat, - description, - expiry_secs, - max_total_lsp_fee_limit_msat, - ) - .await - .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) - } else { - liquidity_source - .lsps2_receive_variable_amount_to_jit_channel( - description, - expiry_secs, - max_proportional_lsp_fee_limit_ppm_msat, - ) - .await - .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) - } - }) + self.runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_receive_to_jit_channel( + amount_msat, + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + payment_hash, + ) + .await + .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) + } else { + liquidity_source + .lsps2_receive_variable_amount_to_jit_channel( + description, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + payment_hash, + ) + .await + .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) + } })?; // Register payment in payment store. @@ -660,12 +725,14 @@ impl Bolt11Payment { hash: payment_hash, preimage, secret: Some(payment_secret.clone()), + counterparty_skimmed_fee_msat: None, lsp_fee_limits, }; let payment = PaymentDetails::new( id, kind, amount_msat, + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -690,17 +757,41 @@ impl Bolt11Payment { /// payment. To mitigate this issue, channels with available liquidity less than the required /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send /// pre-flight probes. - pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + pub fn send_probes( + &self, invoice: &Bolt11Invoice, route_parameters: Option, + ) -> Result<(), Error> { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let (_payment_hash, _recipient_onion, route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + let invoice = maybe_deref(invoice); + let payment_params = PaymentParameters::from_bolt11_invoice(invoice); + + let amount_msat = invoice.amount_milli_satoshis().ok_or_else(|| { log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_probes_using_amount instead."); Error::InvalidInvoice })?; + let mut route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); self.channel_manager @@ -719,35 +810,49 @@ impl Bolt11Payment { /// This can be used to send pre-flight probes for a so-called "zero-amount" invoice, i.e., an /// invoice that leaves the amount paid to be determined by the user. /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + /// /// See [`Self::send_probes`] for more information. pub fn send_probes_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, + route_parameters: Option, ) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = - invoice.amount_milli_satoshis() - { + let invoice = maybe_deref(invoice); + let payment_params = PaymentParameters::from_bolt11_invoice(invoice); + + if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { if amount_msat < invoice_amount_msat { log_error!( self.logger, - "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); + "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", + invoice_amount_msat, + amount_msat + ); return Err(Error::InvalidAmount); } + } - bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being \"zero-amount\"."); - Error::InvalidInvoice - })? - } else { - bolt11_payment::payment_parameters_from_variable_amount_invoice(&invoice, amount_msat).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); - Error::InvalidInvoice - })? - }; + let mut route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 1ff8739bef..337eedf968 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -9,26 +9,41 @@ //! //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::config::LDK_PAYMENT_RETRY_TIMEOUT; +use std::num::NonZeroU64; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use lightning::blinded_path::message::BlindedMessagePath; +use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId, Retry}; +use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; +use lightning::offers::parse::Bolt12SemanticError; +use lightning::routing::router::RouteParametersConfig; +#[cfg(feature = "uniffi")] +use lightning::util::ser::{Readable, Writeable}; +use lightning_types::string::UntrustedString; +use rand::RngCore; + +use crate::config::{AsyncPaymentsRole, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; +use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::store::{ - PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, -}; -use crate::types::ChannelManager; - -use lightning::ln::channelmanager::{PaymentId, Retry}; -use lightning::offers::invoice::Bolt12Invoice; -use lightning::offers::offer::{Amount, Offer, Quantity}; -use lightning::offers::parse::Bolt12SemanticError; -use lightning::offers::refund::Refund; -use lightning::util::string::UntrustedString; +use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::types::{ChannelManager, PaymentStore}; -use rand::RngCore; +#[cfg(not(feature = "uniffi"))] +type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice; +#[cfg(feature = "uniffi")] +type Bolt12Invoice = Arc; -use std::num::NonZeroU64; -use std::sync::{Arc, RwLock}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +#[cfg(not(feature = "uniffi"))] +type Offer = LdkOffer; +#[cfg(feature = "uniffi")] +type Offer = Arc; + +#[cfg(not(feature = "uniffi"))] +type Refund = lightning::offers::refund::Refund; +#[cfg(feature = "uniffi")] +type Refund = Arc; /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// @@ -37,19 +52,20 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [`Node::bolt12_payment`]: crate::Node::bolt12_payment pub struct Bolt12Payment { - runtime: Arc>>>, channel_manager: Arc, - payment_store: Arc>>, + payment_store: Arc, + is_running: Arc>, logger: Arc, + async_payments_role: Option, } impl Bolt12Payment { pub(crate) fn new( - runtime: Arc>>>, - channel_manager: Arc, payment_store: Arc>>, - logger: Arc, + channel_manager: Arc, payment_store: Arc, + is_running: Arc>, logger: Arc, + async_payments_role: Option, ) -> Self { - Self { runtime, channel_manager, payment_store, logger } + Self { channel_manager, payment_store, is_running, logger, async_payments_role } } /// Send a payment given an offer. @@ -61,15 +77,17 @@ impl Bolt12Payment { pub fn send( &self, offer: &Offer, quantity: Option, payer_note: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + + let offer = maybe_deref(offer); + let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let offer_amount_msat = match offer.amount() { Some(Amount::Bitcoin { amount_msats }) => amount_msats, @@ -83,15 +101,19 @@ impl Bolt12Payment { }, }; - match self.channel_manager.pay_for_offer( - &offer, - quantity, - None, - payer_note.clone(), - payment_id, + let params = OptionalOfferPaymentParams { + payer_note: payer_note.clone(), retry_strategy, - max_total_routing_fee_msat, - ) { + route_params_config, + }; + let res = if let Some(quantity) = quantity { + self.channel_manager + .pay_for_offer_with_quantity(&offer, None, payment_id, params, quantity) + } else { + self.channel_manager.pay_for_offer(&offer, None, payment_id, params) + }; + + match res { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); log_info!( @@ -113,6 +135,7 @@ impl Bolt12Payment { payment_id, kind, Some(offer_amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -137,6 +160,7 @@ impl Bolt12Payment { payment_id, kind, Some(offer_amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -160,16 +184,17 @@ impl Bolt12Payment { pub fn send_using_amount( &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + let offer = maybe_deref(offer); + let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let offer_amount_msat = match offer.amount() { Some(Amount::Bitcoin { amount_msats }) => amount_msats, @@ -187,15 +212,24 @@ impl Bolt12Payment { return Err(Error::InvalidAmount); } - match self.channel_manager.pay_for_offer( - &offer, - quantity, - Some(amount_msat), - payer_note.clone(), - payment_id, + let params = OptionalOfferPaymentParams { + payer_note: payer_note.clone(), retry_strategy, - max_total_routing_fee_msat, - ) { + route_params_config, + }; + let res = if let Some(quantity) = quantity { + self.channel_manager.pay_for_offer_with_quantity( + &offer, + Some(amount_msat), + payment_id, + params, + quantity, + ) + } else { + self.channel_manager.pay_for_offer(&offer, Some(amount_msat), payment_id, params) + }; + + match res { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); log_info!( @@ -217,6 +251,7 @@ impl Bolt12Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -241,6 +276,7 @@ impl Bolt12Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -252,22 +288,20 @@ impl Bolt12Payment { } } - /// Returns a payable offer that can be used to request and receive a payment of the amount - /// given. - pub fn receive( + pub(crate) fn receive_inner( &self, amount_msat: u64, description: &str, expiry_secs: Option, quantity: Option, - ) -> Result { - let absolute_expiry = expiry_secs.map(|secs| { - (SystemTime::now() + Duration::from_secs(secs as u64)) - .duration_since(UNIX_EPOCH) - .unwrap() - }); + ) -> Result { + let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; - let offer_builder = - self.channel_manager.create_offer_builder(absolute_expiry).map_err(|e| { - log_error!(self.logger, "Failed to create offer builder: {:?}", e); - Error::OfferCreationFailed - })?; + if let Some(expiry_secs) = expiry_secs { + let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) + .duration_since(UNIX_EPOCH) + .unwrap(); + offer_builder = offer_builder.absolute_expiry(absolute_expiry); + } let mut offer = offer_builder.amount_msats(amount_msat).description(description.to_string()); @@ -289,36 +323,54 @@ impl Bolt12Payment { Ok(finalized_offer) } + /// Returns a payable offer that can be used to request and receive a payment of the amount + /// given. + pub fn receive( + &self, amount_msat: u64, description: &str, expiry_secs: Option, quantity: Option, + ) -> Result { + let offer = self.receive_inner(amount_msat, description, expiry_secs, quantity)?; + Ok(maybe_wrap(offer)) + } + /// Returns a payable offer that can be used to request and receive a payment for which the /// amount is to be determined by the user, also known as a "zero-amount" offer. pub fn receive_variable_amount( &self, description: &str, expiry_secs: Option, ) -> Result { - let absolute_expiry = expiry_secs.map(|secs| { - (SystemTime::now() + Duration::from_secs(secs as u64)) + let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + + if let Some(expiry_secs) = expiry_secs { + let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) .duration_since(UNIX_EPOCH) - .unwrap() - }); + .unwrap(); + offer_builder = offer_builder.absolute_expiry(absolute_expiry); + } - let offer_builder = - self.channel_manager.create_offer_builder(absolute_expiry).map_err(|e| { - log_error!(self.logger, "Failed to create offer builder: {:?}", e); - Error::OfferCreationFailed - })?; let offer = offer_builder.description(description.to_string()).build().map_err(|e| { log_error!(self.logger, "Failed to create offer: {:?}", e); Error::OfferCreationFailed })?; - Ok(offer) + Ok(maybe_wrap(offer)) } /// Requests a refund payment for the given [`Refund`]. /// /// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to /// retrieve the refund). + /// + /// [`Refund`]: lightning::offers::refund::Refund + /// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice pub fn request_refund_payment(&self, refund: &Refund) -> Result { - let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| { + if !*self.is_running.read().unwrap() { + return Err(Error::NotRunning); + } + + let refund = maybe_deref(refund); + let invoice = self.channel_manager.request_refund_payment(&refund).map_err(|e| { log_error!(self.logger, "Failed to request refund payment: {:?}", e); Error::InvoiceRequestCreationFailed })?; @@ -338,16 +390,19 @@ impl Bolt12Payment { payment_id, kind, Some(refund.amount_msats()), + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); self.payment_store.insert(payment)?; - Ok(invoice) + Ok(maybe_wrap(invoice)) } /// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given. + /// + /// [`Refund`]: lightning::offers::refund::Refund pub fn initiate_refund( &self, amount_msat: u64, expiry_secs: u32, quantity: Option, payer_note: Option, @@ -360,7 +415,7 @@ impl Bolt12Payment { .duration_since(UNIX_EPOCH) .unwrap(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let mut refund_builder = self .channel_manager @@ -369,7 +424,7 @@ impl Bolt12Payment { absolute_expiry, payment_id, retry_strategy, - max_total_routing_fee_msat, + route_params_config, ) .map_err(|e| { log_error!(self.logger, "Failed to create refund builder: {:?}", e); @@ -402,12 +457,111 @@ impl Bolt12Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); self.payment_store.insert(payment)?; - Ok(refund) + Ok(maybe_wrap(refund)) + } + + /// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. + /// + /// Will only return an offer if [`Bolt12Payment::set_paths_to_static_invoice_server`] was called and we succeeded + /// in interactively building a [`StaticInvoice`] with the static invoice server. + /// + /// Useful for posting offers to receive payments later, such as posting an offer on a website. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + /// [`Offer`]: lightning::offers::offer::Offer + pub fn receive_async(&self) -> Result { + self.channel_manager + .get_async_receive_offer() + .map(maybe_wrap) + .or(Err(Error::OfferCreationFailed)) + } + + /// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a + /// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(not(feature = "uniffi"))] + pub fn set_paths_to_static_invoice_server( + &self, paths: Vec, + ) -> Result<(), Error> { + self.channel_manager + .set_paths_to_static_invoice_server(paths) + .or(Err(Error::InvalidBlindedPaths)) + } + + /// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a + /// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(feature = "uniffi")] + pub fn set_paths_to_static_invoice_server(&self, paths: Vec) -> Result<(), Error> { + let decoded_paths = as Readable>::read(&mut &paths[..]) + .or(Err(Error::InvalidBlindedPaths))?; + + self.channel_manager + .set_paths_to_static_invoice_server(decoded_paths) + .or(Err(Error::InvalidBlindedPaths)) + } + + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(not(feature = "uniffi"))] + pub fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, + ) -> Result, Error> { + self.blinded_paths_for_async_recipient_internal(recipient_id) + } + + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(feature = "uniffi")] + pub fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, + ) -> Result, Error> { + let paths = self.blinded_paths_for_async_recipient_internal(recipient_id)?; + + let mut bytes = Vec::new(); + paths.write(&mut bytes).or(Err(Error::InvalidBlindedPaths))?; + Ok(bytes) + } + + fn blinded_paths_for_async_recipient_internal( + &self, recipient_id: Vec, + ) -> Result, Error> { + match self.async_payments_role { + Some(AsyncPaymentsRole::Server) => {}, + _ => { + return Err(Error::AsyncPaymentServicesDisabled); + }, + } + + self.channel_manager + .blinded_paths_for_async_recipient(recipient_id, None) + .or(Err(Error::InvalidBlindedPaths)) } } diff --git a/src/payment/mod.rs b/src/payment/mod.rs index b031e37fd5..f629960e1d 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -7,6 +7,7 @@ //! Objects for different types of payments. +pub(crate) mod asynchronous; mod bolt11; mod bolt12; mod onchain; @@ -22,87 +23,3 @@ pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; pub use unified_qr::{QrPaymentResult, UnifiedQrPayment}; - -/// Represents information used to send a payment. -#[derive(Clone, Debug, PartialEq)] -pub struct SendingParameters { - /// The maximum total fees, in millisatoshi, that may accrue during route finding. - /// - /// This limit also applies to the total fees that may arise while retrying failed payment - /// paths. - /// - /// Note that values below a few sats may result in some paths being spuriously ignored. - #[cfg(not(feature = "uniffi"))] - pub max_total_routing_fee_msat: Option>, - /// The maximum total fees, in millisatoshi, that may accrue during route finding. - /// - /// This limit also applies to the total fees that may arise while retrying failed payment - /// paths. - /// - /// Note that values below a few sats may result in some paths being spuriously ignored. - #[cfg(feature = "uniffi")] - pub max_total_routing_fee_msat: Option, - /// The maximum total CLTV delta we accept for the route. - /// - /// Defaults to [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]. - /// - /// [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]: lightning::routing::router::DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA - pub max_total_cltv_expiry_delta: Option, - /// The maximum number of paths that may be used by (MPP) payments. - /// - /// Defaults to [`DEFAULT_MAX_PATH_COUNT`]. - /// - /// [`DEFAULT_MAX_PATH_COUNT`]: lightning::routing::router::DEFAULT_MAX_PATH_COUNT - pub max_path_count: Option, - /// Selects the maximum share of a channel's total capacity which will be sent over a channel, - /// as a power of 1/2. - /// - /// A higher value prefers to send the payment using more MPP parts whereas - /// a lower value prefers to send larger MPP parts, potentially saturating channels and - /// increasing failure probability for those paths. - /// - /// Note that this restriction will be relaxed during pathfinding after paths which meet this - /// restriction have been found. While paths which meet this criteria will be searched for, it - /// is ultimately up to the scorer to select them over other paths. - /// - /// Examples: - /// - /// | Value | Max Proportion of Channel Capacity Used | - /// |-------|-----------------------------------------| - /// | 0 | Up to 100% of the channel’s capacity | - /// | 1 | Up to 50% of the channel’s capacity | - /// | 2 | Up to 25% of the channel’s capacity | - /// | 3 | Up to 12.5% of the channel’s capacity | - /// - /// Default value: 2 - pub max_channel_saturation_power_of_half: Option, -} - -/// Represents the possible states of [`SendingParameters::max_total_routing_fee_msat`]. -// -// Required only in bindings as UniFFI can't expose `Option>`. -#[cfg(feature = "uniffi")] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum MaxTotalRoutingFeeLimit { - None, - Some { amount_msat: u64 }, -} - -#[cfg(feature = "uniffi")] -impl From for Option { - fn from(value: MaxTotalRoutingFeeLimit) -> Self { - match value { - MaxTotalRoutingFeeLimit::Some { amount_msat } => Some(amount_msat), - MaxTotalRoutingFeeLimit::None => None, - } - } -} - -#[cfg(feature = "uniffi")] -impl From> for MaxTotalRoutingFeeLimit { - fn from(value: Option) -> Self { - value.map_or(MaxTotalRoutingFeeLimit::None, |amount_msat| MaxTotalRoutingFeeLimit::Some { - amount_msat, - }) - } -} diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 046d66c693..c5100d7723 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -7,16 +7,16 @@ //! Holds a payment handler allowing to send and receive on-chain payments. +use std::sync::{Arc, RwLock}; + +use bitcoin::{Address, Txid}; + use crate::config::Config; use crate::error::Error; use crate::logger::{log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, Wallet}; use crate::wallet::OnchainSendAmount; -use bitcoin::{Address, Txid}; - -use std::sync::{Arc, RwLock}; - #[cfg(not(feature = "uniffi"))] type FeeRate = bitcoin::FeeRate; #[cfg(feature = "uniffi")] @@ -41,19 +41,19 @@ macro_rules! maybe_map_fee_rate_opt { /// /// [`Node::onchain_payment`]: crate::Node::onchain_payment pub struct OnchainPayment { - runtime: Arc>>>, wallet: Arc, channel_manager: Arc, config: Arc, + is_running: Arc>, logger: Arc, } impl OnchainPayment { pub(crate) fn new( - runtime: Arc>>>, wallet: Arc, - channel_manager: Arc, config: Arc, logger: Arc, + wallet: Arc, channel_manager: Arc, config: Arc, + is_running: Arc>, logger: Arc, ) -> Self { - Self { runtime, wallet, channel_manager, config, logger } + Self { wallet, channel_manager, config, is_running, logger } } /// Retrieve a new on-chain/funding address. @@ -75,8 +75,7 @@ impl OnchainPayment { pub fn send_to_address( &self, address: &bitcoin::Address, amount_sats: u64, fee_rate: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } @@ -106,8 +105,7 @@ impl OnchainPayment { pub fn send_all_to_address( &self, address: &bitcoin::Address, retain_reserves: bool, fee_rate: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 9846198556..6c074f308f 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -7,24 +7,19 @@ //! Holds a payment handler allowing to send spontaneous ("keysend") payments. -use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; -use crate::error::Error; -use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::store::{ - PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, -}; -use crate::payment::SendingParameters; -use crate::types::{ChannelManager, CustomTlvRecord, KeysManager}; +use std::sync::{Arc, RwLock}; +use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; -use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use lightning::sign::EntropySource; - use lightning_types::payment::{PaymentHash, PaymentPreimage}; -use bitcoin::secp256k1::PublicKey; - -use std::sync::{Arc, RwLock}; +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::error::Error; +use crate::logger::{log_error, log_info, LdkLogger, Logger}; +use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::types::{ChannelManager, CustomTlvRecord, KeysManager, PaymentStore}; // The default `final_cltv_expiry_delta` we apply when not set. const LDK_DEFAULT_FINAL_CLTV_EXPIRY_DELTA: u32 = 144; @@ -35,51 +30,70 @@ const LDK_DEFAULT_FINAL_CLTV_EXPIRY_DELTA: u32 = 144; /// /// [`Node::spontaneous_payment`]: crate::Node::spontaneous_payment pub struct SpontaneousPayment { - runtime: Arc>>>, channel_manager: Arc, keys_manager: Arc, - payment_store: Arc>>, + payment_store: Arc, config: Arc, + is_running: Arc>, logger: Arc, } impl SpontaneousPayment { pub(crate) fn new( - runtime: Arc>>>, channel_manager: Arc, keys_manager: Arc, - payment_store: Arc>>, config: Arc, logger: Arc, + payment_store: Arc, config: Arc, is_running: Arc>, + logger: Arc, ) -> Self { - Self { runtime, channel_manager, keys_manager, payment_store, config, logger } + Self { channel_manager, keys_manager, payment_store, config, is_running, logger } } /// Send a spontaneous aka. "keysend", payment. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, None) + self.send_inner(amount_msat, node_id, route_parameters, None, None) } /// Send a spontaneous payment including a list of custom TLVs. pub fn send_with_custom_tlvs( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, - custom_tlvs: Vec, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, custom_tlvs: Vec, + ) -> Result { + self.send_inner(amount_msat, node_id, route_parameters, Some(custom_tlvs), None) + } + + /// Send a spontaneous payment with custom preimage + pub fn send_with_preimage( + &self, amount_msat: u64, node_id: PublicKey, preimage: PaymentPreimage, + route_parameters: Option, + ) -> Result { + self.send_inner(amount_msat, node_id, route_parameters, None, Some(preimage)) + } + + /// Send a spontaneous payment with custom preimage including a list of custom TLVs. + pub fn send_with_preimage_and_custom_tlvs( + &self, amount_msat: u64, node_id: PublicKey, custom_tlvs: Vec, + preimage: PaymentPreimage, route_parameters: Option, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs)) + self.send_inner(amount_msat, node_id, route_parameters, Some(custom_tlvs), Some(preimage)) } fn send_inner( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, - custom_tlvs: Option>, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, custom_tlvs: Option>, + preimage: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); + let payment_preimage = preimage + .unwrap_or_else(|| PaymentPreimage(self.keys_manager.get_secure_random_bytes())); + let payment_hash = PaymentHash::from(payment_preimage); let payment_id = PaymentId(payment_hash.0); @@ -97,20 +111,19 @@ impl SpontaneousPayment { amount_msat, ); - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } let recipient_fields = match custom_tlvs { Some(tlvs) => RecipientOnionFields::spontaneous_empty() @@ -140,6 +153,7 @@ impl SpontaneousPayment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -161,6 +175,7 @@ impl SpontaneousPayment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -180,8 +195,7 @@ impl SpontaneousPayment { /// /// [`Bolt11Payment::send_probes`]: crate::payment::Bolt11Payment pub fn send_probes(&self, amount_msat: u64, node_id: PublicKey) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } diff --git a/src/payment/store.rs b/src/payment/store.rs index 7e677c02d3..b17898d9ce 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -5,33 +5,22 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::hex_utils; -use crate::io::{ - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, -}; -use crate::logger::{log_error, LdkLogger}; -use crate::types::DynStore; -use crate::Error; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use bitcoin::{BlockHash, Txid}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; -use lightning::util::string::UntrustedString; use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, impl_writeable_tlv_based_enum, write_tlv_fields, }; - use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning_types::string::UntrustedString; -use bitcoin::{BlockHash, Txid}; - -use std::collections::hash_map; -use std::collections::HashMap; -use std::ops::Deref; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; +use crate::hex_utils; /// Represents a payment. #[derive(Clone, Debug, PartialEq, Eq)] @@ -41,7 +30,16 @@ pub struct PaymentDetails { /// The kind of the payment. pub kind: PaymentKind, /// The amount transferred. + /// + /// Will be `None` for variable-amount payments until we receive them. pub amount_msat: Option, + /// The fees that were paid for this payment. + /// + /// For Lightning payments, this will only be updated for outbound payments once they + /// succeeded. + /// + /// Will be `None` for Lightning payments made with LDK Node v0.4.x and earlier. + pub fee_paid_msat: Option, /// The direction of the payment. pub direction: PaymentDirection, /// The status of the payment. @@ -52,17 +50,127 @@ pub struct PaymentDetails { impl PaymentDetails { pub(crate) fn new( - id: PaymentId, kind: PaymentKind, amount_msat: Option, direction: PaymentDirection, - status: PaymentStatus, + id: PaymentId, kind: PaymentKind, amount_msat: Option, fee_paid_msat: Option, + direction: PaymentDirection, status: PaymentStatus, ) -> Self { let latest_update_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or(Duration::from_secs(0)) .as_secs(); - Self { id, kind, amount_msat, direction, status, latest_update_timestamp } + Self { id, kind, amount_msat, fee_paid_msat, direction, status, latest_update_timestamp } + } +} + +impl Writeable for PaymentDetails { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + write_tlv_fields!(writer, { + (0, self.id, required), // Used to be `hash` for v0.2.1 and prior + // 1 briefly used to be lsp_fee_limits, could probably be reused at some point in the future. + // 2 used to be `preimage` before it was moved to `kind` in v0.3.0 + (2, None::>, required), + (3, self.kind, required), + // 4 used to be `secret` before it was moved to `kind` in v0.3.0 + (4, None::>, required), + (5, self.latest_update_timestamp, required), + (6, self.amount_msat, required), + (7, self.fee_paid_msat, option), + (8, self.direction, required), + (10, self.status, required) + }); + Ok(()) + } +} + +impl Readable for PaymentDetails { + fn read(reader: &mut R) -> Result { + let unix_time_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, id, required), // Used to be `hash` + (1, lsp_fee_limits, option), + (2, preimage, required), + (3, kind_opt, option), + (4, secret, required), + (5, latest_update_timestamp, (default_value, unix_time_secs)), + (6, amount_msat, required), + (7, fee_paid_msat, option), + (8, direction, required), + (10, status, required) + }); + + let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?; + let preimage: Option = preimage.0.ok_or(DecodeError::InvalidValue)?; + let secret: Option = secret.0.ok_or(DecodeError::InvalidValue)?; + let latest_update_timestamp: u64 = + latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?; + let amount_msat: Option = amount_msat.0.ok_or(DecodeError::InvalidValue)?; + let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?; + let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?; + + let kind = if let Some(kind) = kind_opt { + // If we serialized the payment kind, use it. + // This will always be the case for any version after v0.2.1. + kind + } else { + // Otherwise we persisted with v0.2.1 or before, and puzzle together the kind from the + // provided fields. + + // We used to track everything by hash, but switched to track everything by id + // post-v0.2.1. As both are serialized identically, we just switched the `0`-type field above + // from `PaymentHash` to `PaymentId` and serialize a separate `PaymentHash` in + // `PaymentKind` when needed. Here, for backwards compat, we can just re-create the + // `PaymentHash` from the id, as 'back then' `payment_hash == payment_id` was always + // true. + let hash = PaymentHash(id.0); + + if secret.is_some() { + if let Some(lsp_fee_limits) = lsp_fee_limits { + let counterparty_skimmed_fee_msat = None; + PaymentKind::Bolt11Jit { + hash, + preimage, + secret, + counterparty_skimmed_fee_msat, + lsp_fee_limits, + } + } else { + PaymentKind::Bolt11 { hash, preimage, secret } + } + } else { + PaymentKind::Spontaneous { hash, preimage } + } + }; + + Ok(PaymentDetails { + id, + kind, + amount_msat, + fee_paid_msat, + direction, + status, + latest_update_timestamp, + }) } +} + +impl StorableObjectId for PaymentId { + fn encode_to_hex_str(&self) -> String { + hex_utils::to_string(&self.0) + } +} +impl StorableObject for PaymentDetails { + type Id = PaymentId; + type Update = PaymentDetailsUpdate; - pub(crate) fn update(&mut self, update: &PaymentDetailsUpdate) -> bool { + fn id(&self) -> Self::Id { + self.id + } + + fn update(&mut self, update: &Self::Update) -> bool { debug_assert_eq!( self.id, update.id, "We should only ever override payment data for the same payment id" @@ -154,6 +262,22 @@ impl PaymentDetails { update_if_necessary!(self.amount_msat, amount_opt); } + if let Some(fee_paid_msat_opt) = update.fee_paid_msat { + update_if_necessary!(self.fee_paid_msat, fee_paid_msat_opt); + } + + if let Some(skimmed_fee_msat) = update.counterparty_skimmed_fee_msat { + match self.kind { + PaymentKind::Bolt11Jit { ref mut counterparty_skimmed_fee_msat, .. } => { + update_if_necessary!(*counterparty_skimmed_fee_msat, skimmed_fee_msat); + }, + _ => debug_assert!( + false, + "We should only ever override counterparty_skimmed_fee_msat for JIT payments" + ), + } + } + if let Some(status) = update.status { update_if_necessary!(self.status, status); } @@ -176,84 +300,9 @@ impl PaymentDetails { updated } -} -impl Writeable for PaymentDetails { - fn write( - &self, writer: &mut W, - ) -> Result<(), lightning::io::Error> { - write_tlv_fields!(writer, { - (0, self.id, required), // Used to be `hash` for v0.2.1 and prior - // 1 briefly used to be lsp_fee_limits, could probably be reused at some point in the future. - // 2 used to be `preimage` before it was moved to `kind` in v0.3.0 - (2, None::>, required), - (3, self.kind, required), - // 4 used to be `secret` before it was moved to `kind` in v0.3.0 - (4, None::>, required), - (5, self.latest_update_timestamp, required), - (6, self.amount_msat, required), - (8, self.direction, required), - (10, self.status, required) - }); - Ok(()) - } -} - -impl Readable for PaymentDetails { - fn read(reader: &mut R) -> Result { - let unix_time_secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); - _init_and_read_len_prefixed_tlv_fields!(reader, { - (0, id, required), // Used to be `hash` - (1, lsp_fee_limits, option), - (2, preimage, required), - (3, kind_opt, option), - (4, secret, required), - (5, latest_update_timestamp, (default_value, unix_time_secs)), - (6, amount_msat, required), - (8, direction, required), - (10, status, required) - }); - - let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?; - let preimage: Option = preimage.0.ok_or(DecodeError::InvalidValue)?; - let secret: Option = secret.0.ok_or(DecodeError::InvalidValue)?; - let latest_update_timestamp: u64 = - latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?; - let amount_msat: Option = amount_msat.0.ok_or(DecodeError::InvalidValue)?; - let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?; - let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?; - - let kind = if let Some(kind) = kind_opt { - // If we serialized the payment kind, use it. - // This will always be the case for any version after v0.2.1. - kind - } else { - // Otherwise we persisted with v0.2.1 or before, and puzzle together the kind from the - // provided fields. - - // We used to track everything by hash, but switched to track everything by id - // post-v0.2.1. As both are serialized identically, we just switched the `0`-type field above - // from `PaymentHash` to `PaymentId` and serialize a separate `PaymentHash` in - // `PaymentKind` when needed. Here, for backwards compat, we can just re-create the - // `PaymentHash` from the id, as 'back then' `payment_hash == payment_id` was always - // true. - let hash = PaymentHash(id.0); - - if secret.is_some() { - if let Some(lsp_fee_limits) = lsp_fee_limits { - PaymentKind::Bolt11Jit { hash, preimage, secret, lsp_fee_limits } - } else { - PaymentKind::Bolt11 { hash, preimage, secret } - } - } else { - PaymentKind::Spontaneous { hash, preimage } - } - }; - - Ok(PaymentDetails { id, kind, amount_msat, direction, status, latest_update_timestamp }) + fn to_update(&self) -> Self::Update { + self.into() } } @@ -325,6 +374,12 @@ pub enum PaymentKind { preimage: Option, /// The secret used by the payment. secret: Option, + /// The value, in thousands of a satoshi, that was deducted from this payment as an extra + /// fee taken by our channel counterparty. + /// + /// Will only be `Some` once we received the payment. Will always be `None` for LDK Node + /// v0.4 and prior. + counterparty_skimmed_fee_msat: Option, /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. /// /// Allowing them to deduct this fee from the first inbound payment will pay for the LSP's @@ -402,6 +457,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, }, (4, Bolt11Jit) => { (0, hash, required), + (1, counterparty_skimmed_fee_msat, option), (2, preimage, option), (4, secret, option), (6, lsp_fee_limits, required), @@ -479,6 +535,8 @@ pub(crate) struct PaymentDetailsUpdate { pub preimage: Option>, pub secret: Option>, pub amount_msat: Option>, + pub fee_paid_msat: Option>, + pub counterparty_skimmed_fee_msat: Option>, pub direction: Option, pub status: Option, pub confirmation_status: Option, @@ -492,6 +550,8 @@ impl PaymentDetailsUpdate { preimage: None, secret: None, amount_msat: None, + fee_paid_msat: None, + counterparty_skimmed_fee_msat: None, direction: None, status: None, confirmation_status: None, @@ -515,12 +575,21 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { _ => None, }; + let counterparty_skimmed_fee_msat = match value.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + Some(counterparty_skimmed_fee_msat) + }, + _ => None, + }; + Self { id: value.id, hash: Some(hash), preimage: Some(preimage), secret: Some(secret), amount_msat: Some(value.amount_msat), + fee_paid_msat: Some(value.fee_paid_msat), + counterparty_skimmed_fee_msat, direction: Some(value.direction), status: Some(value.status), confirmation_status, @@ -528,142 +597,18 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { } } -pub(crate) struct PaymentStore -where - L::Target: LdkLogger, -{ - payments: Mutex>, - kv_store: Arc, - logger: L, -} - -impl PaymentStore -where - L::Target: LdkLogger, -{ - pub(crate) fn new(payments: Vec, kv_store: Arc, logger: L) -> Self { - let payments = Mutex::new(HashMap::from_iter( - payments.into_iter().map(|payment| (payment.id, payment)), - )); - Self { payments, kv_store, logger } - } - - pub(crate) fn insert(&self, payment: PaymentDetails) -> Result { - let mut locked_payments = self.payments.lock().unwrap(); - - let updated = locked_payments.insert(payment.id, payment.clone()).is_some(); - self.persist_info(&payment.id, &payment)?; - Ok(updated) - } - - pub(crate) fn insert_or_update(&self, payment: &PaymentDetails) -> Result { - let mut locked_payments = self.payments.lock().unwrap(); - - let updated; - match locked_payments.entry(payment.id) { - hash_map::Entry::Occupied(mut e) => { - let update = payment.into(); - updated = e.get_mut().update(&update); - if updated { - self.persist_info(&payment.id, e.get())?; - } - }, - hash_map::Entry::Vacant(e) => { - e.insert(payment.clone()); - self.persist_info(&payment.id, payment)?; - updated = true; - }, - } - - Ok(updated) - } - - pub(crate) fn remove(&self, id: &PaymentId) -> Result<(), Error> { - let store_key = hex_utils::to_string(&id.0); - self.kv_store - .remove( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key, - false, - ) - .map_err(|e| { - log_error!( - self.logger, - "Removing payment data for key {}/{}/{} failed due to: {}", - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - store_key, - e - ); - Error::PersistenceFailed - }) - } - - pub(crate) fn get(&self, id: &PaymentId) -> Option { - self.payments.lock().unwrap().get(id).cloned() - } - - pub(crate) fn update(&self, update: &PaymentDetailsUpdate) -> Result { - let mut updated = false; - let mut locked_payments = self.payments.lock().unwrap(); - - if let Some(payment) = locked_payments.get_mut(&update.id) { - updated = payment.update(update); - if updated { - self.persist_info(&update.id, payment)?; - } - } - Ok(updated) - } - - pub(crate) fn list_filter bool>( - &self, f: F, - ) -> Vec { - self.payments - .lock() - .unwrap() - .iter() - .map(|(_, p)| p) - .filter(f) - .cloned() - .collect::>() - } - - fn persist_info(&self, id: &PaymentId, payment: &PaymentDetails) -> Result<(), Error> { - let store_key = hex_utils::to_string(&id.0); - let data = payment.encode(); - self.kv_store - .write( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key, - &data, - ) - .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - store_key, - e - ); - Error::PersistenceFailed - })?; - Ok(()) +impl StorableObjectUpdate for PaymentDetailsUpdate { + fn id(&self) -> ::Id { + self.id } } #[cfg(test)] mod tests { - use super::*; use bitcoin::io::Cursor; - use lightning::util::{ - ser::Readable, - test_utils::{TestLogger, TestStore}, - }; - use std::sync::Arc; + use lightning::util::ser::Readable; + + use super::*; /// We refactored `PaymentDetails` to hold a payment id and moved some required fields into /// `PaymentKind`. Here, we keep the old layout available in order test de/ser compatibility. @@ -688,50 +633,6 @@ mod tests { (10, status, required) }); - #[test] - fn payment_info_is_persisted() { - let store: Arc = Arc::new(TestStore::new(false)); - let logger = Arc::new(TestLogger::new()); - let payment_store = PaymentStore::new(Vec::new(), Arc::clone(&store), logger); - - let hash = PaymentHash([42u8; 32]); - let id = PaymentId([42u8; 32]); - assert!(payment_store.get(&id).is_none()); - - let store_key = hex_utils::to_string(&hash.0); - assert!(store - .read( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key - ) - .is_err()); - - let kind = PaymentKind::Bolt11 { hash, preimage: None, secret: None }; - let payment = - PaymentDetails::new(id, kind, None, PaymentDirection::Inbound, PaymentStatus::Pending); - - assert_eq!(Ok(false), payment_store.insert(payment.clone())); - assert!(payment_store.get(&id).is_some()); - assert!(store - .read( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key - ) - .is_ok()); - - assert_eq!(Ok(true), payment_store.insert(payment)); - assert!(payment_store.get(&id).is_some()); - - let mut update = PaymentDetailsUpdate::new(id); - update.status = Some(PaymentStatus::Succeeded); - assert_eq!(Ok(true), payment_store.update(&update)); - assert!(payment_store.get(&id).is_some()); - - assert_eq!(PaymentStatus::Succeeded, payment_store.get(&id).unwrap().status); - } - #[test] fn old_payment_details_deser_compat() { // We refactored `PaymentDetails` to hold a payment id and moved some required fields into @@ -811,10 +712,17 @@ mod tests { ); match bolt11_jit_decoded.kind { - PaymentKind::Bolt11Jit { hash: h, preimage: p, secret: s, lsp_fee_limits: l } => { + PaymentKind::Bolt11Jit { + hash: h, + preimage: p, + secret: s, + counterparty_skimmed_fee_msat: c, + lsp_fee_limits: l, + } => { assert_eq!(hash, h); assert_eq!(preimage, p); assert_eq!(secret, s); + assert_eq!(None, c); assert_eq!(lsp_fee_limits, Some(l)); }, _ => { diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 92c405056c..fc2eca1500 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -11,22 +11,22 @@ //! [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::error::Error; -use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; -use crate::Config; - -use lightning::ln::channelmanager::PaymentId; -use lightning::offers::offer::Offer; -use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +use std::sync::Arc; +use std::vec::IntoIter; use bip21::de::ParamKind; use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams}; use bitcoin::address::{NetworkChecked, NetworkUnchecked}; use bitcoin::{Amount, Txid}; +use lightning::ln::channelmanager::PaymentId; +use lightning::offers::offer::Offer; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; -use std::sync::Arc; -use std::vec::IntoIter; +use crate::error::Error; +use crate::ffi::maybe_wrap; +use crate::logger::{log_error, LdkLogger, Logger}; +use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; @@ -68,6 +68,10 @@ impl UnifiedQrPayment { /// can always pay using the provided on-chain address, while newer wallets will /// typically opt to use the provided BOLT11 invoice or BOLT12 offer. /// + /// The URI will always include an on-chain address. A BOLT11 invoice will be included + /// unless invoice generation fails, while a BOLT12 offer will only be included when + /// the node has suitable channels for routing. + /// /// # Parameters /// - `amount_sats`: The amount to be received, specified in satoshis. /// - `description`: A description or note associated with the payment. @@ -75,9 +79,9 @@ impl UnifiedQrPayment { /// - `expiry_sec`: The expiration time for the payment, specified in seconds. /// /// Returns a payable URI that can be used to request and receive a payment of the amount - /// given. In case of an error, the function returns `Error::WalletOperationFailed`for on-chain - /// address issues, `Error::InvoiceCreationFailed` for BOLT11 invoice issues, or - /// `Error::OfferCreationFailed` for BOLT12 offer issues. + /// given. Failure to generate the on-chain address will result in an error return + /// (`Error::WalletOperationFailed`), while failures in invoice or offer generation will + /// result in those components being omitted from the URI. /// /// The generated URI can then be given to a QR code library. /// @@ -90,14 +94,14 @@ impl UnifiedQrPayment { let amount_msats = amount_sats * 1_000; - let bolt12_offer = match self.bolt12_payment.receive(amount_msats, description, None, None) - { - Ok(offer) => Some(offer), - Err(e) => { - log_error!(self.logger, "Failed to create offer: {}", e); - return Err(Error::OfferCreationFailed); - }, - }; + let bolt12_offer = + match self.bolt12_payment.receive_inner(amount_msats, description, None, None) { + Ok(offer) => Some(offer), + Err(e) => { + log_error!(self.logger, "Failed to create offer: {}", e); + None + }, + }; let invoice_description = Bolt11InvoiceDescription::Direct( Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?, @@ -111,7 +115,7 @@ impl UnifiedQrPayment { Ok(invoice) => Some(invoice), Err(e) => { log_error!(self.logger, "Failed to create invoice {}", e); - return Err(Error::InvoiceCreationFailed); + None }, }; @@ -142,6 +146,7 @@ impl UnifiedQrPayment { uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; if let Some(offer) = uri_network_checked.extras.bolt12_offer { + let offer = maybe_wrap(offer); match self.bolt12_payment.send(&offer, None, None) { Ok(payment_id) => return Ok(QrPaymentResult::Bolt12 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified QR code payment. Falling back to the BOLT11 invoice.", e), @@ -149,6 +154,7 @@ impl UnifiedQrPayment { } if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { + let invoice = maybe_wrap(invoice); match self.bolt11_invoice.send(&invoice, None) { Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e), @@ -181,6 +187,7 @@ impl UnifiedQrPayment { /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [`PaymentId`]: lightning::ln::channelmanager::PaymentId /// [`Txid`]: bitcoin::hash_types::Txid +#[derive(Debug)] pub enum QrPaymentResult { /// An on-chain payment. Onchain { @@ -295,10 +302,12 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { + use std::str::FromStr; + + use bitcoin::{Address, Network}; + use super::*; use crate::payment::unified_qr::Extras; - use bitcoin::{Address, Network}; - use std::str::FromStr; #[test] fn parse_uri() { diff --git a/src/peer_store.rs b/src/peer_store.rs index 4d1c651576..82c80c396f 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -5,6 +5,15 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::{Arc, RwLock}; + +use bitcoin::secp256k1::PublicKey; +use lightning::impl_writeable_tlv_based; +use lightning::util::persist::KVStoreSync; +use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; + use crate::io::{ PEER_INFO_PERSISTENCE_KEY, PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, @@ -13,15 +22,6 @@ use crate::logger::{log_error, LdkLogger}; use crate::types::DynStore; use crate::{Error, SocketAddress}; -use lightning::impl_writeable_tlv_based; -use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; - -use bitcoin::secp256k1::PublicKey; - -use std::collections::HashMap; -use std::ops::Deref; -use std::sync::{Arc, RwLock}; - pub struct PeerStore where L::Target: LdkLogger, @@ -68,24 +68,24 @@ where fn persist_peers(&self, locked_peers: &HashMap) -> Result<(), Error> { let data = PeerStoreSerWrapper(&*locked_peers).encode(); - self.kv_store - .write( + KVStoreSync::write( + &*self.kv_store, + PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PEER_INFO_PERSISTENCE_KEY, + data, + ) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PEER_INFO_PERSISTENCE_KEY, - &data, - ) - .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - PEER_INFO_PERSISTENCE_KEY, - e - ); - Error::PersistenceFailed - })?; + e + ); + Error::PersistenceFailed + })?; Ok(()) } } @@ -149,12 +149,13 @@ impl_writeable_tlv_based!(PeerInfo, { #[cfg(test)] mod tests { - use super::*; - use lightning::util::test_utils::{TestLogger, TestStore}; - use std::str::FromStr; use std::sync::Arc; + use lightning::util::test_utils::{TestLogger, TestStore}; + + use super::*; + #[test] fn peer_info_persistence() { let store: Arc = Arc::new(TestStore::new(false)); @@ -167,23 +168,23 @@ mod tests { .unwrap(); let address = SocketAddress::from_str("127.0.0.1:9738").unwrap(); let expected_peer_info = PeerInfo { node_id, address }; - assert!(store - .read( - PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - PEER_INFO_PERSISTENCE_KEY, - ) - .is_err()); + assert!(KVStoreSync::read( + &*store, + PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PEER_INFO_PERSISTENCE_KEY, + ) + .is_err()); peer_store.add_peer(expected_peer_info.clone()).unwrap(); // Check we can read back what we persisted. - let persisted_bytes = store - .read( - PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - PEER_INFO_PERSISTENCE_KEY, - ) - .unwrap(); + let persisted_bytes = KVStoreSync::read( + &*store, + PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PEER_INFO_PERSISTENCE_KEY, + ) + .unwrap(); let deser_peer_store = PeerStore::read(&mut &persisted_bytes[..], (Arc::clone(&store), logger)).unwrap(); diff --git a/src/prober.rs b/src/prober.rs index 0e11e57699..165da511bf 100644 --- a/src/prober.rs +++ b/src/prober.rs @@ -18,9 +18,8 @@ use crate::{ }; use bitcoin::secp256k1::PublicKey; use lightning::{ - chain::transaction::OutPoint, io::Cursor, - ln::{channel_state::ChannelDetails, channelmanager::PaymentId}, + ln::{channel_state::ChannelDetails, channelmanager::PaymentId, types::ChannelId}, log_error, routing::{ router::{Path, PaymentParameters, Route, RouteParameters, Router as _}, @@ -92,14 +91,14 @@ impl Prober { } /// Returns the latest update ids of the channel monitors. At some point after many updates the probes will start to get slow. - pub fn channel_monitor_update_ids(&self) -> Vec<(OutPoint, u64)> { + pub fn channel_monitor_update_ids(&self) -> Vec<(ChannelId, u64)> { self.chain_monitor .list_monitors() .iter() - .filter_map(|(outpoint, _)| { + .filter_map(|channel_id| { self.chain_monitor - .get_monitor(*outpoint) - .map(|monitor| (*outpoint, monitor.get_latest_update_id())) + .get_monitor(*channel_id) + .map(|monitor| (*channel_id, monitor.get_latest_update_id())) .ok() }) .collect() diff --git a/src/runtime.rs b/src/runtime.rs new file mode 100644 index 0000000000..2275d5bea1 --- /dev/null +++ b/src/runtime.rs @@ -0,0 +1,209 @@ +// 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 std::future::Future; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use tokio::task::{JoinHandle, JoinSet}; + +use crate::config::{ + BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS, LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS, +}; +use crate::logger::{log_debug, log_error, log_trace, LdkLogger, Logger}; + +pub(crate) struct Runtime { + mode: RuntimeMode, + background_tasks: Mutex>, + cancellable_background_tasks: Mutex>, + background_processor_task: Mutex>>, + logger: Arc, +} + +impl Runtime { + pub fn new(logger: Arc) -> Result { + let mode = match tokio::runtime::Handle::try_current() { + Ok(handle) => RuntimeMode::Handle(handle), + Err(_) => { + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; + RuntimeMode::Owned(rt) + }, + }; + let background_tasks = Mutex::new(JoinSet::new()); + let cancellable_background_tasks = Mutex::new(JoinSet::new()); + let background_processor_task = Mutex::new(None); + + Ok(Self { + mode, + background_tasks, + cancellable_background_tasks, + background_processor_task, + logger, + }) + } + + pub fn with_handle(handle: tokio::runtime::Handle, logger: Arc) -> Self { + let mode = RuntimeMode::Handle(handle); + let background_tasks = Mutex::new(JoinSet::new()); + let cancellable_background_tasks = Mutex::new(JoinSet::new()); + let background_processor_task = Mutex::new(None); + + Self { + mode, + background_tasks, + cancellable_background_tasks, + background_processor_task, + logger, + } + } + + pub fn spawn_background_task(&self, future: F) + where + F: Future + Send + 'static, + { + let mut background_tasks = self.background_tasks.lock().unwrap(); + let runtime_handle = self.handle(); + background_tasks.spawn_on(future, runtime_handle); + } + + pub fn spawn_cancellable_background_task(&self, future: F) + where + F: Future + Send + 'static, + { + let mut cancellable_background_tasks = self.cancellable_background_tasks.lock().unwrap(); + let runtime_handle = self.handle(); + cancellable_background_tasks.spawn_on(future, runtime_handle); + } + + pub fn spawn_background_processor_task(&self, future: F) + where + F: Future + Send + 'static, + { + let mut background_processor_task = self.background_processor_task.lock().unwrap(); + debug_assert!(background_processor_task.is_none(), "Expected no background processor_task"); + + let runtime_handle = self.handle(); + let handle = runtime_handle.spawn(future); + *background_processor_task = Some(handle); + } + + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let handle = self.handle(); + handle.spawn_blocking(func) + } + + pub fn block_on(&self, future: F) -> F::Output { + // While we generally decided not to overthink via which call graph users would enter our + // runtime context, we'd still try to reuse whatever current context would be present + // during `block_on`, as this is the context `block_in_place` would operate on. So we try + // to detect the outer context here, and otherwise use whatever was set during + // initialization. + let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle().clone()); + tokio::task::block_in_place(move || handle.block_on(future)) + } + + pub fn abort_cancellable_background_tasks(&self) { + let mut tasks = core::mem::take(&mut *self.cancellable_background_tasks.lock().unwrap()); + debug_assert!(tasks.len() > 0, "Expected some cancellable background_tasks"); + tasks.abort_all(); + self.block_on(async { while let Some(_) = tasks.join_next().await {} }) + } + + pub fn wait_on_background_tasks(&self) { + let mut tasks = core::mem::take(&mut *self.background_tasks.lock().unwrap()); + debug_assert!(tasks.len() > 0, "Expected some background_tasks"); + self.block_on(async { + loop { + let timeout_fut = tokio::time::timeout( + Duration::from_secs(BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS), + tasks.join_next_with_id(), + ); + match timeout_fut.await { + Ok(Some(Ok((id, _)))) => { + log_trace!(self.logger, "Stopped background task with id {}", id); + }, + Ok(Some(Err(e))) => { + tasks.abort_all(); + log_trace!(self.logger, "Stopping background task failed: {}", e); + break; + }, + Ok(None) => { + log_debug!(self.logger, "Stopped all background tasks"); + break; + }, + Err(e) => { + tasks.abort_all(); + log_error!(self.logger, "Stopping background task timed out: {}", e); + break; + }, + } + } + }) + } + + pub fn wait_on_background_processor_task(&self) { + if let Some(background_processor_task) = + self.background_processor_task.lock().unwrap().take() + { + let abort_handle = background_processor_task.abort_handle(); + let timeout_res = self.block_on(async { + tokio::time::timeout( + Duration::from_secs(LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS), + background_processor_task, + ) + .await + }); + + match timeout_res { + Ok(stop_res) => match stop_res { + Ok(()) => log_debug!(self.logger, "Stopped background processing of events."), + Err(e) => { + abort_handle.abort(); + log_error!( + self.logger, + "Stopping event handling failed. This should never happen: {}", + e + ); + panic!("Stopping event handling failed. This should never happen."); + }, + }, + Err(e) => { + abort_handle.abort(); + log_error!(self.logger, "Stopping event handling timed out: {}", e); + }, + } + } else { + debug_assert!(false, "Expected a background processing task"); + }; + } + + #[cfg(tokio_unstable)] + pub fn log_metrics(&self) { + let runtime_handle = self.handle(); + log_trace!( + self.logger, + "Active runtime tasks left prior to shutdown: {}", + runtime_handle.metrics().active_tasks_count() + ); + } + + fn handle(&self) -> &tokio::runtime::Handle { + match &self.mode { + RuntimeMode::Owned(rt) => rt.handle(), + RuntimeMode::Handle(handle) => handle, + } + } +} + +enum RuntimeMode { + Owned(tokio::runtime::Runtime), + Handle(tokio::runtime::Handle), +} diff --git a/src/sweep.rs b/src/sweep.rs deleted file mode 100644 index ba10869b8a..0000000000 --- a/src/sweep.rs +++ /dev/null @@ -1,47 +0,0 @@ -// 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. - -//! The output sweeper used to live here before we upstreamed it to `rust-lightning` and migrated -//! to the upstreamed version with LDK Node v0.3.0 (May 2024). We should drop this module entirely -//! once sufficient time has passed for us to be confident any users completed the migration. - -use lightning::impl_writeable_tlv_based; -use lightning::ln::types::ChannelId; -use lightning::sign::SpendableOutputDescriptor; - -use bitcoin::{Amount, BlockHash, Transaction}; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct DeprecatedSpendableOutputInfo { - pub(crate) id: [u8; 32], - pub(crate) descriptor: SpendableOutputDescriptor, - pub(crate) channel_id: Option, - pub(crate) first_broadcast_hash: Option, - pub(crate) latest_broadcast_height: Option, - pub(crate) latest_spending_tx: Option, - pub(crate) confirmation_height: Option, - pub(crate) confirmation_hash: Option, -} - -impl_writeable_tlv_based!(DeprecatedSpendableOutputInfo, { - (0, id, required), - (2, descriptor, required), - (4, channel_id, option), - (6, first_broadcast_hash, option), - (8, latest_broadcast_height, option), - (10, latest_spending_tx, option), - (12, confirmation_height, option), - (14, confirmation_hash, option), -}); - -pub(crate) fn value_from_descriptor(descriptor: &SpendableOutputDescriptor) -> Amount { - match &descriptor { - SpendableOutputDescriptor::StaticOutput { output, .. } => output.value, - SpendableOutputDescriptor::DelayedPaymentOutput(output) => output.output.value, - SpendableOutputDescriptor::StaticPaymentOutput(output) => output.output.value, - } -} diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 09189b1371..12a1fe650c 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -5,16 +5,13 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::logger::{log_error, LdkLogger}; - -use lightning::chain::chaininterface::BroadcasterInterface; +use std::ops::Deref; use bitcoin::Transaction; +use lightning::chain::chaininterface::BroadcasterInterface; +use tokio::sync::{mpsc, Mutex, MutexGuard}; -use tokio::sync::mpsc; -use tokio::sync::{Mutex, MutexGuard}; - -use std::ops::Deref; +use crate::logger::{log_error, LdkLogger}; const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; @@ -36,7 +33,9 @@ where Self { queue_sender, queue_receiver: Mutex::new(queue_receiver), logger } } - pub(crate) async fn get_broadcast_queue(&self) -> MutexGuard>> { + pub(crate) async fn get_broadcast_queue( + &self, + ) -> MutexGuard<'_, mpsc::Receiver>> { self.queue_receiver.lock().await } } diff --git a/src/types.rs b/src/types.rs index 1c9ab64b97..ddd5879852 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,38 +5,49 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::chain::ChainSource; -use crate::config::ChannelConfig; -use crate::fee_estimator::OnchainFeeEstimator; -use crate::gossip::RuntimeSpawner; -use crate::logger::Logger; -use crate::message_handler::NodeCustomMessageHandler; +use std::sync::{Arc, Mutex}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::OutPoint; use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; -use lightning::ln::msgs::RoutingMessageHandler; -use lightning::ln::msgs::SocketAddress; +use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::types::ChannelId; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters}; use lightning::sign::InMemorySigner; -use lightning::util::persist::KVStore; +use lightning::util::persist::{KVStore, KVStoreSync}; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; - use lightning_block_sync::gossip::{GossipVerifier, UtxoSource}; - +use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_net_tokio::SocketDescriptor; -use bitcoin::secp256k1::PublicKey; -use bitcoin::OutPoint; +use crate::chain::ChainSource; +use crate::config::ChannelConfig; +use crate::data_store::DataStore; +use crate::fee_estimator::OnchainFeeEstimator; +use crate::gossip::RuntimeSpawner; +use crate::logger::Logger; +use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::PaymentDetails; -use std::sync::{Arc, Mutex}; +/// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the +/// same time. +pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {} -pub(crate) type DynStore = dyn KVStore + Sync + Send; +impl SyncAndAsyncKVStore for T +where + T: KVStore, + T: KVStoreSync, +{ +} + +/// A type alias for [`SyncAndAsyncKVStore`] with `Sync`/`Send` markers; +pub type DynStore = dyn SyncAndAsyncKVStore + Sync + Send; pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< InMemorySigner, @@ -45,6 +56,7 @@ pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< Arc, Arc, Arc, + Arc, >; pub(crate) type PeerManager = lightning::ln::peer_handler::PeerManager< @@ -55,10 +67,18 @@ pub(crate) type PeerManager = lightning::ln::peer_handler::PeerManager< Arc, Arc>>, Arc, + Arc, >; -pub(crate) type LiquidityManager = - lightning_liquidity::LiquidityManager, Arc, Arc>; +pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< + Arc, + Arc, + Arc, + Arc, + Arc, + DefaultTimeProvider, + Arc, +>; pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< Arc, @@ -74,11 +94,8 @@ pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< pub(crate) type Broadcaster = crate::tx_broadcaster::TransactionBroadcaster>; -pub(crate) type Wallet = - crate::wallet::Wallet, Arc, Arc>; - -pub(crate) type KeysManager = - crate::wallet::WalletKeysManager, Arc, Arc>; +pub(crate) type Wallet = crate::wallet::Wallet; +pub(crate) type KeysManager = crate::wallet::WalletKeysManager; pub(crate) type Router = DefaultRouter< Arc, @@ -114,7 +131,7 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - IgnoringMessageHandler, + Arc, IgnoringMessageHandler, IgnoringMessageHandler, >; @@ -143,6 +160,8 @@ pub(crate) type BumpTransactionEventHandler = Arc, >; +pub(crate) type PaymentStore = DataStore>; + /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs deleted file mode 100644 index c7a8960f75..0000000000 --- a/src/uniffi_types.rs +++ /dev/null @@ -1,443 +0,0 @@ -// 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. - -// Importing these items ensures they are accessible in the uniffi bindings -// without introducing unused import warnings in lib.rs. -// -// Make sure to add any re-exported items that need to be used in uniffi below. - -pub use crate::config::{ - default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure, -}; -pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; -pub use crate::liquidity::{LSPS1OrderStatus, OnchainPaymentInfo, PaymentInfo}; -pub use crate::logger::{LogLevel, LogRecord, LogWriter}; -pub use crate::payment::store::{ - ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, -}; -pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; - -pub use lightning::chain::channelmonitor::BalanceSource; -pub use lightning::events::{ClosureReason, PaymentFailureReason}; -pub use lightning::ln::types::ChannelId; -pub use lightning::offers::invoice::Bolt12Invoice; -pub use lightning::offers::offer::{Offer, OfferId}; -pub use lightning::offers::refund::Refund; -pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; -pub use lightning::util::string::UntrustedString; - -pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; - -pub use lightning_invoice::{Bolt11Invoice, Description}; - -pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; -pub use lightning_liquidity::lsps1::msgs::{ - Bolt11PaymentInfo, OrderId, OrderParameters, PaymentState, -}; - -pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; - -pub use bip39::Mnemonic; - -pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; - -pub type DateTime = chrono::DateTime; - -use crate::UniffiCustomTypeConverter; - -use crate::builder::sanitize_alias; -use crate::error::Error; -use crate::hex_utils; -use crate::{SocketAddress, UserChannelId}; - -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; -use bitcoin::secp256k1::PublicKey; -use lightning::ln::channelmanager::PaymentId; -use lightning::util::ser::Writeable; -use lightning_invoice::SignedRawBolt11Invoice; - -use std::convert::TryInto; -use std::str::FromStr; - -impl UniffiCustomTypeConverter for PublicKey { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Ok(key) = PublicKey::from_str(&val) { - return Ok(key); - } - - Err(Error::InvalidPublicKey.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for NodeId { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Ok(key) = NodeId::from_str(&val) { - return Ok(key); - } - - Err(Error::InvalidNodeId.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for Address { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Ok(addr) = Address::from_str(&val) { - return Ok(addr.assume_checked()); - } - - Err(Error::InvalidAddress.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for Bolt11Invoice { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Ok(signed) = val.parse::() { - if let Ok(invoice) = Bolt11Invoice::from_signed(signed) { - return Ok(invoice); - } - } - - Err(Error::InvalidInvoice.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for Offer { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Offer::from_str(&val).map_err(|_| Error::InvalidOffer.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for Refund { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Refund::from_str(&val).map_err(|_| Error::InvalidRefund.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for Bolt12Invoice { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Some(bytes_vec) = hex_utils::to_vec(&val) { - if let Ok(invoice) = Bolt12Invoice::try_from(bytes_vec) { - return Ok(invoice); - } - } - Err(Error::InvalidInvoice.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - hex_utils::to_string(&obj.encode()) - } -} - -impl UniffiCustomTypeConverter for OfferId { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Some(bytes_vec) = hex_utils::to_vec(&val) { - let bytes_res = bytes_vec.try_into(); - if let Ok(bytes) = bytes_res { - return Ok(OfferId(bytes)); - } - } - Err(Error::InvalidOfferId.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - hex_utils::to_string(&obj.0) - } -} - -impl UniffiCustomTypeConverter for PaymentId { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Some(bytes_vec) = hex_utils::to_vec(&val) { - let bytes_res = bytes_vec.try_into(); - if let Ok(bytes) = bytes_res { - return Ok(PaymentId(bytes)); - } - } - Err(Error::InvalidPaymentId.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - hex_utils::to_string(&obj.0) - } -} - -impl UniffiCustomTypeConverter for PaymentHash { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Ok(hash) = Sha256::from_str(&val) { - Ok(PaymentHash(hash.to_byte_array())) - } else { - Err(Error::InvalidPaymentHash.into()) - } - } - - fn from_custom(obj: Self) -> Self::Builtin { - Sha256::from_slice(&obj.0).unwrap().to_string() - } -} - -impl UniffiCustomTypeConverter for PaymentPreimage { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Some(bytes_vec) = hex_utils::to_vec(&val) { - let bytes_res = bytes_vec.try_into(); - if let Ok(bytes) = bytes_res { - return Ok(PaymentPreimage(bytes)); - } - } - Err(Error::InvalidPaymentPreimage.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - hex_utils::to_string(&obj.0) - } -} - -impl UniffiCustomTypeConverter for PaymentSecret { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Some(bytes_vec) = hex_utils::to_vec(&val) { - let bytes_res = bytes_vec.try_into(); - if let Ok(bytes) = bytes_res { - return Ok(PaymentSecret(bytes)); - } - } - Err(Error::InvalidPaymentSecret.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - hex_utils::to_string(&obj.0) - } -} - -impl UniffiCustomTypeConverter for ChannelId { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Some(hex_vec) = hex_utils::to_vec(&val) { - if hex_vec.len() == 32 { - let mut channel_id = [0u8; 32]; - channel_id.copy_from_slice(&hex_vec[..]); - return Ok(Self(channel_id)); - } - } - Err(Error::InvalidChannelId.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - hex_utils::to_string(&obj.0) - } -} - -impl UniffiCustomTypeConverter for UserChannelId { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(UserChannelId(u128::from_str(&val).map_err(|_| Error::InvalidChannelId)?)) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.0.to_string() - } -} - -impl UniffiCustomTypeConverter for Txid { - type Builtin = String; - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(Txid::from_str(&val)?) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for BlockHash { - type Builtin = String; - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(BlockHash::from_str(&val)?) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for Mnemonic { - type Builtin = String; - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(Mnemonic::from_str(&val).map_err(|_| Error::InvalidSecretKey)?) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for SocketAddress { - type Builtin = String; - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(SocketAddress::from_str(&val).map_err(|_| Error::InvalidSocketAddress)?) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for UntrustedString { - type Builtin = String; - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(UntrustedString(val)) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -impl UniffiCustomTypeConverter for NodeAlias { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(sanitize_alias(&val).map_err(|_| Error::InvalidNodeAlias)?) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - -/// Represents the description of an invoice which has to be either a directly included string or -/// a hash of a description provided out of band. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Bolt11InvoiceDescription { - /// Contains a full description. - Direct { - /// Description of what the invoice is for - description: String, - }, - /// Contains a hash. - Hash { - /// Hash of the description of what the invoice is for - hash: String, - }, -} - -impl TryFrom<&Bolt11InvoiceDescription> for lightning_invoice::Bolt11InvoiceDescription { - type Error = Error; - - fn try_from(value: &Bolt11InvoiceDescription) -> Result { - match value { - Bolt11InvoiceDescription::Direct { description } => { - Description::new(description.clone()) - .map(lightning_invoice::Bolt11InvoiceDescription::Direct) - .map_err(|_| Error::InvoiceCreationFailed) - }, - Bolt11InvoiceDescription::Hash { hash } => Sha256::from_str(&hash) - .map(lightning_invoice::Sha256) - .map(lightning_invoice::Bolt11InvoiceDescription::Hash) - .map_err(|_| Error::InvoiceCreationFailed), - } - } -} - -impl From for Bolt11InvoiceDescription { - fn from(value: lightning_invoice::Bolt11InvoiceDescription) -> Self { - match value { - lightning_invoice::Bolt11InvoiceDescription::Direct(description) => { - Bolt11InvoiceDescription::Direct { description: description.to_string() } - }, - lightning_invoice::Bolt11InvoiceDescription::Hash(hash) => { - Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) } - }, - } - } -} - -impl UniffiCustomTypeConverter for OrderId { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(Self(val)) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.0 - } -} - -impl UniffiCustomTypeConverter for DateTime { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(DateTime::from_str(&val).map_err(|_| Error::InvalidDateTime)?) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_rfc3339() - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_invoice_description_conversion() { - let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); - let description = Bolt11InvoiceDescription::Hash { hash }; - let converted_description = - lightning_invoice::Bolt11InvoiceDescription::try_from(&description).unwrap(); - let reconverted_description: Bolt11InvoiceDescription = converted_description.into(); - assert_eq!(description, reconverted_description); - } -} diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f623cfc2c7..8f8151b9cb 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -5,51 +5,51 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use persist::KVStoreWalletPersister; - -use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; - -use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; -use crate::payment::store::{ConfirmationStatus, PaymentStore}; -use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; -use crate::Error; +use std::future::Future; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +#[allow(deprecated)] +use bdk_wallet::SignOptions; +use bdk_wallet::{Balance, KeychainKind, PersistedWallet, Update}; +use bitcoin::address::NetworkUnchecked; +use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; +use bitcoin::blockdata::locktime::absolute::LockTime; +use bitcoin::hashes::Hash; +use bitcoin::key::XOnlyPublicKey; +use bitcoin::psbt::Psbt; +use bitcoin::secp256k1::ecdh::SharedSecret; +use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; +use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; +use bitcoin::{ + Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, + WitnessProgram, WitnessVersion, +}; use lightning::chain::chaininterface::BroadcasterInterface; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::chain::{BestBlock, Listen}; - use lightning::events::bump_transaction::{Utxo, WalletSource}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::inbound_payment::ExpandedKey; -use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; +use lightning::ln::msgs::UnsignedGossipMessage; use lightning::ln::script::ShutdownScript; use lightning::sign::{ ChangeDestinationSource, EntropySource, InMemorySigner, KeysManager, NodeSigner, OutputSpender, - Recipient, SignerProvider, SpendableOutputDescriptor, + PeerStorageKey, Recipient, SignerProvider, SpendableOutputDescriptor, }; - use lightning::util::message_signing; use lightning_invoice::RawBolt11Invoice; +use persist::KVStoreWalletPersister; -use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; -use bdk_wallet::{Balance, KeychainKind, PersistedWallet, SignOptions, Update}; - -use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; -use bitcoin::blockdata::locktime::absolute::LockTime; -use bitcoin::hashes::Hash; -use bitcoin::key::XOnlyPublicKey; -use bitcoin::psbt::Psbt; -use bitcoin::secp256k1::ecdh::SharedSecret; -use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; -use bitcoin::{ - Amount, FeeRate, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, WitnessProgram, - WitnessVersion, -}; - -use std::ops::Deref; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::config::Config; +use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::payment::store::ConfirmationStatus; +use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; +use crate::types::{Broadcaster, PaymentStore}; +use crate::Error; pub(crate) enum OnchainSendAmount { ExactRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 }, @@ -60,35 +60,27 @@ pub(crate) enum OnchainSendAmount { pub(crate) mod persist; pub(crate) mod ser; -pub(crate) struct Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +pub(crate) struct Wallet { // A BDK on-chain wallet. inner: Mutex>, persister: Mutex, - broadcaster: B, - fee_estimator: E, - payment_store: Arc>>, - logger: L, + broadcaster: Arc, + fee_estimator: Arc, + payment_store: Arc, + config: Arc, + logger: Arc, } -impl Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl Wallet { pub(crate) fn new( wallet: bdk_wallet::PersistedWallet, - wallet_persister: KVStoreWalletPersister, broadcaster: B, fee_estimator: E, - payment_store: Arc>>, logger: L, + wallet_persister: KVStoreWalletPersister, broadcaster: Arc, + fee_estimator: Arc, payment_store: Arc, + config: Arc, logger: Arc, ) -> Self { let inner = Mutex::new(wallet); let persister = Mutex::new(wallet_persister); - Self { inner, persister, broadcaster, fee_estimator, payment_store, logger } + Self { inner, persister, broadcaster, fee_estimator, payment_store, config, logger } } pub(crate) fn get_full_scan_request(&self) -> FullScanRequest { @@ -99,6 +91,20 @@ where self.inner.lock().unwrap().start_sync_with_revealed_spks().build() } + pub(crate) fn get_cached_txs(&self) -> Vec> { + self.inner.lock().unwrap().tx_graph().full_txs().map(|tx_node| tx_node.tx).collect() + } + + pub(crate) fn get_unconfirmed_txids(&self) -> Vec { + self.inner + .lock() + .unwrap() + .transactions() + .filter(|t| t.chain_position.is_unconfirmed()) + .map(|t| t.tx_node.txid) + .collect() + } + pub(crate) fn current_best_block(&self) -> BestBlock { let checkpoint = self.inner.lock().unwrap().latest_checkpoint(); BestBlock { block_hash: checkpoint.hash(), height: checkpoint.height() } @@ -128,11 +134,12 @@ where } } - pub(crate) fn apply_unconfirmed_txs( - &self, unconfirmed_txs: Vec<(Transaction, u64)>, + pub(crate) fn apply_mempool_txs( + &self, unconfirmed_txs: Vec<(Transaction, u64)>, evicted_txids: Vec<(Txid, u64)>, ) -> Result<(), Error> { let mut locked_wallet = self.inner.lock().unwrap(); locked_wallet.apply_unconfirmed_txs(unconfirmed_txs); + locked_wallet.apply_evicted_txs(evicted_txids); let mut locked_persister = self.persister.lock().unwrap(); locked_wallet.persist(&mut locked_persister).map_err(|e| { @@ -146,11 +153,6 @@ where fn update_payment_store<'a>( &self, locked_wallet: &'a mut PersistedWallet, ) -> Result<(), Error> { - let latest_update_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); - for wtx in locked_wallet.transactions() { let id = PaymentId(wtx.tx_node.txid.to_byte_array()); let txid = wtx.tx_node.txid; @@ -187,32 +189,42 @@ where // here to determine the `PaymentKind`, but that's not really satisfactory, so // we're punting on it until we can come up with a better solution. let kind = crate::payment::PaymentKind::Onchain { txid, status: confirmation_status }; + let fee = locked_wallet.calculate_fee(&wtx.tx_node.tx).unwrap_or(Amount::ZERO); let (sent, received) = locked_wallet.sent_and_received(&wtx.tx_node.tx); let (direction, amount_msat) = if sent > received { let direction = PaymentDirection::Outbound; - let amount_msat = Some(sent.to_sat().saturating_sub(received.to_sat()) * 1000); + let amount_msat = Some( + sent.to_sat().saturating_sub(fee.to_sat()).saturating_sub(received.to_sat()) + * 1000, + ); (direction, amount_msat) } else { let direction = PaymentDirection::Inbound; - let amount_msat = Some(received.to_sat().saturating_sub(sent.to_sat()) * 1000); + let amount_msat = Some( + received.to_sat().saturating_sub(sent.to_sat().saturating_sub(fee.to_sat())) + * 1000, + ); (direction, amount_msat) }; - let payment = PaymentDetails { + let fee_paid_msat = Some(fee.to_sat() * 1000); + + let payment = PaymentDetails::new( id, kind, amount_msat, + fee_paid_msat, direction, - status: payment_status, - latest_update_timestamp, - }; + payment_status, + ); - self.payment_store.insert_or_update(&payment)?; + self.payment_store.insert_or_update(payment)?; } Ok(()) } + #[allow(deprecated)] pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, amount: Amount, confirmation_target: ConfirmationTarget, locktime: LockTime, @@ -295,7 +307,7 @@ where #[cfg(debug_assertions)] if balance.confirmed != Amount::ZERO { debug_assert!( - self.list_confirmed_utxos().map_or(false, |v| !v.is_empty()), + self.list_confirmed_utxos_inner().map_or(false, |v| !v.is_empty()), "Confirmed amounts should always be available for Anchor spending" ); } @@ -320,10 +332,22 @@ where self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s) } + fn parse_and_validate_address( + &self, network: Network, address: &Address, + ) -> Result { + Address::::from_str(address.to_string().as_str()) + .map_err(|_| Error::InvalidAddress)? + .require_network(network) + .map_err(|_| Error::InvalidAddress) + } + + #[allow(deprecated)] pub(crate) fn send_to_address( &self, address: &bitcoin::Address, send_amount: OnchainSendAmount, fee_rate: Option, ) -> Result { + self.parse_and_validate_address(self.config.network, &address)?; + // Use the set fee_rate or default to fee estimation. let confirmation_target = ConfirmationTarget::OnchainPayment; let fee_rate = @@ -333,6 +357,7 @@ where let mut locked_wallet = self.inner.lock().unwrap(); // Prepare the tx_builder. We properly check the reserve requirements (again) further down. + const DUST_LIMIT_SATS: u64 = 546; let tx_builder = match send_amount { OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => { let mut tx_builder = locked_wallet.build_tx(); @@ -340,7 +365,9 @@ where tx_builder.add_recipient(address.script_pubkey(), amount).fee_rate(fee_rate); tx_builder }, - OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } => { + OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } + if cur_anchor_reserve_sats > DUST_LIMIT_SATS => + { let change_address_info = locked_wallet.peek_address(KeychainKind::Internal, 0); let balance = locked_wallet.balance(); let spendable_amount_sats = self @@ -378,6 +405,10 @@ where ); e })?; + + // 'cancel' the transaction to free up any used change addresses + locked_wallet.cancel_tx(&tmp_tx); + let estimated_spendable_amount = Amount::from_sat( spendable_amount_sats.saturating_sub(estimated_tx_fee.to_sat()), ); @@ -397,7 +428,8 @@ where .fee_absolute(estimated_tx_fee); tx_builder }, - OnchainSendAmount::AllDrainingReserve => { + OnchainSendAmount::AllDrainingReserve + | OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats: _ } => { let mut tx_builder = locked_wallet.build_tx(); tx_builder.drain_wallet().drain_to(address.script_pubkey()).fee_rate(fee_rate); tx_builder @@ -526,80 +558,8 @@ where Ok(txid) } -} - -impl Listen for Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ - fn filtered_block_connected( - &self, _header: &bitcoin::block::Header, - _txdata: &lightning::chain::transaction::TransactionData, _height: u32, - ) { - debug_assert!(false, "Syncing filtered blocks is currently not supported"); - // As far as we can tell this would be a no-op anyways as we don't have to tell BDK about - // the header chain of intermediate blocks. According to the BDK team, it's sufficient to - // only connect full blocks starting from the last point of disagreement. - } - - fn block_connected(&self, block: &bitcoin::Block, height: u32) { - let mut locked_wallet = self.inner.lock().unwrap(); - - let pre_checkpoint = locked_wallet.latest_checkpoint(); - if pre_checkpoint.height() != height - 1 - || pre_checkpoint.hash() != block.header.prev_blockhash - { - log_debug!( - self.logger, - "Detected reorg while applying a connected block to on-chain wallet: new block with hash {} at height {}", - block.header.block_hash(), - height - ); - } - - match locked_wallet.apply_block(block, height) { - Ok(()) => { - if let Err(e) = self.update_payment_store(&mut *locked_wallet) { - log_error!(self.logger, "Failed to update payment store: {}", e); - return; - } - }, - Err(e) => { - log_error!( - self.logger, - "Failed to apply connected block to on-chain wallet: {}", - e - ); - return; - }, - }; - - let mut locked_persister = self.persister.lock().unwrap(); - match locked_wallet.persist(&mut locked_persister) { - Ok(_) => (), - Err(e) => { - log_error!(self.logger, "Failed to persist on-chain wallet: {}", e); - return; - }, - }; - } - - fn block_disconnected(&self, _header: &bitcoin::block::Header, _height: u32) { - // This is a no-op as we don't have to tell BDK about disconnections. According to the BDK - // team, it's sufficient in case of a reorg to always connect blocks starting from the last - // point of disagreement. - } -} -impl WalletSource for Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ - fn list_confirmed_utxos(&self) -> Result, ()> { + fn list_confirmed_utxos_inner(&self) -> Result, ()> { let locked_wallet = self.inner.lock().unwrap(); let mut utxos = Vec::new(); let confirmed_txs: Vec = locked_wallet @@ -691,7 +651,8 @@ where Ok(utxos) } - fn get_change_script(&self) -> Result { + #[allow(deprecated)] + fn get_change_script_inner(&self) -> Result { let mut locked_wallet = self.inner.lock().unwrap(); let mut locked_persister = self.persister.lock().unwrap(); @@ -703,7 +664,8 @@ where Ok(address_info.address.script_pubkey()) } - fn sign_psbt(&self, mut psbt: Psbt) -> Result { + #[allow(deprecated)] + fn sign_psbt_inner(&self, mut psbt: Psbt) -> Result { let locked_wallet = self.inner.lock().unwrap(); // While BDK populates both `witness_utxo` and `non_witness_utxo` fields, LDK does not. As @@ -733,34 +695,104 @@ where } } +impl Listen for Wallet { + fn filtered_block_connected( + &self, _header: &bitcoin::block::Header, + _txdata: &lightning::chain::transaction::TransactionData, _height: u32, + ) { + debug_assert!(false, "Syncing filtered blocks is currently not supported"); + // As far as we can tell this would be a no-op anyways as we don't have to tell BDK about + // the header chain of intermediate blocks. According to the BDK team, it's sufficient to + // only connect full blocks starting from the last point of disagreement. + } + + fn block_connected(&self, block: &bitcoin::Block, height: u32) { + let mut locked_wallet = self.inner.lock().unwrap(); + + let pre_checkpoint = locked_wallet.latest_checkpoint(); + if pre_checkpoint.height() != height - 1 + || pre_checkpoint.hash() != block.header.prev_blockhash + { + log_debug!( + self.logger, + "Detected reorg while applying a connected block to on-chain wallet: new block with hash {} at height {}", + block.header.block_hash(), + height + ); + } + + match locked_wallet.apply_block(block, height) { + Ok(()) => { + if let Err(e) = self.update_payment_store(&mut *locked_wallet) { + log_error!(self.logger, "Failed to update payment store: {}", e); + return; + } + }, + Err(e) => { + log_error!( + self.logger, + "Failed to apply connected block to on-chain wallet: {}", + e + ); + return; + }, + }; + + let mut locked_persister = self.persister.lock().unwrap(); + match locked_wallet.persist(&mut locked_persister) { + Ok(_) => (), + Err(e) => { + log_error!(self.logger, "Failed to persist on-chain wallet: {}", e); + return; + }, + }; + } + + fn blocks_disconnected(&self, _fork_point_block: BestBlock) { + // This is a no-op as we don't have to tell BDK about disconnections. According to the BDK + // team, it's sufficient in case of a reorg to always connect blocks starting from the last + // point of disagreement. + } +} + +impl WalletSource for Wallet { + fn list_confirmed_utxos<'a>( + &'a self, + ) -> Pin, ()>> + Send + 'a>> { + Box::pin(async move { self.list_confirmed_utxos_inner() }) + } + + fn get_change_script<'a>( + &'a self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { self.get_change_script_inner() }) + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { self.sign_psbt_inner(psbt) }) + } +} + /// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are /// directly spendable by the BDK wallet. -pub(crate) struct WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +pub(crate) struct WalletKeysManager { inner: KeysManager, - wallet: Arc>, - logger: L, + wallet: Arc, + logger: Arc, } -impl WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl WalletKeysManager { /// Constructs a `WalletKeysManager` that overrides the destination and shutdown scripts. /// /// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, and /// `starting_time_nanos`. pub fn new( - seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, - wallet: Arc>, logger: L, + seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, wallet: Arc, + logger: Arc, ) -> Self { - let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos); + let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos, true); Self { inner, wallet, logger } } @@ -777,12 +809,7 @@ where } } -impl NodeSigner for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl NodeSigner for WalletKeysManager { fn get_node_id(&self, recipient: Recipient) -> Result { self.inner.get_node_id(recipient) } @@ -793,8 +820,16 @@ where self.inner.ecdh(recipient, other_key, tweak) } - fn get_inbound_payment_key(&self) -> ExpandedKey { - self.inner.get_inbound_payment_key() + fn get_expanded_key(&self) -> ExpandedKey { + self.inner.get_expanded_key() + } + + fn get_peer_storage_key(&self) -> PeerStorageKey { + self.inner.get_peer_storage_key() + } + + fn get_receive_auth_key(&self) -> lightning::sign::ReceiveAuthKey { + self.inner.get_receive_auth_key() } fn sign_invoice( @@ -812,19 +847,17 @@ where ) -> Result { self.inner.sign_bolt12_invoice(invoice) } + fn sign_message(&self, msg: &[u8]) -> Result { + self.inner.sign_message(msg) + } } -impl OutputSpender for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl OutputSpender for WalletKeysManager { /// See [`KeysManager::spend_spendable_outputs`] for documentation on this method. - fn spend_spendable_outputs( + fn spend_spendable_outputs( &self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: ScriptBuf, feerate_sat_per_1000_weight: u32, - locktime: Option, secp_ctx: &Secp256k1, + locktime: Option, secp_ctx: &Secp256k1, ) -> Result { self.inner.spend_spendable_outputs( descriptors, @@ -837,39 +870,21 @@ where } } -impl EntropySource for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl EntropySource for WalletKeysManager { fn get_secure_random_bytes(&self) -> [u8; 32] { self.inner.get_secure_random_bytes() } } -impl SignerProvider for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl SignerProvider for WalletKeysManager { type EcdsaSigner = InMemorySigner; - fn generate_channel_keys_id( - &self, inbound: bool, channel_value_satoshis: u64, user_channel_id: u128, - ) -> [u8; 32] { - self.inner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id) + fn generate_channel_keys_id(&self, inbound: bool, user_channel_id: u128) -> [u8; 32] { + self.inner.generate_channel_keys_id(inbound, user_channel_id) } - fn derive_channel_signer( - &self, channel_value_satoshis: u64, channel_keys_id: [u8; 32], - ) -> Self::EcdsaSigner { - self.inner.derive_channel_signer(channel_value_satoshis, channel_keys_id) - } - - fn read_chan_signer(&self, reader: &[u8]) -> Result { - self.inner.read_chan_signer(reader) + fn derive_channel_signer(&self, channel_keys_id: [u8; 32]) -> Self::EcdsaSigner { + self.inner.derive_channel_signer(channel_keys_id) } fn get_destination_script(&self, _channel_keys_id: [u8; 32]) -> Result { @@ -899,16 +914,20 @@ where } } -impl ChangeDestinationSource for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ - fn get_change_destination_script(&self) -> Result { - let address = self.wallet.get_new_internal_address().map_err(|e| { - log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); - })?; - Ok(address.script_pubkey()) +impl ChangeDestinationSource for WalletKeysManager { + fn get_change_destination_script<'a>( + &'a self, + ) -> Pin> + Send + 'a>> { + let wallet = Arc::clone(&self.wallet); + let logger = Arc::clone(&self.logger); + Box::pin(async move { + wallet + .get_new_internal_address() + .map_err(|e| { + log_error!(logger, "Failed to retrieve new address from wallet: {}", e); + }) + .map(|addr| addr.script_pubkey()) + .map_err(|_| ()) + }) } } diff --git a/src/wallet/persist.rs b/src/wallet/persist.rs index d9e4e71359..5c8668937e 100644 --- a/src/wallet/persist.rs +++ b/src/wallet/persist.rs @@ -5,6 +5,11 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::sync::Arc; + +use bdk_chain::Merge; +use bdk_wallet::{ChangeSet, WalletPersister}; + use crate::io::utils::{ read_bdk_wallet_change_set, write_bdk_wallet_change_descriptor, write_bdk_wallet_descriptor, write_bdk_wallet_indexer, write_bdk_wallet_local_chain, write_bdk_wallet_network, @@ -12,11 +17,6 @@ use crate::io::utils::{ }; use crate::logger::{log_error, LdkLogger, Logger}; use crate::types::DynStore; - -use bdk_chain::Merge; -use bdk_wallet::{ChangeSet, WalletPersister}; - -use std::sync::Arc; pub(crate) struct KVStoreWalletPersister { latest_change_set: Option, kv_store: Arc, diff --git a/src/wallet/ser.rs b/src/wallet/ser.rs index 2e33992a8a..c1ad984e62 100644 --- a/src/wallet/ser.rs +++ b/src/wallet/ser.rs @@ -5,26 +5,23 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use lightning::ln::msgs::DecodeError; -use lightning::util::ser::{BigSize, Readable, RequiredWrapper, Writeable, Writer}; -use lightning::{decode_tlv_stream, encode_tlv_stream, read_tlv_fields, write_tlv_fields}; +use std::collections::{BTreeMap, BTreeSet}; +use std::str::FromStr; +use std::sync::Arc; use bdk_chain::bdk_core::{BlockId, ConfirmationBlockTime}; use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet; use bdk_chain::tx_graph::ChangeSet as BdkTxGraphChangeSet; use bdk_chain::DescriptorId; - use bdk_wallet::descriptor::Descriptor; use bdk_wallet::keys::DescriptorPublicKey; - use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::p2p::Magic; use bitcoin::{BlockHash, Network, OutPoint, Transaction, TxOut, Txid}; - -use std::collections::{BTreeMap, BTreeSet}; -use std::str::FromStr; -use std::sync::Arc; +use lightning::ln::msgs::DecodeError; +use lightning::util::ser::{BigSize, Readable, RequiredWrapper, Writeable, Writer}; +use lightning::{decode_tlv_stream, encode_tlv_stream, read_tlv_fields, write_tlv_fields}; const CHANGESET_SERIALIZATION_VERSION: u8 = 1; @@ -107,7 +104,9 @@ impl<'a> Writeable for ChangeSetSerWrapper<'a, BdkTxGraphChangeSet>, > = RequiredWrapper(None); let mut last_seen: RequiredWrapper> = RequiredWrapper(None); + let mut first_seen = None; + let mut last_evicted = None; decode_tlv_stream!(reader, { (0, txs, required), + (1, first_seen, option), (2, txouts, required), + (3, last_evicted, option), (4, anchors, required), (6, last_seen, required), }); @@ -142,6 +145,8 @@ impl Readable for ChangeSetDeserWrapper Writeable for ChangeSetSerWrapper<'a, BdkIndexerChangeSet> { fn write(&self, writer: &mut W) -> Result<(), lightning::io::Error> { CHANGESET_SERIALIZATION_VERSION.write(writer)?; + // Note we don't persist/use the optional spk_cache currently. encode_tlv_stream!(writer, { (0, ChangeSetSerWrapper(&self.0.last_revealed), required) }); Ok(()) } @@ -275,9 +281,13 @@ impl Readable for ChangeSetDeserWrapper { let mut last_revealed: RequiredWrapper>> = RequiredWrapper(None); + // Note we don't persist/use the optional spk_cache currently. decode_tlv_stream!(reader, { (0, last_revealed, required) }); - Ok(Self(BdkIndexerChangeSet { last_revealed: last_revealed.0.unwrap().0 })) + Ok(Self(BdkIndexerChangeSet { + last_revealed: last_revealed.0.unwrap().0, + spk_cache: Default::default(), + })) } } diff --git a/tests/common/logging.rs b/tests/common/logging.rs new file mode 100644 index 0000000000..3ff24d34d4 --- /dev/null +++ b/tests/common/logging.rs @@ -0,0 +1,170 @@ +use std::sync::{Arc, Mutex}; + +use chrono::Utc; +use ldk_node::logger::{LogLevel, LogRecord, LogWriter}; +#[cfg(not(feature = "uniffi"))] +use log::Record as LogFacadeRecord; +use log::{Level as LogFacadeLevel, LevelFilter as LogFacadeLevelFilter, Log as LogFacadeLog}; + +#[derive(Clone)] +pub(crate) enum TestLogWriter { + FileWriter, + LogFacade, + Custom(Arc), +} + +impl Default for TestLogWriter { + fn default() -> Self { + TestLogWriter::FileWriter + } +} + +pub(crate) struct MockLogFacadeLogger { + logs: Arc>>, +} + +impl MockLogFacadeLogger { + pub fn new() -> Self { + Self { logs: Arc::new(Mutex::new(Vec::new())) } + } + + pub fn retrieve_logs(&self) -> Vec { + self.logs.lock().unwrap().to_vec() + } +} + +impl LogFacadeLog for MockLogFacadeLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + let message = format!( + "{} {:<5} [{}:{}] {}", + Utc::now().format("%Y-%m-%d %H:%M:%S"), + record.level().to_string(), + record.module_path().unwrap(), + record.line().unwrap(), + record.args() + ); + self.logs.lock().unwrap().push(message); + } + + fn flush(&self) {} +} + +#[cfg(not(feature = "uniffi"))] +impl LogWriter for MockLogFacadeLogger { + fn log<'a>(&self, record: LogRecord) { + let record = MockLogRecord(record).into(); + LogFacadeLog::log(self, &record); + } +} + +#[cfg(not(feature = "uniffi"))] +struct MockLogRecord<'a>(LogRecord<'a>); +struct MockLogLevel(LogLevel); + +impl From for LogFacadeLevel { + fn from(level: MockLogLevel) -> Self { + match level.0 { + LogLevel::Gossip | LogLevel::Trace => LogFacadeLevel::Trace, + LogLevel::Debug => LogFacadeLevel::Debug, + LogLevel::Info => LogFacadeLevel::Info, + LogLevel::Warn => LogFacadeLevel::Warn, + LogLevel::Error => LogFacadeLevel::Error, + } + } +} + +#[cfg(not(feature = "uniffi"))] +impl<'a> From> for LogFacadeRecord<'a> { + fn from(log_record: MockLogRecord<'a>) -> Self { + let log_record = log_record.0; + let level = MockLogLevel(log_record.level).into(); + + let mut record_builder = LogFacadeRecord::builder(); + let record = record_builder + .level(level) + .module_path(Some(&log_record.module_path)) + .line(Some(log_record.line)) + .args(log_record.args); + + record.build() + } +} + +pub(crate) fn init_log_logger(level: LogFacadeLevelFilter) -> Arc { + let logger = Arc::new(MockLogFacadeLogger::new()); + log::set_boxed_logger(Box::new(logger.clone())).unwrap(); + log::set_max_level(level); + + logger +} + +pub(crate) fn validate_log_entry(entry: &String) { + let parts = entry.splitn(4, ' ').collect::>(); + assert_eq!(parts.len(), 4); + let (day, time, level, path_and_msg) = (parts[0], parts[1], parts[2], parts[3]); + + let day_parts = day.split('-').collect::>(); + assert_eq!(day_parts.len(), 3); + let (year, month, day) = (day_parts[0], day_parts[1], day_parts[2]); + assert!(year.len() == 4 && month.len() == 2 && day.len() == 2); + assert!( + year.chars().all(|c| c.is_digit(10)) + && month.chars().all(|c| c.is_digit(10)) + && day.chars().all(|c| c.is_digit(10)) + ); + + let time_parts = time.split(':').collect::>(); + assert_eq!(time_parts.len(), 3); + let (hour, minute, second) = (time_parts[0], time_parts[1], time_parts[2]); + assert!(hour.len() == 2 && minute.len() == 2 && second.len() == 2); + assert!( + hour.chars().all(|c| c.is_digit(10)) + && minute.chars().all(|c| c.is_digit(10)) + && second.chars().all(|c| c.is_digit(10)) + ); + + assert!(["GOSSIP", "TRACE", "DEBUG", "INFO", "WARN", "ERROR"].contains(&level),); + + let path = path_and_msg.split_whitespace().next().unwrap(); + assert!(path.contains('[') && path.contains(']')); + let module_path = &path[1..path.len() - 1]; + let path_parts = module_path.rsplitn(2, ':').collect::>(); + assert_eq!(path_parts.len(), 2); + let (line_number, module_name) = (path_parts[0], path_parts[1]); + assert!(module_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == ':')); + assert!(line_number.chars().all(|c| c.is_digit(10))); + + let msg_start_index = path_and_msg.find(']').unwrap() + 1; + let msg = &path_and_msg[msg_start_index..]; + assert!(!msg.is_empty()); +} + +pub(crate) struct MultiNodeLogger { + node_id: String, +} + +impl MultiNodeLogger { + pub(crate) fn new(node_id: String) -> Self { + Self { node_id } + } +} + +impl LogWriter for MultiNodeLogger { + fn log(&self, record: LogRecord) { + let log = format!( + "[{}] {} {:<5} [{}:{}] {}\n", + self.node_id, + Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"), + record.level.to_string(), + record.module_path, + record.line, + record.args + ); + + print!("{}", log); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0c0c24b7cd..3ac0e84327 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,52 +5,54 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -#![cfg(any(test, cln_test, vss_test))] +#![cfg(any(test, cln_test, lnd_test, vss_test))] #![allow(dead_code)] -use ldk_node::config::{Config, EsploraSyncConfig}; +pub(crate) mod logging; + +use std::boxed::Box; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::future::Future; +use std::path::PathBuf; +use std::pin::Pin; +use std::sync::{Arc, RwLock}; +use std::time::Duration; + +use bitcoin::hashes::hex::FromHex; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::{ + Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness, +}; +use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD}; +use electrsd::{corepc_node, ElectrsD}; +use electrum_client::ElectrumApi; +use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; -use ldk_node::logger::LogLevel; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance, }; - +use lightning::io; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; -use lightning::util::persist::KVStore; +use lightning::util::persist::{KVStore, KVStoreSync}; use lightning::util::test_utils::TestStore; - use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use lightning_types::payment::{PaymentHash, PaymentPreimage}; - use lightning_persister::fs_store::FilesystemStore; - -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; -use bitcoin::{Address, Amount, Network, OutPoint, Txid}; - -use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; -use bitcoincore_rpc::Client as BitcoindClient; -use bitcoincore_rpc::RpcApi; - -use electrsd::{bitcoind, bitcoind::BitcoinD, ElectrsD}; -use electrum_client::ElectrumApi; - +use lightning_types::payment::{PaymentHash, PaymentPreimage}; +use logging::TestLogWriter; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; - -use std::env; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; -use std::time::Duration; +use serde_json::{json, Value}; macro_rules! expect_event { ($node: expr, $event_type: ident) => {{ match $node.wait_next_event() { ref e @ Event::$event_type { .. } => { println!("{} got event {:?}", $node.node_id(), e); - $node.event_handled(); + $node.event_handled().unwrap(); }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); @@ -67,7 +69,7 @@ macro_rules! expect_channel_pending_event { ref e @ Event::ChannelPending { funding_txo, counterparty_node_id, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(counterparty_node_id, $counterparty_node_id); - $node.event_handled(); + $node.event_handled().unwrap(); funding_txo }, ref e => { @@ -85,7 +87,7 @@ macro_rules! expect_channel_ready_event { ref e @ Event::ChannelReady { user_channel_id, counterparty_node_id, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(counterparty_node_id, Some($counterparty_node_id)); - $node.event_handled(); + $node.event_handled().unwrap(); user_channel_id }, ref e => { @@ -103,7 +105,11 @@ macro_rules! expect_payment_received_event { ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(amount_msat, $amount_msat); - $node.event_handled(); + let payment = $node.payment(&payment_id.unwrap()).unwrap(); + if !matches!(payment.kind, PaymentKind::Onchain { .. }) { + assert_eq!(payment.fee_paid_msat, None); + } + $node.event_handled().unwrap(); payment_id }, ref e => { @@ -128,7 +134,7 @@ macro_rules! expect_payment_claimable_event { assert_eq!(payment_hash, $payment_hash); assert_eq!(payment_id, $payment_id); assert_eq!(claimable_amount_msat, $claimable_amount_msat); - $node.event_handled(); + $node.event_handled().unwrap(); claimable_amount_msat }, ref e => { @@ -148,8 +154,10 @@ macro_rules! expect_payment_successful_event { if let Some(fee_msat) = $fee_paid_msat { assert_eq!(fee_paid_msat, fee_msat); } + let payment = $node.payment(&$payment_id.unwrap()).unwrap(); + assert_eq!(payment.fee_paid_msat, fee_paid_msat); assert_eq!(payment_id, $payment_id); - $node.event_handled(); + $node.event_handled().unwrap(); }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); @@ -162,11 +170,12 @@ pub(crate) use expect_payment_successful_event; pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let bitcoind_exe = - env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect( + env::var("BITCOIND_EXE").ok().or_else(|| corepc_node::downloaded_exe_path().ok()).expect( "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature", ); - let mut bitcoind_conf = bitcoind::Conf::default(); + let mut bitcoind_conf = corepc_node::Conf::default(); bitcoind_conf.network = "regtest"; + bitcoind_conf.args.push("-rest"); let bitcoind = BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap(); let electrs_exe = env::var("ELECTRS_EXE") @@ -190,7 +199,7 @@ pub(crate) fn random_storage_path() -> PathBuf { pub(crate) fn random_port() -> u16 { let mut rng = thread_rng(); - rng.gen_range(5000..65535) + rng.gen_range(5000..32768) } pub(crate) fn random_listening_addresses() -> Vec { @@ -215,29 +224,29 @@ pub(crate) fn random_node_alias() -> Option { Some(NodeAlias(bytes)) } -pub(crate) fn random_config(anchor_channels: bool) -> Config { - let mut config = Config::default(); +pub(crate) fn random_config(anchor_channels: bool) -> TestConfig { + let mut node_config = Config::default(); if !anchor_channels { - config.anchor_channels_config = None; + node_config.anchor_channels_config = None; } - config.network = Network::Regtest; - println!("Setting network: {}", config.network); + node_config.network = Network::Regtest; + println!("Setting network: {}", node_config.network); let rand_dir = random_storage_path(); println!("Setting random LDK storage dir: {}", rand_dir.display()); - config.storage_dir_path = rand_dir.to_str().unwrap().to_owned(); + node_config.storage_dir_path = rand_dir.to_str().unwrap().to_owned(); let rand_listening_addresses = random_listening_addresses(); println!("Setting random LDK listening addresses: {:?}", rand_listening_addresses); - config.listening_addresses = Some(rand_listening_addresses); + node_config.listening_addresses = Some(rand_listening_addresses); let alias = random_node_alias(); println!("Setting random LDK node alias: {:?}", alias); - config.node_alias = alias; + node_config.node_alias = alias; - config + TestConfig { node_config, ..Default::default() } } #[cfg(feature = "uniffi")] @@ -248,7 +257,15 @@ type TestNode = Node; #[derive(Clone)] pub(crate) enum TestChainSource<'a> { Esplora(&'a ElectrsD), - BitcoindRpc(&'a BitcoinD), + Electrum(&'a ElectrsD), + BitcoindRpcSync(&'a BitcoinD), + BitcoindRestSync(&'a BitcoinD), +} + +#[derive(Clone, Default)] +pub(crate) struct TestConfig { + pub node_config: Config, + pub log_writer: TestLogWriter, } macro_rules! setup_builder { @@ -273,10 +290,11 @@ pub(crate) fn setup_two_nodes( println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); if allow_0conf { - config_b.trusted_peers_0conf.push(node_a.node_id()); + config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); } if anchor_channels && anchors_trusted_no_reserve { config_b + .node_config .anchor_channels_config .as_mut() .unwrap() @@ -288,18 +306,28 @@ pub(crate) fn setup_two_nodes( } pub(crate) fn setup_node( - chain_source: &TestChainSource, config: Config, seed_bytes: Option>, + chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, ) -> TestNode { - setup_builder!(builder, config); + setup_node_for_async_payments(chain_source, config, seed_bytes, None) +} + +pub(crate) fn setup_node_for_async_payments( + chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, + async_payments_role: Option, +) -> TestNode { + setup_builder!(builder, config.node_config); match chain_source { TestChainSource::Esplora(electrsd) => { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.onchain_wallet_sync_interval_secs = 100000; - sync_config.lightning_wallet_sync_interval_secs = 100000; + let sync_config = EsploraSyncConfig { background_sync_config: None }; builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); }, - TestChainSource::BitcoindRpc(bitcoind) => { + TestChainSource::Electrum(electrsd) => { + let electrum_url = format!("tcp://{}", electrsd.electrum_url); + let sync_config = ElectrumSyncConfig { background_sync_config: None }; + builder.set_chain_source_electrum(electrum_url.clone(), Some(sync_config)); + }, + TestChainSource::BitcoindRpcSync(bitcoind) => { let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); let rpc_port = bitcoind.params.rpc_socket.port(); let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); @@ -307,16 +335,53 @@ pub(crate) fn setup_node( let rpc_password = values.password; builder.set_chain_source_bitcoind_rpc(rpc_host, rpc_port, rpc_user, rpc_password); }, + TestChainSource::BitcoindRestSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + let rpc_user = values.user; + let rpc_password = values.password; + let rest_host = bitcoind.params.rpc_socket.ip().to_string(); + let rest_port = bitcoind.params.rpc_socket.port(); + builder.set_chain_source_bitcoind_rest( + rest_host, + rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + ); + }, } - let log_file_path = format!("{}/{}", config.storage_dir_path, "ldk_node.log"); - builder.set_filesystem_logger(Some(log_file_path), Some(LogLevel::Gossip)); + match &config.log_writer { + TestLogWriter::FileWriter => { + builder.set_filesystem_logger(None, None); + }, + TestLogWriter::LogFacade => { + builder.set_log_facade_logger(); + }, + TestLogWriter::Custom(custom_log_writer) => { + builder.set_custom_logger(Arc::clone(custom_log_writer)); + }, + } if let Some(seed) = seed_bytes { - builder.set_entropy_seed_bytes(seed).unwrap(); + #[cfg(feature = "uniffi")] + { + builder.set_entropy_seed_bytes(seed).unwrap(); + } + #[cfg(not(feature = "uniffi"))] + { + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(&seed); + builder.set_entropy_seed_bytes(bytes); + } } - let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); + builder.set_async_payments_role(async_payments_role).unwrap(); + + let test_sync_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into())); let node = builder.build_with_store(test_sync_store).unwrap(); node.start().unwrap(); assert!(node.status().is_running); @@ -327,22 +392,34 @@ pub(crate) fn setup_node( pub(crate) fn generate_blocks_and_wait( bitcoind: &BitcoindClient, electrs: &E, num: usize, ) { - let _ = bitcoind.create_wallet("ldk_node_test", None, None, None, None); + let _ = bitcoind.create_wallet("ldk_node_test"); let _ = bitcoind.load_wallet("ldk_node_test"); print!("Generating {} blocks...", num); - let cur_height = bitcoind.get_block_count().expect("failed to get current block height"); - let address = bitcoind - .get_new_address(Some("test"), Some(AddressType::Legacy)) - .expect("failed to get new address") - .require_network(bitcoin::Network::Regtest) - .expect("failed to get new address"); + let blockchain_info = bitcoind.get_blockchain_info().expect("failed to get blockchain info"); + let cur_height = blockchain_info.blocks; + let address = bitcoind.new_address().expect("failed to get new address"); // TODO: expect this Result once the WouldBlock issue is resolved upstream. - let _block_hashes_res = bitcoind.generate_to_address(num as u64, &address); + let _block_hashes_res = bitcoind.generate_to_address(num, &address); wait_for_block(electrs, cur_height as usize + num); print!(" Done!"); println!("\n"); } +pub(crate) fn invalidate_blocks(bitcoind: &BitcoindClient, num_blocks: usize) { + let blockchain_info = bitcoind.get_blockchain_info().expect("failed to get blockchain info"); + let cur_height = blockchain_info.blocks as usize; + let target_height = cur_height - num_blocks + 1; + let block_hash = bitcoind + .get_block_hash(target_height as u64) + .expect("failed to get block hash") + .block_hash() + .expect("block hash should be present"); + bitcoind.invalidate_block(block_hash).expect("failed to invalidate block"); + let blockchain_info = bitcoind.get_blockchain_info().expect("failed to get blockchain info"); + let new_cur_height = blockchain_info.blocks as usize; + assert!(new_cur_height + num_blocks == cur_height); +} + pub(crate) fn wait_for_block(electrs: &E, min_height: usize) { let mut header = match electrs.block_headers_subscribe() { Ok(header) => header, @@ -366,32 +443,31 @@ pub(crate) fn wait_for_block(electrs: &E, min_height: usize) { } pub(crate) fn wait_for_tx(electrs: &E, txid: Txid) { - let mut tx_res = electrs.transaction_get(&txid); - loop { - if tx_res.is_ok() { - break; - } - tx_res = exponential_backoff_poll(|| { - electrs.ping().unwrap(); - Some(electrs.transaction_get(&txid)) - }); + if electrs.transaction_get(&txid).is_ok() { + return; } + + exponential_backoff_poll(|| { + electrs.ping().unwrap(); + electrs.transaction_get(&txid).ok() + }); } pub(crate) fn wait_for_outpoint_spend(electrs: &E, outpoint: OutPoint) { let tx = electrs.transaction_get(&outpoint.txid).unwrap(); let txout_script = tx.output.get(outpoint.vout as usize).unwrap().clone().script_pubkey; - let mut is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); - loop { - if is_spent { - break; - } - is_spent = exponential_backoff_poll(|| { - electrs.ping().unwrap(); - Some(!electrs.script_get_history(&txout_script).unwrap().is_empty()) - }); + let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); + if is_spent { + return; } + + exponential_backoff_poll(|| { + electrs.ping().unwrap(); + + let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); + is_spent.then_some(()) + }); } pub(crate) fn exponential_backoff_poll(mut poll: F) -> T @@ -418,30 +494,121 @@ where pub(crate) fn premine_and_distribute_funds( bitcoind: &BitcoindClient, electrs: &E, addrs: Vec
, amount: Amount, ) { - let _ = bitcoind.create_wallet("ldk_node_test", None, None, None, None); + premine_blocks(bitcoind, electrs); + + distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount); + generate_blocks_and_wait(bitcoind, electrs, 1); +} + +pub(crate) fn premine_blocks(bitcoind: &BitcoindClient, electrs: &E) { + let _ = bitcoind.create_wallet("ldk_node_test"); let _ = bitcoind.load_wallet("ldk_node_test"); generate_blocks_and_wait(bitcoind, electrs, 101); +} - for addr in addrs { - let txid = - bitcoind.send_to_address(&addr, amount, None, None, None, None, None, None).unwrap(); - wait_for_tx(electrs, txid); +pub(crate) fn distribute_funds_unconfirmed( + bitcoind: &BitcoindClient, electrs: &E, addrs: Vec
, amount: Amount, +) -> Txid { + let mut amounts = HashMap::::new(); + for addr in &addrs { + amounts.insert(addr.to_string(), amount.to_btc()); } - generate_blocks_and_wait(bitcoind, electrs, 1); + let empty_account = json!(""); + let amounts_json = json!(amounts); + let txid = bitcoind + .call::("sendmany", &[empty_account, amounts_json]) + .unwrap() + .as_str() + .unwrap() + .parse() + .unwrap(); + + wait_for_tx(electrs, txid); + + txid +} + +pub(crate) fn prepare_rbf( + electrs: &E, txid: Txid, scripts_buf: &HashSet, +) -> (Transaction, usize) { + let tx = electrs.transaction_get(&txid).unwrap(); + + let fee_output_index = tx + .output + .iter() + .position(|output| !scripts_buf.contains(&output.script_pubkey)) + .expect("No output available for fee bumping"); + + (tx, fee_output_index) +} + +pub(crate) fn bump_fee_and_broadcast( + bitcoind: &BitcoindClient, electrs: &E, mut tx: Transaction, fee_output_index: usize, + is_insert_block: bool, +) -> Transaction { + let mut bump_fee_amount_sat = tx.vsize() as u64; + let attempts = 5; + + for _ in 0..attempts { + let fee_output = &mut tx.output[fee_output_index]; + let new_fee_value = fee_output.value.to_sat().saturating_sub(bump_fee_amount_sat); + if new_fee_value < 546 { + panic!("Warning: Fee output approaching dust limit ({} sats)", new_fee_value); + } + fee_output.value = Amount::from_sat(new_fee_value); + + for input in &mut tx.input { + input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME; + input.script_sig = ScriptBuf::new(); + input.witness = Witness::new(); + } + + let signed_result = bitcoind.sign_raw_transaction_with_wallet(&tx).unwrap(); + assert!(signed_result.complete, "Failed to sign RBF transaction"); + + let tx_bytes = Vec::::from_hex(&signed_result.hex).unwrap(); + tx = bitcoin::consensus::encode::deserialize::(&tx_bytes).unwrap(); + + match bitcoind.send_raw_transaction(&tx) { + Ok(res) => { + if is_insert_block { + generate_blocks_and_wait(bitcoind, electrs, 1); + } + let new_txid: Txid = res.0.parse().unwrap(); + wait_for_tx(electrs, new_txid); + return tx; + }, + Err(_) => { + bump_fee_amount_sat += bump_fee_amount_sat * 5; + if tx.output[fee_output_index].value.to_sat() < bump_fee_amount_sat { + panic!("Insufficient funds to increase fee"); + } + }, + } + } + + panic!("Failed to bump fee after {} attempts", attempts); } pub fn open_channel( node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, should_announce: bool, electrsd: &ElectrsD, -) { +) -> OutPoint { + open_channel_push_amt(node_a, node_b, funding_amount_sat, None, should_announce, electrsd) +} + +pub fn open_channel_push_amt( + node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, push_amount_msat: Option, + should_announce: bool, electrsd: &ElectrsD, +) -> OutPoint { if should_announce { node_a .open_announced_channel( node_b.node_id(), node_b.listening_addresses().unwrap().first().unwrap().clone(), funding_amount_sat, - None, + push_amount_msat, None, ) .unwrap(); @@ -451,7 +618,7 @@ pub fn open_channel( node_b.node_id(), node_b.listening_addresses().unwrap().first().unwrap().clone(), funding_amount_sat, - None, + push_amount_msat, None, ) .unwrap(); @@ -462,6 +629,8 @@ pub fn open_channel( let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id()); assert_eq!(funding_txo_a, funding_txo_b); wait_for_tx(&electrsd.client, funding_txo_a.txid); + + funding_txo_a } pub(crate) fn do_channel_full_cycle( @@ -673,7 +842,7 @@ pub(crate) fn do_channel_full_cycle( let received_amount = match node_b.wait_next_event() { ref e @ Event::PaymentReceived { amount_msat, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); - node_b.event_handled(); + node_b.event_handled().unwrap(); amount_msat }, ref e => { @@ -711,7 +880,7 @@ pub(crate) fn do_channel_full_cycle( let received_amount = match node_b.wait_next_event() { ref e @ Event::PaymentReceived { amount_msat, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); - node_b.event_handled(); + node_b.event_handled().unwrap(); amount_msat }, ref e => { @@ -834,7 +1003,7 @@ pub(crate) fn do_channel_full_cycle( let (received_keysend_amount, received_custom_records) = match next_event { ref e @ Event::PaymentReceived { amount_msat, ref custom_records, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); - node_b.event_handled(); + node_b.event_handled().unwrap(); (amount_msat, custom_records) }, ref e => { @@ -1025,14 +1194,121 @@ pub(crate) fn do_channel_full_cycle( // A `KVStore` impl for testing purposes that wraps all our `KVStore`s and asserts their synchronicity. pub(crate) struct TestSyncStore { + inner: Arc, +} + +impl TestSyncStore { + pub(crate) fn new(dest_dir: PathBuf) -> Self { + let inner = Arc::new(TestSyncStoreInner::new(dest_dir)); + Self { inner } + } +} + +impl KVStore for TestSyncStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.read_internal(&primary_namespace, &secondary_namespace, &key) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.write_internal(&primary_namespace, &secondary_namespace, &key, buf) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.remove_internal(&primary_namespace, &secondary_namespace, &key) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.list_internal(&primary_namespace, &secondary_namespace) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } +} + +impl KVStoreSync for TestSyncStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> lightning::io::Result> { + self.inner.read_internal(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> lightning::io::Result<()> { + self.inner.write_internal(primary_namespace, secondary_namespace, key, buf) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> lightning::io::Result<()> { + self.inner.remove_internal(primary_namespace, secondary_namespace, key) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> lightning::io::Result> { + self.inner.list_internal(primary_namespace, secondary_namespace) + } +} + +struct TestSyncStoreInner { serializer: RwLock<()>, test_store: TestStore, fs_store: FilesystemStore, sqlite_store: SqliteStore, } -impl TestSyncStore { - pub(crate) fn new(dest_dir: PathBuf) -> Self { +impl TestSyncStoreInner { + fn new(dest_dir: PathBuf) -> Self { let serializer = RwLock::new(()); let mut fs_dir = dest_dir.clone(); fs_dir.push("fs_store"); @@ -1052,9 +1328,10 @@ impl TestSyncStore { fn do_list( &self, primary_namespace: &str, secondary_namespace: &str, ) -> lightning::io::Result> { - let fs_res = self.fs_store.list(primary_namespace, secondary_namespace); - let sqlite_res = self.sqlite_store.list(primary_namespace, secondary_namespace); - let test_res = self.test_store.list(primary_namespace, secondary_namespace); + let fs_res = KVStoreSync::list(&self.fs_store, primary_namespace, secondary_namespace); + let sqlite_res = + KVStoreSync::list(&self.sqlite_store, primary_namespace, secondary_namespace); + let test_res = KVStoreSync::list(&self.test_store, primary_namespace, secondary_namespace); match fs_res { Ok(mut list) => { @@ -1077,17 +1354,17 @@ impl TestSyncStore { }, } } -} -impl KVStore for TestSyncStore { - fn read( + fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> lightning::io::Result> { let _guard = self.serializer.read().unwrap(); - let fs_res = self.fs_store.read(primary_namespace, secondary_namespace, key); - let sqlite_res = self.sqlite_store.read(primary_namespace, secondary_namespace, key); - let test_res = self.test_store.read(primary_namespace, secondary_namespace, key); + let fs_res = KVStoreSync::read(&self.fs_store, primary_namespace, secondary_namespace, key); + let sqlite_res = + KVStoreSync::read(&self.sqlite_store, primary_namespace, secondary_namespace, key); + let test_res = + KVStoreSync::read(&self.test_store, primary_namespace, secondary_namespace, key); match fs_res { Ok(read) => { @@ -1105,13 +1382,31 @@ impl KVStore for TestSyncStore { } } - fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], + fn write_internal( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); - let fs_res = self.fs_store.write(primary_namespace, secondary_namespace, key, buf); - let sqlite_res = self.sqlite_store.write(primary_namespace, secondary_namespace, key, buf); - let test_res = self.test_store.write(primary_namespace, secondary_namespace, key, buf); + let fs_res = KVStoreSync::write( + &self.fs_store, + primary_namespace, + secondary_namespace, + key, + buf.clone(), + ); + let sqlite_res = KVStoreSync::write( + &self.sqlite_store, + primary_namespace, + secondary_namespace, + key, + buf.clone(), + ); + let test_res = KVStoreSync::write( + &self.test_store, + primary_namespace, + secondary_namespace, + key, + buf.clone(), + ); assert!(self .do_list(primary_namespace, secondary_namespace) @@ -1132,14 +1427,16 @@ impl KVStore for TestSyncStore { } } - fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + fn remove_internal( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); - let fs_res = self.fs_store.remove(primary_namespace, secondary_namespace, key, lazy); + let fs_res = + KVStoreSync::remove(&self.fs_store, primary_namespace, secondary_namespace, key); let sqlite_res = - self.sqlite_store.remove(primary_namespace, secondary_namespace, key, lazy); - let test_res = self.test_store.remove(primary_namespace, secondary_namespace, key, lazy); + KVStoreSync::remove(&self.sqlite_store, primary_namespace, secondary_namespace, key); + let test_res = + KVStoreSync::remove(&self.test_store, primary_namespace, secondary_namespace, key); assert!(!self .do_list(primary_namespace, secondary_namespace) @@ -1160,7 +1457,7 @@ impl KVStore for TestSyncStore { } } - fn list( + fn list_internal( &self, primary_namespace: &str, secondary_namespace: &str, ) -> lightning::io::Result> { let _guard = self.serializer.read().unwrap(); diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index 02d091215c..6fc72b2c25 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -9,32 +9,27 @@ mod common; -use ldk_node::bitcoin::secp256k1::PublicKey; -use ldk_node::bitcoin::Amount; -use ldk_node::lightning::ln::msgs::SocketAddress; -use ldk_node::{Builder, Event}; -use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use std::default::Default; +use std::str::FromStr; use clightningrpc::lightningrpc::LightningRPC; use clightningrpc::responses::NetworkAddress; - -use bitcoincore_rpc::Auth; -use bitcoincore_rpc::Client as BitcoindClient; - +use electrsd::corepc_client::client_sync::Auth; +use electrsd::corepc_node::Client as BitcoindClient; use electrum_client::Client as ElectrumClient; -use lightning_invoice::Bolt11Invoice; - +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::bitcoin::Amount; +use ldk_node::lightning::ln::msgs::SocketAddress; +use ldk_node::{Builder, Event}; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use std::default::Default; -use std::str::FromStr; - #[test] fn test_cln() { // Setup bitcoind / electrs clients - let bitcoind_client = BitcoindClient::new( - "127.0.0.1:18443", + let bitcoind_client = BitcoindClient::new_with_auth( + "http://127.0.0.1:18443", Auth::UserPass("user".to_string(), "pass".to_string()), ) .unwrap(); @@ -45,7 +40,7 @@ fn test_cln() { // Setup LDK Node let config = common::random_config(true); - let mut builder = Builder::from_config(config); + let mut builder = Builder::from_config(config.node_config); builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None); let node = builder.build().unwrap(); @@ -64,7 +59,17 @@ fn test_cln() { // Setup CLN let sock = "/tmp/lightning-rpc"; let cln_client = LightningRPC::new(&sock); - let cln_info = cln_client.getinfo().unwrap(); + let cln_info = { + loop { + let info = cln_client.getinfo().unwrap(); + // Wait for CLN to sync block height before channel open. + // Prevents crash due to unset blockheight (see LDK Node issue #527). + if info.blockheight > 0 { + break info; + } + std::thread::sleep(std::time::Duration::from_millis(250)); + } + }; let cln_node_id = PublicKey::from_str(&cln_info.id).unwrap(); let cln_address: SocketAddress = match cln_info.binding.first().unwrap() { NetworkAddress::Ipv4 { address, port } => { diff --git a/tests/integration_tests_lnd.rs b/tests/integration_tests_lnd.rs new file mode 100755 index 0000000000..7dfc1e4f92 --- /dev/null +++ b/tests/integration_tests_lnd.rs @@ -0,0 +1,223 @@ +#![cfg(lnd_test)] + +mod common; + +use std::default::Default; +use std::str::FromStr; + +use bitcoin::hex::DisplayHex; +use electrsd::corepc_client::client_sync::Auth; +use electrsd::corepc_node::Client as BitcoindClient; +use electrum_client::Client as ElectrumClient; +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::bitcoin::Amount; +use ldk_node::lightning::ln::msgs::SocketAddress; +use ldk_node::{Builder, Event}; +use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use lnd_grpc_rust::lnrpc::invoice::InvoiceState::Settled as LndInvoiceStateSettled; +use lnd_grpc_rust::lnrpc::{ + GetInfoRequest as LndGetInfoRequest, GetInfoResponse as LndGetInfoResponse, + Invoice as LndInvoice, ListInvoiceRequest as LndListInvoiceRequest, + QueryRoutesRequest as LndQueryRoutesRequest, Route as LndRoute, SendRequest as LndSendRequest, +}; +use lnd_grpc_rust::{connect, LndClient}; +use tokio::fs; + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_lnd() { + // Setup bitcoind / electrs clients + let bitcoind_client = BitcoindClient::new_with_auth( + "http://127.0.0.1:18443", + Auth::UserPass("user".to_string(), "pass".to_string()), + ) + .unwrap(); + let electrs_client = ElectrumClient::new("tcp://127.0.0.1:50001").unwrap(); + + // Give electrs a kick. + common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1); + + // Setup LDK Node + let config = common::random_config(true); + let mut builder = Builder::from_config(config.node_config); + builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None); + + let node = builder.build().unwrap(); + node.start().unwrap(); + + // Premine some funds and distribute + let address = node.onchain_payment().new_address().unwrap(); + let premine_amount = Amount::from_sat(5_000_000); + common::premine_and_distribute_funds( + &bitcoind_client, + &electrs_client, + vec![address], + premine_amount, + ); + + // Setup LND + let endpoint = "127.0.0.1:8081"; + let cert_path = std::env::var("LND_CERT_PATH").expect("LND_CERT_PATH not set"); + let macaroon_path = std::env::var("LND_MACAROON_PATH").expect("LND_MACAROON_PATH not set"); + let mut lnd = TestLndClient::new(cert_path, macaroon_path, endpoint.to_string()).await; + + let lnd_node_info = lnd.get_node_info().await; + let lnd_node_id = PublicKey::from_str(&lnd_node_info.identity_pubkey).unwrap(); + let lnd_address: SocketAddress = "127.0.0.1:9735".parse().unwrap(); + + node.sync_wallets().unwrap(); + + // Open the channel + let funding_amount_sat = 1_000_000; + + node.open_channel(lnd_node_id, lnd_address, funding_amount_sat, Some(500_000_000), None) + .unwrap(); + + let funding_txo = common::expect_channel_pending_event!(node, lnd_node_id); + common::wait_for_tx(&electrs_client, funding_txo.txid); + common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 6); + node.sync_wallets().unwrap(); + let user_channel_id = common::expect_channel_ready_event!(node, lnd_node_id); + + // Send a payment to LND + let lnd_invoice = lnd.create_invoice(100_000_000).await; + let parsed_invoice = lightning_invoice::Bolt11Invoice::from_str(&lnd_invoice).unwrap(); + + node.bolt11_payment().send(&parsed_invoice, None).unwrap(); + common::expect_event!(node, PaymentSuccessful); + let lnd_listed_invoices = lnd.list_invoices().await; + assert_eq!(lnd_listed_invoices.len(), 1); + assert_eq!(lnd_listed_invoices.first().unwrap().state, LndInvoiceStateSettled as i32); + + // Check route LND -> LDK + let amount_msat = 9_000_000; + let max_retries = 7; + for attempt in 1..=max_retries { + match lnd.query_routes(&node.node_id().to_string(), amount_msat).await { + Ok(routes) => { + if !routes.is_empty() { + break; + } + }, + Err(err) => { + if attempt == max_retries { + panic!("Failed to find route from LND to LDK: {}", err); + } + }, + }; + // wait for the payment process + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + + // Send a payment to LDK + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new("lndTest".to_string()).unwrap()); + let ldk_invoice = + node.bolt11_payment().receive(amount_msat, &invoice_description, 3600).unwrap(); + lnd.pay_invoice(&ldk_invoice.to_string()).await; + common::expect_event!(node, PaymentReceived); + + node.close_channel(&user_channel_id, lnd_node_id).unwrap(); + common::expect_event!(node, ChannelClosed); + node.stop().unwrap(); +} + +struct TestLndClient { + client: LndClient, +} + +impl TestLndClient { + async fn new(cert_path: String, macaroon_path: String, socket: String) -> Self { + // Read the contents of the file into a vector of bytes + let cert_bytes = fs::read(cert_path).await.expect("Failed to read tls cert file"); + let mac_bytes = fs::read(macaroon_path).await.expect("Failed to read macaroon file"); + + // Convert the bytes to a hex string + let cert = cert_bytes.as_hex().to_string(); + let macaroon = mac_bytes.as_hex().to_string(); + + let client = connect(cert, macaroon, socket).await.expect("Failed to connect to Lnd"); + + TestLndClient { client } + } + + async fn get_node_info(&mut self) -> LndGetInfoResponse { + let response = self + .client + .lightning() + .get_info(LndGetInfoRequest {}) + .await + .expect("Failed to fetch node info from LND") + .into_inner(); + + response + } + + async fn create_invoice(&mut self, amount_msat: u64) -> String { + let invoice = LndInvoice { value_msat: amount_msat as i64, ..Default::default() }; + + self.client + .lightning() + .add_invoice(invoice) + .await + .expect("Failed to create invoice on LND") + .into_inner() + .payment_request + } + + async fn list_invoices(&mut self) -> Vec { + self.client + .lightning() + .list_invoices(LndListInvoiceRequest { ..Default::default() }) + .await + .expect("Failed to list invoices from LND") + .into_inner() + .invoices + } + + async fn query_routes( + &mut self, pubkey: &str, amount_msat: u64, + ) -> Result, String> { + let request = LndQueryRoutesRequest { + pub_key: pubkey.to_string(), + amt_msat: amount_msat as i64, + ..Default::default() + }; + + let response = self + .client + .lightning() + .query_routes(request) + .await + .map_err(|err| format!("Failed to query routes from LND: {:?}", err))? + .into_inner(); + + if response.routes.is_empty() { + return Err(format!("No routes found for pubkey: {}", pubkey)); + } + + Ok(response.routes) + } + + async fn pay_invoice(&mut self, invoice_str: &str) { + let send_req = + LndSendRequest { payment_request: invoice_str.to_string(), ..Default::default() }; + let response = self + .client + .lightning() + .send_payment_sync(send_req) + .await + .expect("Failed to pay invoice on LND") + .into_inner(); + + if !response.payment_error.is_empty() || response.payment_preimage.is_empty() { + panic!( + "LND payment failed: {}", + if response.payment_error.is_empty() { + "No preimage returned" + } else { + &response.payment_error + } + ); + } + } +} diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 2dc74cea98..64a78e11bf 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -7,29 +7,37 @@ mod common; +use std::collections::HashSet; +use std::str::FromStr; +use std::sync::Arc; + +use bitcoin::address::NetworkUnchecked; +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; +use bitcoin::{Address, Amount, ScriptBuf}; +use common::logging::{init_log_logger, validate_log_entry, MultiNodeLogger, TestLogWriter}; use common::{ - do_channel_full_cycle, expect_channel_ready_event, expect_event, expect_payment_received_event, - expect_payment_successful_event, generate_blocks_and_wait, open_channel, - premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle, + expect_channel_pending_event, expect_channel_ready_event, expect_event, + expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, + generate_blocks_and_wait, open_channel, open_channel_push_amt, premine_and_distribute_funds, + premine_blocks, prepare_rbf, random_config, random_listening_addresses, + setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_node_for_async_payments, + setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; - -use ldk_node::config::EsploraSyncConfig; +use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; +use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ - ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, - SendingParameters, + ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, + QrPaymentResult, }; -use ldk_node::{Builder, Event, NodeError}; - +use ldk_node::{Builder, DynStore, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; -use lightning::util::persist::KVStore; - -use bitcoincore_rpc::RpcApi; - -use bitcoin::Amount; +use lightning::routing::gossip::{NodeAlias, NodeId}; +use lightning::routing::router::RouteParametersConfig; use lightning_invoice::{Bolt11InvoiceDescription, Description}; - -use std::sync::Arc; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; +use log::LevelFilter; #[test] fn channel_full_cycle() { @@ -40,9 +48,25 @@ fn channel_full_cycle() { } #[test] -fn channel_full_cycle_bitcoind() { +fn channel_full_cycle_electrum() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Electrum(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); +} + +#[test] +fn channel_full_cycle_bitcoind_rpc_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::BitcoindRpc(&bitcoind); + let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); +} + +#[test] +fn channel_full_cycle_bitcoind_rest_sync() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::BitcoindRestSync(&bitcoind); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } @@ -123,10 +147,8 @@ fn multi_hop_sending() { let mut nodes = Vec::new(); for _ in 0..5 { let config = random_config(true); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.onchain_wallet_sync_interval_secs = 100000; - sync_config.lightning_wallet_sync_interval_secs = 100000; - setup_builder!(builder, config); + let sync_config = EsploraSyncConfig { background_sync_config: None }; + setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let node = builder.build().unwrap(); node.start().unwrap(); @@ -186,11 +208,11 @@ fn multi_hop_sending() { // Sleep a bit for gossip to propagate. std::thread::sleep(std::time::Duration::from_secs(1)); - let sending_params = SendingParameters { - max_total_routing_fee_msat: Some(Some(75_000).into()), - max_total_cltv_expiry_delta: Some(1000), - max_path_count: Some(10), - max_channel_saturation_power_of_half: Some(2), + let route_params = RouteParametersConfig { + max_total_routing_fee_msat: Some(75_000), + max_total_cltv_expiry_delta: 1000, + max_path_count: 10, + max_channel_saturation_power_of_half: 2, }; let invoice_description = @@ -199,10 +221,14 @@ fn multi_hop_sending() { .bolt11_payment() .receive(2_500_000, &invoice_description.clone().into(), 9217) .unwrap(); - nodes[0].bolt11_payment().send(&invoice, Some(sending_params)).unwrap(); + nodes[0].bolt11_payment().send(&invoice, Some(route_params)).unwrap(); expect_event!(nodes[1], PaymentForwarded); - expect_event!(nodes[2], PaymentForwarded); + + // We expect that the payment goes through N2 or N3, so we check both for the PaymentForwarded event. + let node_2_fwd_event = matches!(nodes[2].next_event(), Some(Event::PaymentForwarded { .. })); + let node_3_fwd_event = matches!(nodes[3].next_event(), Some(Event::PaymentForwarded { .. })); + assert!(node_2_fwd_event || node_3_fwd_event); let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); let fee_paid_msat = Some(2000); @@ -216,13 +242,11 @@ fn start_stop_reinit() { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let test_sync_store: Arc = - Arc::new(TestSyncStore::new(config.storage_dir_path.clone().into())); + let test_sync_store: Arc = + Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.clone().into())); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.onchain_wallet_sync_interval_secs = 100000; - sync_config.lightning_wallet_sync_interval_secs = 100000; - setup_builder!(builder, config); + let sync_config = EsploraSyncConfig { background_sync_config: None }; + setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let node = builder.build_with_store(Arc::clone(&test_sync_store)).unwrap(); @@ -246,7 +270,7 @@ fn start_stop_reinit() { node.sync_wallets().unwrap(); assert_eq!(node.list_balances().spendable_onchain_balance_sats, expected_amount.to_sat()); - let log_file = format!("{}/ldk_node.log", config.clone().storage_dir_path); + let log_file = format!("{}/ldk_node.log", config.node_config.clone().storage_dir_path); assert!(std::path::Path::new(&log_file).exists()); node.stop().unwrap(); @@ -259,7 +283,7 @@ fn start_stop_reinit() { assert_eq!(node.stop(), Err(NodeError::NotRunning)); drop(node); - setup_builder!(builder, config); + setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let reinitialized_node = builder.build_with_store(Arc::clone(&test_sync_store)).unwrap(); @@ -281,13 +305,17 @@ fn start_stop_reinit() { } #[test] -fn onchain_spend_receive() { +fn onchain_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); + // This is a Bitcoin Testnet address. Sending funds to this address from the Regtest network will fail + let static_address = "tb1q0d40e5rta4fty63z64gztf8c3v20cvet6v2jdh"; + let unchecked_address = Address::::from_str(static_address).unwrap(); + let addr_c = unchecked_address.assume_checked(); let premine_amount_sat = 1_100_000; premine_and_distribute_funds( @@ -352,12 +380,47 @@ fn onchain_spend_receive() { node_a.onchain_payment().send_to_address(&addr_b, expected_node_a_balance + 1, None) ); - let amount_to_send_sats = 1000; + assert_eq!( + Err(NodeError::InvalidAddress), + node_a.onchain_payment().send_to_address(&addr_c, expected_node_a_balance + 1, None) + ); + + assert_eq!( + Err(NodeError::InvalidAddress), + node_a.onchain_payment().send_all_to_address(&addr_c, true, None) + ); + + let amount_to_send_sats = 54321; let txid = node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap(); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); wait_for_tx(&electrsd.client, txid); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + let payment_id = PaymentId(txid.to_byte_array()); + let payment_a = node_a.payment(&payment_id).unwrap(); + assert_eq!(payment_a.status, PaymentStatus::Pending); + match payment_a.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Unconfirmed)); + }, + _ => panic!("Unexpected payment kind"), + } + assert!(payment_a.fee_paid_msat > Some(0)); + let payment_b = node_b.payment(&payment_id).unwrap(); + assert_eq!(payment_b.status, PaymentStatus::Pending); + match payment_a.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Unconfirmed)); + }, + _ => panic!("Unexpected payment kind"), + } + assert!(payment_b.fee_paid_msat > Some(0)); + assert_eq!(payment_a.amount_msat, Some(amount_to_send_sats * 1000)); + assert_eq!(payment_a.amount_msat, payment_b.amount_msat); + assert_eq!(payment_a.fee_paid_msat, payment_b.fee_paid_msat); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); @@ -375,6 +438,24 @@ fn onchain_spend_receive() { node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); assert_eq!(node_b_payments.len(), 3); + let payment_a = node_a.payment(&payment_id).unwrap(); + match payment_a.kind { + PaymentKind::Onchain { txid: _txid, status } => { + assert_eq!(_txid, txid); + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + + let payment_b = node_a.payment(&payment_id).unwrap(); + match payment_b.kind { + PaymentKind::Onchain { txid: _txid, status } => { + assert_eq!(_txid, txid); + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + let addr_b = node_b.onchain_payment().new_address().unwrap(); let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); @@ -423,6 +504,89 @@ fn onchain_spend_receive() { assert_eq!(node_b_payments.len(), 5); } +#[test] +fn onchain_send_all_retains_reserve() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + // Setup nodes + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 1_000_000; + let reserve_amount_sat = 25_000; + let onchain_fee_buffer_sat = 1000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a.clone(), addr_b.clone()], + Amount::from_sat(premine_amount_sat), + ); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + // Send all over, with 0 reserve as we don't have any channels open. + let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + // Check node a sent all and node b received it + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat * 2 - onchain_fee_buffer_sat)..=(premine_amount_sat * 2)) + .contains(&node_b.list_balances().spendable_onchain_balance_sats)); + + // Refill to make sure we have enough reserve for the channel open. + let txid = bitcoind + .client + .send_to_address(&addr_a, Amount::from_sat(reserve_amount_sat)) + .unwrap() + .0 + .parse() + .unwrap(); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, reserve_amount_sat); + + // Open a channel. + open_channel(&node_b, &node_a, premine_amount_sat, false, &electrsd); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Check node a sent all and node b received it + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) + ..=premine_amount_sat) + .contains(&node_b.list_balances().spendable_onchain_balance_sats)); + + // Send all over again, this time ensuring the reserve is accounted for + let txid = node_b.onchain_payment().send_all_to_address(&addr_a, true, None).unwrap(); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + // Check node b sent all and node a received it + assert_eq!(node_b.list_balances().total_onchain_balance_sats, reserve_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) + ..=premine_amount_sat) + .contains(&node_a.list_balances().spendable_onchain_balance_sats)); +} + #[test] fn onchain_wallet_recovery() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -451,16 +615,10 @@ fn onchain_wallet_recovery() { let txid = bitcoind .client - .send_to_address( - &addr_2, - Amount::from_sat(premine_amount_sat), - None, - None, - None, - None, - None, - None, - ) + .send_to_address(&addr_2, Amount::from_sat(premine_amount_sat)) + .unwrap() + .0 + .parse() .unwrap(); wait_for_tx(&electrsd.client, txid); @@ -493,16 +651,10 @@ fn onchain_wallet_recovery() { let txid = bitcoind .client - .send_to_address( - &addr_6, - Amount::from_sat(premine_amount_sat), - None, - None, - None, - None, - None, - None, - ) + .send_to_address(&addr_6, Amount::from_sat(premine_amount_sat)) + .unwrap() + .0 + .parse() .unwrap(); wait_for_tx(&electrsd.client, txid); @@ -515,6 +667,141 @@ fn onchain_wallet_recovery() { ); } +#[test] +fn test_rbf_via_mempool() { + run_rbf_test(false); +} + +#[test] +fn test_rbf_via_direct_block_insertion() { + run_rbf_test(true); +} + +// `is_insert_block`: +// - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling. +// - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling. +fn run_rbf_test(is_insert_block: bool) { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); + let chain_source_electrsd = TestChainSource::Electrum(&electrsd); + let chain_source_esplora = TestChainSource::Esplora(&electrsd); + + macro_rules! config_node { + ($chain_source: expr, $anchor_channels: expr) => {{ + let config_a = random_config($anchor_channels); + let node = setup_node(&$chain_source, config_a, None); + node + }}; + } + let anchor_channels = false; + let nodes = vec![ + config_node!(chain_source_electrsd, anchor_channels), + config_node!(chain_source_bitcoind, anchor_channels), + config_node!(chain_source_esplora, anchor_channels), + ]; + + let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); + premine_blocks(bitcoind, electrs); + + // Helpers declaration before starting the test + let all_addrs = + nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::>(); + let amount_sat = 2_100_000; + let mut txid; + macro_rules! distribute_funds_all_nodes { + () => { + txid = distribute_funds_unconfirmed( + bitcoind, + electrs, + all_addrs.clone(), + Amount::from_sat(amount_sat), + ); + }; + } + macro_rules! validate_balances { + ($expected_balance_sat: expr, $is_spendable: expr) => { + let spend_balance = if $is_spendable { $expected_balance_sat } else { 0 }; + for node in &nodes { + node.sync_wallets().unwrap(); + let balances = node.list_balances(); + assert_eq!(balances.spendable_onchain_balance_sats, spend_balance); + assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat); + } + }; + } + + let scripts_buf: HashSet = + all_addrs.iter().map(|addr| addr.script_pubkey()).collect(); + let mut tx; + let mut fee_output_index; + + // Modify the output to the nodes + distribute_funds_all_nodes!(); + validate_balances!(amount_sat, false); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + let new_addr = bitcoind.new_address().unwrap(); + output.script_pubkey = new_addr.script_pubkey(); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + validate_balances!(0, is_insert_block); + + // Not modifying the output scripts, but still bumping the fee. + distribute_funds_all_nodes!(); + validate_balances!(amount_sat, false); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + validate_balances!(amount_sat, is_insert_block); + + let mut final_amount_sat = amount_sat * 2; + let value_sat = 21_000; + + // Increase the value of the nodes' outputs + distribute_funds_all_nodes!(); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + output.value = Amount::from_sat(output.value.to_sat() + value_sat); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + final_amount_sat += value_sat; + validate_balances!(final_amount_sat, is_insert_block); + + // Decreases the value of the nodes' outputs + distribute_funds_all_nodes!(); + final_amount_sat += amount_sat; + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + output.value = Amount::from_sat(output.value.to_sat() - value_sat); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + final_amount_sat -= value_sat; + validate_balances!(final_amount_sat, is_insert_block); + + if !is_insert_block { + generate_blocks_and_wait(bitcoind, electrs, 1); + validate_balances!(final_amount_sat, true); + } + + // Check if it is possible to send all funds from the node + let mut txids = Vec::new(); + let addr = bitcoind.new_address().unwrap(); + nodes.iter().for_each(|node| { + let txid = node.onchain_payment().send_all_to_address(&addr, true, None).unwrap(); + txids.push(txid); + }); + txids.iter().for_each(|txid| { + wait_for_tx(electrs, *txid); + }); + generate_blocks_and_wait(bitcoind, electrs, 6); + validate_balances!(0, true); +} + #[test] fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -529,6 +816,21 @@ fn sign_verify_msg() { assert!(node.verify_signature(msg, sig.as_str(), &pkey)); } +#[test] +fn connection_multi_listen() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + + let node_id_b = node_b.node_id(); + + let node_addrs_b = node_b.listening_addresses().unwrap(); + for node_addr_b in &node_addrs_b { + node_a.connect(node_id_b, node_addr_b.clone(), false).unwrap(); + node_a.disconnect(node_id_b).unwrap(); + } +} + #[test] fn connection_restart_behavior() { do_connection_restart_behavior(true); @@ -544,11 +846,6 @@ fn do_connection_restart_behavior(persist: bool) { let node_id_b = node_b.node_id(); let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); - - while !node_b.status().is_listening { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - node_a.connect(node_id_b, node_addr_b, persist).unwrap(); let peer_details_a = node_a.list_peers().first().unwrap().clone(); @@ -598,10 +895,6 @@ fn concurrent_connections_succeed() { let node_id_b = node_b.node_id(); let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); - while !node_b.status().is_listening { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - let mut handles = Vec::new(); for _ in 0..10 { let thread_node = Arc::clone(&node_a); @@ -838,6 +1131,233 @@ fn simple_bolt12_send_receive() { assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); } +#[test] +fn async_payment() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let mut config_sender = random_config(true); + config_sender.node_config.listening_addresses = None; + config_sender.node_config.node_alias = None; + config_sender.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); + let node_sender = setup_node_for_async_payments( + &chain_source, + config_sender, + None, + Some(AsyncPaymentsRole::Client), + ); + + let mut config_sender_lsp = random_config(true); + config_sender_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); + let node_sender_lsp = setup_node_for_async_payments( + &chain_source, + config_sender_lsp, + None, + Some(AsyncPaymentsRole::Server), + ); + + let mut config_receiver_lsp = random_config(true); + config_receiver_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); + + let node_receiver_lsp = setup_node_for_async_payments( + &chain_source, + config_receiver_lsp, + None, + Some(AsyncPaymentsRole::Server), + ); + + let mut config_receiver = random_config(true); + config_receiver.node_config.listening_addresses = None; + config_receiver.node_config.node_alias = None; + config_receiver.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver ".to_string()))); + let node_receiver = setup_node(&chain_source, config_receiver, None); + + let address_sender = node_sender.onchain_payment().new_address().unwrap(); + let address_sender_lsp = node_sender_lsp.onchain_payment().new_address().unwrap(); + let address_receiver_lsp = node_receiver_lsp.onchain_payment().new_address().unwrap(); + let address_receiver = node_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 4_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_sender, address_sender_lsp, address_receiver_lsp, address_receiver], + Amount::from_sat(premine_amount_sat), + ); + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + open_channel(&node_sender, &node_sender_lsp, 400_000, false, &electrsd); + open_channel(&node_sender_lsp, &node_receiver_lsp, 400_000, true, &electrsd); + open_channel_push_amt( + &node_receiver, + &node_receiver_lsp, + 400_000, + Some(200_000_000), + false, + &electrsd, + ); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_sender, node_sender_lsp.node_id()); + expect_channel_ready_event!(node_sender_lsp, node_sender.node_id()); + expect_channel_ready_event!(node_sender_lsp, node_receiver_lsp.node_id()); + expect_channel_ready_event!(node_receiver_lsp, node_sender_lsp.node_id()); + expect_channel_ready_event!(node_receiver_lsp, node_receiver.node_id()); + expect_channel_ready_event!(node_receiver, node_receiver_lsp.node_id()); + + let has_node_announcements = |node: &ldk_node::Node| { + node.network_graph() + .list_nodes() + .iter() + .filter(|n| { + node.network_graph().node(n).map_or(false, |info| info.announcement_info.is_some()) + }) + .count() >= 2 + }; + + // Wait for everyone to see all channels and node announcements. + while node_sender.network_graph().list_channels().len() < 1 + || node_sender_lsp.network_graph().list_channels().len() < 1 + || node_receiver_lsp.network_graph().list_channels().len() < 1 + || node_receiver.network_graph().list_channels().len() < 1 + || !has_node_announcements(&node_sender) + || !has_node_announcements(&node_sender_lsp) + || !has_node_announcements(&node_receiver_lsp) + || !has_node_announcements(&node_receiver) + { + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + let recipient_id = vec![1, 2, 3]; + let blinded_paths = + node_receiver_lsp.bolt12_payment().blinded_paths_for_async_recipient(recipient_id).unwrap(); + node_receiver.bolt12_payment().set_paths_to_static_invoice_server(blinded_paths).unwrap(); + + let offer = loop { + if let Ok(offer) = node_receiver.bolt12_payment().receive_async() { + break offer; + } + + std::thread::sleep(std::time::Duration::from_millis(100)); + }; + + node_receiver.stop().unwrap(); + + let payment_id = + node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None).unwrap(); + + // Sleep to allow the payment reach a state where the htlc is held and waiting for the receiver to come online. + std::thread::sleep(std::time::Duration::from_millis(3000)); + + node_receiver.start().unwrap(); + + expect_payment_successful_event!(node_sender, Some(payment_id), None); +} + +#[test] +fn test_node_announcement_propagation() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + // Node A will use both listening and announcement addresses + let mut config_a = random_config(true); + let node_a_alias_string = "ldk-node-a".to_string(); + let mut node_a_alias_bytes = [0u8; 32]; + node_a_alias_bytes[..node_a_alias_string.as_bytes().len()] + .copy_from_slice(node_a_alias_string.as_bytes()); + let node_a_node_alias = Some(NodeAlias(node_a_alias_bytes)); + let node_a_announcement_addresses = random_listening_addresses(); + config_a.node_config.node_alias = node_a_node_alias.clone(); + config_a.node_config.listening_addresses = Some(random_listening_addresses()); + config_a.node_config.announcement_addresses = Some(node_a_announcement_addresses.clone()); + + // Node B will only use listening addresses + let mut config_b = random_config(true); + let node_b_alias_string = "ldk-node-b".to_string(); + let mut node_b_alias_bytes = [0u8; 32]; + node_b_alias_bytes[..node_b_alias_string.as_bytes().len()] + .copy_from_slice(node_b_alias_string.as_bytes()); + let node_b_node_alias = Some(NodeAlias(node_b_alias_bytes)); + let node_b_listening_addresses = random_listening_addresses(); + config_b.node_config.node_alias = node_b_node_alias.clone(); + config_b.node_config.listening_addresses = Some(node_b_listening_addresses.clone()); + config_b.node_config.announcement_addresses = None; + + let node_a = setup_node(&chain_source, config_a, None); + let node_b = setup_node(&chain_source, config_b, None); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_amount_sat), + ); + + node_a.sync_wallets().unwrap(); + + // Open an announced channel from node_a to node_b + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Wait until node_b broadcasts a node announcement + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // Sleep to make sure the node announcement propagates + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Get node info from the other node's perspective + let node_a_info = node_b.network_graph().node(&NodeId::from_pubkey(&node_a.node_id())).unwrap(); + let node_a_announcement_info = node_a_info.announcement_info.as_ref().unwrap(); + + let node_b_info = node_a.network_graph().node(&NodeId::from_pubkey(&node_b.node_id())).unwrap(); + let node_b_announcement_info = node_b_info.announcement_info.as_ref().unwrap(); + + // Assert that the aliases and addresses match the expected values + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_a_announcement_info.alias(), &node_a_node_alias.unwrap()); + #[cfg(feature = "uniffi")] + assert_eq!(node_a_announcement_info.alias, node_a_alias_string); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_a_announcement_info.addresses(), &node_a_announcement_addresses); + #[cfg(feature = "uniffi")] + assert_eq!(node_a_announcement_info.addresses, node_a_announcement_addresses); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_b_announcement_info.alias(), &node_b_node_alias.unwrap()); + #[cfg(feature = "uniffi")] + assert_eq!(node_b_announcement_info.alias, node_b_alias_string); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_b_announcement_info.addresses(), &node_b_listening_addresses); + #[cfg(feature = "uniffi")] + assert_eq!(node_b_announcement_info.addresses, node_b_listening_addresses); +} + #[test] fn generate_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); @@ -848,6 +1368,21 @@ fn generate_bip21_uri() { let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; + let expected_amount_sats = 100_000; + let expiry_sec = 4_000; + + // Test 1: Verify URI generation (on-chain + BOLT11) works + // even before any channels are opened. This checks the graceful fallback behavior. + let initial_uqr_payment = node_b + .unified_qr_payment() + .receive(expected_amount_sats, "asdf", expiry_sec) + .expect("Failed to generate URI"); + println!("Initial URI (no channels): {}", initial_uqr_payment); + + assert!(initial_uqr_payment.contains("bitcoin:")); + assert!(initial_uqr_payment.contains("lightning=")); + assert!(!initial_uqr_payment.contains("lno=")); // BOLT12 requires channels + premine_and_distribute_funds( &bitcoind.client, &electrsd.client, @@ -865,20 +1400,16 @@ fn generate_bip21_uri() { expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); - let expected_amount_sats = 100_000; - let expiry_sec = 4_000; + // Test 2: Verify URI generation (on-chain + BOLT11 + BOLT12) works after channels are established. + let uqr_payment = node_b + .unified_qr_payment() + .receive(expected_amount_sats, "asdf", expiry_sec) + .expect("Failed to generate URI"); - let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec); - - match uqr_payment.clone() { - Ok(ref uri) => { - println!("Generated URI: {}", uri); - assert!(uri.contains("bitcoin:")); - assert!(uri.contains("lightning=")); - assert!(uri.contains("lno=")); - }, - Err(e) => panic!("Failed to generate URI: {:?}", e), - } + println!("Generated URI: {}", uqr_payment); + assert!(uqr_payment.contains("bitcoin:")); + assert!(uqr_payment.contains("lightning=")); + assert!(uqr_payment.contains("lno=")); } #[test] @@ -990,3 +1521,319 @@ fn unified_qr_send_receive() { assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } + +#[test] +fn lsps2_client_service_integration() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let sync_config = EsploraSyncConfig { background_sync_config: None }; + + // Setup three nodes: service, client, and payer + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config.node_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build().unwrap(); + service_node.start().unwrap(); + + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config.node_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config.node_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build().unwrap(); + payer_node.start().unwrap(); + + let service_addr = service_node.onchain_payment().new_address().unwrap(); + let client_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_addr = payer_node.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 10_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr, client_addr, payer_addr], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + // Open a channel payer -> service that will allow paying the JIT invoice + println!("Opening channel payer_node -> service_node!"); + open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_amount_msat = 100_000_000; + + println!("Generating JIT invoice!"); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_event!(service_node, PaymentForwarded); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_successful_event!(payer_node, Some(payment_id), None); + let client_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let client_payment = client_node.payment(&client_payment_id).unwrap(); + match client_payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); + }, + _ => panic!("Unexpected payment kind"), + } + + let expected_channel_overprovisioning_msat = + (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; + let expected_channel_size_sat = + (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; + let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; + assert_eq!(channel_value_sats, expected_channel_size_sat); + + println!("Generating regular invoice!"); + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()).into(); + let amount_msat = 5_000_000; + let invoice = + client_node.bolt11_payment().receive(amount_msat, &invoice_description, 1024).unwrap(); + + // Have the payer_node pay the invoice, to check regular forwards service_node -> client_node + // are working as expected. + println!("Paying regular invoice!"); + let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_event!(service_node, PaymentForwarded); + expect_payment_received_event!(client_node, amount_msat); + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and claim_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 200_000_000; + let manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + let claimable_amount_msat = expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Claiming payment!"); + client_node + .bolt11_payment() + .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) + .unwrap(); + + expect_event!(service_node, PaymentForwarded); + expect_payment_successful_event!(payer_node, Some(payment_id), None); + let client_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let client_payment = client_node.payment(&client_payment_id).unwrap(); + match client_payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); + }, + _ => panic!("Unexpected payment kind"), + } + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and fail_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 400_000_000; + let manual_preimage = PaymentPreimage([43u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Failing payment!"); + client_node.bolt11_payment().fail_for_hash(manual_payment_hash).unwrap(); + + expect_event!(payer_node, PaymentFailed); + assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); +} + +#[test] +fn facade_logging() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let logger = init_log_logger(LevelFilter::Trace); + let mut config = random_config(false); + config.log_writer = TestLogWriter::LogFacade; + + println!("== Facade logging starts =="); + let _node = setup_node(&chain_source, config, None); + + assert!(!logger.retrieve_logs().is_empty()); + for (_, entry) in logger.retrieve_logs().iter().enumerate() { + validate_log_entry(entry); + } +} + +#[test] +fn spontaneous_send_with_custom_preimage() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_sat = 1_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 500_000, true, &electrsd); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let seed = b"test_payment_preimage"; + let bytes: Sha256Hash = Sha256Hash::hash(seed); + let custom_bytes = bytes.to_byte_array(); + let custom_preimage = PaymentPreimage(custom_bytes); + + let amount_msat = 100_000; + let payment_id = node_a + .spontaneous_payment() + .send_with_preimage(amount_msat, node_b.node_id(), custom_preimage, None) + .unwrap(); + + // check payment status and verify stored preimage + expect_payment_successful_event!(node_a, Some(payment_id), None); + let details: PaymentDetails = + node_a.list_payments_with_filter(|p| p.id == payment_id).first().unwrap().clone(); + assert_eq!(details.status, PaymentStatus::Succeeded); + if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = details.kind { + assert_eq!(pi.0, custom_bytes); + } else { + panic!("Expected a spontaneous PaymentKind with a preimage"); + } + + // Verify receiver side (node_b) + expect_payment_received_event!(node_b, amount_msat); + let receiver_payments: Vec = node_b.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Inbound + && matches!(p.kind, PaymentKind::Spontaneous { .. }) + }); + + assert_eq!(receiver_payments.len(), 1); + let receiver_details = &receiver_payments[0]; + assert_eq!(receiver_details.status, PaymentStatus::Succeeded); + assert_eq!(receiver_details.amount_msat, Some(amount_msat)); + assert_eq!(receiver_details.direction, PaymentDirection::Inbound); + + // Verify receiver also has the same preimage + if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = &receiver_details.kind { + assert_eq!(pi.0, custom_bytes); + } else { + panic!("Expected receiver to have spontaneous PaymentKind with preimage"); + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn drop_in_async_context() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let seed_bytes = vec![42u8; 64]; + + let config = random_config(true); + let node = setup_node(&chain_source, config, Some(seed_bytes)); + node.stop().unwrap(); +} diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 525c1f1f17..bdd8760034 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -9,16 +9,17 @@ mod common; -use ldk_node::Builder; use std::collections::HashMap; +use ldk_node::Builder; + #[test] fn channel_full_cycle_with_vss_store() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); println!("== Node A =="); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let config_a = common::random_config(true); - let mut builder_a = Builder::from_config(config_a); + let mut builder_a = Builder::from_config(config_a.node_config); builder_a.set_chain_source_esplora(esplora_url.clone(), None); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let node_a = builder_a @@ -32,7 +33,7 @@ fn channel_full_cycle_with_vss_store() { println!("\n== Node B =="); let config_b = common::random_config(true); - let mut builder_b = Builder::from_config(config_b); + let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b .build_with_vss_store_and_fixed_headers( diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs new file mode 100644 index 0000000000..03ace908fb --- /dev/null +++ b/tests/reorg_test.rs @@ -0,0 +1,195 @@ +mod common; +use std::collections::HashMap; + +use bitcoin::Amount; +use ldk_node::payment::{PaymentDirection, PaymentKind}; +use ldk_node::{Event, LightningBalance, PendingSweepBalance}; +use proptest::prelude::prop; +use proptest::proptest; + +use crate::common::{ + expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel, + premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_node, + wait_for_outpoint_spend, TestChainSource, +}; + +proptest! { + #![proptest_config(proptest::test_runner::Config::with_cases(5))] + #[test] + fn reorg_test(reorg_depth in 1..=6usize, force_close in prop::bool::ANY) { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); + let chain_source_electrsd = TestChainSource::Electrum(&electrsd); + let chain_source_esplora = TestChainSource::Esplora(&electrsd); + + macro_rules! config_node { + ($chain_source: expr, $anchor_channels: expr) => {{ + let config_a = random_config($anchor_channels); + let node = setup_node(&$chain_source, config_a, None); + node + }}; + } + let anchor_channels = true; + let nodes = vec![ + config_node!(chain_source_electrsd, anchor_channels), + config_node!(chain_source_bitcoind, anchor_channels), + config_node!(chain_source_esplora, anchor_channels), + ]; + + let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); + macro_rules! reorg { + ($reorg_depth: expr) => {{ + invalidate_blocks(bitcoind, $reorg_depth); + generate_blocks_and_wait(bitcoind, electrs, $reorg_depth); + }}; + } + + let amount_sat = 2_100_000; + let addr_nodes = + nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::>(); + premine_and_distribute_funds(bitcoind, electrs, addr_nodes, Amount::from_sat(amount_sat)); + + macro_rules! sync_wallets { + () => { + nodes.iter().for_each(|node| node.sync_wallets().unwrap()) + }; + } + sync_wallets!(); + nodes.iter().for_each(|node| { + assert_eq!(node.list_balances().spendable_onchain_balance_sats, amount_sat); + assert_eq!(node.list_balances().total_onchain_balance_sats, amount_sat); + }); + + + let mut nodes_funding_tx = HashMap::new(); + let funding_amount_sat = 2_000_000; + for (node, next_node) in nodes.iter().zip(nodes.iter().cycle().skip(1)) { + let funding_txo = open_channel(node, next_node, funding_amount_sat, true, &electrsd); + nodes_funding_tx.insert(node.node_id(), funding_txo); + } + + generate_blocks_and_wait(bitcoind, electrs, 6); + sync_wallets!(); + + reorg!(reorg_depth); + sync_wallets!(); + + macro_rules! collect_channel_ready_events { + ($node:expr, $expected:expr) => {{ + let mut user_channels = HashMap::new(); + for _ in 0..$expected { + match $node.wait_next_event() { + Event::ChannelReady { user_channel_id, counterparty_node_id, .. } => { + $node.event_handled().unwrap(); + user_channels.insert(counterparty_node_id, user_channel_id); + }, + other => panic!("Unexpected event: {:?}", other), + } + } + user_channels + }}; + } + + let mut node_channels_id = HashMap::new(); + for (i, node) in nodes.iter().enumerate() { + assert_eq!( + node + .list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 1 + ); + + let user_channels = collect_channel_ready_events!(node, 2); + let next_node = nodes.get((i + 1) % nodes.len()).unwrap(); + let prev_node = nodes.get((i + nodes.len() - 1) % nodes.len()).unwrap(); + + assert!(user_channels.get(&Some(next_node.node_id())) != None); + assert!(user_channels.get(&Some(prev_node.node_id())) != None); + + let user_channel_id = + user_channels.get(&Some(next_node.node_id())).expect("Missing user channel for node"); + node_channels_id.insert(node.node_id(), *user_channel_id); + } + + + for (node, next_node) in nodes.iter().zip(nodes.iter().cycle().skip(1)) { + let user_channel_id = node_channels_id.get(&node.node_id()).expect("user channel id not exist"); + let funding = nodes_funding_tx.get(&node.node_id()).expect("Funding tx not exist"); + + if force_close { + node.force_close_channel(&user_channel_id, next_node.node_id(), None).unwrap(); + } else { + node.close_channel(&user_channel_id, next_node.node_id()).unwrap(); + } + + expect_event!(node, ChannelClosed); + expect_event!(next_node, ChannelClosed); + + wait_for_outpoint_spend(electrs, *funding); + } + + reorg!(reorg_depth); + sync_wallets!(); + + generate_blocks_and_wait(bitcoind, electrs, 1); + sync_wallets!(); + + if force_close { + nodes.iter().for_each(|node| { + node.sync_wallets().unwrap(); + // If there is no more balance, there is nothing to process here. + if node.list_balances().lightning_balances.len() < 1 { + return; + } + match node.list_balances().lightning_balances[0] { + LightningBalance::ClaimableAwaitingConfirmations { + confirmation_height, + .. + } => { + let cur_height = node.status().current_best_block.height; + let blocks_to_go = confirmation_height - cur_height; + generate_blocks_and_wait(bitcoind, electrs, blocks_to_go as usize); + node.sync_wallets().unwrap(); + }, + _ => panic!("Unexpected balance state for node_hub!"), + } + + assert!(node.list_balances().lightning_balances.len() < 2); + assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0); + match node.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + + generate_blocks_and_wait(&bitcoind, electrs, 1); + node.sync_wallets().unwrap(); + assert!(node.list_balances().lightning_balances.len() < 2); + assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0); + match node.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + }); + } + + generate_blocks_and_wait(bitcoind, electrs, 6); + sync_wallets!(); + + reorg!(reorg_depth); + sync_wallets!(); + + let fee_sat = 7000; + // Check balance after close channel + nodes.iter().for_each(|node| { + assert!(node.list_balances().spendable_onchain_balance_sats > amount_sat - fee_sat); + assert!(node.list_balances().spendable_onchain_balance_sats < amount_sat); + + assert_eq!(node.list_balances().total_anchor_channels_reserve_sats, 0); + assert!(node.list_balances().lightning_balances.is_empty()); + + assert_eq!(node.next_event(), None); + }); + } +}