diff --git a/.forgejo/default_merge_message/MERGE_TEMPLATE.md b/.forgejo/default_merge_message/MERGE_TEMPLATE.md new file mode 100644 index 00000000000..669632fc772 --- /dev/null +++ b/.forgejo/default_merge_message/MERGE_TEMPLATE.md @@ -0,0 +1,5 @@ +Merge PR '${PullRequestTitle}' (${PullRequestReference}) +from ${HeadBranch} into ${BaseBranch} + +${ReviewedOn} +${ReviewedBy} diff --git a/.forgejo/workflows/assign-reviewer.yml b/.forgejo/workflows/assign-reviewer.yml new file mode 100644 index 00000000000..2e1ffb77cbf --- /dev/null +++ b/.forgejo/workflows/assign-reviewer.yml @@ -0,0 +1,119 @@ +name: Assign a random reviewer + +# Forgejo has no built-in random/round-robin reviewer assignment (only +# path-based CODEOWNERS), so pick a random developer for each newly opened +# pull request and request their review via the API. +# +# Runs automatically when a PR is opened, and can also be run manually +# (workflow_dispatch) against any PR number to pick an additional reviewer. +# * Someone already requested as a reviewer, or who has already submitted a +# review, is never picked. +# * On automatic runs (including re-runs), nothing is done at all if anyone +# from the REVIEWERS pool has already reviewed or been requested (reviews +# from people outside the pool don't count). Manual runs skip this check +# and always add a reviewer if an eligible candidate remains. + +on: + pull_request_target: + types: [opened] + workflow_dispatch: + inputs: + pr: + description: "Pull request number to assign a reviewer to" + required: true + +enable-openid-connect: true + +jobs: + assign: + runs-on: debian-trixie + permissions: + id-token: write + steps: + - name: Fetch Authorized Integration token + id: jwt + run: | + set -eu + jwt="$(curl -fsS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=u:88:1a17a83c-eae2-4258-8b7f-34a9c9408cec" | jq -r '.value')" + echo "::add-mask::$jwt" + echo "jwt=$jwt" >> "$FORGEJO_OUTPUT" + - name: Request review from a random developer + # This never checks out or runs any PR code -- it only makes API + # calls -- so running in the base-repo context (pull_request_target, + # which is what grants the token write access even for fork PRs) is safe. + env: + API: ${{ github.server_url }}/api/v1 + REPO: ${{ github.repository }} + EVENT_NAME: ${{ github.event_name }} + EVENT_PR: ${{ github.event.pull_request.number }} + INPUT_PR: ${{ github.event.inputs.pr }} + # Space-separated pool of candidate reviewers. + REVIEWERS: "matt val wpaulino joostjager jkczyz benthecarman tankyleo tnull" + run: | + set -eu + AUTH="Authorization: bearer ${{ steps.jwt.outputs.jwt }}" + + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + PR="$INPUT_PR" + else + PR="$EVENT_PR" + fi + case "$PR" in + ''|*[!0-9]*) echo "Invalid PR number: '$PR'"; exit 1 ;; + esac + + PR_JSON="$(curl -fsS -H "$AUTH" "$API/repos/$REPO/pulls/$PR")" + AUTHOR="$(printf '%s' "$PR_JSON" | jq -r '.user.login')" + + # Everyone already on the PR: currently-requested reviewers plus + # anyone who has submitted a review. PENDING (unsubmitted draft) and + # REQUEST_REVIEW (the open-request marker rows in the reviews list) + # are not submitted reviews, so they are not counted as "reviewed". + REQUESTED="$(printf '%s' "$PR_JSON" | + jq -r '(.requested_reviewers // [])[] | .login? // empty')" + REVIEWED="$( + page=1 + while :; do + CHUNK="$(curl -fsS -H "$AUTH" "$API/repos/$REPO/pulls/$PR/reviews?limit=50&page=$page")" + printf '%s' "$CHUNK" | jq -r '.[] + | select(.state == "APPROVED" or .state == "REQUEST_CHANGES" or .state == "COMMENT") + | .user.login? // empty' + if [ "$(printf '%s' "$CHUNK" | jq 'length')" -lt 50 ]; then break; fi + page=$((page + 1)) + done + )" + # The author self-reviewing (commenting on their own PR) doesn't + # count as someone being on the PR. + INVOLVED="$(printf '%s\n%s\n' "$REQUESTED" "$REVIEWED" | + awk -v author="$AUTHOR" '$0 != "" && $0 != author' | sort -u)" + + # On automatic runs, if a pool member is already on the PR there is + # nothing to do. Manual runs go ahead and add another reviewer. + if [ "$EVENT_NAME" != "workflow_dispatch" ]; then + for d in $REVIEWERS; do + if printf '%s\n' "$INVOLVED" | grep -qxF "$d"; then + echo "$d has already reviewed or been requested on PR #$PR; nothing to do." + exit 0 + fi + done + fi + + # Build the candidate pool, excluding the PR author and anyone + # already requested or reviewing. + POOL="" + for d in $REVIEWERS; do + if [ "$d" = "$AUTHOR" ]; then continue; fi + if printf '%s\n' "$INVOLVED" | grep -qxF "$d"; then continue; fi + POOL="$POOL $d" + done + + REVIEWER="$(printf '%s\n' $POOL | shuf -n1)" + if [ -z "$REVIEWER" ]; then + echo "No eligible reviewer left in the pool; skipping." + exit 0 + fi + + echo "Requesting review from $REVIEWER on PR #$PR" + curl -fsS -H "$AUTH" -H 'Content-Type: application/json' \ + -X POST "$API/repos/$REPO/pulls/$PR/requested_reviewers" \ + -d "$(jq -n --arg r "$REVIEWER" '{reviewers: [$r]}')" >/dev/null diff --git a/.forgejo/workflows/audit.yml b/.forgejo/workflows/audit.yml new file mode 100644 index 00000000000..65d702e70aa --- /dev/null +++ b/.forgejo/workflows/audit.yml @@ -0,0 +1,24 @@ +name: Security Audit +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * *' + +jobs: + audit: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust stable toolchain + run: | + rustup default stable + - name: Install cargo-audit + run: cargo install cargo-audit --locked + - name: Run cargo audit + # RUSTSEC-2021-0145 pertains `atty`, which is a depencency of + # `criterion`. While the latter removed the depencency in its + # newest version, it would also require a higher `rustc`. We + # therefore avoid bumping it to allow benchmarking with our + # `rustc` 1.63 MSRV. + run: cargo audit --ignore RUSTSEC-2021-0145 diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 00000000000..d58d0140f71 --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,486 @@ +name: Continuous Integration Checks + +on: + push: + branches-ignore: + - master + pull_request: + branches-ignore: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ext-test: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust stable toolchain + run: | + rustup default stable + - name: Run externalized tests + run: | + cd ext-functional-test-demo + cargo test --verbose --color always + cargo test --verbose --color always --features test-broken + + build-workspace: + uses: ./.forgejo/workflows/ci-build.yml + with: + script: ci/ci-tests-workspace.sh + + build-features: + uses: ./.forgejo/workflows/ci-build.yml + with: + script: ci/ci-tests-features.sh + + build-bindings: + uses: ./.forgejo/workflows/ci-build.yml + with: + script: ci/ci-tests-bindings.sh + + build-nostd: + uses: ./.forgejo/workflows/ci-build.yml + with: + script: ci/ci-tests-nostd.sh + + build-cfg-flags: + uses: ./.forgejo/workflows/ci-build.yml + with: + script: ci/ci-tests-cfg-flags.sh + + build-sync: + uses: ./.forgejo/workflows/ci-build.yml + with: + script: ci/ci-tests-sync.sh + + coverage: + needs: fuzz + strategy: + fail-fast: false + runs-on: debian-trixie + # Codecov auto-detects only a fixed set of CI providers (not Forgejo), so the + # commit/branch/PR context is passed to the CLI explicitly in the steps below. + # CODECOV_PR is empty on non-pull_request events and is then omitted. + env: + CODECOV_SLUG: ${{ github.repository }} + CODECOV_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CODECOV_BRANCH: ${{ github.head_ref || github.ref_name }} + CODECOV_PR: ${{ github.event.pull_request.number }} + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + - name: Install Rust stable toolchain + run: | + rustup default stable + - name: Run tests with coverage generation + run: | + cargo install cargo-llvm-cov + export RUSTFLAGS="-Coverflow-checks=off" + cargo llvm-cov --features rest-client,rpc-client,tokio,serde --codecov --hide-instantiations --output-path=target/codecov.json + curl --verbose -O https://cli.codecov.io/latest/linux/codecov + chmod +x codecov + # Pass the commit context manually since codecov can't detect Forgejo. + CC="--git-service github --slug $CODECOV_SLUG --sha $CODECOV_SHA --branch $CODECOV_BRANCH" + if [ -n "${CODECOV_PR:-}" ]; then CC="$CC --pr $CODECOV_PR"; fi + # Could you use this to fake the coverage report for your PR? Sure. + # Will anyone be impressed by your amazing coverage? No + # Maybe if codecov wasn't broken we wouldn't need to do this... + ./codecov --verbose upload-process --disable-search --fail-on-error $CC -f target/codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'tests' + cargo clean + - name: Clone fuzzing corpus + run: git clone --depth=1 https://github.com/lightningdevkit/ldk-fuzzing-corpus.git fuzz/ldk-fuzzing-corpus + - name: Symlink corpus into hfuzz_workspace + run: | + set -eu + cd fuzz + for D in ldk-fuzzing-corpus/rust-lightning/*/; do + NAME=$(basename "$D") + mkdir -p "hfuzz_workspace/${NAME}_target" + cp -r "ldk-fuzzing-corpus/rust-lightning/${NAME}" "hfuzz_workspace/${NAME}_target/input" + done + - name: Run fuzz coverage generation + run: | + ./contrib/generate_fuzz_coverage.sh --output-dir `pwd` --output-codecov-json + # Pass the commit context manually since codecov can't detect Forgejo. + CC="--git-service github --slug $CODECOV_SLUG --sha $CODECOV_SHA --branch $CODECOV_BRANCH" + if [ -n "${CODECOV_PR:-}" ]; then CC="$CC --pr $CODECOV_PR"; fi + # Could you use this to fake the coverage report for your PR? Sure. + # Will anyone be impressed by your amazing coverage? No + # Maybe if codecov wasn't broken we wouldn't need to do this... + ./codecov --verbose upload-process --disable-search --fail-on-error $CC -f fuzz-fake-hashes-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing-fake-hashes' + ./codecov --verbose upload-process --disable-search --fail-on-error $CC -f fuzz-real-hashes-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing-real-hashes' + - name: Comment the codecov report link on the PR + # Codecov's own PR comment relies on CI environment detection (broken + # under Forgejo), so post a link to the commit's report ourselves. A + # hidden marker makes the comment sticky: update it instead of piling up + # a new comment on every push. Only runs for pull requests. + if: github.event.pull_request.number + env: + API: ${{ github.server_url }}/api/v1 + REPO: ${{ github.repository }} + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + run: | + set -eu + AUTH="Authorization: token ${FORGEJO_TOKEN}" + URL="https://app.codecov.io/github/${REPO}/commit/${CODECOV_SHA}" + MARKER="" + BODY="${MARKER}"$'\n'"[Coverage report for this commit on Codecov](${URL})" + + # Update an existing sticky comment if present, otherwise create one. + CID="$(curl -fsS -H "$AUTH" "$API/repos/$REPO/issues/$CODECOV_PR/comments?limit=50" \ + | jq -r --arg m "$MARKER" 'map(select((.body // "") | contains($m))) | .[0].id // empty')" + if [ -n "$CID" ]; then + curl -fsS -H "$AUTH" -H 'Content-Type: application/json' \ + -X PATCH "$API/repos/$REPO/issues/comments/$CID" \ + -d "$(jq -n --arg b "$BODY" '{body: $b}')" >/dev/null + else + curl -fsS -H "$AUTH" -H 'Content-Type: application/json' \ + -X POST "$API/repos/$REPO/issues/$CODECOV_PR/comments" \ + -d "$(jq -n --arg b "$BODY" '{body: $b}')" >/dev/null + fi + + benchmark: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust stable toolchain + run: | + rustup default stable + - name: Cache routing graph snapshot + id: cache-graph + uses: https://data.forgejo.org/actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: lightning/net_graph-2023-12-10.bin + key: ldk-net_graph-v0.0.118-2023-12-10.bin + - name: Fetch routing graph snapshot + if: steps.cache-graph.outputs.cache-hit != 'true' + run: | + curl --verbose -L -o lightning/net_graph-2023-12-10.bin https://bitcoin.ninja/ldk-net_graph-v0.0.118-2023-12-10.bin + echo "Sha sum: $(sha256sum lightning/net_graph-2023-12-10.bin | awk '{ print $1 }')" + if [ "$(sha256sum lightning/net_graph-2023-12-10.bin | awk '{ print $1 }')" != "${EXPECTED_ROUTING_GRAPH_SNAPSHOT_SHASUM}" ]; then + echo "Bad hash" + exit 1 + fi + env: + EXPECTED_ROUTING_GRAPH_SNAPSHOT_SHASUM: e94b38ef4b3ce683893bf6a3ee28d60cb37c73b059403ff77b7e7458157968c2 + - name: Cache scorer snapshot + id: cache-scorer + uses: https://data.forgejo.org/actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: lightning/scorer-2023-12-10.bin + key: ldk-scorer-v0.0.118-2023-12-10.bin + - name: Fetch scorer snapshot + if: steps.cache-scorer.outputs.cache-hit != 'true' + run: | + curl --verbose -L -o lightning/scorer-2023-12-10.bin https://bitcoin.ninja/ldk-scorer-v0.0.118-2023-12-10.bin + echo "Sha sum: $(sha256sum lightning/scorer-2023-12-10.bin | awk '{ print $1 }')" + if [ "$(sha256sum lightning/scorer-2023-12-10.bin | awk '{ print $1 }')" != "${EXPECTED_SCORER_SNAPSHOT_SHASUM}" ]; then + echo "Bad hash" + exit 1 + fi + env: + EXPECTED_SCORER_SNAPSHOT_SHASUM: 570a26bb28870fe1da7e392cdec9fb794718826b04c43ca053d71a8a9bb9be69 + - name: Fetch rapid graph sync reference input + run: | + curl --verbose -L -o lightning-rapid-gossip-sync/res/full_graph.lngossip https://bitcoin.ninja/ldk-compressed_graph-285cb27df79-2022-07-21.bin + echo "Sha sum: $(sha256sum lightning-rapid-gossip-sync/res/full_graph.lngossip | awk '{ print $1 }')" + if [ "$(sha256sum lightning-rapid-gossip-sync/res/full_graph.lngossip | awk '{ print $1 }')" != "${EXPECTED_RAPID_GOSSIP_SHASUM}" ]; then + echo "Bad hash" + exit 1 + fi + env: + EXPECTED_RAPID_GOSSIP_SHASUM: e0f5d11641c11896d7af3a2246d3d6c3f1720b7d2d17aab321ecce82e6b7deb8 + - name: Test with Network Graph on Rust ${{ matrix.toolchain }} + run: | + cd lightning + RUSTFLAGS="--cfg=require_route_graph_test" cargo test + cd .. + - name: Run benchmarks on Rust ${{ matrix.toolchain }} + run: | + cd bench + RUSTFLAGS="--cfg=ldk_bench --cfg=require_route_graph_test" cargo bench + + check_release: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + - name: Install Rust stable toolchain + run: | + rustup default stable + - name: Run cargo check for release build. + run: | + cargo check --release + cargo check --no-default-features --release + cargo check --no-default-features --features=std --release + cargo doc --release + cargo doc --no-default-features --release + + check_docs: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + # While docs.rs builds using a nightly compiler (and we use some nightly features), + # nightly ends up randomly breaking builds occasionally, so we instead use beta + # and set RUSTC_BOOTSTRAP in check-docsrs.sh + - name: Install Rust beta toolchain + run: | + rustup default beta + - name: Simulate docs.rs build + run: ci/check-docsrs.sh + + fuzz_sanity: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust 1.75 toolchain + run: | + rustup default 1.75 + - name: Sanity check fuzz targets on Rust 1.75 + run: | + cd fuzz + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz --cfg=chacha20_poly1305_fuzz" cargo test --quiet --color always --lib -j8 + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz --cfg=chacha20_poly1305_fuzz" cargo test --manifest-path fuzz-fake-hashes/Cargo.toml --quiet --color always --bins -j8 + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=chacha20_poly1305_fuzz" cargo test --manifest-path fuzz-real-hashes/Cargo.toml --quiet --color always --bins -j8 + + fuzz: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust 1.75 toolchain + run: | + rustup default 1.75 + - name: Clone fuzzing corpus + # Clone from this Forgejo instance (rather than the GitHub copy) so + # that new entries are detected against the repository the corpus + # sweep will open its pull requests on. + run: git clone --depth=1 ${{ github.server_url }}/lightningdevkit/ldk-fuzzing-corpus.git fuzz/ldk-fuzzing-corpus + - name: Symlink corpus into hfuzz_workspace + run: | + set -eu + cd fuzz + for D in ldk-fuzzing-corpus/rust-lightning/*/; do + NAME=$(basename "$D") + mkdir -p "hfuzz_workspace/${NAME}_target" + ln -sfn "../../ldk-fuzzing-corpus/rust-lightning/${NAME}" \ + "hfuzz_workspace/${NAME}_target/input" + done + - name: Run fuzzers + run: cd fuzz && ./ci-fuzz.sh && cd .. + env: + FUZZ_MINIMIZE: ${{ contains(github.event.pull_request.labels.*.name, 'fuzz-minimize') }} + - name: Stage new corpus entries for upload + # New fuzzer inputs are written straight into the corpus checkout (the + # input dirs are symlinked into it above), so they show up as + # untracked files there. + # + # This run can't contribute them to the corpus repo itself: it mostly + # runs for pull requests from forks, and Forgejo withholds all + # credentials (secrets and identity tokens alike) from fork-PR runs. + # Instead the new entries are uploaded as a short-lived artifact + # below, which the corpus repo's nightly job sweeps into a pull + # request. + if: success() || failure() + run: | + set -eu + WORKSPACE="$(pwd)" + rm -rf "$WORKSPACE/new-corpus" + mkdir -p "$WORKSPACE/new-corpus" + + cd fuzz/ldk-fuzzing-corpus + while IFS= read -r F; do + mkdir -p "$WORKSPACE/new-corpus/$(dirname "$F")" + cp -a "$F" "$WORKSPACE/new-corpus/$F" + done < <(git ls-files --others --exclude-standard rust-lightning/) + cd "$WORKSPACE" + + for D in fuzz/hfuzz_workspace/*_target/; do + [ -d "$D" ] || continue + BASE=$(basename "$D") + NAME="${BASE%_target}" + for F in "$D"/SIG*; do + [ -f "$F" ] || continue + FILE="$(basename "$F")" + [ -f "$WORKSPACE/new-corpus/rust-lightning/$NAME/$FILE" ] && continue + mkdir -p "$WORKSPACE/new-corpus/rust-lightning/$NAME" + cp "$F" "$WORKSPACE/new-corpus/rust-lightning/$NAME/$FILE" + done + done + + NEW=$(find new-corpus -type f 2>/dev/null | wc -l) + echo "Staged $NEW new corpus entries (including any SIG* crashes)" + - name: Upload new corpus entries + if: success() || failure() + # The forgejo/ fork, not the actions/ mirror: upstream's @actions/artifact + # client refuses to talk to any server that isn't github.com. + uses: https://data.forgejo.org/forgejo/upload-artifact@cb8afe72b42edc798abfb8fcb556cf660d894245 # v5 + with: + name: hfuzz-corpus + path: new-corpus + compression-level: 0 + if-no-files-found: ignore + # The nightly sweep deletes artifacts it has processed; the + # retention only has to bridge a missed nightly run. + retention-days: 2 + + linting: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust stable toolchain + run: | + rustup default stable + - name: Install clippy + run: | + rustup component add clippy + - name: shellcheck the CI and `contrib` scripts + run: | + shellcheck ci/*.sh -aP ci + shellcheck contrib/*.sh -aP contrib + - name: Run default clippy linting + run: | + ./ci/check-lint.sh + + rustfmt: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust 1.75 toolchain + run: | + rustup default 1.75 + - name: Install rustfmt + run: | + rustup component add rustfmt + - name: Run rustfmt checks + run: cargo fmt --check + - name: Run rustfmt checks on lightning-tests + run: cd lightning-tests && cargo fmt --check + - name: Run rustfmt checks on fuzz + run: cd fuzz && cargo fmt --check + tor-connect: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Install Rust 1.75 toolchain + run: | + rustup default 1.75 + - name: Test tor connections using lightning-net-tokio + run: | + set -eu + # tor is preinstalled in the runner image, but we have no sudo to + # start the system service, so run it in the background for this step. + # The test routes real traffic (including to a .onion address) through + # the proxy, so we must wait until tor is fully bootstrapped. + TOR_DATA="$(mktemp -d)" + tor --SocksPort 9050 --DataDirectory "$TOR_DATA" \ + --Log "notice file $TOR_DATA/tor.log" & + TOR_PID=$! + trap 'kill "$TOR_PID" 2>/dev/null || true' EXIT + for _ in $(seq 1 90); do + if grep -q "Bootstrapped 100%" "$TOR_DATA/tor.log" 2>/dev/null; then + break + fi + if ! kill -0 "$TOR_PID" 2>/dev/null; then + echo "tor exited before bootstrapping:"; cat "$TOR_DATA/tor.log"; exit 1 + fi + sleep 2 + done + if ! grep -q "Bootstrapped 100%" "$TOR_DATA/tor.log"; then + echo "tor failed to bootstrap within timeout:"; cat "$TOR_DATA/tor.log"; exit 1 + fi + TOR_PROXY="127.0.0.1:9050" RUSTFLAGS="--cfg=tor" cargo test --verbose --color always -p lightning-net-tokio + + notify-failure: + needs: [build-workspace, build-features, build-bindings, build-nostd, build-cfg-flags, build-sync, fuzz_sanity, fuzz, linting, rustfmt, check_release, check_docs, benchmark, ext-test, tor-connect, coverage] + if: failure() && github.ref == 'refs/heads/main' + runs-on: debian-trixie + steps: + - name: Configure fj credentials + # `fj` reads its token from keys.json; it has no token environment + # variable, so write the automatic Actions token there. + env: + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + FORGEJO_USER: ${{ github.actor }} + run: | + install -d -m 700 "$HOME/.local/share/forgejo-cli" + printf '{"hosts":{"git.rust-bitcoin.org":{"type":"Application","name":"%s","token":"%s"}}}' "$FORGEJO_USER" "$FORGEJO_TOKEN" > "$HOME/.local/share/forgejo-cli/keys.json" + chmod 600 "$HOME/.local/share/forgejo-cli/keys.json" + - name: Create or update failure issue + # Deduplicate by label (like the GitHub job): comment on the top open + # issue carrying the "build failed" label, otherwise open a new one. + # fj handles search/create/comment; the raw API is used only to attach + # the label after creating, since fj cannot set or create labels. The + # automatic token has write access to this repo for non-fork events. + env: + HOST: git.rust-bitcoin.org + API: ${{ github.server_url }}/api/v1 + REPO: ${{ github.repository }} + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + LABEL: build failed + run: | + set -eu + AUTH="Authorization: token ${FORGEJO_TOKEN}" + + TITLE="Failed build: ${{ github.workflow }}" + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}" + REPO_URL="${{ github.server_url }}/${{ github.repository }}" + COMMITTER="${{ github.event.head_commit.author.username }}" + BODY="Forgejo Actions workflow [${{ github.workflow }} #${{ github.run_number }}](${RUN_URL}) failed." + BODY="${BODY}"$'\n\n'"Event: ${{ github.event_name }}" + BRANCH="${{ github.ref_name }}" + BODY="${BODY}"$'\n'"Branch: [${BRANCH}](${REPO_URL}/src/branch/${BRANCH})" + BODY="${BODY}"$'\n'"Commit: [${{ github.sha }}](${REPO_URL}/commit/${{ github.sha }})" + if [ -n "$COMMITTER" ]; then + BODY="${BODY}"$'\n'"Committer: @${COMMITTER}" + fi + + # Find the top open issue carrying the label. With `--style minimal`, + # `fj issue search` prints a totals line, then one + # "#: (by <author>)" line per match; take the first. + NUM="$(fj -H "$HOST" --style minimal issue search --repo "$REPO" --labels "$LABEL" --state open \ + | head -n2 | tail -n1 | awk '/^#[0-9]/ { print $1 }' | tr -d '#:')" + + if [ -n "$NUM" ]; then + fj -H "$HOST" issue comment "${REPO}#${NUM}" "$BODY" + else + # Create with fj, then parse the new number from "created issue #N:". + if ! CREATED="$(fj -H "$HOST" --style minimal issue create "$TITLE" --body "$BODY" --repo "$REPO" 2>&1)"; then + echo "fj issue create failed:"; echo "$CREATED"; exit 1 + fi + echo "$CREATED" + NUM="$(printf '%s\n' "$CREATED" | grep -oE '#[0-9]+' | head -n1 | tr -d '#')" + + # Attach the label via the raw API (fj cannot set or create labels): + # resolve the label id, creating the label if it does not exist yet. + if [ -n "$NUM" ]; then + LABEL_ID="$(curl -fsS -H "$AUTH" "$API/repos/$REPO/labels" \ + | jq -r --arg n "$LABEL" 'map(select(.name == $n)) | .[0].id // empty')" + if [ -z "$LABEL_ID" ]; then + LABEL_ID="$(curl -fsS -H "$AUTH" -H 'Content-Type: application/json' \ + -X POST "$API/repos/$REPO/labels" \ + -d "$(jq -n --arg n "$LABEL" '{name: $n, color: "#e11d21"}')" | jq -r '.id')" + fi + curl -fsS -H "$AUTH" -H 'Content-Type: application/json' \ + -X POST "$API/repos/$REPO/issues/$NUM/labels" \ + -d "$(jq -n --argjson l "[$LABEL_ID]" '{labels: $l}')" >/dev/null + else + echo "Could not parse the new issue number; label not attached." >&2 + fi + fi diff --git a/.forgejo/workflows/check_commits.yml b/.forgejo/workflows/check_commits.yml new file mode 100644 index 00000000000..4778bd53ff2 --- /dev/null +++ b/.forgejo/workflows/check_commits.yml @@ -0,0 +1,31 @@ +name: CI check_commits + +on: + pull_request: + branches-ignore: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check_commits: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + - name: Install Rust stable toolchain + run: | + rustup default stable + - name: Fetch full tree and rebase on upstream + run: | + git remote add upstream https://git.rust-bitcoin.org/lightningdevkit/rust-lightning + git fetch upstream + export GIT_COMMITTER_EMAIL="rl-ci@example.com" + export GIT_COMMITTER_NAME="RL CI" + git rebase upstream/${{ github.base_ref }} + - name: For each commit, run cargo check (including in fuzz) + run: ci/check-each-commit.sh upstream/${{ github.base_ref }} diff --git a/.forgejo/workflows/check_unicode.yml b/.forgejo/workflows/check_unicode.yml new file mode 100644 index 00000000000..e13c0776a48 --- /dev/null +++ b/.forgejo/workflows/check_unicode.yml @@ -0,0 +1,35 @@ +name: Unicode listing up to date +on: + workflow_dispatch: + schedule: + - cron: '42 3 * * *' + +jobs: + check-unicode: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Configure fj credentials + # `fj` reads its token from keys.json; it has no token environment + # variable, so write the automatic Actions token there for the API call. + env: + FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} + FORGEJO_USER: ${{ github.actor }} + run: | + install -d -m 700 "$HOME/.local/share/forgejo-cli" + printf '{"hosts":{"git.rust-bitcoin.org":{"type":"Application","name":"%s","token":"%s"}}}' "$FORGEJO_USER" "$FORGEJO_TOKEN" > "$HOME/.local/share/forgejo-cli/keys.json" + chmod 600 "$HOME/.local/share/forgejo-cli/keys.json" + - name: Check unicode file state + env: + HOST: git.rust-bitcoin.org + REPO: ${{ github.repository }} + run: | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/UnicodeData.txt https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt + contrib/gen_unicode_general_category.py /tmp/UnicodeData.txt -o /tmp/unicode.rs + if ! diff -u lightning-types/src/unicode.rs /tmp/unicode.rs; then + TITLE="Unicode listing out of date: ${{ github.workflow }}" + RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_number }}" + BODY="The unicode character listing is out of date, see $RUN_URL" + fj -H "$HOST" issue create "$TITLE" --body "$BODY" --repo "$REPO" + fi diff --git a/.forgejo/workflows/ci-build.yml b/.forgejo/workflows/ci-build.yml new file mode 100644 index 00000000000..d9a0329cf42 --- /dev/null +++ b/.forgejo/workflows/ci-build.yml @@ -0,0 +1,76 @@ +name: CI Build Job + +on: + workflow_call: + inputs: + script: + description: CI script to run (relative to repo root) + required: true + type: string + +jobs: + build: + strategy: + fail-fast: false + matrix: + platform: >- + ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' + && fromJSON('["debian-trixie","windows","macos"]') + || fromJSON('["debian-trixie"]') }} + toolchain: >- + ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' + && fromJSON('["stable","beta","1.75.0"]') + || fromJSON('["1.75.0"]') }} + exclude: + - platform: windows + - platform: macos + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: Select Rust toolchain + run: | + rustup default ${{ matrix.toolchain }} + - name: Use rust-lld linker on Windows + if: matrix.platform == 'windows' + shell: bash + run: echo "RUSTFLAGS=-C linker=rust-lld" >> "$GITHUB_ENV" + - name: Set RUSTFLAGS to deny warnings + if: "matrix.toolchain == '1.75.0'" + run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" + - name: Install no-std-check dependencies for ARM Embedded + if: matrix.platform == 'debian-trixie' + run: | + rustup target add thumbv7m-none-eabi + - name: Enable caching for bitcoind + if: matrix.platform != 'windows' + id: cache-bitcoind + uses: https://data.forgejo.org/actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + key: bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Enable caching for electrs + if: matrix.platform != 'windows' + id: cache-electrs + uses: https://data.forgejo.org/actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: bin/electrs-${{ runner.os }}-${{ runner.arch }} + key: electrs-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind/electrs + if: >- + matrix.platform != 'windows' + && (steps.cache-bitcoind.outputs.cache-hit != 'true' + || steps.cache-electrs.outputs.cache-hit != 'true') + run: | + source ./contrib/download_bitcoind_electrs.sh + mkdir bin + mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} + - name: Set bitcoind/electrs environment variables + if: matrix.platform != 'windows' + run: | + echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" + echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" + - name: Run CI script + shell: bash + run: CI_ENV=1 CI_MINIMIZE_DISK_USAGE=1 ./${{ inputs.script }} diff --git a/.forgejo/workflows/semver.yml b/.forgejo/workflows/semver.yml new file mode 100644 index 00000000000..3322de850b3 --- /dev/null +++ b/.forgejo/workflows/semver.yml @@ -0,0 +1,27 @@ +name: SemVer checks +on: + push: + branches-ignore: + - master + pull_request: + branches-ignore: + - master + +jobs: + semver-checks: + runs-on: debian-trixie + steps: + - name: Checkout source code + uses: https://data.forgejo.org/actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + fetch-depth: 0 + - name: Install Rust stable toolchain + run: | + rustup default stable + rustup override set stable + - name: Install SemVer Checker + run: cargo install cargo-semver-checks --locked + - name: Check SemVer with all features + run: cargo semver-checks + - name: Check SemVer without any non-default features + run: cargo semver-checks --only-explicit-features diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index e617573a381..790fd0f26e9 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -11,7 +11,7 @@ jobs: issues: write checks: write steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: rustsec/audit-check@v1.4.1 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ae6d83ddd3..5862302bd00 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,71 +26,36 @@ jobs: cd ext-functional-test-demo cargo test --verbose --color always cargo test --verbose --color always --features test-broken - build: - strategy: - fail-fast: false - matrix: - platform: [ self-hosted, windows-latest, macos-latest ] - toolchain: [ stable, beta, 1.75.0 ] # 1.75.0 is the MSRV for all crates - exclude: - - platform: windows-latest - toolchain: 1.75.0 - - platform: windows-latest - toolchain: beta - - platform: macos-latest - toolchain: beta - runs-on: ${{ matrix.platform }} - steps: - - name: Checkout source code - uses: actions/checkout@v4 - - name: Install Rust ${{ matrix.toolchain }} toolchain - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ matrix.toolchain }} - - name: Use rust-lld linker on Windows - if: matrix.platform == 'windows-latest' - shell: bash - run: echo "RUSTFLAGS=-C linker=rust-lld" >> "$GITHUB_ENV" - - name: Install no-std-check dependencies for ARM Embedded - if: "matrix.platform == 'self-hosted'" - run: | - rustup target add thumbv7m-none-eabi - - name: shellcheck the CI and `contrib` scripts - if: "matrix.platform == 'self-hosted'" - run: | - shellcheck ci/*.sh -aP ci - shellcheck contrib/*.sh -aP contrib - - name: Set RUSTFLAGS to deny warnings - if: "matrix.toolchain == '1.75.0'" - run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" - - name: Enable caching for bitcoind - if: matrix.platform != 'windows-latest' - id: cache-bitcoind - uses: actions/cache@v4 - with: - path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }} - key: bitcoind-${{ runner.os }}-${{ runner.arch }} - - name: Enable caching for electrs - if: matrix.platform != 'windows-latest' - id: cache-electrs - uses: actions/cache@v4 - with: - path: bin/electrs-${{ runner.os }}-${{ runner.arch }} - key: electrs-${{ runner.os }}-${{ runner.arch }} - - name: Download bitcoind/electrs - if: "matrix.platform != 'windows-latest' && (steps.cache-bitcoind.outputs.cache-hit != 'true' || steps.cache-electrs.outputs.cache-hit != 'true')" - run: | - source ./contrib/download_bitcoind_electrs.sh - mkdir bin - mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} - mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} - - name: Set bitcoind/electrs environment variables - if: matrix.platform != 'windows-latest' - run: | - echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" - - name: Run CI script - shell: bash # Default on Winblows is powershell - run: CI_ENV=1 CI_MINIMIZE_DISK_USAGE=1 ./ci/ci-tests.sh + + build-workspace: + uses: ./.github/workflows/ci-build.yml + with: + script: ci/ci-tests-workspace.sh + + build-features: + uses: ./.github/workflows/ci-build.yml + with: + script: ci/ci-tests-features.sh + + build-bindings: + uses: ./.github/workflows/ci-build.yml + with: + script: ci/ci-tests-bindings.sh + + build-nostd: + uses: ./.github/workflows/ci-build.yml + with: + script: ci/ci-tests-nostd.sh + + build-cfg-flags: + uses: ./.github/workflows/ci-build.yml + with: + script: ci/ci-tests-cfg-flags.sh + + build-sync: + uses: ./.github/workflows/ci-build.yml + with: + script: ci/ci-tests-sync.sh coverage: needs: fuzz @@ -117,18 +82,25 @@ jobs: # Maybe if codecov wasn't broken we wouldn't need to do this... ./codecov --verbose upload-process --disable-search --fail-on-error -f target/codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'tests' cargo clean - - name: Download honggfuzz corpus - uses: actions/download-artifact@v4 - with: - name: hfuzz-corpus - path: fuzz/hfuzz_workspace + - name: Clone fuzzing corpus + run: git clone --depth=1 https://github.com/lightningdevkit/ldk-fuzzing-corpus.git fuzz/ldk-fuzzing-corpus + - name: Symlink corpus into hfuzz_workspace + run: | + set -eu + cd fuzz + for D in ldk-fuzzing-corpus/rust-lightning/*/; do + NAME=$(basename "$D") + mkdir -p "hfuzz_workspace/${NAME}_target" + cp -r "ldk-fuzzing-corpus/rust-lightning/${NAME}" "hfuzz_workspace/${NAME}_target/input" + done - name: Run fuzz coverage generation run: | ./contrib/generate_fuzz_coverage.sh --output-dir `pwd` --output-codecov-json # Could you use this to fake the coverage report for your PR? Sure. # Will anyone be impressed by your amazing coverage? No # Maybe if codecov wasn't broken we wouldn't need to do this... - ./codecov --verbose upload-process --disable-search --fail-on-error -f fuzz-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing' + ./codecov --verbose upload-process --disable-search --fail-on-error -f fuzz-fake-hashes-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing-fake-hashes' + ./codecov --verbose upload-process --disable-search --fail-on-error -f fuzz-real-hashes-codecov.json -t "f421b687-4dc2-4387-ac3d-dc3b2528af57" -F 'fuzzing-real-hashes' benchmark: runs-on: ubuntu-latest @@ -240,7 +212,7 @@ jobs: - name: Simulate docs.rs build run: ci/check-docsrs.sh - fuzz: + fuzz_sanity: runs-on: self-hosted env: TOOLCHAIN: 1.75 @@ -250,41 +222,76 @@ jobs: - name: Install Rust ${{ env.TOOLCHAIN }} toolchain run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} - # This is read-only for PRs. It seeds the fuzzer for a more effective run. - # NOTE: The `key` is unique and will always miss, forcing a fallback to - # the `restore-keys` to find the latest global cache from the `main` branch. - - name: Restore persistent fuzz corpus (PR) - if: ${{ github.ref != 'refs/heads/main' }} - uses: actions/cache/restore@v4 - with: - path: fuzz/hfuzz_workspace - key: fuzz-corpus-${{ github.ref }}-${{ github.sha }} - restore-keys: | - fuzz-corpus-refs/heads/main- - # The `restore-keys` performs a prefix search to find the most recent - # cache from a previous `main` run. We then save with a new, unique - # `key` (using the SHA) to ensure the cache is always updated, - # as caches are immutable. - - name: Restore/Save persistent honggfuzz corpus (Main) - if: ${{ github.ref == 'refs/heads/main' }} - uses: actions/cache@v4 - with: - path: fuzz/hfuzz_workspace - key: fuzz-corpus-refs/heads/main-${{ github.sha }} - restore-keys: | - fuzz-corpus-refs/heads/main- - name: Sanity check fuzz targets on Rust ${{ env.TOOLCHAIN }} run: | cd fuzz - RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo test --verbose --color always --lib --bins -j8 - cargo clean + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz --cfg=chacha20_poly1305_fuzz" cargo test --quiet --color always --lib -j8 + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz --cfg=chacha20_poly1305_fuzz" cargo test --manifest-path fuzz-fake-hashes/Cargo.toml --quiet --color always --bins -j8 + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=chacha20_poly1305_fuzz" cargo test --manifest-path fuzz-real-hashes/Cargo.toml --quiet --color always --bins -j8 + + fuzz: + runs-on: self-hosted + env: + TOOLCHAIN: 1.75 + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Install Rust ${{ env.TOOLCHAIN }} toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ env.TOOLCHAIN }} + - name: Clone fuzzing corpus + run: git clone --depth=1 https://github.com/lightningdevkit/ldk-fuzzing-corpus.git fuzz/ldk-fuzzing-corpus + - name: Symlink corpus into hfuzz_workspace + run: | + set -eu + cd fuzz + for D in ldk-fuzzing-corpus/rust-lightning/*/; do + NAME=$(basename "$D") + mkdir -p "hfuzz_workspace/${NAME}_target" + ln -sfn "../../ldk-fuzzing-corpus/rust-lightning/${NAME}" \ + "hfuzz_workspace/${NAME}_target/input" + done - name: Run fuzzers run: cd fuzz && ./ci-fuzz.sh && cd .. - - name: Upload honggfuzz corpus + env: + FUZZ_MINIMIZE: ${{ contains(github.event.pull_request.labels.*.name, 'fuzz-minimize') }} + - name: Stage new corpus entries for upload + if: success() || failure() + run: | + set -eu + WORKSPACE="$(pwd)" + rm -rf "$WORKSPACE/new-corpus" + mkdir -p "$WORKSPACE/new-corpus" + + cd fuzz/ldk-fuzzing-corpus + while IFS= read -r F; do + mkdir -p "$WORKSPACE/new-corpus/$(dirname "$F")" + cp -a "$F" "$WORKSPACE/new-corpus/$F" + done < <(git ls-files --others --exclude-standard rust-lightning/) + cd "$WORKSPACE" + + for D in fuzz/hfuzz_workspace/*_target/; do + [ -d "$D" ] || continue + BASE=$(basename "$D") + NAME="${BASE%_target}" + [ -d "$WORKSPACE/new-corpus/$NAME" ] || continue + for F in "$D"/SIG*; do + FILE="$(basename "$F")" + [ -f "$F" -a ! -f "$WORKSPACE/new-corpus/$NAME/$FILE" ] && + cp "$F" "$WORKSPACE/new-corpus/$NAME/$FILE" + done + done + + NEW=$(find new-corpus -type f 2>/dev/null | wc -l) + echo "Staged $NEW new corpus entries (including any SIG* crashes)" + - name: Upload new corpus entries + if: success() || failure() uses: actions/upload-artifact@v4 with: name: hfuzz-corpus - path: fuzz/hfuzz_workspace + path: new-corpus + compression-level: 0 + if-no-files-found: ignore linting: runs-on: ubuntu-latest @@ -299,6 +306,10 @@ jobs: - name: Install clippy run: | rustup component add clippy + - name: shellcheck the CI and `contrib` scripts + run: | + shellcheck ci/*.sh -aP ci + shellcheck contrib/*.sh -aP contrib - name: Run default clippy linting run: | ./ci/check-lint.sh @@ -320,6 +331,8 @@ jobs: run: cargo fmt --check - name: Run rustfmt checks on lightning-tests run: cd lightning-tests && cargo fmt --check + - name: Run rustfmt checks on fuzz + run: cd fuzz && cargo fmt --check tor-connect: runs-on: ubuntu-latest env: @@ -336,3 +349,50 @@ jobs: - name: Test tor connections using lightning-net-tokio run: | TOR_PROXY="127.0.0.1:9050" RUSTFLAGS="--cfg=tor" cargo test --verbose --color always -p lightning-net-tokio + + notify-failure: + needs: [build-workspace, build-features, build-bindings, build-nostd, build-cfg-flags, build-sync, fuzz_sanity, fuzz, linting, rustfmt, check_release, check_docs, benchmark, ext-test, tor-connect, coverage] + if: failure() && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Create or update failure issue + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + run: | + LABEL="build failed" + TITLE="Failed build: ${{ github.workflow }}" + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + REPO_URL="https://github.com/${{ github.repository }}" + COMMITTER="${{ github.event.head_commit.author.username }}" + BODY="GitHub Actions workflow [${{ github.workflow }} #${{ github.run_number }}](${RUN_URL}) failed." + BODY="${BODY}"$'\n\n'"Event: ${{ github.event_name }}" + BRANCH="${{ github.ref_name }}" + BODY="${BODY}"$'\n'"Branch: [${BRANCH}](${REPO_URL}/tree/${BRANCH})" + BODY="${BODY}"$'\n'"Commit: [${{ github.sha }}](${REPO_URL}/commit/${{ github.sha }})" + if [ -n "$COMMITTER" ]; then + BODY="${BODY}"$'\n'"Committer: @${COMMITTER}" + fi + + # Ensure label exists + if ! gh label list --search "$LABEL" --json name --jq '.[].name' | grep -qxF "$LABEL"; then + gh label create "$LABEL" + fi + + # Find existing open issue with this label + ISSUE_NUMBER=$(gh issue list --label "$LABEL" --state open --json number --jq '.[0].number // empty') + + if [ -n "$ISSUE_NUMBER" ]; then + gh issue comment "$ISSUE_NUMBER" --body "$BODY" + else + ISSUE_URL=$(gh issue create --title "$TITLE" --label "$LABEL" --body "$BODY") + ISSUE_NUMBER=$(echo "$ISSUE_URL" | grep -o '[0-9]*$') + fi + + # Assign issue to committer if no one is assigned yet + ASSIGNEE_COUNT=$(gh issue view "$ISSUE_NUMBER" --json assignees --jq '.assignees | length') + if [ "$ASSIGNEE_COUNT" = "0" ] && [ -n "$COMMITTER" ]; then + gh issue edit "$ISSUE_NUMBER" --add-assignee "$COMMITTER" || true + fi diff --git a/.github/workflows/check_unicode.yml b/.github/workflows/check_unicode.yml new file mode 100644 index 00000000000..a01add3f814 --- /dev/null +++ b/.github/workflows/check_unicode.yml @@ -0,0 +1,26 @@ +name: Unicode listing up to date +on: + workflow_dispatch: + schedule: + - cron: '42 3 * * *' + +jobs: + check-unicode: + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Check unicode file state + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + curl --proto '=https' --tlsv1.2 -fsSL -o /tmp/UnicodeData.txt https://www.unicode.org/Public/UCD/latest/ucd/UnicodeData.txt + contrib/gen_unicode_general_category.py /tmp/UnicodeData.txt -o /tmp/unicode.rs + if ! diff -u lightning-types/src/unicode.rs /tmp/unicode.rs; then + TITLE="Unicode listing out of date: ${{ github.workflow }}" + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + BODY="The unicode character listing is out of date, see $RUN_URL" + gh issue create --title "$TITLE" --body "$BODY" + fi diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml new file mode 100644 index 00000000000..4c56619d3ad --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,80 @@ +name: CI Build Job + +on: + workflow_call: + inputs: + script: + description: CI script to run (relative to repo root) + required: true + type: string + +jobs: + build: + strategy: + fail-fast: false + matrix: + platform: >- + ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' + && fromJSON('["self-hosted","windows-latest","macos-latest"]') + || fromJSON('["self-hosted"]') }} + toolchain: >- + ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' + && fromJSON('["stable","beta","1.75.0"]') + || fromJSON('["1.75.0"]') }} + exclude: + - platform: windows-latest + toolchain: 1.75.0 + - platform: windows-latest + toolchain: beta + - platform: macos-latest + toolchain: beta + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout source code + uses: actions/checkout@v4 + - name: Install Rust ${{ matrix.toolchain }} toolchain + run: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain ${{ matrix.toolchain }} + - name: Use rust-lld linker on Windows + if: matrix.platform == 'windows-latest' + shell: bash + run: echo "RUSTFLAGS=-C linker=rust-lld" >> "$GITHUB_ENV" + - name: Set RUSTFLAGS to deny warnings + if: "matrix.toolchain == '1.75.0'" + run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" + - name: Install no-std-check dependencies for ARM Embedded + if: matrix.platform == 'self-hosted' + run: | + rustup target add thumbv7m-none-eabi + - name: Enable caching for bitcoind + if: matrix.platform != 'windows-latest' + id: cache-bitcoind + uses: actions/cache@v4 + with: + path: bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + key: bitcoind-${{ runner.os }}-${{ runner.arch }} + - name: Enable caching for electrs + if: matrix.platform != 'windows-latest' + id: cache-electrs + uses: actions/cache@v4 + with: + path: bin/electrs-${{ runner.os }}-${{ runner.arch }} + key: electrs-${{ runner.os }}-${{ runner.arch }} + - name: Download bitcoind/electrs + if: >- + matrix.platform != 'windows-latest' + && (steps.cache-bitcoind.outputs.cache-hit != 'true' + || steps.cache-electrs.outputs.cache-hit != 'true') + run: | + source ./contrib/download_bitcoind_electrs.sh + mkdir bin + mv "$BITCOIND_EXE" bin/bitcoind-${{ runner.os }}-${{ runner.arch }} + mv "$ELECTRS_EXE" bin/electrs-${{ runner.os }}-${{ runner.arch }} + - name: Set bitcoind/electrs environment variables + if: matrix.platform != 'windows-latest' + run: | + echo "BITCOIND_EXE=$( pwd )/bin/bitcoind-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" + echo "ELECTRS_EXE=$( pwd )/bin/electrs-${{ runner.os }}-${{ runner.arch }}" >> "$GITHUB_ENV" + - name: Run CI script + shell: bash + run: CI_ENV=1 CI_MINIMIZE_DISK_USAGE=1 ./${{ inputs.script }} diff --git a/.github/workflows/ldk-node-integration.yml b/.github/workflows/ldk-node-integration.yml deleted file mode 100644 index 446abd40a07..00000000000 --- a/.github/workflows/ldk-node-integration.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: LDK Node Integration Tests - -on: [push, pull_request] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - check-api: - runs-on: self-hosted - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - path: rust-lightning - - name: Checkout LDK Node - uses: actions/checkout@v3 - with: - repository: lightningdevkit/ldk-node - path: ldk-node - - name: Install Rust stable toolchain - run: | - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable - - name: Run LDK Node Integration Tests - run: | - cd ldk-node - cat <<EOF >> Cargo.toml - [patch.crates-io] - lightning = { path = "../rust-lightning/lightning" } - lightning-types = { path = "../rust-lightning/lightning-types" } - lightning-invoice = { path = "../rust-lightning/lightning-invoice" } - 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" } - lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } - lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" } - lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" } - lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" } - lightning-macros = { path = "../rust-lightning/lightning-macros" } - - [patch."https://github.com/lightningdevkit/rust-lightning"] - lightning = { path = "../rust-lightning/lightning" } - lightning-types = { path = "../rust-lightning/lightning-types" } - lightning-invoice = { path = "../rust-lightning/lightning-invoice" } - 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" } - lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } - lightning-block-sync = { path = "../rust-lightning/lightning-block-sync" } - lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync" } - lightning-liquidity = { path = "../rust-lightning/lightning-liquidity" } - lightning-macros = { path = "../rust-lightning/lightning-macros" } - EOF - cargo check - cargo check --features uniffi diff --git a/.github/workflows/push-fuzz-corpus.yml b/.github/workflows/push-fuzz-corpus.yml new file mode 100644 index 00000000000..4551de19cc5 --- /dev/null +++ b/.github/workflows/push-fuzz-corpus.yml @@ -0,0 +1,92 @@ +name: Push fuzz corpus + +# Triggered after the main CI workflow finishes. Because `workflow_run` always +# runs in the *base* repo's context (its workflow file as of `main`, with +# full secrets access) it's safe to handle the corpus push here even for fork +# PRs — none of the PR's modified code or scripts execute in this job. +# +# Caveat: GitHub only fires `workflow_run` for workflow files that live on +# the default branch, so this workflow does nothing until it's merged to +# `master`. +on: + # zizmor flags `workflow_run` as a dangerous trigger because it runs with + # repo secrets in base-branch context. That's exactly why we use it here: + # this workflow never touches any PR-supplied code (no checkout, no script + # execution from the artifact — just cp/git on opaque corpus blobs), so + # the warning is a false positive. + workflow_run: # zizmor: ignore[dangerous-triggers] + workflows: ["Continuous Integration Checks"] + types: [completed] + +permissions: + # download-artifact across runs requires `actions: read`. + actions: read + +jobs: + push-corpus: + # Run on either success or fuzzer crash; skip on cancellation. + if: >- + github.event.workflow_run.conclusion == 'success' || + github.event.workflow_run.conclusion == 'failure' + runs-on: ubuntu-latest + steps: + - name: Download fuzz corpus artifact + id: download + # The artifact only exists when the fuzz job got far enough to upload + # it. Don't fail this workflow if the upload was skipped. + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: hfuzz-corpus + path: hfuzz-corpus + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Clone fuzzing corpus + if: steps.download.outcome == 'success' + run: git clone --depth=1 https://github.com/lightningdevkit/ldk-fuzzing-corpus.git + + - name: Copy new corpus entries into the corpus checkout + if: steps.download.outcome == 'success' + run: | + set -eu + if [ -d hfuzz-corpus/rust-lightning ]; then + cp -rn hfuzz-corpus/rust-lightning/. ldk-fuzzing-corpus/rust-lightning/ + fi + + - name: Open PR with new corpus entries + if: steps.download.outcome == 'success' + env: + GH_TOKEN: ${{ secrets.CORPUS_PUSH_TOKEN }} + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} + RUN_URL: ${{ github.event.workflow_run.html_url }} + RUN_ID: ${{ github.event.workflow_run.id }} + run: | + set -eu + cd ldk-fuzzing-corpus + if [ -z "$(git status --porcelain)" ]; then + echo "No new corpus entries to contribute." + exit 0 + fi + if [ -z "${GH_TOKEN:-}" ]; then + echo "Found new corpus entries but CORPUS_PUSH_TOKEN is unset; skipping PR." + git status --short + exit 0 + fi + BRANCH="ci/new-corpus-${RUN_ID}" + git config user.email "ldk-ci@users.noreply.github.com" + git config user.name "LDK CI" + git checkout -b "$BRANCH" + git add rust-lightning + git commit \ + -m "Add corpus entries from rust-lightning CI" \ + -m "Source commit: ${SOURCE_SHA}" \ + -m "Run: ${RUN_URL}" + REMOTE=$(git config --get remote.origin.url) + PUSH_URL="https://x-access-token:${GH_TOKEN}@${REMOTE#https://}" + git push "$PUSH_URL" "HEAD:$BRANCH" + gh pr create \ + --title "New corpus entries from rust-lightning CI run ${RUN_ID}" \ + --body "Discovered while running fuzz CI against \`${SOURCE_SHA}\`. Source: ${RUN_URL}" \ + --head "$BRANCH" \ + --base master diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml index de10e562f98..0e196804517 100644 --- a/.github/workflows/semver.yml +++ b/.github/workflows/semver.yml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Rust stable toolchain run: | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile=minimal --default-toolchain stable diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 00000000000..681311eb9cf --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e83ef2a14d..12f926cacad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -201,6 +201,8 @@ generated for inclusion in BOLT 12 `Offer`s will no longer be accepted. As most blinded message paths are ephemeral, this should only invalidate issued BOLT 12 `Refund`s in practice (#3917). + * Blinded message paths included in BOLT 12 `Offer`s generated by LDK 0.2 will + not be accepted by prior versions of LDK after downgrade (#3917). * Once a channel has been spliced, LDK can no longer be downgraded. `UserConfig::reject_inbound_splices` can be set to block inbound ones (#4150) * Downgrading after setting `UserConfig::enable_htlc_hold` is not supported diff --git a/CLAUDE.md b/CLAUDE.md index f87bc665bd4..cecd79c4981 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,13 @@ See [README.md](README.md) for the workspace layout and [ARCH.md](ARCH.md) for s of the full task you might prompt the user whether they want you to run the full CI tests via `./ci/ci-tests.sh`. Note however that this script will run for a very long time, so please don't timeout when you do. -- Run `cargo +1.75.0 fmt --all` after every code change +- Run `cargo +1.75.0 fmt --all` before committing code changes. If rust 1.75.0 is + not installed, skip this step. - Never add new dependencies unless explicitly requested - Please always disclose the use of any AI tools in commit messages and PR descriptions using a `Co-Authored-By:` line. - When adding new `.rs` files, please ensure to always add the licensing header as found, e.g., in `lightning/src/lib.rs` and other files. +- When adding comments, do not refer to internal logic in other modules, instead + make sure comments make sense in the context they're in without needing other + context. +- Try to keep code DRY - if new code you add is duplicate with other code, + deduplicate it. diff --git a/Cargo.toml b/Cargo.toml index 1eb7b572d8b..98bf30683bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,12 +58,12 @@ check-cfg = [ "cfg(fuzzing)", "cfg(secp256k1_fuzz)", "cfg(hashes_fuzz)", + "cfg(chacha20_poly1305_fuzz)", "cfg(test)", "cfg(debug_assertions)", "cfg(c_bindings)", "cfg(ldk_bench)", "cfg(ldk_test_vectors)", - "cfg(taproot)", "cfg(require_route_graph_test)", "cfg(simple_close)", "cfg(peer_storage)", diff --git a/SECURITY.md b/SECURITY.md index ed19bc544aa..b4cfa1bf92e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -16,4 +16,3 @@ your own public key as an attachment or inline for replies. * 0A156842CF60B58BD826ABDD808FC696767C6147 (Wilmer Paulino) * BD6EED4D339EDBF7E7CE7F8836153082BDF676FD (Elias Rohrer) * 6E0287D8849AE741E47CC586FD3E106A2CE099B4 (Valentine Wallace) - * 69CFEA635D0E6E6F13FD9D9136D932FCAC0305F0 (Arik Sosman) diff --git a/bench/benches/bench.rs b/bench/benches/bench.rs index b854ffb93ce..35a458ac1af 100644 --- a/bench/benches/bench.rs +++ b/bench/benches/bench.rs @@ -18,7 +18,7 @@ criterion_group!(benches, lightning::routing::router::benches::generate_large_mpp_routes_with_nonlinear_probabilistic_scorer, lightning::sign::benches::bench_get_secure_random_bytes, lightning::ln::channelmanager::bench::bench_sends, - lightning_persister::fs_store::bench::bench_sends, + lightning_persister::fs_store::v1::bench::bench_sends, lightning_rapid_gossip_sync::bench::bench_reading_full_graph_from_file, lightning::routing::gossip::benches::read_network_graph, lightning::routing::gossip::benches::write_network_graph, diff --git a/ci/check-compiles.sh b/ci/check-compiles.sh index a067861fb56..30f7518c727 100755 --- a/ci/check-compiles.sh +++ b/ci/check-compiles.sh @@ -5,6 +5,10 @@ echo "Testing $(git log -1 --oneline)" cargo check cargo doc cargo doc --document-private-items -cd fuzz && RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" cargo check --features=stdin_fuzz +cd fuzz +RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz --cfg=chacha20_poly1305_fuzz" \ + cargo check --manifest-path fuzz-fake-hashes/Cargo.toml --features=stdin_fuzz +RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=chacha20_poly1305_fuzz" \ + cargo check --manifest-path fuzz-real-hashes/Cargo.toml --features=stdin_fuzz cd ../lightning && cargo check --no-default-features cd .. && RUSTC_BOOTSTRAP=1 RUSTFLAGS="--cfg=c_bindings" cargo check -Z avoid-dev-deps diff --git a/ci/ci-tests-bindings.sh b/ci/ci-tests-bindings.sh new file mode 100755 index 00000000000..74b471a391b --- /dev/null +++ b/ci/ci-tests-bindings.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -eox pipefail + +# shellcheck source=ci/ci-tests-common.sh +source "$(dirname "$0")/ci-tests-common.sh" + +echo -e "\n\nTesting c_bindings builds" +# Note that because `$RUSTFLAGS` is not passed through to doctest builds we cannot selectively +# disable doctests in `c_bindings` so we skip doctests entirely here. +RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test --quiet --color always --lib --bins --tests + +for DIR in lightning-invoice lightning-rapid-gossip-sync; do + # check if there is a conflict between no_std and the c_bindings cfg + RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test -p $DIR --quiet --color always --no-default-features +done + +# Note that because `$RUSTFLAGS` is not passed through to doctest builds we cannot selectively +# disable doctests in `c_bindings` so we skip doctests entirely here. +RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test -p lightning-background-processor --quiet --color always --no-default-features --lib --bins --tests +RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test -p lightning --quiet --color always --no-default-features --lib --bins --tests diff --git a/ci/ci-tests-cfg-flags.sh b/ci/ci-tests-cfg-flags.sh new file mode 100755 index 00000000000..5f40086e6d0 --- /dev/null +++ b/ci/ci-tests-cfg-flags.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -eox pipefail + +# shellcheck source=ci/ci-tests-common.sh +source "$(dirname "$0")/ci-tests-common.sh" + +echo -e "\n\nTest cfg-flag builds" +RUSTFLAGS="--cfg=simple_close" cargo test --quiet --color always -p lightning +[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean +RUSTFLAGS="--cfg=peer_storage" cargo test --quiet --color always -p lightning diff --git a/ci/ci-tests-common.sh b/ci/ci-tests-common.sh new file mode 100755 index 00000000000..2d8956a40ba --- /dev/null +++ b/ci/ci-tests-common.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# ci/ci-tests-common.sh - Shared helpers for CI test scripts. +# Source this file; do not execute it directly. +# shellcheck disable=SC2002,SC2207 + +RUSTC_MINOR_VERSION=$(rustc --version | awk '{ split($2,a,"."); print a[2] }') + +# Some crates require pinning to meet our MSRV even for our downstream users, +# which we do here. +# Further crates which appear only as dev-dependencies are pinned further down. +function PIN_RELEASE_DEPS { + return 0 # Don't fail the script if our rustc is higher than the last check +} + +PIN_RELEASE_DEPS # pin the release dependencies in our main workspace + +# The backtrace v0.3.75 crate relies on rustc 1.82 +[ "$RUSTC_MINOR_VERSION" -lt 82 ] && cargo update -p backtrace --precise "0.3.74" --quiet + +# Starting with version 1.9.0, the `zeroize` crate uses Rust 2024. +[ "$RUSTC_MINOR_VERSION" -lt 85 ] && cargo update -p zeroize --precise "1.8.2" --quiet + +# Starting with version 0.1.35, the `jobserver` crate relies on rustc 1.85. +[ "$RUSTC_MINOR_VERSION" -lt 85 ] && cargo update -p jobserver --precise "0.1.34" --quiet + +export RUST_BACKTRACE=1 diff --git a/ci/ci-tests-features.sh b/ci/ci-tests-features.sh new file mode 100755 index 00000000000..f01e7fd8fec --- /dev/null +++ b/ci/ci-tests-features.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -eox pipefail + +# shellcheck source=ci/ci-tests-common.sh +source "$(dirname "$0")/ci-tests-common.sh" + +echo -e "\n\nChecking and testing lightning with features" +cargo test -p lightning --quiet --color always --features dnssec +cargo check -p lightning --quiet --color always --features dnssec +cargo doc -p lightning --quiet --document-private-items --features dnssec + +echo -e "\n\nChecking and testing lightning-persister with features" +cargo test -p lightning-persister --quiet --color always --features tokio +cargo check -p lightning-persister --quiet --color always --features tokio +cargo doc -p lightning-persister --quiet --document-private-items --features tokio + +echo -e "\n\nTest backtrace-debug builds" +cargo test -p lightning --quiet --color always --features backtrace + +echo -e "\n\nTesting other crate-specific builds" +# Note that outbound_commitment_test only runs in this mode because of hardcoded signature values +RUSTFLAGS="$RUSTFLAGS --cfg=ldk_test_vectors" cargo test -p lightning --quiet --color always --no-default-features --features=std +# This one only works for lightning-invoice +# check that compile with no_std and serde works in lightning-invoice +cargo test -p lightning-invoice --quiet --color always --no-default-features --features serde diff --git a/ci/ci-tests-nostd.sh b/ci/ci-tests-nostd.sh new file mode 100755 index 00000000000..7d3acb15e06 --- /dev/null +++ b/ci/ci-tests-nostd.sh @@ -0,0 +1,27 @@ +#!/bin/bash +set -eox pipefail + +# shellcheck source=ci/ci-tests-common.sh +source "$(dirname "$0")/ci-tests-common.sh" + +echo -e "\n\nTesting no_std builds" +for DIR in lightning-invoice lightning-rapid-gossip-sync lightning-liquidity; do + cargo test -p $DIR --quiet --color always --no-default-features +done + +cargo test -p lightning --quiet --color always --no-default-features +cargo test -p lightning-background-processor --quiet --color always --no-default-features + +echo -e "\n\nTesting no_std build on a downstream no-std crate" +# check no-std compatibility across dependencies +pushd no-std-check +cargo check --quiet --color always +[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean +popd + +if [ -f "$(which arm-none-eabi-gcc)" ]; then + pushd no-std-check + cargo build --quiet --target=thumbv7m-none-eabi + [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean + popd +fi diff --git a/ci/ci-tests-sync.sh b/ci/ci-tests-sync.sh new file mode 100755 index 00000000000..ef836a60413 --- /dev/null +++ b/ci/ci-tests-sync.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -eox pipefail + +# shellcheck source=ci/ci-tests-common.sh +source "$(dirname "$0")/ci-tests-common.sh" + +echo -e "\n\nChecking and testing Block Sync Clients with features" + +cargo test -p lightning-block-sync --quiet --color always --features rest-client +cargo check -p lightning-block-sync --quiet --color always --features rest-client +cargo test -p lightning-block-sync --quiet --color always --features rpc-client +cargo check -p lightning-block-sync --quiet --color always --features rpc-client +cargo test -p lightning-block-sync --quiet --color always --features rpc-client,rest-client +cargo check -p lightning-block-sync --quiet --color always --features rpc-client,rest-client +cargo test -p lightning-block-sync --quiet --color always --features rpc-client,rest-client,tokio +cargo check -p lightning-block-sync --quiet --color always --features rpc-client,rest-client,tokio + +echo -e "\n\nChecking Transaction Sync Clients with features." +cargo check -p lightning-transaction-sync --quiet --color always --features esplora-blocking +cargo check -p lightning-transaction-sync --quiet --color always --features esplora-async +cargo check -p lightning-transaction-sync --quiet --color always --features esplora-async-https +cargo check -p lightning-transaction-sync --quiet --color always --features electrum + +if [ -z "$CI_ENV" ] && [[ -z "$BITCOIND_EXE" || -z "$ELECTRS_EXE" ]]; then + echo -e "\n\nSkipping testing Transaction Sync Clients due to BITCOIND_EXE or ELECTRS_EXE being unset." + cargo check -p lightning-transaction-sync --tests +else + echo -e "\n\nTesting Transaction Sync Clients with features." + cargo test -p lightning-transaction-sync --quiet --color always --features esplora-blocking + cargo test -p lightning-transaction-sync --quiet --color always --features esplora-async + cargo test -p lightning-transaction-sync --quiet --color always --features esplora-async-https + cargo test -p lightning-transaction-sync --quiet --color always --features electrum +fi diff --git a/ci/ci-tests-workspace.sh b/ci/ci-tests-workspace.sh new file mode 100755 index 00000000000..f8be49bba7d --- /dev/null +++ b/ci/ci-tests-workspace.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -eox pipefail + +# shellcheck source=ci/ci-tests-common.sh +source "$(dirname "$0")/ci-tests-common.sh" + +echo -e "\n\nChecking the workspace." +cargo check --quiet --color always + +echo -e "\n\nTesting the workspace." +cargo test --quiet --color always + +echo -e "\n\nTesting upgrade from prior versions of LDK" +pushd lightning-tests +cargo test --quiet +popd + +echo -e "\n\nBuilding docs for all workspace members." +cargo doc --workspace --quiet --document-private-items + +[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean + +# Test that we can build downstream code with only the "release pins". +pushd msrv-no-dev-deps-check +PIN_RELEASE_DEPS +cargo check --quiet +[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean +popd diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index 83b2af277f5..57691ad9d27 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -1,146 +1,13 @@ #!/bin/bash -#shellcheck disable=SC2002,SC2207 set -eox pipefail -RUSTC_MINOR_VERSION=$(rustc --version | awk '{ split($2,a,"."); print a[2] }') +# Run all CI test groups sequentially for local testing. +# In GitHub Actions, these run as separate parallel jobs. -# Some crates require pinning to meet our MSRV even for our downstream users, -# which we do here. -# Further crates which appear only as dev-dependencies are pinned further down. -function PIN_RELEASE_DEPS { - return 0 # Don't fail the script if our rustc is higher than the last check -} - -PIN_RELEASE_DEPS # pin the release dependencies in our main workspace - -# The backtrace v0.3.75 crate relies on rustc 1.82 -[ "$RUSTC_MINOR_VERSION" -lt 82 ] && cargo update -p backtrace --precise "0.3.74" --quiet - -# Starting with version 1.2.0, the `idna_adapter` crate has an MSRV of rustc 1.81.0. -[ "$RUSTC_MINOR_VERSION" -lt 81 ] && cargo update -p idna_adapter --precise "1.1.0" --quiet - -export RUST_BACKTRACE=1 - -echo -e "\n\nChecking the workspace, except lightning-transaction-sync." -cargo check --quiet --color always - -WORKSPACE_MEMBERS=( $(cat Cargo.toml | tr '\n' '\r' | sed 's/\r //g' | tr '\r' '\n' | grep '^members =' | sed 's/members.*=.*\[//' | tr -d '"' | tr ',' ' ') ) - -echo -e "\n\nTesting the workspace, except lightning-transaction-sync." -cargo test --quiet --color always - -echo -e "\n\nTesting upgrade from prior versions of LDK" -pushd lightning-tests -cargo test --quiet -popd - -echo -e "\n\nChecking and building docs for all workspace members individually..." -for DIR in "${WORKSPACE_MEMBERS[@]}"; do - cargo check -p "$DIR" --quiet --color always - cargo doc -p "$DIR" --quiet --document-private-items -done - -echo -e "\n\nChecking and testing lightning with features" -cargo test -p lightning --quiet --color always --features dnssec -cargo check -p lightning --quiet --color always --features dnssec -cargo doc -p lightning --quiet --document-private-items --features dnssec - -echo -e "\n\nChecking and testing Block Sync Clients with features" - -cargo test -p lightning-block-sync --quiet --color always --features rest-client -cargo check -p lightning-block-sync --quiet --color always --features rest-client -cargo test -p lightning-block-sync --quiet --color always --features rpc-client -cargo check -p lightning-block-sync --quiet --color always --features rpc-client -cargo test -p lightning-block-sync --quiet --color always --features rpc-client,rest-client -cargo check -p lightning-block-sync --quiet --color always --features rpc-client,rest-client -cargo test -p lightning-block-sync --quiet --color always --features rpc-client,rest-client,tokio -cargo check -p lightning-block-sync --quiet --color always --features rpc-client,rest-client,tokio - -echo -e "\n\nChecking Transaction Sync Clients with features." -cargo check -p lightning-transaction-sync --quiet --color always --features esplora-blocking -cargo check -p lightning-transaction-sync --quiet --color always --features esplora-async -cargo check -p lightning-transaction-sync --quiet --color always --features esplora-async-https -cargo check -p lightning-transaction-sync --quiet --color always --features electrum - -if [ -z "$CI_ENV" ] && [[ -z "$BITCOIND_EXE" || -z "$ELECTRS_EXE" ]]; then - echo -e "\n\nSkipping testing Transaction Sync Clients due to BITCOIND_EXE or ELECTRS_EXE being unset." - cargo check -p lightning-transaction-sync --tests -else - echo -e "\n\nTesting Transaction Sync Clients with features." - cargo test -p lightning-transaction-sync --quiet --color always --features esplora-blocking - cargo test -p lightning-transaction-sync --quiet --color always --features esplora-async - cargo test -p lightning-transaction-sync --quiet --color always --features esplora-async-https - cargo test -p lightning-transaction-sync --quiet --color always --features electrum -fi - -echo -e "\n\nChecking and testing lightning-persister with features" -cargo test -p lightning-persister --quiet --color always --features tokio -cargo check -p lightning-persister --quiet --color always --features tokio -cargo doc -p lightning-persister --quiet --document-private-items --features tokio - -echo -e "\n\nTest Custom Message Macros" -cargo test -p lightning-custom-message --quiet --color always -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean - -echo -e "\n\nTest backtrace-debug builds" -cargo test -p lightning --quiet --color always --features backtrace - -echo -e "\n\nTesting no_std builds" -for DIR in lightning-invoice lightning-rapid-gossip-sync lightning-liquidity; do - cargo test -p $DIR --quiet --color always --no-default-features -done - -cargo test -p lightning --quiet --color always --no-default-features -cargo test -p lightning-background-processor --quiet --color always --no-default-features - -echo -e "\n\nTesting c_bindings builds" -# Note that because `$RUSTFLAGS` is not passed through to doctest builds we cannot selectively -# disable doctests in `c_bindings` so we skip doctests entirely here. -RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test --quiet --color always --lib --bins --tests - -for DIR in lightning-invoice lightning-rapid-gossip-sync; do - # check if there is a conflict between no_std and the c_bindings cfg - RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test -p $DIR --quiet --color always --no-default-features -done - -# Note that because `$RUSTFLAGS` is not passed through to doctest builds we cannot selectively -# disable doctests in `c_bindings` so we skip doctests entirely here. -RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test -p lightning-background-processor --quiet --color always --no-default-features --lib --bins --tests -RUSTFLAGS="$RUSTFLAGS --cfg=c_bindings" cargo test -p lightning --quiet --color always --no-default-features --lib --bins --tests - -echo -e "\n\nTesting other crate-specific builds" -# Note that outbound_commitment_test only runs in this mode because of hardcoded signature values -RUSTFLAGS="$RUSTFLAGS --cfg=ldk_test_vectors" cargo test -p lightning --quiet --color always --no-default-features --features=std -# This one only works for lightning-invoice -# check that compile with no_std and serde works in lightning-invoice -cargo test -p lightning-invoice --quiet --color always --no-default-features --features serde - -echo -e "\n\nTesting no_std build on a downstream no-std crate" -# check no-std compatibility across dependencies -pushd no-std-check -cargo check --quiet --color always -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean -popd - -# Test that we can build downstream code with only the "release pins". -pushd msrv-no-dev-deps-check -PIN_RELEASE_DEPS -cargo check --quiet -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean -popd - -if [ -f "$(which arm-none-eabi-gcc)" ]; then - pushd no-std-check - cargo build --quiet --target=thumbv7m-none-eabi - [ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean - popd -fi - -echo -e "\n\nTest cfg-flag builds" -RUSTFLAGS="--cfg=taproot" cargo test --quiet --color always -p lightning -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean -RUSTFLAGS="--cfg=simple_close" cargo test --quiet --color always -p lightning -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean -RUSTFLAGS="--cfg=lsps1_service" cargo test --quiet --color always -p lightning-liquidity -[ "$CI_MINIMIZE_DISK_USAGE" != "" ] && cargo clean -RUSTFLAGS="--cfg=peer_storage" cargo test --quiet --color always -p lightning +DIR="$(dirname "$0")" +"$DIR/ci-tests-workspace.sh" +"$DIR/ci-tests-features.sh" +"$DIR/ci-tests-bindings.sh" +"$DIR/ci-tests-nostd.sh" +"$DIR/ci-tests-cfg-flags.sh" +"$DIR/ci-tests-sync.sh" diff --git a/contrib/gen_unicode_general_category.py b/contrib/gen_unicode_general_category.py new file mode 100755 index 00000000000..4871e967b55 --- /dev/null +++ b/contrib/gen_unicode_general_category.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +# This file is Copyright its original authors, visible in version control +# history. +# +# This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +# or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +# You may not use this file except in accordance with one or both of these +# licenses. + +"""Generate Unicode general-category predicates from `UnicodeData.txt`. + +Emits two `pub(crate)` functions taking a `char`, split into two disjoint +buckets across the Unicode top-level `C` ("Other") category so callers can +compose them: + + is_unicode_general_category_other — Cc / Cf / Cs / Co (assigned) + is_unicode_general_category_unassigned — Cn (plus codepoints above + U+10FFFF, which aren't + valid codepoints at all) + +`UnicodeData.txt` is the canonical machine-readable listing of every assigned +codepoint in the Unicode Character Database. Each line is `;`-separated; field +0 is the codepoint (hex), field 1 is the name, and field 2 is the two-letter +general category (e.g. `Lu`, `Cf`, `Mn`). Codepoints absent from the file have +category `Cn` (Unassigned) by convention. + +Two encoding details to preserve: + * Large blocks of contiguous same-category codepoints are written as two + consecutive entries whose names end in `, First>` and `, Last>`. Every + codepoint between First and Last (inclusive) shares the listed category. + * The codepoint range is U+0000..=U+10FFFF. + +Each `matches!` arm in the assigned-Other table carries an end-of-line comment +derived from the `UnicodeData.txt` name field — typically the longest common +word prefix or suffix across the names in the range, falling back to the set +of categories when the names share nothing meaningful. The unassigned table +omits per-arm comments since every range there has the same meaning by +construction. + +Usage: + contrib/gen_unicode_general_category.py UnicodeData.txt > out.rs +""" + +import argparse +import sys +from pathlib import Path + +MAX_CODEPOINT = 0x10FFFF + +LICENSE_HEADER = """\ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. +""" + +GENERATED_NOTICE = """\ +// Auto-generated from the Unicode Character Database (UnicodeData.txt) by +// contrib/gen_unicode_general_category.py. Do not edit by hand; rerun the +// generator with an updated UnicodeData.txt to refresh the table. +""" + + +def _normalize_name(name): + """Strip the `<...>` wrapping and `, First` / `, Last` range markers so + that, e.g., `<Non Private Use High Surrogate, First>` becomes + `Non Private Use High Surrogate` and `<control>` becomes `control`. + """ + if name.startswith("<") and name.endswith(">"): + inner = name[1:-1] + for suffix in (", First", ", Last"): + if inner.endswith(suffix): + inner = inner[: -len(suffix)] + return inner + return name + + +def parse_categories(path): + """Return `(cats, names)` mapping every codepoint listed in `path` to its + general category and to its (normalised) name. Codepoints absent from the + returned dicts have category `Cn` (Unassigned) and no name. + """ + cats = {} + names = {} + pending_first = None # (first_cp, first_cat, normalised_name) once a range opens. + with path.open() as f: + for lineno, raw in enumerate(f, 1): + line = raw.rstrip("\n") + if not line: + continue + fields = line.split(";") + if len(fields) < 3: + raise ValueError(f"{path}:{lineno}: expected at least 3 fields, got {len(fields)}") + cp = int(fields[0], 16) + name = fields[1] + cat = fields[2] + if pending_first is not None: + first_cp, first_cat, first_name = pending_first + if not name.endswith(", Last>"): + raise ValueError( + f"{path}:{lineno}: expected `, Last>` to close range " + f"opened at U+{first_cp:04X}, got name {name!r}" + ) + if cat != first_cat: + raise ValueError( + f"{path}:{lineno}: range U+{first_cp:04X}..=U+{cp:04X} " + f"has mismatched categories {first_cat!r} / {cat!r}" + ) + for x in range(first_cp, cp + 1): + cats[x] = cat + names[x] = first_name + pending_first = None + elif name.endswith(", First>"): + pending_first = (cp, cat, _normalize_name(name)) + else: + cats[cp] = cat + names[cp] = _normalize_name(name) + if pending_first is not None: + raise ValueError(f"{path}: dangling `, First>` entry at U+{pending_first[0]:04X}") + return cats, names + + +ASSIGNED_OTHER_CATS = frozenset({"Cc", "Cf", "Cs", "Co"}) + + +def coalesce_ranges(cats, names, target_cats, *, label): + """Walk U+0000..=U+10FFFF and return a list of `(start, end, label)` for + every contiguous run of codepoints whose general category is in + `target_cats`. Codepoints absent from `cats` are treated as `Cn`. + + If `label` is `True`, attach a comment summarising the codepoint names in + each range; otherwise every range gets an empty label. + """ + ranges = [] + start = None + for cp in range(MAX_CODEPOINT + 1): + in_target = cats.get(cp, "Cn") in target_cats + if in_target and start is None: + start = cp + elif not in_target and start is not None: + ranges.append((start, cp - 1)) + start = None + if start is not None: + ranges.append((start, MAX_CODEPOINT)) + + if not label: + return [(s, e, "") for s, e in ranges] + + labelled = [] + for s, e in ranges: + range_names = [] + range_cats = set() + for cp in range(s, e + 1): + range_cats.add(cats.get(cp, "Cn")) + n = names.get(cp) + if n is not None: + range_names.append(n) + labelled.append((s, e, _make_label(range_names, range_cats))) + return labelled + + +def _common_word_run(names, *, from_end): + """Return the longest sequence of words shared by every name, taken from + either the start (`from_end=False`) or the end (`from_end=True`) of each + name's whitespace-split tokens. + """ + if not names: + return "" + tokenised = [n.split() for n in names] + if from_end: + tokenised = [list(reversed(t)) for t in tokenised] + limit = min(len(t) for t in tokenised) + common = [] + for i in range(limit): + token = tokenised[0][i] + if all(t[i] == token for t in tokenised): + common.append(token) + else: + break + if from_end: + common.reverse() + return " ".join(common) + + +def _make_label(names, cats_in_range): + """Build a short human-readable label for a coalesced range. Applied to + the assigned-Other buckets only; each range there is `Cc`, `Cf`, `Cs`, + `Co`, or some contiguous union thereof. + + Rules, in order: + 1. All names identical → that name (e.g. `control`). + 2. Common leading or trailing words → the longer of the two. + 3. Otherwise, list the categories present (e.g. `Co / Cs`). + """ + unique = list(dict.fromkeys(names)) + if len(unique) == 1: + return unique[0] + + prefix = _common_word_run(names, from_end=False) + suffix = _common_word_run(names, from_end=True) + # Pick whichever is more informative; when both are non-empty, prefer the + # longer one. A multi-word prefix beats a single-word suffix. + label = prefix if len(prefix) >= len(suffix) else suffix + if label: + return label + return " / ".join(sorted(cats_in_range)) + + +def fmt_codepoint(cp): + # `UnicodeData.txt` uses 4-digit hex for the BMP and wider for higher + # planes; mirror that so the output stays readable next to the source data. + return f"0x{cp:04X}" if cp <= 0xFFFF else f"0x{cp:X}" + + +def _pattern(start, end): + if start == end: + return fmt_codepoint(start) + return f"{fmt_codepoint(start)}..={fmt_codepoint(end)}" + + +def _emit_matches_body(lines, arms): + """Append a `matches!(c as u32, ...)` body to `lines`, with one + `(pattern, label)` tuple per arm. The first arm sits at the `matches!` + argument indent and continuation `| ...` arms indent one level deeper, + matching the rustfmt convention used elsewhere in the tree. + """ + lines.append("\tmatches!(") + lines.append("\t\tc as u32,") + for i, (pattern, label) in enumerate(arms): + prefix = "\t\t" if i == 0 else "\t\t\t| " + comment = f" // {label}" if label else "" + lines.append(f"{prefix}{pattern}{comment}") + lines.append("\t)") + + +def render_rust(other_ranges, unassigned_ranges): + """Render the final Rust source defining both `char`-taking predicates. + + `other_ranges` and `unassigned_ranges` are lists of `(start, end, label)`. + The unassigned function additionally gets a synthetic final arm catching + `u32` values above U+10FFFF — these aren't valid Unicode codepoints, so + by definition they have no general category and the unassigned bucket is + the closest match. + """ + lines = [LICENSE_HEADER, GENERATED_NOTICE] + + lines.append("/// Returns `true` if `c` is in Unicode general category `Cc` (Control), `Cf`") + lines.append("/// (Format), `Cs` (Surrogate), or `Co` (Private Use) — the assigned codepoints") + lines.append("/// in the top-level `C` (\"Other\") category. The `Cs` portion of the table is") + lines.append("/// unreachable for `char` input (a `char` cannot hold a surrogate) but is kept") + lines.append("/// so the table mirrors the source UCD data verbatim. The disjoint `Cn`") + lines.append("/// (Unassigned) bucket is `is_unicode_general_category_unassigned`.") + lines.append("#[allow(dead_code)]") + lines.append("pub(crate) fn is_unicode_general_category_other(c: char) -> bool {") + other_arms = [(_pattern(s, e), label) for s, e, label in other_ranges] + _emit_matches_body(lines, other_arms) + lines.append("}") + lines.append("") + + lines.append("/// Returns `true` if `c` is in Unicode general category `Cn` (Unassigned), or") + lines.append("/// strictly above U+10FFFF. The trailing `0x110000..=u32::MAX` arm is") + lines.append("/// unreachable for `char` input (a `char` is bounded to U+10FFFF) but is kept") + lines.append("/// for defensive coverage of the underlying `u32`. The disjoint Cc / Cf / Cs /") + lines.append("/// Co bucket is `is_unicode_general_category_other`.") + lines.append("#[allow(dead_code)]") + lines.append("pub(crate) fn is_unicode_general_category_unassigned(c: char) -> bool {") + unassigned_arms = [(_pattern(s, e), label) for s, e, label in unassigned_ranges] + unassigned_arms.append(("0x110000..=u32::MAX", "above U+10FFFF — unreachable for `char`")) + _emit_matches_body(lines, unassigned_arms) + lines.append("}") + lines.append("") + + return "\n".join(lines) + + +def main(argv): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("unicode_data", type=Path, help="Path to UnicodeData.txt") + ap.add_argument( + "-o", "--output", type=Path, default=None, + help="Output Rust file (default: stdout)", + ) + args = ap.parse_args(argv) + + cats, names = parse_categories(args.unicode_data) + other = coalesce_ranges(cats, names, ASSIGNED_OTHER_CATS, label=True) + unassigned = coalesce_ranges(cats, names, frozenset({"Cn"}), label=False) + rust = render_rust(other, unassigned) + + if args.output is None: + sys.stdout.write(rust) + else: + args.output.write_text(rust) + print( + f"Wrote {args.output} " + f"({len(other)} assigned-Other ranges, " + f"{len(unassigned)} unassigned ranges).", + file=sys.stderr, + ) + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/contrib/generate_fuzz_coverage.sh b/contrib/generate_fuzz_coverage.sh index 09d37656f47..45119c5517b 100755 --- a/contrib/generate_fuzz_coverage.sh +++ b/contrib/generate_fuzz_coverage.sh @@ -55,16 +55,37 @@ fi # Create output directory if it doesn't exist mkdir -p "$OUTPUT_DIR" -export RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" +generate_coverage_report() { + local manifest_path="$1" + local output_path="$2" + local rustflags="$3" + + cargo llvm-cov clean --workspace + RUSTFLAGS="$rustflags" cargo llvm-cov -j8 --manifest-path "$manifest_path" --codecov \ + --dep-coverage lightning,lightning-invoice,lightning-liquidity,lightning-rapid-gossip-sync,lightning-persister \ + --no-default-ignore-filename-regex \ + --ignore-filename-regex "(\.cargo/registry|\.rustup/toolchains|/fuzz/)" \ + --output-path "$output_path" --tests +} # dont run this command when running in CI if [ "$OUTPUT_CODECOV_JSON" = "0" ]; then - cargo llvm-cov --html --ignore-filename-regex "fuzz/" --output-dir "$OUTPUT_DIR" - echo "Coverage report generated in $OUTPUT_DIR/html/index.html" -else - # Clean previous coverage artifacts to ensure a fresh run. cargo llvm-cov clean --workspace - + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" \ + cargo llvm-cov --manifest-path fuzz-fake-hashes/Cargo.toml --html \ + --dep-coverage lightning,lightning-invoice,lightning-liquidity,lightning-rapid-gossip-sync,lightning-persister \ + --no-default-ignore-filename-regex \ + --ignore-filename-regex "(\.cargo/registry|\.rustup/toolchains|/fuzz/)" \ + --output-dir "$OUTPUT_DIR/fake-hashes" --tests + cargo llvm-cov clean --workspace + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz" \ + cargo llvm-cov --manifest-path fuzz-real-hashes/Cargo.toml --html \ + --dep-coverage lightning,lightning-invoice,lightning-liquidity,lightning-rapid-gossip-sync,lightning-persister \ + --no-default-ignore-filename-regex \ + --ignore-filename-regex "(\.cargo/registry|\.rustup/toolchains|/fuzz/)" \ + --output-dir "$OUTPUT_DIR/real-hashes" --tests + echo "Coverage reports generated in $OUTPUT_DIR/fake-hashes and $OUTPUT_DIR/real-hashes" +else # Import honggfuzz corpus if the artifact was downloaded. if [ -d "hfuzz_workspace" ]; then echo "Importing corpus from hfuzz_workspace..." @@ -80,8 +101,14 @@ else fi echo "Replaying imported corpus (if found) via tests to generate coverage..." - cargo llvm-cov -j8 --codecov --ignore-filename-regex "fuzz/" \ - --output-path "$OUTPUT_DIR/fuzz-codecov.json" --tests + generate_coverage_report \ + "fuzz-fake-hashes/Cargo.toml" \ + "$OUTPUT_DIR/fuzz-fake-hashes-codecov.json" \ + "--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" + generate_coverage_report \ + "fuzz-real-hashes/Cargo.toml" \ + "$OUTPUT_DIR/fuzz-real-hashes-codecov.json" \ + "--cfg=fuzzing --cfg=secp256k1_fuzz" - echo "Fuzz codecov report available at $OUTPUT_DIR/fuzz-codecov.json" + echo "Fuzz codecov reports available at $OUTPUT_DIR/fuzz-fake-hashes-codecov.json and $OUTPUT_DIR/fuzz-real-hashes-codecov.json" fi diff --git a/ext-functional-test-demo/src/main.rs b/ext-functional-test-demo/src/main.rs index 654cf91e01c..67eb8c776fe 100644 --- a/ext-functional-test-demo/src/main.rs +++ b/ext-functional-test-demo/src/main.rs @@ -17,6 +17,7 @@ mod tests { impl TestSignerFactory for BrokenSignerFactory { fn make_signer( &self, _seed: &[u8; 32], _now: Duration, _v2_remote_key_derivation: bool, + _phantom_seed: Option<&[u8; 32]>, ) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> { panic!() } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 86ad12e8961..bf0d463f0fe 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -4,21 +4,9 @@ version = "0.0.1" authors = ["Automatically generated"] publish = false edition = "2021" -# Because the function is unused it gets dropped before we link lightning, so -# we have to duplicate build.rs here. Note that this is only required for -# fuzzing mode. - -[package.metadata] -cargo-fuzz = true - -[features] -afl_fuzz = ["afl"] -honggfuzz_fuzz = ["honggfuzz"] -libfuzzer_fuzz = ["libfuzzer-sys"] -stdin_fuzz = [] [dependencies] -lightning = { path = "../lightning", features = ["regex", "_test_utils"] } +lightning = { path = "../lightning", default-features = false, features = ["std", "regex", "_test_utils"] } lightning-invoice = { path = "../lightning-invoice" } lightning-liquidity = { path = "../lightning-liquidity" } lightning-rapid-gossip-sync = { path = "../lightning-rapid-gossip-sync" } @@ -27,18 +15,12 @@ bech32 = "0.11.0" bitcoin = { version = "0.32.4", features = ["secp-lowmemory"] } tokio = { version = "~1.35", default-features = false, features = ["rt-multi-thread"] } -afl = { version = "0.12", optional = true } -honggfuzz = { version = "0.5", optional = true, default-features = false } -libfuzzer-sys = { version = "0.4", optional = true } - -[build-dependencies] -cc = "1.0" - # Prevent this from interfering with workspaces [workspace] -members = ["."] +members = [".", "fuzz-fake-hashes", "fuzz-real-hashes", "write-seeds"] [profile.release] +panic = "abort" lto = true codegen-units = 1 debug-assertions = true @@ -46,12 +28,13 @@ overflow-checks = true # When testing a large fuzz corpus, -O1 offers a nice speedup [profile.dev] +panic = "abort" opt-level = 1 [lib] name = "lightning_fuzz" path = "src/lib.rs" -crate-type = ["rlib", "dylib", "staticlib"] +crate-type = ["rlib", "staticlib"] [lints.rust.unexpected_cfgs] level = "forbid" @@ -60,5 +43,5 @@ check-cfg = [ "cfg(fuzzing)", "cfg(secp256k1_fuzz)", "cfg(hashes_fuzz)", - "cfg(taproot)", + "cfg(chacha20_poly1305_fuzz)" ] diff --git a/fuzz/README.md b/fuzz/README.md index cfdab4940bc..f4a2ef8c6d2 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -10,6 +10,11 @@ configured for. Fuzzing is further only effective with a lot of CPU time, indica scenarios are discovered on CI with its low runtime constraints, the crash is caused relatively easily. +The `fuzz/` directory now contains three crates: +- `fuzz/`, the shared fuzz target logic and corpus directories +- `fuzz/fuzz-fake-hashes`, the fuzz targets that require `--cfg=hashes_fuzz` +- `fuzz/fuzz-real-hashes`, the real-hashes fuzz targets, currently `chanmon_consistency_target` + ## How do I run fuzz tests locally? We support multiple fuzzing engines such as `honggfuzz`, `libFuzzer` and `AFL`. You typically won't @@ -47,34 +52,45 @@ cargo install --force cargo-fuzz To run fuzzing using `honggfuzz`, do ```shell +cd fuzz export CPU_COUNT=1 # replace as needed export HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" export HFUZZ_RUN_ARGS="-n $CPU_COUNT --exit_upon_crash" export TARGET="msg_ping_target" # replace with the target to be fuzzed -cargo hfuzz run $TARGET +export RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" +cargo hfuzz run --manifest-path fuzz-fake-hashes/Cargo.toml $TARGET ``` -(Or, for a prettier output, replace the last line with `cargo --color always hfuzz run $TARGET`.) +(For `fuzz-real-hashes`, use +`RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz" cargo hfuzz run --manifest-path fuzz-real-hashes/Cargo.toml chanmon_consistency_target`.) +For a prettier output, replace the last line with +`cargo --color always hfuzz run --manifest-path fuzz-fake-hashes/Cargo.toml $TARGET`. #### cargo-fuzz / libFuzzer To run fuzzing using `cargo-fuzz / libFuzzer`, run ```shell rustup install nightly # Note: libFuzzer requires a nightly version of rust. +cd fuzz export RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" -cargo +nightly fuzz run --features "libfuzzer_fuzz" msg_ping_target +cargo +nightly fuzz run --fuzz-dir fuzz-fake-hashes --features "libfuzzer_fuzz" msg_ping_target ``` Note: If you encounter a `SIGKILL` during run/build check for OOM in kernel logs and consider increasing RAM size for VM. +For `fuzz-real-hashes`, use +`RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz" cargo +nightly fuzz run --fuzz-dir fuzz-real-hashes --features "libfuzzer_fuzz" chanmon_consistency_target`. + ##### Fast builds for development The default build uses LTO and single codegen unit, which is slow. For faster iteration during development, use the `-D` (dev) flag: ```shell -cargo +nightly fuzz run --features "libfuzzer_fuzz" -D msg_ping_target +cd fuzz +RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" \ + cargo +nightly fuzz run --fuzz-dir fuzz-fake-hashes --features "libfuzzer_fuzz" -D msg_ping_target ``` The `-D` flag builds in development mode with faster compilation (still has optimizations via @@ -83,7 +99,9 @@ sanitizer instrumentation, but subsequent builds will be fast. If you wish to just generate fuzzing binary executables for `libFuzzer` and not run them: ```shell -cargo +nightly fuzz build --features "libfuzzer_fuzz" msg_ping_target +cd fuzz +RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" \ + cargo +nightly fuzz build --fuzz-dir fuzz-fake-hashes --features "libfuzzer_fuzz" msg_ping_target # Generates binary artifact in path ./target/aarch64-unknown-linux-gnu/release/msg_ping_target # Exact path depends on your system architecture. ``` @@ -93,7 +111,8 @@ You can upload the build artifact generated above to `ClusterFuzz` for distribut To see a list of available fuzzing targets, run: ```shell -ls ./src/bin/ +ls ./fuzz-fake-hashes/src/bin/ +ls ./fuzz-real-hashes/src/bin/ ``` ## A fuzz test failed, what do I do? @@ -134,8 +153,8 @@ mkdir -p ./test_cases/$TARGET echo $HEX | xxd -r -p > ./test_cases/$TARGET/any_filename_works export RUST_BACKTRACE=1 -export RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" -cargo test +RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" \ + cargo test --manifest-path fuzz-fake-hashes/Cargo.toml --bin "${TARGET}_target" ``` Note that if the fuzz test failed locally, moving the offending run's trace @@ -148,6 +167,19 @@ mv hfuzz_workspace/fuzz_target/SIGABRT.PC.7ffff7e21ce1.STACK.[…].fuzz ./test_c This will reproduce the failing fuzz input and yield a usable stack trace. +Alternatively, you can use the `stdin_fuzz` feature to pipe the crash input directly without +creating test case files on disk: + +```shell +cd fuzz +echo -ne '\x2d\x31\x36\x38\x37\x34\x09\x01...' | \ + RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" \ + cargo run --manifest-path fuzz-fake-hashes/Cargo.toml --features stdin_fuzz --bin full_stack_target +``` + +Panics will abort the process directly (the crate uses `panic = "abort"`), resulting in a +non-zero exit code. Piping via stdin is useful for reproducing crashes during `git bisect` or +when working with AI agents that can construct and pipe byte sequences directly. ## How do I add a new fuzz test? @@ -162,10 +194,13 @@ file are `do_test`, `my_fuzzy_experiment_test`, and `my_fuzzy_experiment_run`. 3. Adjust the body (not the signature!) of `do_test` as necessary for the new fuzz test. -4. In `fuzz/src/bin/gen_target.sh`, add a line reading `GEN_TEST my_fuzzy_experiment` to the -first group of `GEN_TEST` lines (starting in line 9). +4. In `fuzz/src/bin/gen_target.sh`, add a line reading `GEN_FAKE_HASHES_TEST my_fuzzy_experiment` +to the appropriate target list. Use `GEN_REAL_HASHES_TEST` only for targets that must run without +`hashes_fuzz`. 5. If your test relies on a new local crate, add that crate as a dependency to `fuzz/Cargo.toml`. +If the dependency is only needed by a specific runner crate or fuzz engine setup, add it to the +matching target crate under `fuzz/fuzz-fake-hashes/Cargo.toml` or `fuzz/fuzz-real-hashes/Cargo.toml` instead. 6. In `fuzz/src/lib.rs`, add the line `pub mod my_fuzzy_experiment`. Additionally, if you added a new crate dependency, add the `extern crate […]` import line. diff --git a/fuzz/ci-fuzz.sh b/fuzz/ci-fuzz.sh index d57a5ad78fa..a9cb9b21e57 100755 --- a/fuzz/ci-fuzz.sh +++ b/fuzz/ci-fuzz.sh @@ -8,16 +8,16 @@ rm msg_*.rs [ "$(git diff)" != "" ] && exit 1 popd pushd src/bin -rm *_target.rs +rm -f ../../fuzz-fake-hashes/src/bin/*_target.rs ../../fuzz-real-hashes/src/bin/*_target.rs ./gen_target.sh [ "$(git diff)" != "" ] && exit 1 popd -export RUSTFLAGS="--cfg=secp256k1_fuzz --cfg=hashes_fuzz" +export RUSTFLAGS="--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" mkdir -p hfuzz_workspace/full_stack_target/input pushd write-seeds -RUSTFLAGS="$RUSTFLAGS --cfg=fuzzing" cargo run ../hfuzz_workspace/full_stack_target/input +cargo run ../hfuzz_workspace/full_stack_target/input cargo clean popd @@ -27,23 +27,116 @@ cargo install --color always --force honggfuzz --no-default-features # compiler optimizations aren't necessary, so we turn off LTO sed -i 's/lto = true//' Cargo.toml -export HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" +SUMMARY="" -cargo --color always hfuzz build -j8 -for TARGET in src/bin/*.rs; do - FILENAME=$(basename $TARGET) - FILE="${FILENAME%.*}" - HFUZZ_RUN_ARGS="--exit_upon_crash -v -n8 --run_time 30" - if [ "$FILE" = "chanmon_consistency_target" -o "$FILE" = "fs_store_target" ]; then - HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -F 64" - fi - export HFUZZ_RUN_ARGS - cargo --color always hfuzz run $FILE - if [ -f hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT ]; then - cat hfuzz_workspace/$FILE/HONGGFUZZ.REPORT.TXT - for CASE in hfuzz_workspace/$FILE/SIG*; do - cat $CASE | xxd -p +check_crash() { + local WORKSPACE_DIR=$1 + local FILE=$2 + if [ -f "$WORKSPACE_DIR/$FILE/HONGGFUZZ.REPORT.TXT" ]; then + cat "$WORKSPACE_DIR/$FILE/HONGGFUZZ.REPORT.TXT" + for CASE in "$WORKSPACE_DIR/$FILE"/SIG*; do + cat "$CASE" | xxd -p done exit 1 fi +} + +corpus_count() { + local CORPUS_DIR=$1 + # CI links cloned corpus directories into hfuzz_workspace. + find -L "$CORPUS_DIR" -type f 2>/dev/null | wc -l +} + +check_linked_corpus() { + local CORPUS_DIR=$1 + local FILE=$2 + local CORPUS_COUNT=$3 + + if [ -L "$CORPUS_DIR" ] && [ "$CORPUS_COUNT" -eq 0 ]; then + echo "Linked corpus for $FILE has no visible input files: $CORPUS_DIR" + exit 1 + fi +} + +run_targets() { + local CRATE_DIR=$1 + local TARGET_RUSTFLAGS=$2 + + pushd "$CRATE_DIR" + export HFUZZ_WORKSPACE="../hfuzz_workspace" + export HFUZZ_BUILD_ARGS="--features honggfuzz_fuzz" + export RUSTFLAGS="$TARGET_RUSTFLAGS" + cargo --color always hfuzz build -j8 + + for TARGET in src/bin/*.rs; do + FILENAME=$(basename "$TARGET") + FILE="${FILENAME%.*}" + CORPUS_DIR="$HFUZZ_WORKSPACE/$FILE/input" + CORPUS_COUNT=$(corpus_count "$CORPUS_DIR") + check_linked_corpus "$CORPUS_DIR" "$FILE" "$CORPUS_COUNT" + # Run 8x the corpus size plus a baseline, ensuring full corpus replay + # with room for new mutations. The 10-minute hard cap (--run_time 600) + # prevents slow-per-iteration targets from running too long. + ITERATIONS=$((CORPUS_COUNT * 8 + 1000)) + HFUZZ_RUN_ARGS="--exit_upon_crash -q -n8 -t 3 -N $ITERATIONS --run_time 600" + if [ "$FILE" = "chanmon_consistency_target" -o "$FILE" = "fs_store_target" ]; then + HFUZZ_RUN_ARGS="$HFUZZ_RUN_ARGS -F 64" + fi + export HFUZZ_RUN_ARGS + FUZZ_START=$(date +%s) + cargo --color always hfuzz run "$FILE" + FUZZ_END=$(date +%s) + FUZZ_TIME=$((FUZZ_END - FUZZ_START)) + FUZZ_CORPUS_COUNT=$(corpus_count "$CORPUS_DIR") + check_crash "$HFUZZ_WORKSPACE" "$FILE" + if [ "$GITHUB_REF" = "refs/heads/main" ] || [ "$FUZZ_MINIMIZE" = "true" ]; then + HFUZZ_RUN_ARGS="-M -q -n8 -t 3" + export HFUZZ_RUN_ARGS + MIN_START=$(date +%s) + cargo --color always hfuzz run "$FILE" + MIN_END=$(date +%s) + MIN_TIME=$((MIN_END - MIN_START)) + MIN_CORPUS_COUNT=$(corpus_count "$CORPUS_DIR") + check_crash "$HFUZZ_WORKSPACE" "$FILE" + SUMMARY="${SUMMARY}${FILE}|${ITERATIONS}|${CORPUS_COUNT}|${FUZZ_CORPUS_COUNT}|${FUZZ_TIME}|${MIN_CORPUS_COUNT}|${MIN_TIME}\n" + else + SUMMARY="${SUMMARY}${FILE}|${ITERATIONS}|${CORPUS_COUNT}|${FUZZ_CORPUS_COUNT}|${FUZZ_TIME}|-|-\n" + fi + done + + popd +} + +run_targets fuzz-fake-hashes "--cfg=fuzzing --cfg=secp256k1_fuzz --cfg=hashes_fuzz" +run_targets fuzz-real-hashes "--cfg=fuzzing --cfg=secp256k1_fuzz" + +fmt_time() { + local secs=$1 + local m=$((secs / 60)) + local s=$((secs % 60)) + if [ "$m" -gt 0 ]; then + printf "%dm %ds" "$m" "$s" + else + printf "%ds" "$s" + fi +} + +# Print summary table +set +x +echo "" +echo "==== Fuzz Summary ====" +HDR="%-40s %7s %7s %-15s %9s %-15s %9s\n" +FMT="%-40s %7s %7s %6s %-9s %9s %6s %-9s %9s\n" +printf "$HDR" "Target" "Iters" "Corpus" " Fuzzed" "Fuzz time" " Minimized" "Min. time" +printf "$HDR" "------" "-----" "------" "---------------" "---------" "---------------" "---------" +echo -e "$SUMMARY" | while IFS='|' read -r name iters orig fuzzed ftime minimized mtime; do + [ -z "$name" ] && continue + fuzz_delta=$((fuzzed - orig)) + if [ "$minimized" = "-" ]; then + printf "$FMT" "$name" "$iters" "$orig" "$fuzzed" "(+$fuzz_delta)" "$(fmt_time "$ftime")" "-" "" "-" + else + min_delta=$((minimized - fuzzed)) + printf "$FMT" "$name" "$iters" "$orig" "$fuzzed" "(+$fuzz_delta)" "$(fmt_time "$ftime")" "$minimized" "($min_delta)" "$(fmt_time "$mtime")" + fi done +echo "======================" diff --git a/fuzz/fuzz-fake-hashes/Cargo.toml b/fuzz/fuzz-fake-hashes/Cargo.toml new file mode 100644 index 00000000000..d027540a056 --- /dev/null +++ b/fuzz/fuzz-fake-hashes/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "lightning-fuzz-fake-hashes" +version = "0.0.1" +authors = ["Automatically generated"] +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[features] +afl_fuzz = ["afl"] +honggfuzz_fuzz = ["honggfuzz"] +libfuzzer_fuzz = ["libfuzzer-sys"] +stdin_fuzz = [] + +[dependencies] +lightning-fuzz = { path = ".." } + +afl = { version = "0.12", optional = true } +honggfuzz = { version = "0.5", optional = true, default-features = false } +libfuzzer-sys = { version = "0.4", optional = true } + +[lints.rust.unexpected_cfgs] +level = "forbid" +# When adding a new cfg attribute, ensure that it is added to this list. +check-cfg = [ + "cfg(fuzzing)", + "cfg(secp256k1_fuzz)", + "cfg(hashes_fuzz)", +] diff --git a/fuzz/src/bin/base32_target.rs b/fuzz/fuzz-fake-hashes/src/bin/base32_target.rs similarity index 74% rename from fuzz/src/bin/base32_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/base32_target.rs index 7937f30855c..58f2799a1a8 100644 --- a/fuzz/src/bin/base32_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/base32_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::base32::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - base32_run(data.as_ptr(), data.len()); + base32_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - base32_run(data.as_ptr(), data.len()); + base32_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - base32_run(data.as_ptr(), data.len()); + base32_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - base32_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + base32_test(&data, test_logger::DevNull {}); + } else { + base32_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - base32_run(data.as_ptr(), data.len()); + base32_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/base32") { + if let Ok(tests) = fs::read_dir("../test_cases/base32") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/bech32_parse_target.rs b/fuzz/fuzz-fake-hashes/src/bin/bech32_parse_target.rs similarity index 73% rename from fuzz/src/bin/bech32_parse_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/bech32_parse_target.rs index 62f588d3169..947d04f4b0e 100644 --- a/fuzz/src/bin/bech32_parse_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/bech32_parse_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::bech32_parse::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - bech32_parse_run(data.as_ptr(), data.len()); + bech32_parse_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - bech32_parse_run(data.as_ptr(), data.len()); + bech32_parse_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - bech32_parse_run(data.as_ptr(), data.len()); + bech32_parse_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - bech32_parse_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + bech32_parse_test(&data, test_logger::DevNull {}); + } else { + bech32_parse_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - bech32_parse_run(data.as_ptr(), data.len()); + bech32_parse_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/bech32_parse") { + if let Ok(tests) = fs::read_dir("../test_cases/bech32_parse") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/bolt11_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/bolt11_deser_target.rs similarity index 73% rename from fuzz/src/bin/bolt11_deser_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/bolt11_deser_target.rs index f79140ae5eb..f79b82019d0 100644 --- a/fuzz/src/bin/bolt11_deser_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/bolt11_deser_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::bolt11_deser::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - bolt11_deser_run(data.as_ptr(), data.len()); + bolt11_deser_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - bolt11_deser_run(data.as_ptr(), data.len()); + bolt11_deser_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - bolt11_deser_run(data.as_ptr(), data.len()); + bolt11_deser_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - bolt11_deser_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + bolt11_deser_test(&data, test_logger::DevNull {}); + } else { + bolt11_deser_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - bolt11_deser_run(data.as_ptr(), data.len()); + bolt11_deser_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/bolt11_deser") { + if let Ok(tests) = fs::read_dir("../test_cases/bolt11_deser") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/chanmon_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/chanmon_deser_target.rs similarity index 73% rename from fuzz/src/bin/chanmon_deser_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/chanmon_deser_target.rs index e58b8030217..95be82b89ee 100644 --- a/fuzz/src/bin/chanmon_deser_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/chanmon_deser_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::chanmon_deser::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - chanmon_deser_run(data.as_ptr(), data.len()); + chanmon_deser_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - chanmon_deser_run(data.as_ptr(), data.len()); + chanmon_deser_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - chanmon_deser_run(data.as_ptr(), data.len()); + chanmon_deser_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - chanmon_deser_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + chanmon_deser_test(&data, test_logger::DevNull {}); + } else { + chanmon_deser_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - chanmon_deser_run(data.as_ptr(), data.len()); + chanmon_deser_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/chanmon_deser") { + if let Ok(tests) = fs::read_dir("../test_cases/chanmon_deser") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/feature_flags_target.rs b/fuzz/fuzz-fake-hashes/src/bin/feature_flags_target.rs similarity index 73% rename from fuzz/src/bin/feature_flags_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/feature_flags_target.rs index 1be8fd12e8c..d7f04b00c7f 100644 --- a/fuzz/src/bin/feature_flags_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/feature_flags_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::feature_flags::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - feature_flags_run(data.as_ptr(), data.len()); + feature_flags_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - feature_flags_run(data.as_ptr(), data.len()); + feature_flags_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - feature_flags_run(data.as_ptr(), data.len()); + feature_flags_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - feature_flags_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + feature_flags_test(&data, test_logger::DevNull {}); + } else { + feature_flags_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - feature_flags_run(data.as_ptr(), data.len()); + feature_flags_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/feature_flags") { + if let Ok(tests) = fs::read_dir("../test_cases/feature_flags") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/fromstr_to_netaddress_target.rs b/fuzz/fuzz-fake-hashes/src/bin/fromstr_to_netaddress_target.rs similarity index 72% rename from fuzz/src/bin/fromstr_to_netaddress_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/fromstr_to_netaddress_target.rs index d86d521c762..76cdbb96d3a 100644 --- a/fuzz/src/bin/fromstr_to_netaddress_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/fromstr_to_netaddress_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::fromstr_to_netaddress::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - fromstr_to_netaddress_run(data.as_ptr(), data.len()); + fromstr_to_netaddress_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - fromstr_to_netaddress_run(data.as_ptr(), data.len()); + fromstr_to_netaddress_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - fromstr_to_netaddress_run(data.as_ptr(), data.len()); + fromstr_to_netaddress_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - fromstr_to_netaddress_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + fromstr_to_netaddress_test(&data, test_logger::DevNull {}); + } else { + fromstr_to_netaddress_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - fromstr_to_netaddress_run(data.as_ptr(), data.len()); + fromstr_to_netaddress_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/fromstr_to_netaddress") { + if let Ok(tests) = fs::read_dir("../test_cases/fromstr_to_netaddress") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/fs_store_target.rs b/fuzz/fuzz-fake-hashes/src/bin/fs_store_target.rs similarity index 74% rename from fuzz/src/bin/fs_store_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/fs_store_target.rs index 804b09a84cf..b02d69ad1b1 100644 --- a/fuzz/src/bin/fs_store_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/fs_store_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::fs_store::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - fs_store_run(data.as_ptr(), data.len()); + fs_store_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - fs_store_run(data.as_ptr(), data.len()); + fs_store_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - fs_store_run(data.as_ptr(), data.len()); + fs_store_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - fs_store_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + fs_store_test(&data, test_logger::DevNull {}); + } else { + fs_store_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - fs_store_run(data.as_ptr(), data.len()); + fs_store_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/fs_store") { + if let Ok(tests) = fs::read_dir("../test_cases/fs_store") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/full_stack_target.rs b/fuzz/fuzz-fake-hashes/src/bin/full_stack_target.rs similarity index 74% rename from fuzz/src/bin/full_stack_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/full_stack_target.rs index 33bac418f38..6d0710f249d 100644 --- a/fuzz/src/bin/full_stack_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/full_stack_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::full_stack::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - full_stack_run(data.as_ptr(), data.len()); + full_stack_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - full_stack_run(data.as_ptr(), data.len()); + full_stack_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - full_stack_run(data.as_ptr(), data.len()); + full_stack_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - full_stack_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + full_stack_test(&data, test_logger::DevNull {}); + } else { + full_stack_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - full_stack_run(data.as_ptr(), data.len()); + full_stack_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/full_stack") { + if let Ok(tests) = fs::read_dir("../test_cases/full_stack") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/fuzz-fake-hashes/src/bin/gossip_discovery_target.rs b/fuzz/fuzz-fake-hashes/src/bin/gossip_discovery_target.rs new file mode 100644 index 00000000000..7c5a55f1036 --- /dev/null +++ b/fuzz/fuzz-fake-hashes/src/bin/gossip_discovery_target.rs @@ -0,0 +1,137 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] +#![cfg_attr(rustfmt, rustfmt_skip)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +#[cfg(not(hashes_fuzz))] +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); + +#[cfg(not(secp256k1_fuzz))] +compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); + +extern crate lightning_fuzz; +use lightning_fuzz::gossip_discovery::*; +use lightning_fuzz::utils::test_logger; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + gossip_discovery_test(&data, test_logger::DevNull {}); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + gossip_discovery_test(&data, test_logger::DevNull {}); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + gossip_discovery_test(data, test_logger::DevNull {}); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + gossip_discovery_test(&data, test_logger::DevNull {}); + } else { + gossip_discovery_test(&data, test_logger::Stdout {}); + } +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec<u8> = vec![0]; + gossip_discovery_test(&data, test_logger::DevNull {}); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("../test_cases/gossip_discovery") { + for test in tests { + let mut data: Vec<u8> = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + gossip_discovery_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/bin/indexedmap_target.rs b/fuzz/fuzz-fake-hashes/src/bin/indexedmap_target.rs similarity index 74% rename from fuzz/src/bin/indexedmap_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/indexedmap_target.rs index 3830e6a24a6..b375c768cde 100644 --- a/fuzz/src/bin/indexedmap_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/indexedmap_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::indexedmap::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - indexedmap_run(data.as_ptr(), data.len()); + indexedmap_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - indexedmap_run(data.as_ptr(), data.len()); + indexedmap_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - indexedmap_run(data.as_ptr(), data.len()); + indexedmap_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - indexedmap_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + indexedmap_test(&data, test_logger::DevNull {}); + } else { + indexedmap_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - indexedmap_run(data.as_ptr(), data.len()); + indexedmap_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/indexedmap") { + if let Ok(tests) = fs::read_dir("../test_cases/indexedmap") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/invoice_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/invoice_deser_target.rs similarity index 73% rename from fuzz/src/bin/invoice_deser_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/invoice_deser_target.rs index ed79d246a58..14dcbfeaf3c 100644 --- a/fuzz/src/bin/invoice_deser_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/invoice_deser_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::invoice_deser::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - invoice_deser_run(data.as_ptr(), data.len()); + invoice_deser_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - invoice_deser_run(data.as_ptr(), data.len()); + invoice_deser_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - invoice_deser_run(data.as_ptr(), data.len()); + invoice_deser_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - invoice_deser_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + invoice_deser_test(&data, test_logger::DevNull {}); + } else { + invoice_deser_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - invoice_deser_run(data.as_ptr(), data.len()); + invoice_deser_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/invoice_deser") { + if let Ok(tests) = fs::read_dir("../test_cases/invoice_deser") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/invoice_request_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/invoice_request_deser_target.rs similarity index 72% rename from fuzz/src/bin/invoice_request_deser_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/invoice_request_deser_target.rs index 47fd3361fcc..25ce271043e 100644 --- a/fuzz/src/bin/invoice_request_deser_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/invoice_request_deser_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::invoice_request_deser::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - invoice_request_deser_run(data.as_ptr(), data.len()); + invoice_request_deser_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - invoice_request_deser_run(data.as_ptr(), data.len()); + invoice_request_deser_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - invoice_request_deser_run(data.as_ptr(), data.len()); + invoice_request_deser_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - invoice_request_deser_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + invoice_request_deser_test(&data, test_logger::DevNull {}); + } else { + invoice_request_deser_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - invoice_request_deser_run(data.as_ptr(), data.len()); + invoice_request_deser_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/invoice_request_deser") { + if let Ok(tests) = fs::read_dir("../test_cases/invoice_request_deser") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/lsps_message_target.rs b/fuzz/fuzz-fake-hashes/src/bin/lsps_message_target.rs similarity index 73% rename from fuzz/src/bin/lsps_message_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/lsps_message_target.rs index 7ba7469ebc0..e82d75f6c5d 100644 --- a/fuzz/src/bin/lsps_message_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/lsps_message_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::lsps_message::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - lsps_message_run(data.as_ptr(), data.len()); + lsps_message_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - lsps_message_run(data.as_ptr(), data.len()); + lsps_message_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - lsps_message_run(data.as_ptr(), data.len()); + lsps_message_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - lsps_message_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + lsps_message_test(&data, test_logger::DevNull {}); + } else { + lsps_message_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - lsps_message_run(data.as_ptr(), data.len()); + lsps_message_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/lsps_message") { + if let Ok(tests) = fs::read_dir("../test_cases/lsps_message") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_accept_channel_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_accept_channel_target.rs similarity index 73% rename from fuzz/src/bin/msg_accept_channel_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_accept_channel_target.rs index 0b5fa27bcc7..47f4fa074b4 100644 --- a/fuzz/src/bin/msg_accept_channel_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_accept_channel_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_accept_channel::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_accept_channel_run(data.as_ptr(), data.len()); + msg_accept_channel_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_accept_channel_run(data.as_ptr(), data.len()); + msg_accept_channel_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_accept_channel_run(data.as_ptr(), data.len()); + msg_accept_channel_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_accept_channel_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_accept_channel_test(&data, test_logger::DevNull {}); + } else { + msg_accept_channel_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_accept_channel_run(data.as_ptr(), data.len()); + msg_accept_channel_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_accept_channel") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_accept_channel") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_accept_channel_v2_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_accept_channel_v2_target.rs similarity index 73% rename from fuzz/src/bin/msg_accept_channel_v2_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_accept_channel_v2_target.rs index efb02c7acf4..656e3906914 100644 --- a/fuzz/src/bin/msg_accept_channel_v2_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_accept_channel_v2_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_accept_channel_v2::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_accept_channel_v2_run(data.as_ptr(), data.len()); + msg_accept_channel_v2_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_accept_channel_v2_run(data.as_ptr(), data.len()); + msg_accept_channel_v2_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_accept_channel_v2_run(data.as_ptr(), data.len()); + msg_accept_channel_v2_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_accept_channel_v2_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_accept_channel_v2_test(&data, test_logger::DevNull {}); + } else { + msg_accept_channel_v2_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_accept_channel_v2_run(data.as_ptr(), data.len()); + msg_accept_channel_v2_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_accept_channel_v2") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_accept_channel_v2") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_announcement_signatures_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_announcement_signatures_target.rs similarity index 72% rename from fuzz/src/bin/msg_announcement_signatures_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_announcement_signatures_target.rs index 684f1361f38..a215f0e4816 100644 --- a/fuzz/src/bin/msg_announcement_signatures_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_announcement_signatures_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_announcement_signatures::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_announcement_signatures_run(data.as_ptr(), data.len()); + msg_announcement_signatures_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_announcement_signatures_run(data.as_ptr(), data.len()); + msg_announcement_signatures_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_announcement_signatures_run(data.as_ptr(), data.len()); + msg_announcement_signatures_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_announcement_signatures_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_announcement_signatures_test(&data, test_logger::DevNull {}); + } else { + msg_announcement_signatures_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_announcement_signatures_run(data.as_ptr(), data.len()); + msg_announcement_signatures_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_announcement_signatures") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_announcement_signatures") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_blinded_message_path_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_blinded_message_path_target.rs similarity index 72% rename from fuzz/src/bin/msg_blinded_message_path_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_blinded_message_path_target.rs index 5b8ec215bc4..241493902b7 100644 --- a/fuzz/src/bin/msg_blinded_message_path_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_blinded_message_path_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_blinded_message_path::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_blinded_message_path_run(data.as_ptr(), data.len()); + msg_blinded_message_path_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_blinded_message_path_run(data.as_ptr(), data.len()); + msg_blinded_message_path_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_blinded_message_path_run(data.as_ptr(), data.len()); + msg_blinded_message_path_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_blinded_message_path_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_blinded_message_path_test(&data, test_logger::DevNull {}); + } else { + msg_blinded_message_path_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_blinded_message_path_run(data.as_ptr(), data.len()); + msg_blinded_message_path_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_blinded_message_path") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_blinded_message_path") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_channel_announcement_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_announcement_target.rs similarity index 72% rename from fuzz/src/bin/msg_channel_announcement_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_channel_announcement_target.rs index 8f326790e0a..1597fb05502 100644 --- a/fuzz/src/bin/msg_channel_announcement_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_announcement_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_channel_announcement::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_channel_announcement_run(data.as_ptr(), data.len()); + msg_channel_announcement_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_channel_announcement_run(data.as_ptr(), data.len()); + msg_channel_announcement_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_channel_announcement_run(data.as_ptr(), data.len()); + msg_channel_announcement_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_channel_announcement_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_channel_announcement_test(&data, test_logger::DevNull {}); + } else { + msg_channel_announcement_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_channel_announcement_run(data.as_ptr(), data.len()); + msg_channel_announcement_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_channel_announcement") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_channel_announcement") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_channel_details_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_details_target.rs similarity index 73% rename from fuzz/src/bin/msg_channel_details_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_channel_details_target.rs index 34f51a30bde..c1f8d9a24ee 100644 --- a/fuzz/src/bin/msg_channel_details_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_details_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_channel_details::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_channel_details_run(data.as_ptr(), data.len()); + msg_channel_details_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_channel_details_run(data.as_ptr(), data.len()); + msg_channel_details_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_channel_details_run(data.as_ptr(), data.len()); + msg_channel_details_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_channel_details_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_channel_details_test(&data, test_logger::DevNull {}); + } else { + msg_channel_details_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_channel_details_run(data.as_ptr(), data.len()); + msg_channel_details_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_channel_details") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_channel_details") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_channel_ready_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_ready_target.rs similarity index 73% rename from fuzz/src/bin/msg_channel_ready_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_channel_ready_target.rs index 76733dbecfe..3330fc6679c 100644 --- a/fuzz/src/bin/msg_channel_ready_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_ready_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_channel_ready::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_channel_ready_run(data.as_ptr(), data.len()); + msg_channel_ready_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_channel_ready_run(data.as_ptr(), data.len()); + msg_channel_ready_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_channel_ready_run(data.as_ptr(), data.len()); + msg_channel_ready_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_channel_ready_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_channel_ready_test(&data, test_logger::DevNull {}); + } else { + msg_channel_ready_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_channel_ready_run(data.as_ptr(), data.len()); + msg_channel_ready_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_channel_ready") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_channel_ready") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_channel_reestablish_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_reestablish_target.rs similarity index 72% rename from fuzz/src/bin/msg_channel_reestablish_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_channel_reestablish_target.rs index cdb4f1048e3..77bc4b5579f 100644 --- a/fuzz/src/bin/msg_channel_reestablish_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_reestablish_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_channel_reestablish::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_channel_reestablish_run(data.as_ptr(), data.len()); + msg_channel_reestablish_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_channel_reestablish_run(data.as_ptr(), data.len()); + msg_channel_reestablish_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_channel_reestablish_run(data.as_ptr(), data.len()); + msg_channel_reestablish_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_channel_reestablish_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_channel_reestablish_test(&data, test_logger::DevNull {}); + } else { + msg_channel_reestablish_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_channel_reestablish_run(data.as_ptr(), data.len()); + msg_channel_reestablish_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_channel_reestablish") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_channel_reestablish") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_channel_update_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_update_target.rs similarity index 73% rename from fuzz/src/bin/msg_channel_update_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_channel_update_target.rs index 0b567c18b81..a7ef9d294ba 100644 --- a/fuzz/src/bin/msg_channel_update_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_channel_update_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_channel_update::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_channel_update_run(data.as_ptr(), data.len()); + msg_channel_update_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_channel_update_run(data.as_ptr(), data.len()); + msg_channel_update_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_channel_update_run(data.as_ptr(), data.len()); + msg_channel_update_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_channel_update_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_channel_update_test(&data, test_logger::DevNull {}); + } else { + msg_channel_update_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_channel_update_run(data.as_ptr(), data.len()); + msg_channel_update_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_channel_update") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_channel_update") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_closing_complete_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_closing_complete_target.rs similarity index 73% rename from fuzz/src/bin/msg_closing_complete_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_closing_complete_target.rs index d097e0f6b81..bfbcb25b7f9 100644 --- a/fuzz/src/bin/msg_closing_complete_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_closing_complete_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_closing_complete::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_closing_complete_run(data.as_ptr(), data.len()); + msg_closing_complete_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_closing_complete_run(data.as_ptr(), data.len()); + msg_closing_complete_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_closing_complete_run(data.as_ptr(), data.len()); + msg_closing_complete_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_closing_complete_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_closing_complete_test(&data, test_logger::DevNull {}); + } else { + msg_closing_complete_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_closing_complete_run(data.as_ptr(), data.len()); + msg_closing_complete_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_closing_complete") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_closing_complete") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_closing_sig_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_closing_sig_target.rs similarity index 73% rename from fuzz/src/bin/msg_closing_sig_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_closing_sig_target.rs index 67150cef167..99e173378b1 100644 --- a/fuzz/src/bin/msg_closing_sig_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_closing_sig_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_closing_sig::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_closing_sig_run(data.as_ptr(), data.len()); + msg_closing_sig_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_closing_sig_run(data.as_ptr(), data.len()); + msg_closing_sig_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_closing_sig_run(data.as_ptr(), data.len()); + msg_closing_sig_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_closing_sig_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_closing_sig_test(&data, test_logger::DevNull {}); + } else { + msg_closing_sig_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_closing_sig_run(data.as_ptr(), data.len()); + msg_closing_sig_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_closing_sig") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_closing_sig") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_closing_signed_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_closing_signed_target.rs similarity index 73% rename from fuzz/src/bin/msg_closing_signed_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_closing_signed_target.rs index 1634b109da9..f5162b3f960 100644 --- a/fuzz/src/bin/msg_closing_signed_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_closing_signed_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_closing_signed::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_closing_signed_run(data.as_ptr(), data.len()); + msg_closing_signed_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_closing_signed_run(data.as_ptr(), data.len()); + msg_closing_signed_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_closing_signed_run(data.as_ptr(), data.len()); + msg_closing_signed_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_closing_signed_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_closing_signed_test(&data, test_logger::DevNull {}); + } else { + msg_closing_signed_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_closing_signed_run(data.as_ptr(), data.len()); + msg_closing_signed_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_closing_signed") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_closing_signed") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_commitment_signed_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_commitment_signed_target.rs similarity index 73% rename from fuzz/src/bin/msg_commitment_signed_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_commitment_signed_target.rs index 0c00a4ceb5a..b0e0908772d 100644 --- a/fuzz/src/bin/msg_commitment_signed_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_commitment_signed_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_commitment_signed::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_commitment_signed_run(data.as_ptr(), data.len()); + msg_commitment_signed_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_commitment_signed_run(data.as_ptr(), data.len()); + msg_commitment_signed_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_commitment_signed_run(data.as_ptr(), data.len()); + msg_commitment_signed_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_commitment_signed_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_commitment_signed_test(&data, test_logger::DevNull {}); + } else { + msg_commitment_signed_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_commitment_signed_run(data.as_ptr(), data.len()); + msg_commitment_signed_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_commitment_signed") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_commitment_signed") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_decoded_onion_error_packet_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_decoded_onion_error_packet_target.rs similarity index 72% rename from fuzz/src/bin/msg_decoded_onion_error_packet_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_decoded_onion_error_packet_target.rs index 93f3c66b207..c2f08b932db 100644 --- a/fuzz/src/bin/msg_decoded_onion_error_packet_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_decoded_onion_error_packet_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_decoded_onion_error_packet::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_decoded_onion_error_packet_run(data.as_ptr(), data.len()); + msg_decoded_onion_error_packet_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_decoded_onion_error_packet_run(data.as_ptr(), data.len()); + msg_decoded_onion_error_packet_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_decoded_onion_error_packet_run(data.as_ptr(), data.len()); + msg_decoded_onion_error_packet_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_decoded_onion_error_packet_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_decoded_onion_error_packet_test(&data, test_logger::DevNull {}); + } else { + msg_decoded_onion_error_packet_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_decoded_onion_error_packet_run(data.as_ptr(), data.len()); + msg_decoded_onion_error_packet_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_decoded_onion_error_packet") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_decoded_onion_error_packet") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_error_message_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_error_message_target.rs similarity index 73% rename from fuzz/src/bin/msg_error_message_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_error_message_target.rs index 4840e2bdfe9..94744288387 100644 --- a/fuzz/src/bin/msg_error_message_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_error_message_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_error_message::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_error_message_run(data.as_ptr(), data.len()); + msg_error_message_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_error_message_run(data.as_ptr(), data.len()); + msg_error_message_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_error_message_run(data.as_ptr(), data.len()); + msg_error_message_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_error_message_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_error_message_test(&data, test_logger::DevNull {}); + } else { + msg_error_message_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_error_message_run(data.as_ptr(), data.len()); + msg_error_message_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_error_message") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_error_message") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_funding_created_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_funding_created_target.rs similarity index 73% rename from fuzz/src/bin/msg_funding_created_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_funding_created_target.rs index f8884116710..f680fa2146e 100644 --- a/fuzz/src/bin/msg_funding_created_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_funding_created_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_funding_created::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_funding_created_run(data.as_ptr(), data.len()); + msg_funding_created_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_funding_created_run(data.as_ptr(), data.len()); + msg_funding_created_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_funding_created_run(data.as_ptr(), data.len()); + msg_funding_created_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_funding_created_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_funding_created_test(&data, test_logger::DevNull {}); + } else { + msg_funding_created_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_funding_created_run(data.as_ptr(), data.len()); + msg_funding_created_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_funding_created") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_funding_created") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_funding_signed_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_funding_signed_target.rs similarity index 73% rename from fuzz/src/bin/msg_funding_signed_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_funding_signed_target.rs index 42d0316dc9a..1421741339a 100644 --- a/fuzz/src/bin/msg_funding_signed_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_funding_signed_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_funding_signed::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_funding_signed_run(data.as_ptr(), data.len()); + msg_funding_signed_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_funding_signed_run(data.as_ptr(), data.len()); + msg_funding_signed_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_funding_signed_run(data.as_ptr(), data.len()); + msg_funding_signed_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_funding_signed_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_funding_signed_test(&data, test_logger::DevNull {}); + } else { + msg_funding_signed_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_funding_signed_run(data.as_ptr(), data.len()); + msg_funding_signed_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_funding_signed") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_funding_signed") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_gossip_timestamp_filter_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_gossip_timestamp_filter_target.rs similarity index 72% rename from fuzz/src/bin/msg_gossip_timestamp_filter_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_gossip_timestamp_filter_target.rs index 0a47f773114..0164801f68a 100644 --- a/fuzz/src/bin/msg_gossip_timestamp_filter_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_gossip_timestamp_filter_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_gossip_timestamp_filter::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_gossip_timestamp_filter_run(data.as_ptr(), data.len()); + msg_gossip_timestamp_filter_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_gossip_timestamp_filter_run(data.as_ptr(), data.len()); + msg_gossip_timestamp_filter_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_gossip_timestamp_filter_run(data.as_ptr(), data.len()); + msg_gossip_timestamp_filter_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_gossip_timestamp_filter_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_gossip_timestamp_filter_test(&data, test_logger::DevNull {}); + } else { + msg_gossip_timestamp_filter_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_gossip_timestamp_filter_run(data.as_ptr(), data.len()); + msg_gossip_timestamp_filter_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_gossip_timestamp_filter") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_gossip_timestamp_filter") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_init_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_init_target.rs similarity index 74% rename from fuzz/src/bin/msg_init_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_init_target.rs index db0c8a8894f..bc55ee4c036 100644 --- a/fuzz/src/bin/msg_init_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_init_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_init::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_init_run(data.as_ptr(), data.len()); + msg_init_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_init_run(data.as_ptr(), data.len()); + msg_init_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_init_run(data.as_ptr(), data.len()); + msg_init_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_init_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_init_test(&data, test_logger::DevNull {}); + } else { + msg_init_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_init_run(data.as_ptr(), data.len()); + msg_init_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_init") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_init") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_node_announcement_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_node_announcement_target.rs similarity index 73% rename from fuzz/src/bin/msg_node_announcement_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_node_announcement_target.rs index 1c20a999aaa..3f8e42848b5 100644 --- a/fuzz/src/bin/msg_node_announcement_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_node_announcement_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_node_announcement::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_node_announcement_run(data.as_ptr(), data.len()); + msg_node_announcement_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_node_announcement_run(data.as_ptr(), data.len()); + msg_node_announcement_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_node_announcement_run(data.as_ptr(), data.len()); + msg_node_announcement_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_node_announcement_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_node_announcement_test(&data, test_logger::DevNull {}); + } else { + msg_node_announcement_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_node_announcement_run(data.as_ptr(), data.len()); + msg_node_announcement_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_node_announcement") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_node_announcement") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_open_channel_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_open_channel_target.rs similarity index 73% rename from fuzz/src/bin/msg_open_channel_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_open_channel_target.rs index fc6df814dd1..6b59ece9eb0 100644 --- a/fuzz/src/bin/msg_open_channel_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_open_channel_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_open_channel::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_open_channel_run(data.as_ptr(), data.len()); + msg_open_channel_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_open_channel_run(data.as_ptr(), data.len()); + msg_open_channel_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_open_channel_run(data.as_ptr(), data.len()); + msg_open_channel_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_open_channel_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_open_channel_test(&data, test_logger::DevNull {}); + } else { + msg_open_channel_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_open_channel_run(data.as_ptr(), data.len()); + msg_open_channel_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_open_channel") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_open_channel") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_open_channel_v2_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_open_channel_v2_target.rs similarity index 73% rename from fuzz/src/bin/msg_open_channel_v2_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_open_channel_v2_target.rs index 732daed18c3..57492d4bd4f 100644 --- a/fuzz/src/bin/msg_open_channel_v2_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_open_channel_v2_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_open_channel_v2::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_open_channel_v2_run(data.as_ptr(), data.len()); + msg_open_channel_v2_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_open_channel_v2_run(data.as_ptr(), data.len()); + msg_open_channel_v2_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_open_channel_v2_run(data.as_ptr(), data.len()); + msg_open_channel_v2_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_open_channel_v2_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_open_channel_v2_test(&data, test_logger::DevNull {}); + } else { + msg_open_channel_v2_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_open_channel_v2_run(data.as_ptr(), data.len()); + msg_open_channel_v2_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_open_channel_v2") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_open_channel_v2") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_ping_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_ping_target.rs similarity index 74% rename from fuzz/src/bin/msg_ping_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_ping_target.rs index bb1a59b9bad..dc2061a81cc 100644 --- a/fuzz/src/bin/msg_ping_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_ping_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_ping::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_ping_run(data.as_ptr(), data.len()); + msg_ping_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_ping_run(data.as_ptr(), data.len()); + msg_ping_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_ping_run(data.as_ptr(), data.len()); + msg_ping_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_ping_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_ping_test(&data, test_logger::DevNull {}); + } else { + msg_ping_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_ping_run(data.as_ptr(), data.len()); + msg_ping_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_ping") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_ping") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_pong_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_pong_target.rs similarity index 74% rename from fuzz/src/bin/msg_pong_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_pong_target.rs index 7a97d93e785..2dc355438cc 100644 --- a/fuzz/src/bin/msg_pong_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_pong_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_pong::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_pong_run(data.as_ptr(), data.len()); + msg_pong_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_pong_run(data.as_ptr(), data.len()); + msg_pong_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_pong_run(data.as_ptr(), data.len()); + msg_pong_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_pong_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_pong_test(&data, test_logger::DevNull {}); + } else { + msg_pong_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_pong_run(data.as_ptr(), data.len()); + msg_pong_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_pong") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_pong") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_query_channel_range_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_query_channel_range_target.rs similarity index 72% rename from fuzz/src/bin/msg_query_channel_range_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_query_channel_range_target.rs index 4fd3260db0a..392371e036b 100644 --- a/fuzz/src/bin/msg_query_channel_range_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_query_channel_range_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_query_channel_range::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_query_channel_range_run(data.as_ptr(), data.len()); + msg_query_channel_range_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_query_channel_range_run(data.as_ptr(), data.len()); + msg_query_channel_range_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_query_channel_range_run(data.as_ptr(), data.len()); + msg_query_channel_range_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_query_channel_range_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_query_channel_range_test(&data, test_logger::DevNull {}); + } else { + msg_query_channel_range_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_query_channel_range_run(data.as_ptr(), data.len()); + msg_query_channel_range_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_query_channel_range") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_query_channel_range") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_query_short_channel_ids_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_query_short_channel_ids_target.rs similarity index 72% rename from fuzz/src/bin/msg_query_short_channel_ids_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_query_short_channel_ids_target.rs index 63f8c48fb3b..d52489d6a81 100644 --- a/fuzz/src/bin/msg_query_short_channel_ids_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_query_short_channel_ids_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_query_short_channel_ids::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_query_short_channel_ids_run(data.as_ptr(), data.len()); + msg_query_short_channel_ids_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_query_short_channel_ids_run(data.as_ptr(), data.len()); + msg_query_short_channel_ids_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_query_short_channel_ids_run(data.as_ptr(), data.len()); + msg_query_short_channel_ids_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_query_short_channel_ids_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_query_short_channel_ids_test(&data, test_logger::DevNull {}); + } else { + msg_query_short_channel_ids_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_query_short_channel_ids_run(data.as_ptr(), data.len()); + msg_query_short_channel_ids_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_query_short_channel_ids") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_query_short_channel_ids") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_reply_channel_range_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_reply_channel_range_target.rs similarity index 72% rename from fuzz/src/bin/msg_reply_channel_range_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_reply_channel_range_target.rs index 8e5ce619fa4..149d9592058 100644 --- a/fuzz/src/bin/msg_reply_channel_range_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_reply_channel_range_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_reply_channel_range::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_reply_channel_range_run(data.as_ptr(), data.len()); + msg_reply_channel_range_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_reply_channel_range_run(data.as_ptr(), data.len()); + msg_reply_channel_range_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_reply_channel_range_run(data.as_ptr(), data.len()); + msg_reply_channel_range_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_reply_channel_range_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_reply_channel_range_test(&data, test_logger::DevNull {}); + } else { + msg_reply_channel_range_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_reply_channel_range_run(data.as_ptr(), data.len()); + msg_reply_channel_range_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_reply_channel_range") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_reply_channel_range") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_reply_short_channel_ids_end_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_reply_short_channel_ids_end_target.rs similarity index 71% rename from fuzz/src/bin/msg_reply_short_channel_ids_end_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_reply_short_channel_ids_end_target.rs index 9b9b528abe5..65f3145c21e 100644 --- a/fuzz/src/bin/msg_reply_short_channel_ids_end_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_reply_short_channel_ids_end_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_reply_short_channel_ids_end::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_reply_short_channel_ids_end_run(data.as_ptr(), data.len()); + msg_reply_short_channel_ids_end_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_reply_short_channel_ids_end_run(data.as_ptr(), data.len()); + msg_reply_short_channel_ids_end_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_reply_short_channel_ids_end_run(data.as_ptr(), data.len()); + msg_reply_short_channel_ids_end_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_reply_short_channel_ids_end_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_reply_short_channel_ids_end_test(&data, test_logger::DevNull {}); + } else { + msg_reply_short_channel_ids_end_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_reply_short_channel_ids_end_run(data.as_ptr(), data.len()); + msg_reply_short_channel_ids_end_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_reply_short_channel_ids_end") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_reply_short_channel_ids_end") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_revoke_and_ack_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_revoke_and_ack_target.rs similarity index 73% rename from fuzz/src/bin/msg_revoke_and_ack_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_revoke_and_ack_target.rs index 1f401dae773..8ba9474da9b 100644 --- a/fuzz/src/bin/msg_revoke_and_ack_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_revoke_and_ack_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_revoke_and_ack::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_revoke_and_ack_run(data.as_ptr(), data.len()); + msg_revoke_and_ack_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_revoke_and_ack_run(data.as_ptr(), data.len()); + msg_revoke_and_ack_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_revoke_and_ack_run(data.as_ptr(), data.len()); + msg_revoke_and_ack_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_revoke_and_ack_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_revoke_and_ack_test(&data, test_logger::DevNull {}); + } else { + msg_revoke_and_ack_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_revoke_and_ack_run(data.as_ptr(), data.len()); + msg_revoke_and_ack_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_revoke_and_ack") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_revoke_and_ack") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_shutdown_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_shutdown_target.rs similarity index 74% rename from fuzz/src/bin/msg_shutdown_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_shutdown_target.rs index c29bb93bb0b..ad5fda13b1f 100644 --- a/fuzz/src/bin/msg_shutdown_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_shutdown_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_shutdown::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_shutdown_run(data.as_ptr(), data.len()); + msg_shutdown_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_shutdown_run(data.as_ptr(), data.len()); + msg_shutdown_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_shutdown_run(data.as_ptr(), data.len()); + msg_shutdown_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_shutdown_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_shutdown_test(&data, test_logger::DevNull {}); + } else { + msg_shutdown_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_shutdown_run(data.as_ptr(), data.len()); + msg_shutdown_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_shutdown") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_shutdown") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_splice_ack_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_splice_ack_target.rs similarity index 73% rename from fuzz/src/bin/msg_splice_ack_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_splice_ack_target.rs index 9957a85552f..23860eef49f 100644 --- a/fuzz/src/bin/msg_splice_ack_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_splice_ack_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_splice_ack::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_splice_ack_run(data.as_ptr(), data.len()); + msg_splice_ack_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_splice_ack_run(data.as_ptr(), data.len()); + msg_splice_ack_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_splice_ack_run(data.as_ptr(), data.len()); + msg_splice_ack_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_splice_ack_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_splice_ack_test(&data, test_logger::DevNull {}); + } else { + msg_splice_ack_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_splice_ack_run(data.as_ptr(), data.len()); + msg_splice_ack_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_splice_ack") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_splice_ack") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_splice_init_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_splice_init_target.rs similarity index 73% rename from fuzz/src/bin/msg_splice_init_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_splice_init_target.rs index 83df6454623..229e3f298ca 100644 --- a/fuzz/src/bin/msg_splice_init_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_splice_init_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_splice_init::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_splice_init_run(data.as_ptr(), data.len()); + msg_splice_init_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_splice_init_run(data.as_ptr(), data.len()); + msg_splice_init_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_splice_init_run(data.as_ptr(), data.len()); + msg_splice_init_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_splice_init_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_splice_init_test(&data, test_logger::DevNull {}); + } else { + msg_splice_init_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_splice_init_run(data.as_ptr(), data.len()); + msg_splice_init_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_splice_init") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_splice_init") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_splice_locked_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_splice_locked_target.rs similarity index 73% rename from fuzz/src/bin/msg_splice_locked_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_splice_locked_target.rs index d9dfcf956be..86cebcaf52f 100644 --- a/fuzz/src/bin/msg_splice_locked_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_splice_locked_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_splice_locked::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_splice_locked_run(data.as_ptr(), data.len()); + msg_splice_locked_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_splice_locked_run(data.as_ptr(), data.len()); + msg_splice_locked_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_splice_locked_run(data.as_ptr(), data.len()); + msg_splice_locked_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_splice_locked_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_splice_locked_test(&data, test_logger::DevNull {}); + } else { + msg_splice_locked_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_splice_locked_run(data.as_ptr(), data.len()); + msg_splice_locked_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_splice_locked") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_splice_locked") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_stfu_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_stfu_target.rs similarity index 74% rename from fuzz/src/bin/msg_stfu_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_stfu_target.rs index bdef12d4c32..8adc61075c0 100644 --- a/fuzz/src/bin/msg_stfu_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_stfu_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_stfu::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_stfu_run(data.as_ptr(), data.len()); + msg_stfu_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_stfu_run(data.as_ptr(), data.len()); + msg_stfu_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_stfu_run(data.as_ptr(), data.len()); + msg_stfu_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_stfu_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_stfu_test(&data, test_logger::DevNull {}); + } else { + msg_stfu_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_stfu_run(data.as_ptr(), data.len()); + msg_stfu_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_stfu") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_stfu") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_abort_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_abort_target.rs similarity index 74% rename from fuzz/src/bin/msg_tx_abort_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_abort_target.rs index 76f098b1e2c..368e694591d 100644 --- a/fuzz/src/bin/msg_tx_abort_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_abort_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_abort::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_abort_run(data.as_ptr(), data.len()); + msg_tx_abort_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_abort_run(data.as_ptr(), data.len()); + msg_tx_abort_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_abort_run(data.as_ptr(), data.len()); + msg_tx_abort_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_abort_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_abort_test(&data, test_logger::DevNull {}); + } else { + msg_tx_abort_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_abort_run(data.as_ptr(), data.len()); + msg_tx_abort_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_abort") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_abort") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_ack_rbf_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_ack_rbf_target.rs similarity index 73% rename from fuzz/src/bin/msg_tx_ack_rbf_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_ack_rbf_target.rs index 1f549a5703f..26bcf2ac201 100644 --- a/fuzz/src/bin/msg_tx_ack_rbf_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_ack_rbf_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_ack_rbf::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_ack_rbf_run(data.as_ptr(), data.len()); + msg_tx_ack_rbf_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_ack_rbf_run(data.as_ptr(), data.len()); + msg_tx_ack_rbf_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_ack_rbf_run(data.as_ptr(), data.len()); + msg_tx_ack_rbf_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_ack_rbf_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_ack_rbf_test(&data, test_logger::DevNull {}); + } else { + msg_tx_ack_rbf_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_ack_rbf_run(data.as_ptr(), data.len()); + msg_tx_ack_rbf_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_ack_rbf") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_ack_rbf") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_add_input_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_add_input_target.rs similarity index 73% rename from fuzz/src/bin/msg_tx_add_input_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_add_input_target.rs index 9b7e1cfe7e6..4d54686c173 100644 --- a/fuzz/src/bin/msg_tx_add_input_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_add_input_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_add_input::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_add_input_run(data.as_ptr(), data.len()); + msg_tx_add_input_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_add_input_run(data.as_ptr(), data.len()); + msg_tx_add_input_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_add_input_run(data.as_ptr(), data.len()); + msg_tx_add_input_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_add_input_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_add_input_test(&data, test_logger::DevNull {}); + } else { + msg_tx_add_input_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_add_input_run(data.as_ptr(), data.len()); + msg_tx_add_input_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_add_input") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_add_input") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_add_output_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_add_output_target.rs similarity index 73% rename from fuzz/src/bin/msg_tx_add_output_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_add_output_target.rs index b8ad29581bc..c7d06753dd3 100644 --- a/fuzz/src/bin/msg_tx_add_output_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_add_output_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_add_output::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_add_output_run(data.as_ptr(), data.len()); + msg_tx_add_output_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_add_output_run(data.as_ptr(), data.len()); + msg_tx_add_output_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_add_output_run(data.as_ptr(), data.len()); + msg_tx_add_output_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_add_output_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_add_output_test(&data, test_logger::DevNull {}); + } else { + msg_tx_add_output_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_add_output_run(data.as_ptr(), data.len()); + msg_tx_add_output_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_add_output") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_add_output") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_complete_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_complete_target.rs similarity index 76% rename from fuzz/src/bin/msg_tx_complete_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_complete_target.rs index 28b295b0d25..8a201ec0739 100644 --- a/fuzz/src/bin/msg_tx_complete_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_complete_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_complete::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_complete_run(data.as_ptr(), data.len()); + msg_tx_complete_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_complete_run(data.as_ptr(), data.len()); + msg_tx_complete_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_complete_run(data.as_ptr(), data.len()); + msg_tx_complete_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_complete_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_complete_test(&data, test_logger::DevNull {}); + } else { + msg_tx_complete_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_complete_run(data.as_ptr(), data.len()); + msg_tx_complete_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_complete") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_complete") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_init_rbf_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_init_rbf_target.rs similarity index 73% rename from fuzz/src/bin/msg_tx_init_rbf_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_init_rbf_target.rs index 24fa793315d..cc889207ba1 100644 --- a/fuzz/src/bin/msg_tx_init_rbf_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_init_rbf_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_init_rbf::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_init_rbf_run(data.as_ptr(), data.len()); + msg_tx_init_rbf_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_init_rbf_run(data.as_ptr(), data.len()); + msg_tx_init_rbf_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_init_rbf_run(data.as_ptr(), data.len()); + msg_tx_init_rbf_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_init_rbf_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_init_rbf_test(&data, test_logger::DevNull {}); + } else { + msg_tx_init_rbf_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_init_rbf_run(data.as_ptr(), data.len()); + msg_tx_init_rbf_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_init_rbf") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_init_rbf") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_remove_input_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_remove_input_target.rs similarity index 73% rename from fuzz/src/bin/msg_tx_remove_input_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_remove_input_target.rs index abe4190a354..f28ad5951ea 100644 --- a/fuzz/src/bin/msg_tx_remove_input_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_remove_input_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_remove_input::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_remove_input_run(data.as_ptr(), data.len()); + msg_tx_remove_input_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_remove_input_run(data.as_ptr(), data.len()); + msg_tx_remove_input_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_remove_input_run(data.as_ptr(), data.len()); + msg_tx_remove_input_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_remove_input_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_remove_input_test(&data, test_logger::DevNull {}); + } else { + msg_tx_remove_input_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_remove_input_run(data.as_ptr(), data.len()); + msg_tx_remove_input_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_remove_input") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_remove_input") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_remove_output_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_remove_output_target.rs similarity index 73% rename from fuzz/src/bin/msg_tx_remove_output_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_remove_output_target.rs index 3d084e0048d..de691b20fc0 100644 --- a/fuzz/src/bin/msg_tx_remove_output_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_remove_output_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_remove_output::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_remove_output_run(data.as_ptr(), data.len()); + msg_tx_remove_output_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_remove_output_run(data.as_ptr(), data.len()); + msg_tx_remove_output_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_remove_output_run(data.as_ptr(), data.len()); + msg_tx_remove_output_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_remove_output_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_remove_output_test(&data, test_logger::DevNull {}); + } else { + msg_tx_remove_output_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_remove_output_run(data.as_ptr(), data.len()); + msg_tx_remove_output_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_remove_output") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_remove_output") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_tx_signatures_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_signatures_target.rs similarity index 73% rename from fuzz/src/bin/msg_tx_signatures_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_tx_signatures_target.rs index fa3b966b478..260ffde5695 100644 --- a/fuzz/src/bin/msg_tx_signatures_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_tx_signatures_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_tx_signatures::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_tx_signatures_run(data.as_ptr(), data.len()); + msg_tx_signatures_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_tx_signatures_run(data.as_ptr(), data.len()); + msg_tx_signatures_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_tx_signatures_run(data.as_ptr(), data.len()); + msg_tx_signatures_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_tx_signatures_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_tx_signatures_test(&data, test_logger::DevNull {}); + } else { + msg_tx_signatures_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_tx_signatures_run(data.as_ptr(), data.len()); + msg_tx_signatures_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_tx_signatures") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_tx_signatures") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_update_add_htlc_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_update_add_htlc_target.rs similarity index 73% rename from fuzz/src/bin/msg_update_add_htlc_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_update_add_htlc_target.rs index f3c25a37524..2fb5ad1bc41 100644 --- a/fuzz/src/bin/msg_update_add_htlc_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_update_add_htlc_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_update_add_htlc::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_update_add_htlc_run(data.as_ptr(), data.len()); + msg_update_add_htlc_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_update_add_htlc_run(data.as_ptr(), data.len()); + msg_update_add_htlc_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_update_add_htlc_run(data.as_ptr(), data.len()); + msg_update_add_htlc_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_update_add_htlc_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_update_add_htlc_test(&data, test_logger::DevNull {}); + } else { + msg_update_add_htlc_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_update_add_htlc_run(data.as_ptr(), data.len()); + msg_update_add_htlc_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_update_add_htlc") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_update_add_htlc") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_update_fail_htlc_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fail_htlc_target.rs similarity index 73% rename from fuzz/src/bin/msg_update_fail_htlc_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_update_fail_htlc_target.rs index 9698ae92cfe..8500c3e0b6f 100644 --- a/fuzz/src/bin/msg_update_fail_htlc_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fail_htlc_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_update_fail_htlc::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_update_fail_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_htlc_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_update_fail_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_htlc_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_update_fail_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_htlc_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_update_fail_htlc_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_update_fail_htlc_test(&data, test_logger::DevNull {}); + } else { + msg_update_fail_htlc_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_update_fail_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_htlc_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_update_fail_htlc") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_update_fail_htlc") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_update_fail_malformed_htlc_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fail_malformed_htlc_target.rs similarity index 72% rename from fuzz/src/bin/msg_update_fail_malformed_htlc_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_update_fail_malformed_htlc_target.rs index b7f511c5ff5..e8ed16dbb13 100644 --- a/fuzz/src/bin/msg_update_fail_malformed_htlc_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fail_malformed_htlc_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_update_fail_malformed_htlc::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_update_fail_malformed_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_malformed_htlc_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_update_fail_malformed_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_malformed_htlc_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_update_fail_malformed_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_malformed_htlc_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_update_fail_malformed_htlc_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_update_fail_malformed_htlc_test(&data, test_logger::DevNull {}); + } else { + msg_update_fail_malformed_htlc_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_update_fail_malformed_htlc_run(data.as_ptr(), data.len()); + msg_update_fail_malformed_htlc_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_update_fail_malformed_htlc") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_update_fail_malformed_htlc") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_update_fee_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fee_target.rs similarity index 73% rename from fuzz/src/bin/msg_update_fee_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_update_fee_target.rs index b021107f150..aec31d4892c 100644 --- a/fuzz/src/bin/msg_update_fee_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fee_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_update_fee::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_update_fee_run(data.as_ptr(), data.len()); + msg_update_fee_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_update_fee_run(data.as_ptr(), data.len()); + msg_update_fee_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_update_fee_run(data.as_ptr(), data.len()); + msg_update_fee_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_update_fee_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_update_fee_test(&data, test_logger::DevNull {}); + } else { + msg_update_fee_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_update_fee_run(data.as_ptr(), data.len()); + msg_update_fee_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_update_fee") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_update_fee") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/msg_update_fulfill_htlc_target.rs b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fulfill_htlc_target.rs similarity index 72% rename from fuzz/src/bin/msg_update_fulfill_htlc_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/msg_update_fulfill_htlc_target.rs index d87cd5bd490..89b1e845fd9 100644 --- a/fuzz/src/bin/msg_update_fulfill_htlc_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/msg_update_fulfill_htlc_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::msg_targets::msg_update_fulfill_htlc::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - msg_update_fulfill_htlc_run(data.as_ptr(), data.len()); + msg_update_fulfill_htlc_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - msg_update_fulfill_htlc_run(data.as_ptr(), data.len()); + msg_update_fulfill_htlc_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - msg_update_fulfill_htlc_run(data.as_ptr(), data.len()); + msg_update_fulfill_htlc_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - msg_update_fulfill_htlc_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + msg_update_fulfill_htlc_test(&data, test_logger::DevNull {}); + } else { + msg_update_fulfill_htlc_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - msg_update_fulfill_htlc_run(data.as_ptr(), data.len()); + msg_update_fulfill_htlc_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/msg_update_fulfill_htlc") { + if let Ok(tests) = fs::read_dir("../test_cases/msg_update_fulfill_htlc") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/offer_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/offer_deser_target.rs similarity index 74% rename from fuzz/src/bin/offer_deser_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/offer_deser_target.rs index 51cdb09adec..34514e5439e 100644 --- a/fuzz/src/bin/offer_deser_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/offer_deser_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::offer_deser::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - offer_deser_run(data.as_ptr(), data.len()); + offer_deser_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - offer_deser_run(data.as_ptr(), data.len()); + offer_deser_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - offer_deser_run(data.as_ptr(), data.len()); + offer_deser_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - offer_deser_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + offer_deser_test(&data, test_logger::DevNull {}); + } else { + offer_deser_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - offer_deser_run(data.as_ptr(), data.len()); + offer_deser_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/offer_deser") { + if let Ok(tests) = fs::read_dir("../test_cases/offer_deser") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/onion_hop_data_target.rs b/fuzz/fuzz-fake-hashes/src/bin/onion_hop_data_target.rs similarity index 73% rename from fuzz/src/bin/onion_hop_data_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/onion_hop_data_target.rs index 50d98043d05..e35213a3b24 100644 --- a/fuzz/src/bin/onion_hop_data_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/onion_hop_data_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::onion_hop_data::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - onion_hop_data_run(data.as_ptr(), data.len()); + onion_hop_data_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - onion_hop_data_run(data.as_ptr(), data.len()); + onion_hop_data_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - onion_hop_data_run(data.as_ptr(), data.len()); + onion_hop_data_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - onion_hop_data_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + onion_hop_data_test(&data, test_logger::DevNull {}); + } else { + onion_hop_data_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - onion_hop_data_run(data.as_ptr(), data.len()); + onion_hop_data_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/onion_hop_data") { + if let Ok(tests) = fs::read_dir("../test_cases/onion_hop_data") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/onion_message_target.rs b/fuzz/fuzz-fake-hashes/src/bin/onion_message_target.rs similarity index 73% rename from fuzz/src/bin/onion_message_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/onion_message_target.rs index 7bb09477ec5..c85b7008a65 100644 --- a/fuzz/src/bin/onion_message_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/onion_message_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::onion_message::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - onion_message_run(data.as_ptr(), data.len()); + onion_message_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - onion_message_run(data.as_ptr(), data.len()); + onion_message_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - onion_message_run(data.as_ptr(), data.len()); + onion_message_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - onion_message_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + onion_message_test(&data, test_logger::DevNull {}); + } else { + onion_message_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - onion_message_run(data.as_ptr(), data.len()); + onion_message_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/onion_message") { + if let Ok(tests) = fs::read_dir("../test_cases/onion_message") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/fuzz-fake-hashes/src/bin/payer_proof_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/payer_proof_deser_target.rs new file mode 100644 index 00000000000..c7f9d8619b8 --- /dev/null +++ b/fuzz/fuzz-fake-hashes/src/bin/payer_proof_deser_target.rs @@ -0,0 +1,137 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// This file is auto-generated by gen_target.sh based on target_template.txt +// To modify it, modify target_template.txt and run gen_target.sh instead. + +#![cfg_attr(feature = "libfuzzer_fuzz", no_main)] +#![cfg_attr(rustfmt, rustfmt_skip)] + +#[cfg(not(fuzzing))] +compile_error!("Fuzz targets need cfg=fuzzing"); + +#[cfg(not(hashes_fuzz))] +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); + +#[cfg(not(secp256k1_fuzz))] +compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); + +extern crate lightning_fuzz; +use lightning_fuzz::payer_proof_deser::*; +use lightning_fuzz::utils::test_logger; + +#[cfg(feature = "afl")] +#[macro_use] extern crate afl; +#[cfg(feature = "afl")] +fn main() { + fuzz!(|data| { + payer_proof_deser_test(&data, test_logger::DevNull {}); + }); +} + +#[cfg(feature = "honggfuzz")] +#[macro_use] extern crate honggfuzz; +#[cfg(feature = "honggfuzz")] +fn main() { + loop { + fuzz!(|data| { + payer_proof_deser_test(&data, test_logger::DevNull {}); + }); + } +} + +#[cfg(feature = "libfuzzer_fuzz")] +#[macro_use] extern crate libfuzzer_sys; +#[cfg(feature = "libfuzzer_fuzz")] +fuzz_target!(|data: &[u8]| { + payer_proof_deser_test(data, test_logger::DevNull {}); +}); + +#[cfg(feature = "stdin_fuzz")] +fn main() { + use std::io::Read; + + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + + let mut data = Vec::with_capacity(8192); + std::io::stdin().read_to_end(&mut data).unwrap(); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + payer_proof_deser_test(&data, test_logger::DevNull {}); + } else { + payer_proof_deser_test(&data, test_logger::Stdout {}); + } +} + +#[test] +fn run_test_cases() { + use std::fs; + use std::io::Read; + use lightning_fuzz::utils::test_logger::StringBuffer; + + use std::sync::{atomic, Arc}; + { + let data: Vec<u8> = vec![0]; + payer_proof_deser_test(&data, test_logger::DevNull {}); + } + let mut threads = Vec::new(); + let threads_running = Arc::new(atomic::AtomicUsize::new(0)); + if let Ok(tests) = fs::read_dir("../test_cases/payer_proof_deser") { + for test in tests { + let mut data: Vec<u8> = Vec::new(); + let path = test.unwrap().path(); + fs::File::open(&path).unwrap().read_to_end(&mut data).unwrap(); + threads_running.fetch_add(1, atomic::Ordering::AcqRel); + + let thread_count_ref = Arc::clone(&threads_running); + let main_thread_ref = std::thread::current(); + threads.push((path.file_name().unwrap().to_str().unwrap().to_string(), + std::thread::spawn(move || { + let string_logger = StringBuffer::new(); + + let panic_logger = string_logger.clone(); + let res = if ::std::panic::catch_unwind(move || { + payer_proof_deser_test(&data, panic_logger); + }).is_err() { + Some(string_logger.into_string()) + } else { None }; + thread_count_ref.fetch_sub(1, atomic::Ordering::AcqRel); + main_thread_ref.unpark(); + res + }) + )); + while threads_running.load(atomic::Ordering::Acquire) > 32 { + std::thread::park(); + } + } + } + let mut failed_outputs = Vec::new(); + for (test, thread) in threads.drain(..) { + if let Some(output) = thread.join().unwrap() { + println!("\nOutput of {}:\n{}\n", test, output); + failed_outputs.push(test); + } + } + if !failed_outputs.is_empty() { + println!("Test cases which failed: "); + for case in failed_outputs { + println!("{}", case); + } + panic!(); + } +} diff --git a/fuzz/src/bin/peer_crypt_target.rs b/fuzz/fuzz-fake-hashes/src/bin/peer_crypt_target.rs similarity index 74% rename from fuzz/src/bin/peer_crypt_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/peer_crypt_target.rs index 0ba0252c963..2564fde509e 100644 --- a/fuzz/src/bin/peer_crypt_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/peer_crypt_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::peer_crypt::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - peer_crypt_run(data.as_ptr(), data.len()); + peer_crypt_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - peer_crypt_run(data.as_ptr(), data.len()); + peer_crypt_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - peer_crypt_run(data.as_ptr(), data.len()); + peer_crypt_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - peer_crypt_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + peer_crypt_test(&data, test_logger::DevNull {}); + } else { + peer_crypt_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - peer_crypt_run(data.as_ptr(), data.len()); + peer_crypt_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/peer_crypt") { + if let Ok(tests) = fs::read_dir("../test_cases/peer_crypt") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/process_network_graph_target.rs b/fuzz/fuzz-fake-hashes/src/bin/process_network_graph_target.rs similarity index 72% rename from fuzz/src/bin/process_network_graph_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/process_network_graph_target.rs index 4ce10e6d4df..c684f35e5a3 100644 --- a/fuzz/src/bin/process_network_graph_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/process_network_graph_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::process_network_graph::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - process_network_graph_run(data.as_ptr(), data.len()); + process_network_graph_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - process_network_graph_run(data.as_ptr(), data.len()); + process_network_graph_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - process_network_graph_run(data.as_ptr(), data.len()); + process_network_graph_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - process_network_graph_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + process_network_graph_test(&data, test_logger::DevNull {}); + } else { + process_network_graph_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - process_network_graph_run(data.as_ptr(), data.len()); + process_network_graph_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/process_network_graph") { + if let Ok(tests) = fs::read_dir("../test_cases/process_network_graph") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/process_onion_failure_target.rs b/fuzz/fuzz-fake-hashes/src/bin/process_onion_failure_target.rs similarity index 72% rename from fuzz/src/bin/process_onion_failure_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/process_onion_failure_target.rs index 1d2cdb28593..05a209fefea 100644 --- a/fuzz/src/bin/process_onion_failure_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/process_onion_failure_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::process_onion_failure::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - process_onion_failure_run(data.as_ptr(), data.len()); + process_onion_failure_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - process_onion_failure_run(data.as_ptr(), data.len()); + process_onion_failure_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - process_onion_failure_run(data.as_ptr(), data.len()); + process_onion_failure_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - process_onion_failure_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + process_onion_failure_test(&data, test_logger::DevNull {}); + } else { + process_onion_failure_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - process_onion_failure_run(data.as_ptr(), data.len()); + process_onion_failure_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/process_onion_failure") { + if let Ok(tests) = fs::read_dir("../test_cases/process_onion_failure") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/refund_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/refund_deser_target.rs similarity index 73% rename from fuzz/src/bin/refund_deser_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/refund_deser_target.rs index fea8a9c4c6d..57eb4c9c074 100644 --- a/fuzz/src/bin/refund_deser_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/refund_deser_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::refund_deser::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - refund_deser_run(data.as_ptr(), data.len()); + refund_deser_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - refund_deser_run(data.as_ptr(), data.len()); + refund_deser_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - refund_deser_run(data.as_ptr(), data.len()); + refund_deser_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - refund_deser_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + refund_deser_test(&data, test_logger::DevNull {}); + } else { + refund_deser_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - refund_deser_run(data.as_ptr(), data.len()); + refund_deser_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/refund_deser") { + if let Ok(tests) = fs::read_dir("../test_cases/refund_deser") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/router_target.rs b/fuzz/fuzz-fake-hashes/src/bin/router_target.rs similarity index 74% rename from fuzz/src/bin/router_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/router_target.rs index 0ebec549455..cca57db2b05 100644 --- a/fuzz/src/bin/router_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/router_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::router::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - router_run(data.as_ptr(), data.len()); + router_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - router_run(data.as_ptr(), data.len()); + router_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - router_run(data.as_ptr(), data.len()); + router_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - router_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + router_test(&data, test_logger::DevNull {}); + } else { + router_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - router_run(data.as_ptr(), data.len()); + router_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/router") { + if let Ok(tests) = fs::read_dir("../test_cases/router") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/static_invoice_deser_target.rs b/fuzz/fuzz-fake-hashes/src/bin/static_invoice_deser_target.rs similarity index 73% rename from fuzz/src/bin/static_invoice_deser_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/static_invoice_deser_target.rs index 573f0aa0b22..a06cd51cffb 100644 --- a/fuzz/src/bin/static_invoice_deser_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/static_invoice_deser_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::static_invoice_deser::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - static_invoice_deser_run(data.as_ptr(), data.len()); + static_invoice_deser_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - static_invoice_deser_run(data.as_ptr(), data.len()); + static_invoice_deser_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - static_invoice_deser_run(data.as_ptr(), data.len()); + static_invoice_deser_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - static_invoice_deser_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + static_invoice_deser_test(&data, test_logger::DevNull {}); + } else { + static_invoice_deser_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - static_invoice_deser_run(data.as_ptr(), data.len()); + static_invoice_deser_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/static_invoice_deser") { + if let Ok(tests) = fs::read_dir("../test_cases/static_invoice_deser") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/zbase32_target.rs b/fuzz/fuzz-fake-hashes/src/bin/zbase32_target.rs similarity index 74% rename from fuzz/src/bin/zbase32_target.rs rename to fuzz/fuzz-fake-hashes/src/bin/zbase32_target.rs index 35aa53d1fff..f66381ad3a5 100644 --- a/fuzz/src/bin/zbase32_target.rs +++ b/fuzz/fuzz-fake-hashes/src/bin/zbase32_target.rs @@ -17,20 +17,21 @@ compile_error!("Fuzz targets need cfg=fuzzing"); #[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +compile_error!("Fuzz target does not support cfg(not(hashes_fuzz))"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::zbase32::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - zbase32_run(data.as_ptr(), data.len()); + zbase32_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - zbase32_run(data.as_ptr(), data.len()); + zbase32_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - zbase32_run(data.as_ptr(), data.len()); + zbase32_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - zbase32_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + zbase32_test(&data, test_logger::DevNull {}); + } else { + zbase32_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - zbase32_run(data.as_ptr(), data.len()); + zbase32_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/zbase32") { + if let Ok(tests) = fs::read_dir("../test_cases/zbase32") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/fuzz-real-hashes/Cargo.toml b/fuzz/fuzz-real-hashes/Cargo.toml new file mode 100644 index 00000000000..a6d77d28137 --- /dev/null +++ b/fuzz/fuzz-real-hashes/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "lightning-fuzz-real-hashes" +version = "0.0.1" +authors = ["Automatically generated"] +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[features] +afl_fuzz = ["afl"] +honggfuzz_fuzz = ["honggfuzz"] +libfuzzer_fuzz = ["libfuzzer-sys"] +stdin_fuzz = [] + +[dependencies] +lightning-fuzz = { path = ".." } + +afl = { version = "0.12", optional = true } +honggfuzz = { version = "0.5", optional = true, default-features = false } +libfuzzer-sys = { version = "0.4", optional = true } + +[lints.rust.unexpected_cfgs] +level = "forbid" +# When adding a new cfg attribute, ensure that it is added to this list. +check-cfg = [ + "cfg(fuzzing)", + "cfg(secp256k1_fuzz)", + "cfg(hashes_fuzz)", +] diff --git a/fuzz/src/bin/chanmon_consistency_target.rs b/fuzz/fuzz-real-hashes/src/bin/chanmon_consistency_target.rs similarity index 72% rename from fuzz/src/bin/chanmon_consistency_target.rs rename to fuzz/fuzz-real-hashes/src/bin/chanmon_consistency_target.rs index c4788b0c1b2..86791e46905 100644 --- a/fuzz/src/bin/chanmon_consistency_target.rs +++ b/fuzz/fuzz-real-hashes/src/bin/chanmon_consistency_target.rs @@ -16,21 +16,22 @@ #[cfg(not(fuzzing))] compile_error!("Fuzz targets need cfg=fuzzing"); -#[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +#[cfg(hashes_fuzz)] +compile_error!("Fuzz target does not support cfg(hashes_fuzz)"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::chanmon_consistency::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - chanmon_consistency_run(data.as_ptr(), data.len()); + chanmon_consistency_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - chanmon_consistency_run(data.as_ptr(), data.len()); + chanmon_consistency_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - chanmon_consistency_run(data.as_ptr(), data.len()); + chanmon_consistency_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - chanmon_consistency_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + chanmon_consistency_test(&data, test_logger::DevNull {}); + } else { + chanmon_consistency_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - chanmon_consistency_run(data.as_ptr(), data.len()); + chanmon_consistency_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/chanmon_consistency") { + if let Ok(tests) = fs::read_dir("../test_cases/chanmon_consistency") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/bin/gen_target.sh b/fuzz/src/bin/gen_target.sh index b4f0c7a12b9..96268712f7e 100755 --- a/fuzz/src/bin/gen_target.sh +++ b/fuzz/src/bin/gen_target.sh @@ -2,87 +2,104 @@ echo "#include <stdint.h>" > ../../targets.h GEN_TEST() { - cat target_template.txt | sed s/TARGET_NAME/$1/ | sed s/TARGET_MOD/$2$1/ > $1_target.rs - echo "void $1_run(const unsigned char* data, size_t data_len);" >> ../../targets.h + dest_dir=$1 + target_name=$2 + target_mod=$3 + hashes_flag=$4 + + mkdir -p "$dest_dir" + sed "s/TARGET_NAME/$target_name/g; s|TARGET_MOD|$target_mod$target_name|g; s/HASHES_FLAG/$hashes_flag/g" \ + target_template.txt > "$dest_dir/${target_name}_target.rs" + echo "void ${target_name}_run(const unsigned char* data, size_t data_len);" >> ../../targets.h +} + +GEN_FAKE_HASHES_TEST() { + GEN_TEST ../../fuzz-fake-hashes/src/bin "$1" "$2" "not(hashes_fuzz)" +} + +GEN_REAL_HASHES_TEST() { + GEN_TEST ../../fuzz-real-hashes/src/bin "$1" "$2" "hashes_fuzz" } -GEN_TEST bech32_parse -GEN_TEST chanmon_deser -GEN_TEST chanmon_consistency -GEN_TEST full_stack -GEN_TEST invoice_deser -GEN_TEST invoice_request_deser -GEN_TEST offer_deser -GEN_TEST bolt11_deser -GEN_TEST static_invoice_deser -GEN_TEST onion_message -GEN_TEST peer_crypt -GEN_TEST process_network_graph -GEN_TEST process_onion_failure -GEN_TEST refund_deser -GEN_TEST router -GEN_TEST zbase32 -GEN_TEST indexedmap -GEN_TEST onion_hop_data -GEN_TEST base32 -GEN_TEST fromstr_to_netaddress -GEN_TEST feature_flags -GEN_TEST lsps_message -GEN_TEST fs_store +GEN_FAKE_HASHES_TEST bech32_parse +GEN_FAKE_HASHES_TEST chanmon_deser +GEN_REAL_HASHES_TEST chanmon_consistency +GEN_FAKE_HASHES_TEST full_stack +GEN_FAKE_HASHES_TEST invoice_deser +GEN_FAKE_HASHES_TEST invoice_request_deser +GEN_FAKE_HASHES_TEST offer_deser +GEN_FAKE_HASHES_TEST bolt11_deser +GEN_FAKE_HASHES_TEST static_invoice_deser +GEN_FAKE_HASHES_TEST onion_message +GEN_FAKE_HASHES_TEST peer_crypt +GEN_FAKE_HASHES_TEST process_network_graph +GEN_FAKE_HASHES_TEST process_onion_failure +GEN_FAKE_HASHES_TEST payer_proof_deser +GEN_FAKE_HASHES_TEST refund_deser +GEN_FAKE_HASHES_TEST router +GEN_FAKE_HASHES_TEST zbase32 +GEN_FAKE_HASHES_TEST indexedmap +GEN_FAKE_HASHES_TEST onion_hop_data +GEN_FAKE_HASHES_TEST base32 +GEN_FAKE_HASHES_TEST fromstr_to_netaddress +GEN_FAKE_HASHES_TEST feature_flags +GEN_FAKE_HASHES_TEST lsps_message +GEN_FAKE_HASHES_TEST fs_store +GEN_FAKE_HASHES_TEST gossip_discovery -GEN_TEST msg_accept_channel msg_targets:: -GEN_TEST msg_announcement_signatures msg_targets:: -GEN_TEST msg_channel_reestablish msg_targets:: -GEN_TEST msg_closing_signed msg_targets:: -GEN_TEST msg_closing_complete msg_targets:: -GEN_TEST msg_closing_sig msg_targets:: -GEN_TEST msg_commitment_signed msg_targets:: -GEN_TEST msg_decoded_onion_error_packet msg_targets:: -GEN_TEST msg_funding_created msg_targets:: -GEN_TEST msg_channel_ready msg_targets:: -GEN_TEST msg_funding_signed msg_targets:: -GEN_TEST msg_init msg_targets:: -GEN_TEST msg_open_channel msg_targets:: -GEN_TEST msg_revoke_and_ack msg_targets:: -GEN_TEST msg_shutdown msg_targets:: -GEN_TEST msg_update_fail_htlc msg_targets:: -GEN_TEST msg_update_fail_malformed_htlc msg_targets:: -GEN_TEST msg_update_fee msg_targets:: -GEN_TEST msg_update_fulfill_htlc msg_targets:: +GEN_FAKE_HASHES_TEST msg_accept_channel msg_targets:: +GEN_FAKE_HASHES_TEST msg_announcement_signatures msg_targets:: +GEN_FAKE_HASHES_TEST msg_channel_reestablish msg_targets:: +GEN_FAKE_HASHES_TEST msg_closing_signed msg_targets:: +GEN_FAKE_HASHES_TEST msg_closing_complete msg_targets:: +GEN_FAKE_HASHES_TEST msg_closing_sig msg_targets:: +GEN_FAKE_HASHES_TEST msg_commitment_signed msg_targets:: +GEN_FAKE_HASHES_TEST msg_decoded_onion_error_packet msg_targets:: +GEN_FAKE_HASHES_TEST msg_funding_created msg_targets:: +GEN_FAKE_HASHES_TEST msg_channel_ready msg_targets:: +GEN_FAKE_HASHES_TEST msg_funding_signed msg_targets:: +GEN_FAKE_HASHES_TEST msg_init msg_targets:: +GEN_FAKE_HASHES_TEST msg_open_channel msg_targets:: +GEN_FAKE_HASHES_TEST msg_revoke_and_ack msg_targets:: +GEN_FAKE_HASHES_TEST msg_shutdown msg_targets:: +GEN_FAKE_HASHES_TEST msg_update_fail_htlc msg_targets:: +GEN_FAKE_HASHES_TEST msg_update_fail_malformed_htlc msg_targets:: +GEN_FAKE_HASHES_TEST msg_update_fee msg_targets:: +GEN_FAKE_HASHES_TEST msg_update_fulfill_htlc msg_targets:: -GEN_TEST msg_channel_announcement msg_targets:: -GEN_TEST msg_node_announcement msg_targets:: -GEN_TEST msg_query_short_channel_ids msg_targets:: -GEN_TEST msg_reply_short_channel_ids_end msg_targets:: -GEN_TEST msg_query_channel_range msg_targets:: -GEN_TEST msg_reply_channel_range msg_targets:: -GEN_TEST msg_gossip_timestamp_filter msg_targets:: +GEN_FAKE_HASHES_TEST msg_channel_announcement msg_targets:: +GEN_FAKE_HASHES_TEST msg_node_announcement msg_targets:: +GEN_FAKE_HASHES_TEST msg_query_short_channel_ids msg_targets:: +GEN_FAKE_HASHES_TEST msg_reply_short_channel_ids_end msg_targets:: +GEN_FAKE_HASHES_TEST msg_query_channel_range msg_targets:: +GEN_FAKE_HASHES_TEST msg_reply_channel_range msg_targets:: +GEN_FAKE_HASHES_TEST msg_gossip_timestamp_filter msg_targets:: -GEN_TEST msg_update_add_htlc msg_targets:: -GEN_TEST msg_error_message msg_targets:: -GEN_TEST msg_channel_update msg_targets:: +GEN_FAKE_HASHES_TEST msg_update_add_htlc msg_targets:: +GEN_FAKE_HASHES_TEST msg_error_message msg_targets:: +GEN_FAKE_HASHES_TEST msg_channel_update msg_targets:: -GEN_TEST msg_ping msg_targets:: -GEN_TEST msg_pong msg_targets:: +GEN_FAKE_HASHES_TEST msg_ping msg_targets:: +GEN_FAKE_HASHES_TEST msg_pong msg_targets:: -GEN_TEST msg_channel_details msg_targets:: +GEN_FAKE_HASHES_TEST msg_channel_details msg_targets:: -GEN_TEST msg_open_channel_v2 msg_targets:: -GEN_TEST msg_accept_channel_v2 msg_targets:: -GEN_TEST msg_tx_add_input msg_targets:: -GEN_TEST msg_tx_add_output msg_targets:: -GEN_TEST msg_tx_remove_input msg_targets:: -GEN_TEST msg_tx_remove_output msg_targets:: -GEN_TEST msg_tx_complete msg_targets:: -GEN_TEST msg_tx_signatures msg_targets:: -GEN_TEST msg_tx_init_rbf msg_targets:: -GEN_TEST msg_tx_ack_rbf msg_targets:: -GEN_TEST msg_tx_abort msg_targets:: +GEN_FAKE_HASHES_TEST msg_open_channel_v2 msg_targets:: +GEN_FAKE_HASHES_TEST msg_accept_channel_v2 msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_add_input msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_add_output msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_remove_input msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_remove_output msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_complete msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_signatures msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_init_rbf msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_ack_rbf msg_targets:: +GEN_FAKE_HASHES_TEST msg_tx_abort msg_targets:: -GEN_TEST msg_stfu msg_targets:: +GEN_FAKE_HASHES_TEST msg_stfu msg_targets:: -GEN_TEST msg_splice_init msg_targets:: -GEN_TEST msg_splice_ack msg_targets:: -GEN_TEST msg_splice_locked msg_targets:: +GEN_FAKE_HASHES_TEST msg_splice_init msg_targets:: +GEN_FAKE_HASHES_TEST msg_splice_ack msg_targets:: +GEN_FAKE_HASHES_TEST msg_splice_locked msg_targets:: -GEN_TEST msg_blinded_message_path msg_targets:: +GEN_FAKE_HASHES_TEST msg_blinded_message_path msg_targets:: diff --git a/fuzz/src/bin/target_template.txt b/fuzz/src/bin/target_template.txt index e828aa998b1..1dbd40aa6b8 100644 --- a/fuzz/src/bin/target_template.txt +++ b/fuzz/src/bin/target_template.txt @@ -16,21 +16,22 @@ #[cfg(not(fuzzing))] compile_error!("Fuzz targets need cfg=fuzzing"); -#[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); +#[cfg(HASHES_FLAG)] +compile_error!("Fuzz target does not support cfg(HASHES_FLAG)"); #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); extern crate lightning_fuzz; use lightning_fuzz::TARGET_MOD::*; +use lightning_fuzz::utils::test_logger; #[cfg(feature = "afl")] #[macro_use] extern crate afl; #[cfg(feature = "afl")] fn main() { fuzz!(|data| { - TARGET_NAME_run(data.as_ptr(), data.len()); + TARGET_NAME_test(&data, test_logger::DevNull {}); }); } @@ -40,7 +41,7 @@ fn main() { fn main() { loop { fuzz!(|data| { - TARGET_NAME_run(data.as_ptr(), data.len()); + TARGET_NAME_test(&data, test_logger::DevNull {}); }); } } @@ -49,16 +50,32 @@ fn main() { #[macro_use] extern crate libfuzzer_sys; #[cfg(feature = "libfuzzer_fuzz")] fuzz_target!(|data: &[u8]| { - TARGET_NAME_run(data.as_ptr(), data.len()); + TARGET_NAME_test(data, test_logger::DevNull {}); }); #[cfg(feature = "stdin_fuzz")] fn main() { use std::io::Read; + // On macOS, panic=abort causes the process to send SIGABRT which can leave it + // stuck in an uninterruptible state due to the ReportCrash daemon. Using + // process::exit in a panic hook avoids this by terminating cleanly. + #[cfg(target_os = "macos")] + std::panic::set_hook(Box::new(|panic_info| { + use std::io::Write; + let _ = std::io::stdout().flush(); + eprintln!("{}\n{}", panic_info, std::backtrace::Backtrace::force_capture()); + let _ = std::io::stderr().flush(); + std::process::exit(1); + })); + let mut data = Vec::with_capacity(8192); std::io::stdin().read_to_end(&mut data).unwrap(); - TARGET_NAME_run(data.as_ptr(), data.len()); + if std::env::var_os("LDK_FUZZ_SUPPRESS_LOGS").is_some() { + TARGET_NAME_test(&data, test_logger::DevNull {}); + } else { + TARGET_NAME_test(&data, test_logger::Stdout {}); + } } #[test] @@ -70,11 +87,11 @@ fn run_test_cases() { use std::sync::{atomic, Arc}; { let data: Vec<u8> = vec![0]; - TARGET_NAME_run(data.as_ptr(), data.len()); + TARGET_NAME_test(&data, test_logger::DevNull {}); } let mut threads = Vec::new(); let threads_running = Arc::new(atomic::AtomicUsize::new(0)); - if let Ok(tests) = fs::read_dir("test_cases/TARGET_NAME") { + if let Ok(tests) = fs::read_dir("../test_cases/TARGET_NAME") { for test in tests { let mut data: Vec<u8> = Vec::new(); let path = test.unwrap().path(); diff --git a/fuzz/src/chanmon_consistency.rs b/fuzz/src/chanmon_consistency.rs index 87d58da4832..ecc87c6e346 100644 --- a/fuzz/src/chanmon_consistency.rs +++ b/fuzz/src/chanmon_consistency.rs @@ -14,9 +14,10 @@ //! To test this we stand up a network of three nodes and read bytes from the fuzz input to denote //! actions such as sending payments, handling events, or changing monitor update return values on //! a per-node basis. This should allow it to find any cases where the ordering of actions results -//! in us getting out of sync with ourselves, and, assuming at least one of our recieve- or -//! send-side handling is correct, other peers. We consider it a failure if any action results in a -//! channel being force-closed. +//! in us getting out of sync with ourselves, and, assuming at least one of our receive- or +//! send-side handling is correct, other peers. We consider it a failure if any action results in +//! a channel being force-closed. The fuzzer also models transaction relay through a harness +//! mempool, making transaction confirmation and block delivery closer to normal node behavior. use bitcoin::amount::Amount; use bitcoin::constants::genesis_block; @@ -26,8 +27,11 @@ use bitcoin::opcodes; use bitcoin::script::{Builder, ScriptBuf}; use bitcoin::transaction::Version; use bitcoin::transaction::{Transaction, TxOut}; +use bitcoin::FeeRate; +use bitcoin::OutPoint as BitcoinOutPoint; -use bitcoin::hash_types::BlockHash; +use bitcoin::block::Header; +use bitcoin::hash_types::Txid; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::sha256d::Hash as Sha256dHash; use bitcoin::hashes::Hash as TraitImport; @@ -37,26 +41,25 @@ use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, Messa use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; use lightning::chain; use lightning::chain::chaininterface::{ - TransactionType, BroadcasterInterface, ConfirmationTarget, FeeEstimator, + BroadcasterInterface, ConfirmationTarget, FeeEstimator, TransactionType, }; -use lightning::chain::channelmonitor::{ChannelMonitor, MonitorEvent}; -use lightning::chain::transaction::OutPoint; +use lightning::chain::channelmonitor::{ChannelMonitor, ANTI_REORG_DELAY}; use lightning::chain::{ - chainmonitor, channelmonitor, BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch, + chainmonitor, channelmonitor, BlockLocator, ChannelMonitorUpdateStatus, Confirm, Watch, }; -use lightning::events; +use lightning::events::{self, EventsProvider}; use lightning::ln::channel::{ FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS, }; -use lightning::ln::channel_state::ChannelDetails; +use lightning::ln::channel_state::{ChannelDetails, InboundHTLCStateDetails, OutboundHTLCSource}; use lightning::ln::channelmanager::{ ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, RecentPaymentDetails, + TrustedChannelFeatures, }; use lightning::ln::functional_test_utils::*; -use lightning::ln::funding::{FundingTxInput, SpliceContribution}; use lightning::ln::inbound_payment::ExpandedKey; use lightning::ln::msgs::{ - BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, Init, MessageSendEvent, + self, BaseMessageHandler, ChannelMessageHandler, CommitmentUpdate, Init, MessageSendEvent, UpdateAddHTLC, }; use lightning::ln::outbound_payment::RecipientOnionFields; @@ -76,13 +79,17 @@ use lightning::util::config::UserConfig; use lightning::util::errors::APIError; use lightning::util::hash_tables::*; use lightning::util::logger::Logger; +use lightning::util::native_async::{MaybeSend, MaybeSync}; use lightning::util::ser::{LengthReadable, ReadableArgs, Writeable, Writer}; -use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner}; +use lightning::util::test_channel_signer::{EnforcementState, SignerOp, TestChannelSigner}; +use lightning::util::test_utils::TestWalletSource; +use lightning::util::wallet_utils::{WalletSourceSync, WalletSync}; + +use lightning::events::bump_transaction::sync::BumpTransactionEventHandlerSync; use lightning_invoice::RawBolt11Invoice; use crate::utils::test_logger::{self, Output}; -use crate::utils::test_persister::TestPersister; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; @@ -91,16 +98,31 @@ use bitcoin::secp256k1::{self, Message, PublicKey, Scalar, Secp256k1, SecretKey} use lightning::util::dyn_signer::DynSigner; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::cmp; +use std::collections::HashSet; use std::mem; use std::sync::atomic; use std::sync::{Arc, Mutex}; const MAX_FEE: u32 = 10_000; +const MAX_SETTLE_ITERATIONS: usize = 256; +const FORCE_CLOSE_CLEANUP_ROUNDS: usize = 512; +// Each wallet is seeded with enough confirmed UTXOs that repeated splice +// transactions don't run out of inputs mid-run. +const NUM_WALLET_UTXOS: u32 = 50; +// A single fuzz byte can mine more than one block so a corpus entry does not +// need long runs of identical "mine one block" commands to reach CSV or CLTV +// boundaries. Mining commands are capped in `safe_mine_block_count` if +// unresolved HTLCs are near expiry. +const MINE_BLOCK_COUNTS: [u32; 8] = [1, 2, 3, 6, 12, 24, 48, 144]; +// Finish-time relay/mining rounds are capped so cleanup cannot spin forever. +const MAX_FINISH_RELAY_MINE_ROUNDS: usize = 32; + struct FuzzEstimator { ret_val: atomic::AtomicU32, } + impl FeeEstimator for FuzzEstimator { fn get_est_sat_per_1000_weight(&self, conf_target: ConfirmationTarget) -> u32 { // We force-close channels if our counterparty sends us a feerate which is a small multiple @@ -123,6 +145,13 @@ impl FeeEstimator for FuzzEstimator { } } +impl FuzzEstimator { + fn feerate_sat_per_kw(&self) -> FeeRate { + let feerate = self.ret_val.load(atomic::Ordering::Acquire); + FeeRate::from_sat_per_kwu(feerate as u64) + } +} + struct FuzzRouter {} impl Router for FuzzRouter { @@ -168,208 +197,567 @@ impl BroadcasterInterface for TestBroadcaster { } } -pub struct VecWriter(pub Vec<u8>); -impl Writer for VecWriter { - fn write_all(&mut self, buf: &[u8]) -> Result<(), ::lightning::io::Error> { - self.0.extend_from_slice(buf); - Ok(()) - } +struct ChainState { + blocks: Vec<(Header, Vec<Transaction>)>, + confirmed_txids: HashSet<Txid>, + /// Unconfirmed transactions admitted to the mempool, in valid block order: + /// every input is either confirmed already or created by an earlier + /// transaction in this vector. + pending_txs: Vec<(Txid, Transaction)>, + /// Unspent outputs created by confirmed transactions. Mempool admission + /// checks inputs against this set, adjusted for outputs created and spent + /// by the transactions already in `pending_txs`. + utxos: HashSet<BitcoinOutPoint>, } -pub struct TestWallet { - secret_key: SecretKey, - utxos: Mutex<Vec<lightning::events::bump_transaction::Utxo>>, - secp: Secp256k1<bitcoin::secp256k1::All>, -} +impl ChainState { + fn new() -> Self { + let genesis_hash = genesis_block(Network::Bitcoin).block_hash(); + let genesis_header = create_dummy_header(genesis_hash, 42); + Self { + blocks: vec![(genesis_header, Vec::new())], + confirmed_txids: HashSet::new(), + pending_txs: Vec::new(), + utxos: HashSet::new(), + } + } + + fn tip_height(&self) -> u32 { + (self.blocks.len() - 1) as u32 + } + + fn is_unspent(&self, outpoint: &BitcoinOutPoint) -> bool { + self.utxos.contains(outpoint) + } + + fn confirmed_output(&self, outpoint: &BitcoinOutPoint) -> Option<&TxOut> { + if !self.confirmed_txids.contains(&outpoint.txid) { + return None; + } + self.blocks.iter().find_map(|(_, txs)| { + txs.iter().find_map(|tx| { + if tx.compute_txid() == outpoint.txid { + tx.output.get(outpoint.vout as usize) + } else { + None + } + }) + }) + } + + // Initial channel funding is represented by a no-input transaction. It is + // not a valid Bitcoin transaction, but it gives LDK a stable funding + // outpoint without modeling coin selection during channel setup. + fn is_synthetic_funding_tx(tx: &Transaction) -> bool { + !tx.is_coinbase() && tx.input.is_empty() + } + + // Checks whether a transaction spends an input twice or spends an output + // not present in `utxos`. + fn has_invalid_inputs(tx: &Transaction, utxos: &HashSet<BitcoinOutPoint>) -> bool { + let mut spent_inputs = HashSet::new(); + for input in &tx.input { + if !spent_inputs.insert(input.previous_output) { + return true; + } + if !utxos.contains(&input.previous_output) { + return true; + } + } + false + } + + fn apply_tx_to_utxos(&mut self, txid: Txid, tx: &Transaction) { + for input in &tx.input { + self.utxos.remove(&input.previous_output); + } + for idx in 0..tx.output.len() { + self.utxos.insert(BitcoinOutPoint { txid, vout: idx as u32 }); + } + } + + fn mine_block(&mut self, txs: Vec<Transaction>) { + let prev_hash = self.blocks.last().unwrap().0.block_hash(); + let header = create_dummy_header(prev_hash, 42); + self.blocks.push((header, txs)); + } -impl TestWallet { - pub fn new(secret_key: SecretKey) -> Self { - Self { secret_key, utxos: Mutex::new(Vec::new()), secp: Secp256k1::new() } + fn mine_empty_blocks(&mut self, count: u32) { + for _ in 0..count { + self.mine_block(Vec::new()); + } } - fn get_change_script(&self) -> Result<ScriptBuf, ()> { - let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); - Ok(ScriptBuf::new_p2wpkh(&public_key.wpubkey_hash().unwrap())) + // Mines a setup transaction directly into a block, bypassing the mempool, + // and buries it to `depth`. Wallet seeding and synthetic funding + // transactions are not relayable, so they cannot go through normal + // admission. + fn mine_setup_tx_to_depth(&mut self, tx: Transaction, depth: u32) { + assert!( + tx.is_coinbase() || Self::is_synthetic_funding_tx(&tx), + "direct setup mining is only for coinbase and synthetic funding transactions: {:?}", + tx, + ); + let txid = tx.compute_txid(); + assert!( + self.confirmed_txids.insert(txid), + "direct setup transaction was already confirmed: {:?}", + tx, + ); + self.apply_tx_to_utxos(txid, &tx); + + self.mine_block(vec![tx]); + self.mine_empty_blocks(depth.saturating_sub(1)); } - pub fn add_utxo(&self, outpoint: bitcoin::OutPoint, value: Amount) -> TxOut { - let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); - let utxo = lightning::events::bump_transaction::Utxo::new_v0_p2wpkh( - outpoint, - value, - &public_key.wpubkey_hash().unwrap(), + // Attempts to admit a broadcast transaction to the mempool, enforcing + // locktime, input, and RBF rules. Mining later confirms the whole mempool + // without further selection. + fn admit_tx_to_mempool(&mut self, tx: Transaction) { + let txid = tx.compute_txid(); + let lock_time = tx.lock_time.to_consensus_u32(); + let locktime_enabled = + tx.input.iter().any(|input| input.sequence.enables_absolute_lock_time()); + + let is_ldk_commitment_obscured_locktime = + tx.input.len() == 1 && tx.input[0].sequence.0 >> 24 == 0x80 && lock_time >> 24 == 0x20; + + let immature_absolute_locktime = + locktime_enabled && tx.lock_time.is_block_height() && self.tip_height() < lock_time; + assert!( + !immature_absolute_locktime, + "broadcast immature locktime transaction into chanmon harness mempool: {:?}", + tx, ); - self.utxos.lock().unwrap().push(utxo.clone()); - utxo.output - } - - pub fn sign_tx( - &self, mut tx: Transaction, - ) -> Result<Transaction, bitcoin::sighash::P2wpkhError> { - let utxos = self.utxos.lock().unwrap(); - for i in 0..tx.input.len() { - if let Some(utxo) = - utxos.iter().find(|utxo| utxo.outpoint == tx.input[i].previous_output) - { - let sighash = bitcoin::sighash::SighashCache::new(&tx).p2wpkh_signature_hash( - i, - &utxo.output.script_pubkey, - utxo.output.value, - bitcoin::EcdsaSighashType::All, - )?; - let signature = self.secp.sign_ecdsa( - &secp256k1::Message::from_digest(sighash.to_byte_array()), - &self.secret_key, - ); - let bitcoin_sig = bitcoin::ecdsa::Signature { - signature, - sighash_type: bitcoin::EcdsaSighashType::All, - }; - tx.input[i].witness = - bitcoin::Witness::p2wpkh(&bitcoin_sig, &self.secret_key.public_key(&self.secp)); + + let unmodeled_time_locktime = locktime_enabled + && tx.lock_time.is_block_time() + && !is_ldk_commitment_obscured_locktime; + assert!( + !unmodeled_time_locktime, + "broadcast time-locked transaction into chanmon harness mempool: {:?}", + tx, + ); + + assert!( + !tx.is_coinbase() && !Self::is_synthetic_funding_tx(&tx), + "setup-only transaction entered chanmon harness mempool: {:?}", + tx, + ); + + if self.confirmed_txids.contains(&txid) { + return; + } + if self.pending_txs.iter().any(|(pending_txid, _)| *pending_txid == txid) { + return; + } + + // Fee-rate policy is not modeled, so among conflicting RBF candidates + // the last one relayed wins. + let mut conflicting_pending_txids = HashSet::new(); + for (pending_txid, pending_tx) in &self.pending_txs { + let signals_rbf = pending_tx.input.iter().any(|input| input.sequence.is_rbf()); + let conflicts_with_new_tx = pending_tx.input.iter().any(|pending_input| { + tx.input.iter().any(|input| input.previous_output == pending_input.previous_output) + }); + if conflicts_with_new_tx { + if !signals_rbf { + return; + } + conflicting_pending_txids.insert(*pending_txid); + } + } + if !conflicting_pending_txids.is_empty() { + let mut removed_outputs = HashSet::new(); + let mut retained_txs = Vec::new(); + for (pending_txid, pending_tx) in self.pending_txs.drain(..) { + let direct_conflict = conflicting_pending_txids.contains(&pending_txid); + let spends_removed_tx = pending_tx + .input + .iter() + .any(|input| removed_outputs.contains(&input.previous_output)); + if direct_conflict || spends_removed_tx { + for idx in 0..pending_tx.output.len() { + removed_outputs + .insert(BitcoinOutPoint { txid: pending_txid, vout: idx as u32 }); + } + } else { + retained_txs.push((pending_txid, pending_tx)); + } + } + self.pending_txs = retained_txs; + } + + // Build the UTXO set this transaction would see if the current mempool + // confirmed. + let mut available_utxos = self.utxos.clone(); + for (pending_txid, pending_tx) in &self.pending_txs { + for input in &pending_tx.input { + available_utxos.remove(&input.previous_output); } + for idx in 0..pending_tx.output.len() { + available_utxos.insert(BitcoinOutPoint { txid: *pending_txid, vout: idx as u32 }); + } + } + if Self::has_invalid_inputs(&tx, &available_utxos) { + return; + } + self.pending_txs.push((txid, tx)); + } + + fn relay_transactions(&mut self, txs: Vec<Transaction>) { + for tx in txs { + self.admit_tx_to_mempool(tx); } - Ok(tx) } + + // Mines `count` blocks, confirming the current mempool in the first block. + fn mine_blocks(&mut self, count: u32) -> Vec<Transaction> { + assert!(count > 0, "mining zero blocks should not be requested"); + + let mempool_txs = std::mem::take(&mut self.pending_txs); + let confirmed_txs = if mempool_txs.is_empty() { + self.mine_empty_blocks(1); + Vec::new() + } else { + let mut confirmed = Vec::new(); + for (txid, tx) in mempool_txs { + assert!( + !Self::has_invalid_inputs(&tx, &self.utxos), + "mempool transaction was no longer valid at mining time: {:?}", + tx, + ); + assert!( + self.confirmed_txids.insert(txid), + "mempool transaction was already confirmed at mining time: {:?}", + tx, + ); + self.apply_tx_to_utxos(txid, &tx); + confirmed.push(tx); + } + let confirmed_txs = confirmed.clone(); + self.mine_block(confirmed); + confirmed_txs + }; + self.mine_empty_blocks(count - 1); + confirmed_txs + } + + fn block_at(&self, height: u32) -> &(Header, Vec<Transaction>) { + &self.blocks[height as usize] + } +} + +pub struct VecWriter(pub Vec<u8>); +impl Writer for VecWriter { + fn write_all(&mut self, buf: &[u8]) -> Result<(), ::lightning::io::Error> { + self.0.extend_from_slice(buf); + Ok(()) + } +} + +fn serialize_monitor(monitor: &ChannelMonitor<TestChannelSigner>) -> Vec<u8> { + let mut ser = VecWriter(Vec::new()); + monitor.write(&mut ser).unwrap(); + ser.0 } -/// The LDK API requires that any time we tell it we're done persisting a `ChannelMonitor[Update]` -/// we never pass it in as the "latest" `ChannelMonitor` on startup. However, we can pass -/// out-of-date monitors as long as we never told LDK we finished persisting them, which we do by -/// storing both old `ChannelMonitor`s and ones that are "being persisted" here. +/// LDK requires the `ChannelMonitor` loaded on startup to be at least as current as the +/// `ChannelManager` state, except for monitor updates that `ChannelManager` still records as +/// in-flight and can replay. This harness tracks the monitor blobs that remain valid restart +/// candidates under that rule. /// -/// Note that such "being persisted" `ChannelMonitor`s are stored in `ChannelManager` and will -/// simply be replayed on startup. +/// Separately, we track every `InProgress` persistence operation that still needs a +/// `channel_monitor_updated` call. A newer persisted monitor can make an older monitor invalid for +/// restart while the older update still needs to be completed to unblock the live `ChainMonitor`. +/// +/// Off-chain monitor updates that are still "being persisted" are stored in `ChannelManager` and +/// will be replayed on startup. Full-monitor snapshots from chain sync or archive paths that return +/// `InProgress` are only restart candidates; losing one on restart does not require a +/// `channel_monitor_updated` callback. struct LatestMonitorState { /// The latest monitor id which we told LDK we've persisted. /// - /// Note that there may still be earlier pending monitor updates in [`Self::pending_monitors`] - /// which we haven't yet completed. We're allowed to reload with those as well, at least until - /// they're completed. + /// Note that earlier updates may still need a `channel_monitor_updated` callback via + /// [`Self::pending_monitor_completions`]. persisted_monitor_id: u64, /// The latest serialized `ChannelMonitor` that we told LDK we persisted. persisted_monitor: Vec<u8>, - /// A set of (monitor id, serialized `ChannelMonitor`)s which we're currently "persisting", - /// from LDK's perspective. + /// An ordered list of (monitor id, serialized `ChannelMonitor`)s which remain safe to use as + /// stale monitors on reload. pending_monitors: Vec<(u64, Vec<u8>)>, + /// An ordered list of (monitor id, serialized `ChannelMonitor`)s which still need a + /// `channel_monitor_updated` callback. + pending_monitor_completions: Vec<(u64, Vec<u8>)>, } +impl LatestMonitorState { + fn insert_pending_entry( + pending: &mut Vec<(u64, Vec<u8>)>, monitor_id: u64, serialized_monitor: Vec<u8>, + ) { + // Monitor update ids must arrive in order. Assert at insertion time so duplicates or + // out-of-order updates fail close to the write that caused them instead of being sorted + // into place. + assert!( + pending.last().map_or(true, |(last_id, _)| *last_id < monitor_id), + "pending monitor updates should arrive in order" + ); + pending.push((monitor_id, serialized_monitor)); + } -struct TestChainMonitor { - pub logger: Arc<dyn Logger>, - pub keys: Arc<KeyProvider>, - pub persister: Arc<TestPersister>, - pub chain_monitor: Arc< - chainmonitor::ChainMonitor< - TestChannelSigner, - Arc<dyn chain::Filter>, - Arc<TestBroadcaster>, - Arc<FuzzEstimator>, - Arc<dyn Logger>, - Arc<TestPersister>, - Arc<KeyProvider>, - >, - >, - pub latest_monitors: Mutex<HashMap<ChannelId, LatestMonitorState>>, -} -impl TestChainMonitor { - pub fn new( - broadcaster: Arc<TestBroadcaster>, logger: Arc<dyn Logger>, feeest: Arc<FuzzEstimator>, - persister: Arc<TestPersister>, keys: Arc<KeyProvider>, - ) -> Self { - Self { - chain_monitor: Arc::new(chainmonitor::ChainMonitor::new( - None, - broadcaster, - logger.clone(), - feeest, - Arc::clone(&persister), - Arc::clone(&keys), - keys.get_peer_storage_key(), - )), - logger, - keys, - persister, - latest_monitors: Mutex::new(new_hash_map()), + fn insert_pending_monitor_candidate(&mut self, monitor_id: u64, serialized_monitor: Vec<u8>) { + // Full-monitor persists from chain sync or archive paths use the monitor's current + // latest_update_id rather than a fresh ChannelMonitorUpdate id. Keep duplicate ids so + // reload can choose between multiple same-id full snapshots that were in flight together. + if let Some((last_id, _)) = self.pending_monitors.last() { + assert!(*last_id <= monitor_id, "pending monitor updates should arrive in order"); } + self.pending_monitors.push((monitor_id, serialized_monitor)); } -} -impl chain::Watch<TestChannelSigner> for TestChainMonitor { - fn watch_channel( - &self, channel_id: ChannelId, monitor: channelmonitor::ChannelMonitor<TestChannelSigner>, - ) -> Result<chain::ChannelMonitorUpdateStatus, ()> { - let mut ser = VecWriter(Vec::new()); - monitor.write(&mut ser).unwrap(); - let monitor_id = monitor.get_latest_update_id(); - let res = self.chain_monitor.watch_channel(channel_id, monitor); - let state = match res { - Ok(chain::ChannelMonitorUpdateStatus::Completed) => LatestMonitorState { - persisted_monitor_id: monitor_id, - persisted_monitor: ser.0, - pending_monitors: Vec::new(), + + fn mark_persisted(&mut self, monitor_id: u64, serialized_monitor: Vec<u8>) { + // Once a monitor is durable, use it as the restart baseline and stop tracking candidates + // at or behind that update id. Completion obligations are tracked separately and are + // deliberately not pruned here. + self.pending_monitors.retain(|(id, _)| *id > monitor_id); + if monitor_id >= self.persisted_monitor_id { + self.persisted_monitor_id = monitor_id; + self.persisted_monitor = serialized_monitor; + } + } + + fn insert_pending( + &mut self, monitor_id: u64, serialized_monitor: Vec<u8>, needs_completion: bool, + ) { + if needs_completion { + // persist_new_channel and update_persisted_channel(Some(_)) require a later + // channel_monitor_updated callback if persistence returns InProgress. + Self::insert_pending_entry( + &mut self.pending_monitors, + monitor_id, + serialized_monitor.clone(), + ); + Self::insert_pending_entry( + &mut self.pending_monitor_completions, + monitor_id, + serialized_monitor, + ); + } else { + // This harness treats update_persisted_channel(None, ...) as the chain-sync/archive + // case: the full monitor may be used on restart, but ChainMonitor does not wait for a + // channel_monitor_updated callback. + self.insert_pending_monitor_candidate(monitor_id, serialized_monitor); + } + } + + fn mark_completed_update_persisted(&mut self, monitor_id: u64, serialized_monitor: Vec<u8>) { + // The selector/drain path should already have removed this entry before + // finish_monitor_update calls channel_monitor_updated. This check catches accidental + // double-completion or pruning of the wrong list. + assert!( + self.pending_monitor_completions.iter().all(|(id, _)| *id != monitor_id), + "completed monitor update should already be removed from the completion queue" + ); + self.mark_persisted(monitor_id, serialized_monitor); + } + + fn drain_pending_completions(&mut self) -> Vec<(u64, Vec<u8>)> { + std::mem::take(&mut self.pending_monitor_completions) + } + + fn take_pending_completion( + &mut self, selector: MonitorUpdateSelector, + ) -> Option<(u64, Vec<u8>)> { + // The fuzzer chooses which outstanding callback to deliver. These choices apply to + // completion obligations, not to the set of monitors that may be used on restart. + match selector { + MonitorUpdateSelector::First => { + if self.pending_monitor_completions.is_empty() { + None + } else { + Some(self.pending_monitor_completions.remove(0)) + } + }, + MonitorUpdateSelector::Second => { + if self.pending_monitor_completions.len() > 1 { + Some(self.pending_monitor_completions.remove(1)) + } else { + None + } }, - Ok(chain::ChannelMonitorUpdateStatus::InProgress) => LatestMonitorState { - persisted_monitor_id: monitor_id, - persisted_monitor: Vec::new(), - pending_monitors: vec![(monitor_id, ser.0)], + MonitorUpdateSelector::Last => self.pending_monitor_completions.pop(), + } + } + + fn select_monitor_for_reload(&mut self, selector: MonitorReloadSelector) { + // A restart can load the last monitor we told LDK was persisted, or a monitor snapshot + // whose write was started before the simulated crash. + let old_mon = (self.persisted_monitor_id, std::mem::take(&mut self.persisted_monitor)); + let (monitor_id, serialized_monitor) = match selector { + MonitorReloadSelector::Persisted => old_mon, + MonitorReloadSelector::FirstPending => { + if self.pending_monitors.is_empty() { + old_mon + } else { + self.pending_monitors.remove(0) + } }, - Ok(chain::ChannelMonitorUpdateStatus::UnrecoverableError) => panic!(), - Err(()) => panic!(), + MonitorReloadSelector::LastPending => self.pending_monitors.pop().unwrap_or(old_mon), }; - if self.latest_monitors.lock().unwrap().insert(channel_id, state).is_some() { - panic!("Already had monitor pre-watch_channel"); + self.persisted_monitor_id = monitor_id; + self.persisted_monitor = serialized_monitor; + // After restart, stop tracking pre-restart in-flight writes. ChannelManager will replay + // off-chain monitor updates that still matter; full-monitor snapshots may simply be absent. + self.pending_monitors.clear(); + self.pending_monitor_completions.clear(); + } +} + +struct HarnessPersister { + pub update_ret: Mutex<chain::ChannelMonitorUpdateStatus>, + pub latest_monitors: Mutex<HashMap<ChannelId, LatestMonitorState>>, +} +impl HarnessPersister { + fn track_monitor_update( + &self, channel_id: ChannelId, monitor_id: u64, serialized_monitor: Vec<u8>, + status: chain::ChannelMonitorUpdateStatus, needs_completion: bool, + ) { + let mut latest_monitors = self.latest_monitors.lock().unwrap(); + if let Some(state) = latest_monitors.get_mut(&channel_id) { + match status { + chain::ChannelMonitorUpdateStatus::Completed => { + // A completed write advances the restart baseline. Once LDK can rely on that + // monitor state being durable, the harness stops offering candidates at or + // behind that update id. + state.mark_persisted(monitor_id, serialized_monitor); + }, + chain::ChannelMonitorUpdateStatus::InProgress => { + // InProgress always creates a restart candidate, but only some calls also need + // an explicit channel_monitor_updated completion. + state.insert_pending(monitor_id, serialized_monitor, needs_completion); + }, + chain::ChannelMonitorUpdateStatus::UnrecoverableError => {}, + } + } else { + let state = match status { + chain::ChannelMonitorUpdateStatus::Completed => LatestMonitorState { + persisted_monitor_id: monitor_id, + persisted_monitor: serialized_monitor, + pending_monitors: Vec::new(), + pending_monitor_completions: Vec::new(), + }, + chain::ChannelMonitorUpdateStatus::InProgress => { + // The first persist for a channel is persist_new_channel, which always needs a + // completion callback when it returns InProgress. A full-monitor update without + // existing state would mean the harness missed the channel's initial monitor. + assert!(needs_completion, "missing monitor state for full monitor update"); + LatestMonitorState { + persisted_monitor_id: monitor_id, + persisted_monitor: Vec::new(), + pending_monitors: vec![(monitor_id, serialized_monitor.clone())], + pending_monitor_completions: vec![(monitor_id, serialized_monitor)], + } + }, + chain::ChannelMonitorUpdateStatus::UnrecoverableError => return, + }; + assert!( + latest_monitors.insert(channel_id, state).is_none(), + "Already had monitor state pre-persist" + ); } - res } - fn update_channel( - &self, channel_id: ChannelId, update: &channelmonitor::ChannelMonitorUpdate, - ) -> chain::ChannelMonitorUpdateStatus { - let mut map_lock = self.latest_monitors.lock().unwrap(); - let map_entry = map_lock.get_mut(&channel_id).expect("Didn't have monitor on update call"); - let latest_monitor_data = map_entry - .pending_monitors - .last() - .as_ref() - .map(|(_, data)| data) - .unwrap_or(&map_entry.persisted_monitor); - let deserialized_monitor = - <(BlockHash, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( - &mut &latest_monitor_data[..], - (&*self.keys, &*self.keys), - ) + fn mark_update_completed( + &self, channel_id: ChannelId, monitor_id: u64, serialized_monitor: Vec<u8>, + ) { + let mut latest_monitors = self.latest_monitors.lock().unwrap(); + let state = latest_monitors + .get_mut(&channel_id) + .expect("missing monitor state for completed update"); + // Once we tell LDK update N is completed, use the completed monitor as the restart + // baseline and drop restart candidates at or behind N. + state.mark_completed_update_persisted(monitor_id, serialized_monitor); + } + + fn drain_pending_updates(&self, channel_id: &ChannelId) -> Vec<(u64, Vec<u8>)> { + self.latest_monitors + .lock() .unwrap() - .1; - deserialized_monitor - .update_monitor( - update, - &&TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }, - &&FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }, - &self.logger, - ) - .unwrap(); - let mut ser = VecWriter(Vec::new()); - deserialized_monitor.write(&mut ser).unwrap(); - let res = self.chain_monitor.update_channel(channel_id, update); - match res { - chain::ChannelMonitorUpdateStatus::Completed => { - map_entry.persisted_monitor_id = update.update_id; - map_entry.persisted_monitor = ser.0; - }, - chain::ChannelMonitorUpdateStatus::InProgress => { - map_entry.pending_monitors.push((update.update_id, ser.0)); - }, - chain::ChannelMonitorUpdateStatus::UnrecoverableError => panic!(), + .get_mut(channel_id) + .map_or_else(Vec::new, |state| state.drain_pending_completions()) + } + + fn drain_all_pending_updates(&self) -> Vec<(ChannelId, u64, Vec<u8>)> { + let mut completed_updates = Vec::new(); + for (channel_id, state) in self.latest_monitors.lock().unwrap().iter_mut() { + for (monitor_id, data) in state.drain_pending_completions() { + completed_updates.push((*channel_id, monitor_id, data)); + } } - res + completed_updates } - fn release_pending_monitor_events( - &self, - ) -> Vec<(OutPoint, ChannelId, Vec<MonitorEvent>, PublicKey)> { - return self.chain_monitor.release_pending_monitor_events(); + fn take_pending_update( + &self, channel_id: &ChannelId, selector: MonitorUpdateSelector, + ) -> Option<(u64, Vec<u8>)> { + self.latest_monitors + .lock() + .unwrap() + .get_mut(channel_id) + .and_then(|state| state.take_pending_completion(selector)) + } +} +impl chainmonitor::Persist<TestChannelSigner> for HarnessPersister { + fn persist_new_channel( + &self, _monitor_name: lightning::util::persist::MonitorName, + data: &channelmonitor::ChannelMonitor<TestChannelSigner>, + ) -> chain::ChannelMonitorUpdateStatus { + let status = self.update_ret.lock().unwrap().clone(); + let monitor_id = data.get_latest_update_id(); + let serialized_monitor = serialize_monitor(data); + self.track_monitor_update(data.channel_id(), monitor_id, serialized_monitor, status, true); + status + } + + fn update_persisted_channel( + &self, _monitor_name: lightning::util::persist::MonitorName, + update: Option<&channelmonitor::ChannelMonitorUpdate>, + data: &channelmonitor::ChannelMonitor<TestChannelSigner>, + ) -> chain::ChannelMonitorUpdateStatus { + let status = self.update_ret.lock().unwrap().clone(); + let monitor_id = update.map_or_else(|| data.get_latest_update_id(), |upd| upd.update_id); + let serialized_monitor = serialize_monitor(data); + self.track_monitor_update( + data.channel_id(), + monitor_id, + serialized_monitor, + status, + // `None` normally comes from chain-sync or archive writes, which need no completion + // callback. `update_channel_internal` can also use `None` after `update_monitor` + // fails, but this harness does not model that error-recovery path. + update.is_some(), + ); + status } + + fn archive_persisted_channel(&self, _monitor_name: lightning::util::persist::MonitorName) {} } +type TestChainMonitor = chainmonitor::ChainMonitor< + TestChannelSigner, + Arc<dyn chain::Filter>, + Arc<TestBroadcaster>, + Arc<FuzzEstimator>, + Arc<dyn Logger + MaybeSend + MaybeSync>, + Arc<HarnessPersister>, + Arc<KeyProvider>, +>; +type TestBumpTransactionEventHandler = BumpTransactionEventHandlerSync< + Arc<TestBroadcaster>, + Arc<WalletSync<Arc<TestWalletSource>, Arc<dyn Logger + MaybeSend + MaybeSync>>>, + Arc<KeyProvider>, + Arc<dyn Logger + MaybeSend + MaybeSync>, +>; + struct KeyProvider { node_secret: SecretKey, rand_bytes_id: atomic::AtomicU32, @@ -380,7 +768,8 @@ impl EntropySource for KeyProvider { fn get_secure_random_bytes(&self) -> [u8; 32] { let id = self.rand_bytes_id.fetch_add(1, atomic::Ordering::Relaxed); #[rustfmt::skip] - let mut res = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, self.node_secret[31]]; + let mut res = [self.node_secret[31], 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, self.node_secret[31]]; + res[2..6].copy_from_slice(&id.to_le_bytes()); res[30 - 4..30].copy_from_slice(&id.to_le_bytes()); res } @@ -449,8 +838,6 @@ impl NodeSigner for KeyProvider { impl SignerProvider for KeyProvider { type EcdsaSigner = TestChannelSigner; - #[cfg(taproot)] - type TaprootSigner = TestChannelSigner; fn generate_channel_keys_id(&self, _inbound: bool, _user_channel_id: u128) -> [u8; 32] { let id = self.rand_bytes_id.fetch_add(1, atomic::Ordering::Relaxed) as u8; @@ -502,6 +889,18 @@ impl SignerProvider for KeyProvider { } } +// These signer operations can be blocked by fuzz bytes. The first four cover +// live-channel and splice signing, while the holder-side operations cover local +// on-chain claim signing after LDK has moved a channel to chain handling. +const SUPPORTED_SIGNER_OPS: [SignerOp; 6] = [ + SignerOp::SignCounterpartyCommitment, + SignerOp::GetPerCommitmentPoint, + SignerOp::ReleaseCommitmentSecret, + SignerOp::SignSpliceSharedInput, + SignerOp::SignHolderCommitment, + SignerOp::SignHolderHtlcTransaction, +]; + impl KeyProvider { fn make_enforcement_state_cell( &self, commitment_seed: [u8; 32], @@ -514,23 +913,22 @@ impl KeyProvider { let cell = revoked_commitments.get(&commitment_seed).unwrap(); Arc::clone(cell) } -} -// Returns a bool indicating whether the payment failed. -#[inline] -fn check_payment_send_events(source: &ChanMan, sent_payment_id: PaymentId) -> bool { - for payment in source.list_recent_payments() { - match payment { - RecentPaymentDetails::Pending { payment_id, .. } if payment_id == sent_payment_id => { - return true; - }, - RecentPaymentDetails::Abandoned { payment_id, .. } if payment_id == sent_payment_id => { - return false; - }, - _ => {}, + fn disable_supported_ops_for_all_signers(&self) { + let enforcement_states = self.enforcement_states.lock().unwrap(); + for (_, state) in enforcement_states.iter() { + for signer_op in SUPPORTED_SIGNER_OPS { + state.lock().unwrap().disabled_signer_ops.insert(signer_op); + } + } + } + + fn enable_op_for_all_signers(&self, signer_op: SignerOp) { + let enforcement_states = self.enforcement_states.lock().unwrap(); + for (_, state) in enforcement_states.iter() { + state.lock().unwrap().disabled_signer_ops.remove(&signer_op); } } - return false; } type ChanMan<'a> = ChannelManager< @@ -542,2080 +940,3656 @@ type ChanMan<'a> = ChannelManager< Arc<FuzzEstimator>, &'a FuzzRouter, &'a FuzzRouter, - Arc<dyn Logger>, + Arc<dyn Logger + MaybeSend + MaybeSync>, >; #[inline] -fn get_payment_secret_hash(dest: &ChanMan, payment_ctr: &mut u64) -> (PaymentSecret, PaymentHash) { - *payment_ctr += 1; - let payment_hash = PaymentHash(Sha256::hash(&[*payment_ctr as u8]).to_byte_array()); - let payment_secret = dest - .create_inbound_payment_for_hash(payment_hash, None, 3600, None) - .expect("create_inbound_payment_for_hash failed"); - (payment_secret, payment_hash) -} - -#[inline] -fn send_payment( - source: &ChanMan, dest: &ChanMan, dest_chan_id: u64, amt: u64, payment_secret: PaymentSecret, - payment_hash: PaymentHash, payment_id: PaymentId, -) -> bool { - let (min_value_sendable, max_value_sendable) = source - .list_usable_channels() - .iter() - .find(|chan| chan.short_channel_id == Some(dest_chan_id)) - .map(|chan| (chan.next_outbound_htlc_minimum_msat, chan.next_outbound_htlc_limit_msat)) - .unwrap_or((0, 0)); - let route_params = RouteParameters::from_payment_params_and_value( - PaymentParameters::from_node_id(source.get_our_node_id(), TEST_FINAL_CLTV), - amt, - ); - let route = Route { - paths: vec![Path { - hops: vec![RouteHop { - pubkey: dest.get_our_node_id(), - node_features: dest.node_features(), - short_channel_id: dest_chan_id, - channel_features: dest.channel_features(), - fee_msat: amt, - cltv_expiry_delta: 200, - maybe_announced_channel: true, - }], - blinded_tail: None, - }], - route_params: Some(route_params.clone()), - }; - let onion = RecipientOnionFields::secret_only(payment_secret); - let res = source.send_payment_with_route(route, payment_hash, onion, payment_id); - match res { - Err(err) => { - panic!("Errored with {:?} on initial payment send", err); +fn assert_disconnect_action<'a>( + action: &'a msgs::ErrorAction, close_tracker: &ChannelCloseTracker, +) -> ExpectedControlAction<'a> { + match action { + msgs::ErrorAction::DisconnectPeerWithWarning { ref msg } => { + // Since sending/receiving messages may be delayed, `timer_tick_occurred` may cause + // a node to disconnect their counterparty if they're expecting a timely response. + let is_quiescent_msg = msg.data.contains("already sent splice_locked, cannot RBF") + || msg.data.contains("contribution no longer valid at quiescence"); + assert!( + msg.data.contains("Disconnecting due to timeout awaiting response") + || is_quiescent_msg, + "Unexpected disconnect case: {}", + msg.data, + ); + ExpectedControlAction::Warning(msg, is_quiescent_msg) }, - Ok(()) => { - let expect_failure = amt < min_value_sendable || amt > max_value_sendable; - let succeeded = check_payment_send_events(source, payment_id); - assert_eq!(succeeded, !expect_failure); - succeeded + msgs::ErrorAction::SendErrorMessage { ref msg } => { + assert!( + close_tracker.is_expected_closed_channel_error_msg(msg), + "Expected closed-channel error, got: {:?}", + msg, + ); + ExpectedControlAction::Error(msg) + }, + msgs::ErrorAction::SendWarningMessage { ref msg, .. } => { + assert!( + close_tracker.is_expected_closed_channel_warning_msg(msg), + "Expected closed-channel warning, got: {:?}", + msg, + ); + ExpectedControlAction::Warning(msg, false) }, + _ => panic!("Expected harness control error, got: {:?}", action), } } -#[inline] -fn send_hop_payment( - source: &ChanMan, middle: &ChanMan, middle_scid: u64, dest: &ChanMan, dest_scid: u64, amt: u64, - payment_secret: PaymentSecret, payment_hash: PaymentHash, payment_id: PaymentId, -) -> bool { - let (min_value_sendable, max_value_sendable) = source - .list_usable_channels() - .iter() - .find(|chan| chan.short_channel_id == Some(middle_scid)) - .map(|chan| (chan.next_outbound_htlc_minimum_msat, chan.next_outbound_htlc_limit_msat)) - .unwrap_or((0, 0)); - let first_hop_fee = 50_000; - let route_params = RouteParameters::from_payment_params_and_value( - PaymentParameters::from_node_id(source.get_our_node_id(), TEST_FINAL_CLTV), - amt, - ); - let route = Route { - paths: vec![Path { - hops: vec![ - RouteHop { - pubkey: middle.get_our_node_id(), - node_features: middle.node_features(), - short_channel_id: middle_scid, - channel_features: middle.channel_features(), - fee_msat: first_hop_fee, - cltv_expiry_delta: 100, - maybe_announced_channel: true, - }, - RouteHop { - pubkey: dest.get_our_node_id(), - node_features: dest.node_features(), - short_channel_id: dest_scid, - channel_features: dest.channel_features(), - fee_msat: amt, - cltv_expiry_delta: 200, - maybe_announced_channel: true, - }, - ], - blinded_tail: None, - }], - route_params: Some(route_params.clone()), - }; - let onion = RecipientOnionFields::secret_only(payment_secret); - let res = source.send_payment_with_route(route, payment_hash, onion, payment_id); - match res { - Err(err) => { - panic!("Errored with {:?} on initial payment send", err); - }, - Ok(()) => { - let sent_amt = amt + first_hop_fee; - let expect_failure = sent_amt < min_value_sendable || sent_amt > max_value_sendable; - let succeeded = check_payment_send_events(source, payment_id); - assert_eq!(succeeded, !expect_failure); - succeeded - }, - } +enum ExpectedControlAction<'a> { + Warning(&'a msgs::WarningMessage, bool), + Error(&'a msgs::ErrorMessage), } -/// Send an MPP payment directly from source to dest using multiple channels. -#[inline] -fn send_mpp_payment( - source: &ChanMan, dest: &ChanMan, dest_scids: &[u64], amt: u64, payment_secret: PaymentSecret, - payment_hash: PaymentHash, payment_id: PaymentId, -) -> bool { - let num_paths = dest_scids.len(); - if num_paths == 0 { - return false; - } +struct ChannelCloseTracker { + // Channels this input explicitly requested to close, with the error reason + // passed to `force_close_broadcasting_latest_txn`. + closed_channels: HashMap<ChannelId, String>, +} - let amt_per_path = amt / num_paths as u64; - let mut paths = Vec::with_capacity(num_paths); +impl ChannelCloseTracker { + fn new() -> Self { + Self { closed_channels: new_hash_map() } + } - for (i, &dest_scid) in dest_scids.iter().enumerate() { - let path_amt = if i == num_paths - 1 { - amt - amt_per_path * (num_paths as u64 - 1) - } else { - amt_per_path - }; + fn is_closed_or_closing(&self, channel_id: &ChannelId) -> bool { + self.closed_channels.contains_key(channel_id) + } - paths.push(Path { - hops: vec![RouteHop { - pubkey: dest.get_our_node_id(), - node_features: dest.node_features(), - short_channel_id: dest_scid, - channel_features: dest.channel_features(), - fee_msat: path_amt, - cltv_expiry_delta: 200, - maybe_announced_channel: true, - }], - blinded_tail: None, - }); + fn is_open(&self, channel_id: &ChannelId) -> bool { + !self.is_closed_or_closing(channel_id) } - let route_params = RouteParameters::from_payment_params_and_value( - PaymentParameters::from_node_id(dest.get_our_node_id(), TEST_FINAL_CLTV), - amt, - ); - let route = Route { paths, route_params: Some(route_params) }; - let onion = RecipientOnionFields::secret_only(payment_secret); - let res = source.send_payment_with_route(route, payment_hash, onion, payment_id); - match res { - Err(_) => false, - Ok(()) => check_payment_send_events(source, payment_id), + fn open_channels(&self, channel_ids: &[ChannelId]) -> Vec<ChannelId> { + channel_ids.iter().copied().filter(|channel_id| self.is_open(channel_id)).collect() } -} -/// Send an MPP payment from source to dest via middle node. -/// Supports multiple channels on either or both hops. -#[inline] -fn send_mpp_hop_payment( - source: &ChanMan, middle: &ChanMan, middle_scids: &[u64], dest: &ChanMan, dest_scids: &[u64], - amt: u64, payment_secret: PaymentSecret, payment_hash: PaymentHash, payment_id: PaymentId, -) -> bool { - // Create paths by pairing middle_scids with dest_scids - let num_paths = middle_scids.len().max(dest_scids.len()); - if num_paths == 0 { - return false; + fn has_closed_channels(&self) -> bool { + !self.closed_channels.is_empty() } - let first_hop_fee = 50_000; - let amt_per_path = amt / num_paths as u64; - let fee_per_path = first_hop_fee / num_paths as u64; - let mut paths = Vec::with_capacity(num_paths); + fn expect_channel_close(&mut self, channel_id: ChannelId, reason: String) { + assert!( + self.closed_channels.insert(channel_id, reason).is_none(), + "Channel {:?} close was already tracked", + channel_id, + ); + } - for i in 0..num_paths { - let middle_scid = middle_scids[i % middle_scids.len()]; - let dest_scid = dest_scids[i % dest_scids.len()]; + fn verify_channel_closed_event( + &mut self, channel_id: ChannelId, reason: &events::ClosureReason, + ) { + assert!( + self.closed_channels.contains_key(&channel_id), + "Channel {:?} closed without an explicit force-close: {:?}", + channel_id, + reason, + ); + } - let path_amt = if i == num_paths - 1 { - amt - amt_per_path * (num_paths as u64 - 1) - } else { - amt_per_path - }; - let path_fee = if i == num_paths - 1 { - first_hop_fee - fee_per_path * (num_paths as u64 - 1) - } else { - fee_per_path + fn is_expected_closed_channel_error_msg(&self, msg: &msgs::ErrorMessage) -> bool { + let expected_reason = match self.closed_channels.get(&msg.channel_id) { + Some(reason) => reason, + None => return false, }; + msg.data == *expected_reason + || msg.data + == "Channel closed because commitment or closing transaction was confirmed on chain." + // Messages queued before the close can be delivered + // after the counterparty has removed the channel. + || msg.data.starts_with( + "Got a message for a channel from the wrong node! No such channel_id", + ) + // A stale channel message may already have been delivered before + // the harness observes the close. If it errors against the same + // tracked channel, the result is part of explicit-close cleanup. + || msg.data + == "Peer sent an invalid channel_reestablish to force close in a non-standard way" + || msg.data.contains("when we needed a channel_reestablish") + } - paths.push(Path { - hops: vec![ - RouteHop { - pubkey: middle.get_our_node_id(), - node_features: middle.node_features(), - short_channel_id: middle_scid, - channel_features: middle.channel_features(), - fee_msat: path_fee, - cltv_expiry_delta: 100, - maybe_announced_channel: true, - }, - RouteHop { - pubkey: dest.get_our_node_id(), - node_features: dest.node_features(), - short_channel_id: dest_scid, - channel_features: dest.channel_features(), - fee_msat: path_amt, - cltv_expiry_delta: 200, - maybe_announced_channel: true, - }, - ], - blinded_tail: None, - }); + fn is_expected_closed_channel_warning_msg(&self, msg: &msgs::WarningMessage) -> bool { + self.closed_channels.contains_key(&msg.channel_id) + && msg.data == "Peer sent `stfu` when we were not in a live state" } +} + +#[derive(Clone, Copy, PartialEq)] +enum ChanType { + Legacy, + KeyedAnchors, + ZeroFeeCommitments, +} + +// While delivering messages, select across three possible message selection +// processes to maximize coverage. See the individual enum variants for details. +#[derive(Copy, Clone, PartialEq, Eq)] +enum ProcessMessages { + /// Deliver all available messages, including fetching any new messages from + /// `get_and_clear_pending_msg_events()` which may have side effects. + AllMessages, + /// Call `get_and_clear_pending_msg_events()` first, then deliver up to one + /// message, which may already be queued. + OneMessage, + /// Deliver up to one already-queued message. This avoids the side effects of + /// `get_and_clear_pending_msg_events()`, such as freeing the HTLC holding cell. + OnePendingMessage, +} + +struct HarnessNode<'a> { + node_id: u8, + node: ChanMan<'a>, + monitor: Arc<TestChainMonitor>, + persister: Arc<HarnessPersister>, + keys_manager: Arc<KeyProvider>, + logger: Arc<dyn Logger + MaybeSend + MaybeSync>, + broadcaster: Arc<TestBroadcaster>, + fee_estimator: Arc<FuzzEstimator>, + wallet: Arc<TestWalletSource>, + wallet_sync: Arc<WalletSync<Arc<TestWalletSource>, Arc<dyn Logger + MaybeSend + MaybeSync>>>, + bump_tx_handler: TestBumpTransactionEventHandler, + persistence_style: ChannelMonitorUpdateStatus, + deferred: bool, + serialized_manager: Vec<u8>, + serialized_manager_generation: u64, + last_htlc_clear_fee: u32, +} + +impl<'a> std::ops::Deref for HarnessNode<'a> { + type Target = ChanMan<'a>; - let route_params = RouteParameters::from_payment_params_and_value( - PaymentParameters::from_node_id(dest.get_our_node_id(), TEST_FINAL_CLTV), - amt, - ); - let route = Route { paths, route_params: Some(route_params) }; - let onion = RecipientOnionFields::secret_only(payment_secret); - let res = source.send_payment_with_route(route, payment_hash, onion, payment_id); - match res { - Err(_) => false, - Ok(()) => check_payment_send_events(source, payment_id), + fn deref(&self) -> &Self::Target { + &self.node } } -#[inline] -pub fn do_test<Out: Output>(data: &[u8], underlying_out: Out, anchors: bool) { - let out = SearchingOutput::new(underlying_out); - let broadcast = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); - let router = FuzzRouter {}; +impl<'a> HarnessNode<'a> { + fn build_logger<Out: Output + MaybeSend + MaybeSync>( + node_id: u8, out: &Out, + ) -> Arc<dyn Logger + MaybeSend + MaybeSync> { + Arc::new(test_logger::TestLogger::new(node_id.to_string(), out.clone())) + } - // Read initial monitor styles from fuzz input (1 byte: 2 bits per node) - let initial_mon_styles = if !data.is_empty() { data[0] } else { 0 }; - let mon_style = [ - RefCell::new(if initial_mon_styles & 0b01 != 0 { - ChannelMonitorUpdateStatus::InProgress - } else { - ChannelMonitorUpdateStatus::Completed - }), - RefCell::new(if initial_mon_styles & 0b10 != 0 { - ChannelMonitorUpdateStatus::InProgress - } else { - ChannelMonitorUpdateStatus::Completed - }), - RefCell::new(if initial_mon_styles & 0b100 != 0 { - ChannelMonitorUpdateStatus::InProgress - } else { - ChannelMonitorUpdateStatus::Completed - }), - ]; - - macro_rules! make_node { - ($node_id: expr, $fee_estimator: expr) => {{ - let logger: Arc<dyn Logger> = - Arc::new(test_logger::TestLogger::new($node_id.to_string(), out.clone())); - let node_secret = SecretKey::from_slice(&[ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 1, $node_id, - ]) - .unwrap(); - let keys_manager = Arc::new(KeyProvider { - node_secret, - rand_bytes_id: atomic::AtomicU32::new(0), - enforcement_states: Mutex::new(new_hash_map()), - }); - let monitor = Arc::new(TestChainMonitor::new( - broadcast.clone(), - logger.clone(), - $fee_estimator.clone(), - Arc::new(TestPersister { - update_ret: Mutex::new(mon_style[$node_id as usize].borrow().clone()), - }), - Arc::clone(&keys_manager), - )); - - let mut config = UserConfig::default(); - config.channel_config.forwarding_fee_proportional_millionths = 0; - config.channel_handshake_config.announce_for_forwarding = true; - config.reject_inbound_splices = false; - if !anchors { - config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; - } - let network = Network::Bitcoin; - let best_block_timestamp = genesis_block(network).header.time; - let params = ChainParameters { network, best_block: BestBlock::from_network(network) }; - ( - ChannelManager::new( - $fee_estimator.clone(), - monitor.clone(), - broadcast.clone(), - &router, - &router, - Arc::clone(&logger), - keys_manager.clone(), - keys_manager.clone(), - keys_manager.clone(), - config, - params, - best_block_timestamp, - ), - monitor, - keys_manager, - ) - }}; - } - - let reload_node = |ser: &Vec<u8>, - node_id: u8, - old_monitors: &TestChainMonitor, - mut use_old_mons, - keys, - fee_estimator| { - let keys_manager = Arc::clone(keys); - let logger: Arc<dyn Logger> = - Arc::new(test_logger::TestLogger::new(node_id.to_string(), out.clone())); - let chain_monitor = Arc::new(TestChainMonitor::new( - broadcast.clone(), - logger.clone(), + fn build_persister(persistence_style: ChannelMonitorUpdateStatus) -> Arc<HarnessPersister> { + Arc::new(HarnessPersister { + update_ret: Mutex::new(persistence_style), + latest_monitors: Mutex::new(new_hash_map()), + }) + } + + fn build_chain_monitor( + broadcaster: &Arc<TestBroadcaster>, fee_estimator: &Arc<FuzzEstimator>, + keys_manager: &Arc<KeyProvider>, logger: Arc<dyn Logger + MaybeSend + MaybeSync>, + persister: &Arc<HarnessPersister>, deferred: bool, + ) -> Arc<TestChainMonitor> { + Arc::new(chainmonitor::ChainMonitor::new( + None, + Arc::clone(broadcaster), + logger, Arc::clone(fee_estimator), - Arc::new(TestPersister { - update_ret: Mutex::new(ChannelMonitorUpdateStatus::Completed), - }), - Arc::clone(keys), - )); - - let mut config = UserConfig::default(); - config.channel_config.forwarding_fee_proportional_millionths = 0; - config.channel_handshake_config.announce_for_forwarding = true; - config.reject_inbound_splices = false; - if !anchors { - config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; - } + Arc::clone(persister), + Arc::clone(keys_manager), + keys_manager.get_peer_storage_key(), + deferred, + )) + } - let mut monitors = new_hash_map(); - let mut old_monitors = old_monitors.latest_monitors.lock().unwrap(); - for (channel_id, mut prev_state) in old_monitors.drain() { - let (mon_id, serialized_mon) = if use_old_mons % 3 == 0 { - // Reload with the oldest `ChannelMonitor` (the one that we already told - // `ChannelManager` we finished persisting). - (prev_state.persisted_monitor_id, prev_state.persisted_monitor) - } else if use_old_mons % 3 == 1 { - // Reload with the second-oldest `ChannelMonitor` - let old_mon = (prev_state.persisted_monitor_id, prev_state.persisted_monitor); - prev_state.pending_monitors.drain(..).next().unwrap_or(old_mon) - } else { - // Reload with the newest `ChannelMonitor` - let old_mon = (prev_state.persisted_monitor_id, prev_state.persisted_monitor); - prev_state.pending_monitors.pop().unwrap_or(old_mon) - }; - // Use a different value of `use_old_mons` if we have another monitor (only for node B) - // by shifting `use_old_mons` one in base-3. - use_old_mons /= 3; - let mon = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read( - &mut &serialized_mon[..], - (&**keys, &**keys), - ) - .expect("Failed to read monitor"); - monitors.insert(channel_id, mon.1); - // Update the latest `ChannelMonitor` state to match what we just told LDK. - prev_state.persisted_monitor = serialized_mon; - prev_state.persisted_monitor_id = mon_id; - // Wipe any `ChannelMonitor`s which we never told LDK we finished persisting, - // considering them discarded. LDK should replay these for us as they're stored in - // the `ChannelManager`. - prev_state.pending_monitors.clear(); - chain_monitor.latest_monitors.lock().unwrap().insert(channel_id, prev_state); + fn new<Out: Output + MaybeSend + MaybeSync>( + node_id: u8, wallet: Arc<TestWalletSource>, fee_estimator: Arc<FuzzEstimator>, + broadcaster: Arc<TestBroadcaster>, persistence_style: ChannelMonitorUpdateStatus, + deferred: bool, out: &Out, router: &'a FuzzRouter, chan_type: ChanType, + ) -> Self { + let logger = Self::build_logger(node_id, out); + let node_secret = SecretKey::from_slice(&[ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, node_id, + ]) + .unwrap(); + let keys_manager = Arc::new(KeyProvider { + node_secret, + rand_bytes_id: atomic::AtomicU32::new(0), + enforcement_states: Mutex::new(new_hash_map()), + }); + let persister = Self::build_persister(persistence_style); + let monitor = Self::build_chain_monitor( + &broadcaster, + &fee_estimator, + &keys_manager, + Arc::clone(&logger), + &persister, + deferred, + ); + let wallet_sync = Arc::new(WalletSync::new(Arc::clone(&wallet), Arc::clone(&logger))); + // Wallet-backed handler that completes and broadcasts the transactions + // requested by monitor BumpTransaction events. It shares the node's + // wallet sync so anchor spends and splice funding share UTXO lock state. + let bump_tx_handler = BumpTransactionEventHandlerSync::new( + Arc::clone(&broadcaster), + Arc::clone(&wallet_sync), + Arc::clone(&keys_manager), + Arc::clone(&logger), + ); + let network = Network::Bitcoin; + let best_block_timestamp = genesis_block(network).header.time; + let params = ChainParameters { network, best_block: BlockLocator::from_network(network) }; + let node = ChannelManager::new( + Arc::clone(&fee_estimator), + Arc::clone(&monitor), + Arc::clone(&broadcaster), + router, + router, + Arc::clone(&logger), + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + build_node_config(chan_type), + params, + best_block_timestamp, + ); + Self { + node_id, + node, + monitor, + persister, + keys_manager, + logger, + broadcaster, + fee_estimator, + wallet, + wallet_sync, + bump_tx_handler, + persistence_style, + deferred, + serialized_manager: Vec::new(), + serialized_manager_generation: 0, + last_htlc_clear_fee: 253, } - let mut monitor_refs = new_hash_map(); - for (channel_id, monitor) in monitors.iter() { - monitor_refs.insert(*channel_id, monitor); + } + + fn set_persistence_style(&mut self, style: ChannelMonitorUpdateStatus) { + // Store the style for the next reload. The active persister is intentionally not changed + // in place. + self.persistence_style = style; + } + + fn finish_monitor_update(&self, chan_id: ChannelId, monitor_id: u64, data: Vec<u8>) { + self.monitor.channel_monitor_updated(chan_id, monitor_id).unwrap(); + self.persister.mark_update_completed(chan_id, monitor_id, data); + } + + fn complete_all_monitor_updates(&self, chan_id: &ChannelId) -> bool { + let completed_updates = self.persister.drain_pending_updates(chan_id); + let completed_any = !completed_updates.is_empty(); + for (monitor_id, data) in completed_updates { + self.finish_monitor_update(*chan_id, monitor_id, data); } + completed_any + } - let read_args = ChannelManagerReadArgs { - entropy_source: Arc::clone(&keys_manager), - node_signer: Arc::clone(&keys_manager), - signer_provider: keys_manager, - fee_estimator: Arc::clone(fee_estimator), - chain_monitor: chain_monitor.clone(), - tx_broadcaster: broadcast.clone(), - router: &router, - message_router: &router, - logger, - config, - channel_monitors: monitor_refs, - }; + fn complete_all_pending_monitor_updates(&self) { + for (channel_id, monitor_id, data) in self.persister.drain_all_pending_updates() { + self.finish_monitor_update(channel_id, monitor_id, data); + } + } - let manager = - <(BlockHash, ChanMan)>::read(&mut &ser[..], read_args).expect("Failed to read manager"); - let res = (manager.1, chain_monitor.clone()); - for (channel_id, mon) in monitors.drain() { - assert_eq!( - chain_monitor.chain_monitor.watch_channel(channel_id, mon), - Ok(ChannelMonitorUpdateStatus::Completed) - ); + fn complete_monitor_update(&self, chan_id: &ChannelId, selector: MonitorUpdateSelector) { + if let Some((monitor_id, data)) = self.persister.take_pending_update(chan_id, selector) { + self.finish_monitor_update(*chan_id, monitor_id, data); } - *chain_monitor.persister.update_ret.lock().unwrap() = *mon_style[node_id as usize].borrow(); - res - }; + } - let mut channel_txn = Vec::new(); - macro_rules! complete_all_pending_monitor_updates { - ($monitor: expr) => {{ - for (channel_id, state) in $monitor.latest_monitors.lock().unwrap().iter_mut() { - for (id, data) in state.pending_monitors.drain(..) { - $monitor.chain_monitor.channel_monitor_updated(*channel_id, id).unwrap(); - if id >= state.persisted_monitor_id { - state.persisted_monitor_id = id; - state.persisted_monitor = data; - } - } + fn manager_height(&self) -> u32 { + self.node.current_best_block().height + } + + // Connects a block range to the ChannelManager, and to the ChainMonitor when + // sync_monitors is set. Reload syncs monitors separately because they can be + // at different heights than the manager, so it leaves them out here. + fn connect_chain_range( + &mut self, chain_state: &ChainState, start_height: u32, target_height: u32, + sync_monitors: bool, + ) { + assert!( + target_height >= start_height, + "connect_chain_range cannot move height backward ({} -> {})", + start_height, + target_height + ); + let mut height = start_height; + while height < target_height { + let mut next_height = height + 1; + while next_height <= target_height && chain_state.block_at(next_height).1.is_empty() { + next_height += 1; } - }}; - } - macro_rules! connect_peers { - ($source: expr, $dest: expr) => {{ - let init_dest = Init { - features: $dest.init_features(), - networks: None, - remote_network_address: None, - }; - $source.peer_connected($dest.get_our_node_id(), &init_dest, true).unwrap(); - let init_src = Init { - features: $source.init_features(), - networks: None, - remote_network_address: None, - }; - $dest.peer_connected($source.get_our_node_id(), &init_src, false).unwrap(); - }}; - } - macro_rules! make_channel { - ($source: expr, $dest: expr, $source_monitor: expr, $dest_monitor: expr, $dest_keys_manager: expr, $chan_id: expr) => {{ - $source.create_channel($dest.get_our_node_id(), 100_000, 42, 0, None, None).unwrap(); - let open_channel = { - let events = $source.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - if let MessageSendEvent::SendOpenChannel { ref msg, .. } = events[0] { - msg.clone() - } else { - panic!("Wrong event type"); + if next_height > target_height { + // The rest of the range is empty. One best-block update to the + // final height is enough because LDK's Confirm API explicitly + // allows best_block_updated to skip intermediary blocks. + height = target_height; + let (header, _) = chain_state.block_at(height); + if sync_monitors { + self.monitor.best_block_updated(header, height); } - }; + self.node.best_block_updated(header, height); + break; + } + height = next_height; + let (header, txn) = chain_state.block_at(height); + let txdata: Vec<_> = txn.iter().enumerate().map(|(i, tx)| (i + 1, tx)).collect(); + if sync_monitors { + self.monitor.transactions_confirmed(header, &txdata, height); + } + self.node.transactions_confirmed(header, &txdata, height); + if sync_monitors { + self.monitor.best_block_updated(header, height); + } + self.node.best_block_updated(header, height); + } + } - $dest.handle_open_channel($source.get_our_node_id(), &open_channel); - let accept_channel = { - let events = $dest.get_and_clear_pending_events(); - assert_eq!(events.len(), 1); - if let events::Event::OpenChannelRequest { - ref temporary_channel_id, - ref counterparty_node_id, - .. - } = events[0] - { - let mut random_bytes = [0u8; 16]; - random_bytes - .copy_from_slice(&$dest_keys_manager.get_secure_random_bytes()[..16]); - let user_channel_id = u128::from_be_bytes(random_bytes); - $dest - .accept_inbound_channel( - temporary_channel_id, - counterparty_node_id, - user_channel_id, - None, - ) - .unwrap(); - } else { - panic!("Wrong event type"); - } - let events = $dest.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - if let MessageSendEvent::SendAcceptChannel { ref msg, .. } = events[0] { - msg.clone() - } else { - panic!("Wrong event type"); - } - }; + fn sync_with_chain_state(&mut self, chain_state: &ChainState, num_blocks: Option<u32>) { + let target_height = if let Some(num_blocks) = num_blocks { + std::cmp::min(self.manager_height() + num_blocks, chain_state.tip_height()) + } else { + chain_state.tip_height() + }; - $source.handle_accept_channel($dest.get_our_node_id(), &accept_channel); - { - let mut events = $source.get_and_clear_pending_events(); - assert_eq!(events.len(), 1); - if let events::Event::FundingGenerationReady { - temporary_channel_id, - channel_value_satoshis, - output_script, - .. - } = events.pop().unwrap() - { - let tx = Transaction { - version: Version($chan_id), - lock_time: LockTime::ZERO, - input: Vec::new(), - output: vec![TxOut { - value: Amount::from_sat(channel_value_satoshis), - script_pubkey: output_script, - }], - }; - $source - .funding_transaction_generated( - temporary_channel_id, - $dest.get_our_node_id(), - tx.clone(), - ) - .unwrap(); - channel_txn.push(tx); - } else { - panic!("Wrong event type"); - } - } + let start_height = self.manager_height(); + self.connect_chain_range(chain_state, start_height, target_height, true); + } - let funding_created = { - let events = $source.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - if let MessageSendEvent::SendFundingCreated { ref msg, .. } = events[0] { - msg.clone() - } else { - panic!("Wrong event type"); - } + // Brings every channel monitor up to the chain tip from its own best block. + // On reload monitors can sit at different heights, so syncing them one by + // one avoids replaying a block into a monitor that already saw it, which the + // monitor would treat as a reorg. Each block is connected the same way as + // live operation: confirm its transactions, then advance the best block, + // ending with a best-block update to the tip for the trailing empty blocks. + fn sync_monitors_to_tip(&self, chain_state: &ChainState) { + let target_height = chain_state.tip_height(); + for chan_id in self.monitor.list_monitors() { + let monitor = match self.monitor.get_monitor(chan_id) { + Ok(monitor) => monitor, + Err(_) => continue, }; - $dest.handle_funding_created($source.get_our_node_id(), &funding_created); - // Complete any pending monitor updates for dest after watch_channel - complete_all_pending_monitor_updates!($dest_monitor); - - let (funding_signed, channel_id) = { - let events = $dest.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - if let MessageSendEvent::SendFundingSigned { ref msg, .. } = events[0] { - (msg.clone(), msg.channel_id.clone()) - } else { - panic!("Wrong event type"); + let start_height = monitor.current_best_block().height; + if start_height >= target_height { + continue; + } + for height in (start_height + 1)..=target_height { + let (header, txn) = chain_state.block_at(height); + if txn.is_empty() { + continue; } - }; - let events = $dest.get_and_clear_pending_events(); - assert_eq!(events.len(), 1); - if let events::Event::ChannelPending { ref counterparty_node_id, .. } = events[0] { - assert_eq!(counterparty_node_id, &$source.get_our_node_id()); - } else { - panic!("Wrong event type"); + let txdata: Vec<_> = txn.iter().enumerate().map(|(i, tx)| (i + 1, tx)).collect(); + monitor.transactions_confirmed( + header, + &txdata, + height, + &self.broadcaster, + &self.fee_estimator, + &self.logger, + ); + monitor.best_block_updated( + header, + height, + &self.broadcaster, + &self.fee_estimator, + &self.logger, + ); + } + let (header, txn) = chain_state.block_at(target_height); + if txn.is_empty() { + // The tip block carried no transactions, so it was skipped above. + // Advance the best block over the trailing empty blocks to the tip. + monitor.best_block_updated( + header, + target_height, + &self.broadcaster, + &self.fee_estimator, + &self.logger, + ); } + } + } - $source.handle_funding_signed($dest.get_our_node_id(), &funding_signed); - // Complete any pending monitor updates for source after watch_channel - complete_all_pending_monitor_updates!($source_monitor); - - let events = $source.get_and_clear_pending_events(); - assert_eq!(events.len(), 1); - if let events::Event::ChannelPending { - ref counterparty_node_id, - channel_id: ref event_channel_id, - .. - } = events[0] - { - assert_eq!(counterparty_node_id, &$dest.get_our_node_id()); - assert_eq!(*event_channel_id, channel_id); + fn checkpoint_manager_persistence(&mut self) -> bool { + if self.node.get_and_clear_needs_persistence() { + let pending_monitor_writes = self.monitor.pending_operation_count(); + self.serialized_manager = self.node.encode(); + self.serialized_manager_generation += 1; + if self.deferred { + self.monitor.flush(pending_monitor_writes, &self.logger); } else { - panic!("Wrong event type"); + assert_eq!(pending_monitor_writes, 0); } + true + } else { + assert_eq!(self.monitor.pending_operation_count(), 0); + false + } + } + + fn force_checkpoint_manager_persistence(&mut self) { + let pending_monitor_writes = self.monitor.pending_operation_count(); + self.serialized_manager = self.node.encode(); + self.serialized_manager_generation += 1; + self.node.get_and_clear_needs_persistence(); + if self.deferred { + self.monitor.flush(pending_monitor_writes, &self.logger); + } else { + assert_eq!(pending_monitor_writes, 0); + } + } - channel_id - }}; + fn next_manager_persistence_generation(&self) -> u64 { + self.serialized_manager_generation + 1 } - macro_rules! confirm_txn { - ($node: expr) => {{ - let chain_hash = genesis_block(Network::Bitcoin).block_hash(); - let mut header = create_dummy_header(chain_hash, 42); - let txdata: Vec<_> = - channel_txn.iter().enumerate().map(|(i, tx)| (i + 1, tx)).collect(); - $node.transactions_confirmed(&header, &txdata, 1); - for _ in 2..100 { - header = create_dummy_header(header.block_hash(), 42); - } - $node.best_block_updated(&header, 99); - }}; + fn bump_fee_estimate(&mut self, chan_type: ChanType) { + let mut max_feerate = self.last_htlc_clear_fee; + if matches!(chan_type, ChanType::Legacy) { + max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; + } + if self.fee_estimator.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate { + self.fee_estimator.ret_val.store(max_feerate, atomic::Ordering::Release); + } + self.node.timer_tick_occurred(); } - macro_rules! lock_fundings { - ($nodes: expr) => {{ - let mut node_events = Vec::new(); - for node in $nodes.iter() { - node_events.push(node.get_and_clear_pending_msg_events()); - } - for (idx, node_event) in node_events.iter().enumerate() { - for event in node_event { - if let MessageSendEvent::SendChannelReady { ref node_id, ref msg } = event { - for node in $nodes.iter() { - if node.get_our_node_id() == *node_id { - node.handle_channel_ready($nodes[idx].get_our_node_id(), msg); - } - } - } else { - panic!("Wrong event type"); - } - } + fn reset_fee_estimate(&self) { + self.fee_estimator.ret_val.store(253, atomic::Ordering::Release); + self.node.timer_tick_occurred(); + } + + // Re-enables holder claim signing and asks the chain monitor to retry + // pending claim transactions. Different on-chain claim paths use + // SignHolderCommitment or SignHolderHtlcTransaction for force-closed channels. + fn enable_holder_signer_ops(&self) { + self.keys_manager.enable_op_for_all_signers(SignerOp::SignHolderCommitment); + self.keys_manager.enable_op_for_all_signers(SignerOp::SignHolderHtlcTransaction); + self.monitor.signer_unblocked(None); + } + + fn current_feerate_sat_per_kw(&self) -> FeeRate { + self.fee_estimator.feerate_sat_per_kw() + } + + fn record_last_htlc_clear_fee(&mut self) { + self.last_htlc_clear_fee = self.fee_estimator.ret_val.load(atomic::Ordering::Acquire); + } + + // Drains raw ChannelMonitor events. Monitor-generated BumpTransaction events + // do not flow through the manager event queue but still produce transactions + // the harness must mine. SpendableOutputs and DiscardFunding may also surface + // here, but the harness does not model an external sweeper wallet. + fn process_monitor_pending_events(&self) -> bool { + // process_pending_events takes an Fn handler, so use interior mutability + // to report whether the callback saw anything. + let had_events = Cell::new(false); + self.monitor.process_pending_events(&|event: events::Event| { + had_events.set(true); + match event { + events::Event::BumpTransaction(bump) => { + self.bump_tx_handler.handle_event(&bump); + }, + events::Event::SpendableOutputs { .. } => {}, + events::Event::DiscardFunding { .. } => {}, + event => panic!("Unhandled monitor event: {:?}", event), } + Ok(()) + }); + had_events.get() + } - for node in $nodes.iter() { - let events = node.get_and_clear_pending_msg_events(); - for event in events { - if let MessageSendEvent::SendAnnouncementSignatures { .. } = event { - } else { - panic!("Wrong event type"); - } + fn splice_in(&self, counterparty_node_id: &PublicKey, channel_id: &ChannelId) { + match self.node.splice_channel(channel_id, counterparty_node_id) { + Ok(funding_template) => { + let feerate = + funding_template.min_rbf_feerate().unwrap_or(self.current_feerate_sat_per_kw()); + if let Ok(contribution) = funding_template.splice_in_sync( + Amount::from_sat(10_000), + feerate, + FeeRate::MAX, + self.wallet_sync.as_ref(), + ) { + let _ = self.node.funding_contributed( + channel_id, + counterparty_node_id, + contribution, + None, + ); } - } - }}; - } - - let wallet_a = TestWallet::new(SecretKey::from_slice(&[1; 32]).unwrap()); - let wallet_b = TestWallet::new(SecretKey::from_slice(&[2; 32]).unwrap()); - let wallet_c = TestWallet::new(SecretKey::from_slice(&[3; 32]).unwrap()); - let wallets = vec![wallet_a, wallet_b, wallet_c]; - let coinbase_tx = bitcoin::Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![bitcoin::TxIn { ..Default::default() }], - output: wallets + }, + Err(e) => { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")), + "{:?}", + e + ); + }, + } + } + + fn splice_out(&self, counterparty_node_id: &PublicKey, channel_id: &ChannelId) { + // We conditionally splice out `MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS` only when the node + // has double the balance required to send a payment upon a `0xff` byte. We do this to + // ensure there's always liquidity available for a payment to succeed then. + let outbound_capacity_msat = self + .node + .list_channels() .iter() - .map(|w| TxOut { - value: Amount::from_sat(100_000), - script_pubkey: w.get_change_script().unwrap(), - }) - .collect(), - }; - let coinbase_txid = coinbase_tx.compute_txid(); - wallets.iter().enumerate().for_each(|(i, w)| { - w.add_utxo( - bitcoin::OutPoint { txid: coinbase_txid, vout: i as u32 }, - Amount::from_sat(100_000), + .find(|chan| chan.channel_id == *channel_id) + .map(|chan| chan.outbound_capacity_msat) + .unwrap(); + if outbound_capacity_msat < 20_000_000 { + return; + } + match self.node.splice_channel(channel_id, counterparty_node_id) { + Ok(funding_template) => { + let feerate = + funding_template.min_rbf_feerate().unwrap_or(self.current_feerate_sat_per_kw()); + let outputs = vec![TxOut { + value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), + script_pubkey: self.wallet.get_change_script().unwrap(), + }]; + if let Ok(contribution) = + funding_template.splice_out(outputs, feerate, FeeRate::MAX) + { + let _ = self.node.funding_contributed( + channel_id, + counterparty_node_id, + contribution, + None, + ); + } + }, + Err(e) => { + assert!( + matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice")), + "{:?}", + e + ); + }, + } + } + + fn reload<Out: Output + MaybeSend + MaybeSync>( + &mut self, use_old_mons: u8, out: &Out, router: &'a FuzzRouter, chan_type: ChanType, + ) -> u64 { + let loaded_manager_generation = self.serialized_manager_generation; + let logger = Self::build_logger(self.node_id, out); + let persister = Self::build_persister(self.persistence_style); + let chain_monitor = Self::build_chain_monitor( + &self.broadcaster, + &self.fee_estimator, + &self.keys_manager, + Arc::clone(&logger), + &persister, + self.deferred, ); - }); - - let fee_est_a = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); - let mut last_htlc_clear_fee_a = 253; - let fee_est_b = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); - let mut last_htlc_clear_fee_b = 253; - let fee_est_c = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); - let mut last_htlc_clear_fee_c = 253; - - // 3 nodes is enough to hit all the possible cases, notably unknown-source-unknown-dest - // forwarding. - let (node_a, mut monitor_a, keys_manager_a) = make_node!(0, fee_est_a); - let (node_b, mut monitor_b, keys_manager_b) = make_node!(1, fee_est_b); - let (node_c, mut monitor_c, keys_manager_c) = make_node!(2, fee_est_c); - - let mut nodes = [node_a, node_b, node_c]; - - // Connect peers first, then create channels - connect_peers!(nodes[0], nodes[1]); - connect_peers!(nodes[1], nodes[2]); - - // Create 3 channels between A-B and 3 channels between B-C (6 total). - // - // Use version numbers 1-6 to avoid txid collisions under fuzz hashing. - // Fuzz mode uses XOR-based hashing (all bytes XOR to one byte), and - // versions 0-5 cause collisions between A-B and B-C channel pairs - // (e.g., A-B with Version(1) collides with B-C with Version(3)). - let chan_ab_ids = [ - make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 1), - make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 2), - make_channel!(nodes[0], nodes[1], monitor_a, monitor_b, keys_manager_b, 3), - ]; - let chan_bc_ids = [ - make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 4), - make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 5), - make_channel!(nodes[1], nodes[2], monitor_b, monitor_c, keys_manager_c, 6), - ]; - - // Wipe the transactions-broadcasted set to make sure we don't broadcast any transactions - // during normal operation in `test_return`. - broadcast.txn_broadcasted.borrow_mut().clear(); - for node in nodes.iter() { - confirm_txn!(node); - } - - lock_fundings!(nodes); - - // Get SCIDs for all A-B channels (from node A's perspective) - let node_a_chans: Vec<_> = nodes[0].list_usable_channels(); - let chan_ab_scids: [u64; 3] = [ - node_a_chans[0].short_channel_id.unwrap(), - node_a_chans[1].short_channel_id.unwrap(), - node_a_chans[2].short_channel_id.unwrap(), - ]; - let chan_ab_chan_ids: [ChannelId; 3] = - [node_a_chans[0].channel_id, node_a_chans[1].channel_id, node_a_chans[2].channel_id]; - // Get SCIDs for all B-C channels (from node C's perspective) - let node_c_chans: Vec<_> = nodes[2].list_usable_channels(); - let chan_bc_scids: [u64; 3] = [ - node_c_chans[0].short_channel_id.unwrap(), - node_c_chans[1].short_channel_id.unwrap(), - node_c_chans[2].short_channel_id.unwrap(), - ]; - let chan_bc_chan_ids: [ChannelId; 3] = - [node_c_chans[0].channel_id, node_c_chans[1].channel_id, node_c_chans[2].channel_id]; - // Keep old names for backward compatibility in existing code - let chan_a = chan_ab_scids[0]; - let chan_a_id = chan_ab_chan_ids[0]; - let chan_b = chan_bc_scids[0]; - let chan_b_id = chan_bc_chan_ids[0]; - - let mut p_ctr: u64 = 0; - - let mut peers_ab_disconnected = false; - let mut peers_bc_disconnected = false; - let mut ab_events = Vec::new(); - let mut ba_events = Vec::new(); - let mut bc_events = Vec::new(); - let mut cb_events = Vec::new(); - - let mut node_a_ser = nodes[0].encode(); - let mut node_b_ser = nodes[1].encode(); - let mut node_c_ser = nodes[2].encode(); - - let pending_payments = RefCell::new([Vec::new(), Vec::new(), Vec::new()]); - let resolved_payments = RefCell::new([Vec::new(), Vec::new(), Vec::new()]); - - macro_rules! test_return { - () => {{ - assert_eq!(nodes[0].list_channels().len(), 3); - assert_eq!(nodes[1].list_channels().len(), 6); - assert_eq!(nodes[2].list_channels().len(), 3); - - // At no point should we have broadcasted any transactions after the initial channel - // opens. - assert!(broadcast.txn_broadcasted.borrow().is_empty()); + let mut monitors = new_hash_map(); + let mut use_old_mons = use_old_mons; + { + let mut old_monitors = self.persister.latest_monitors.lock().unwrap(); + for (channel_id, mut prev_state) in old_monitors.drain() { + let selector = match use_old_mons % 3 { + 0 => MonitorReloadSelector::Persisted, + 1 => MonitorReloadSelector::FirstPending, + _ => MonitorReloadSelector::LastPending, + }; + prev_state.select_monitor_for_reload(selector); + // Use a different trit for each monitor so one restart byte can vary the stale + // monitor depth across multiple monitors for the node. + use_old_mons /= 3; + let mon = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read( + &mut &prev_state.persisted_monitor[..], + (&*self.keys_manager, &*self.keys_manager), + ) + .expect("Failed to read monitor"); + monitors.insert(channel_id, mon.1); + persister.latest_monitors.lock().unwrap().insert(channel_id, prev_state); + } + } + let mut monitor_refs = new_hash_map(); + for (channel_id, monitor) in monitors.iter() { + monitor_refs.insert(*channel_id, monitor); + } - return; - }}; + let read_args = ChannelManagerReadArgs { + entropy_source: Arc::clone(&self.keys_manager), + node_signer: Arc::clone(&self.keys_manager), + signer_provider: Arc::clone(&self.keys_manager), + fee_estimator: Arc::clone(&self.fee_estimator), + chain_monitor: Arc::clone(&chain_monitor), + tx_broadcaster: Arc::clone(&self.broadcaster), + router, + message_router: router, + logger: Arc::clone(&logger), + config: build_node_config(chan_type), + channel_monitors: monitor_refs, + }; + + let manager = <(BlockLocator, ChanMan)>::read(&mut &self.serialized_manager[..], read_args) + .expect("Failed to read manager"); + let expected_status = if self.deferred { + ChannelMonitorUpdateStatus::InProgress + } else { + self.persistence_style + }; + for (channel_id, mon) in monitors.drain() { + assert_eq!(chain_monitor.watch_channel(channel_id, mon), Ok(expected_status)); + } + self.node = manager.1; + self.monitor = chain_monitor; + self.persister = persister; + self.logger = logger; + // In deferred mode, the startup watch_channel registrations above queue monitor operations + // even if the reloaded ChannelManager does not need persistence. Always checkpoint here so + // those registrations can be flushed against the manager snapshot they belong to. + self.force_checkpoint_manager_persistence(); + loaded_manager_generation + } +} + +#[derive(Copy, Clone)] +enum MonitorReloadSelector { + Persisted, + FirstPending, + LastPending, +} + +#[derive(Copy, Clone)] +enum MonitorUpdateSelector { + First, + Second, + Last, +} + +#[derive(Copy, Clone)] +enum MppDirectChannels { + All, + RepeatedFirst, +} + +#[derive(Copy, Clone)] +enum MppHopChannels { + FirstHop, + BothHops, + SecondHop, +} + +struct EventQueues { + ab: Vec<MessageSendEvent>, + ba: Vec<MessageSendEvent>, + bc: Vec<MessageSendEvent>, + cb: Vec<MessageSendEvent>, +} + +impl EventQueues { + fn new() -> Self { + Self { ab: Vec::new(), ba: Vec::new(), bc: Vec::new(), cb: Vec::new() } + } + + fn take_for_node(&mut self, node_idx: usize) -> Vec<MessageSendEvent> { + match node_idx { + 0 => { + let mut events = Vec::new(); + mem::swap(&mut events, &mut self.ab); + events + }, + 1 => { + let mut events = Vec::new(); + mem::swap(&mut events, &mut self.ba); + events.extend_from_slice(&self.bc[..]); + self.bc.clear(); + events + }, + 2 => { + let mut events = Vec::new(); + mem::swap(&mut events, &mut self.cb); + events + }, + _ => panic!("invalid node index"), + } + } + + fn push_for_node(&mut self, node_idx: usize, event: MessageSendEvent) { + match node_idx { + 0 => self.ab.push(event), + 2 => self.cb.push(event), + _ => panic!("cannot directly queue messages for node {}", node_idx), + } + } + + fn extend_for_node<I: IntoIterator<Item = MessageSendEvent>>( + &mut self, node_idx: usize, events: I, + ) { + match node_idx { + 0 => self.ab.extend(events), + 2 => self.cb.extend(events), + _ => panic!("cannot directly queue messages for node {}", node_idx), + } } - let mut read_pos = 1; // First byte was consumed for initial mon_style - macro_rules! get_slice { - ($len: expr) => {{ - let slice_len = $len as usize; - if data.len() < read_pos + slice_len { - test_return!(); + fn route_from_middle<'a, I: IntoIterator<Item = MessageSendEvent>>( + &mut self, excess_events: I, expect_drop_node: Option<usize>, nodes: &[HarnessNode<'a>; 3], + close_tracker: &ChannelCloseTracker, + ) { + // Push any events from Node B onto queues.ba and queues.bc. + let a_id = nodes[0].get_our_node_id(); + let expect_drop_id = expect_drop_node.map(|id| nodes[id].get_our_node_id()); + for event in excess_events { + let push_a = match event { + MessageSendEvent::UpdateHTLCs { ref node_id, .. } + | MessageSendEvent::SendRevokeAndACK { ref node_id, .. } + | MessageSendEvent::SendChannelReestablish { ref node_id, .. } + | MessageSendEvent::SendStfu { ref node_id, .. } + | MessageSendEvent::SendSpliceInit { ref node_id, .. } + | MessageSendEvent::SendSpliceAck { ref node_id, .. } + | MessageSendEvent::SendSpliceLocked { ref node_id, .. } + | MessageSendEvent::SendTxAddInput { ref node_id, .. } + | MessageSendEvent::SendTxAddOutput { ref node_id, .. } + | MessageSendEvent::SendTxRemoveInput { ref node_id, .. } + | MessageSendEvent::SendTxRemoveOutput { ref node_id, .. } + | MessageSendEvent::SendTxComplete { ref node_id, .. } + | MessageSendEvent::SendTxAbort { ref node_id, .. } + | MessageSendEvent::SendTxInitRbf { ref node_id, .. } + | MessageSendEvent::SendTxAckRbf { ref node_id, .. } + | MessageSendEvent::SendTxSignatures { ref node_id, .. } + | MessageSendEvent::SendChannelUpdate { ref node_id, .. } => { + if Some(*node_id) == expect_drop_id { + panic!( + "peer_disconnected should drop msgs bound for the disconnected peer" + ); + } + *node_id == a_id + }, + MessageSendEvent::HandleError { ref action, ref node_id } => { + assert_disconnect_action(action, close_tracker); + if Some(*node_id) == expect_drop_id { + panic!( + "peer_disconnected should drop msgs bound for the disconnected peer" + ); + } + *node_id == a_id + }, + MessageSendEvent::SendChannelReady { .. } + | MessageSendEvent::SendAnnouncementSignatures { .. } + | MessageSendEvent::BroadcastChannelUpdate { .. } => continue, + _ => panic!("Unhandled message event {:?}", event), + }; + if push_a { + self.ba.push(event); + } else { + self.bc.push(event); } - read_pos += slice_len; - &data[read_pos - slice_len..read_pos] - }}; - } - - loop { - // Push any events from Node B onto ba_events and bc_events - macro_rules! push_excess_b_events { - ($excess_events: expr, $expect_drop_node: expr) => { { - let a_id = nodes[0].get_our_node_id(); - let expect_drop_node: Option<usize> = $expect_drop_node; - let expect_drop_id = if let Some(id) = expect_drop_node { Some(nodes[id].get_our_node_id()) } else { None }; - for event in $excess_events { - let push_a = match event { - MessageSendEvent::UpdateHTLCs { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendRevokeAndACK { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendChannelReestablish { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendStfu { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendSpliceInit { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendSpliceAck { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendSpliceLocked { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendTxAddInput { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendTxAddOutput { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendTxComplete { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendTxAbort { ref node_id, .. } => { - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - MessageSendEvent::SendChannelReady { .. } => continue, - MessageSendEvent::SendAnnouncementSignatures { .. } => continue, - MessageSendEvent::SendChannelUpdate { ref node_id, ref msg } => { - assert_eq!(msg.contents.channel_flags & 2, 0); // The disable bit must never be set! - if Some(*node_id) == expect_drop_id { panic!("peer_disconnected should drop msgs bound for the disconnected peer"); } - *node_id == a_id - }, - _ => panic!("Unhandled message event {:?}", event), - }; - if push_a { ba_events.push(event); } else { bc_events.push(event); } - } - } } - } - - // While delivering messages, we select across three possible message selection processes - // to ensure we get as much coverage as possible. See the individual enum variants for more - // details. - #[derive(PartialEq)] - enum ProcessMessages { - /// Deliver all available messages, including fetching any new messages from - /// `get_and_clear_pending_msg_events()` (which may have side effects). - AllMessages, - /// Call `get_and_clear_pending_msg_events()` first, and then deliver up to one - /// message (which may already be queued). - OneMessage, - /// Deliver up to one already-queued message. This avoids any potential side-effects - /// of `get_and_clear_pending_msg_events()` (eg freeing the HTLC holding cell), which - /// provides potentially more coverage. - OnePendingMessage, - } - - macro_rules! process_msg_events { - ($node: expr, $corrupt_forward: expr, $limit_events: expr) => { { - let mut events = if $node == 1 { - let mut new_events = Vec::new(); - mem::swap(&mut new_events, &mut ba_events); - new_events.extend_from_slice(&bc_events[..]); - bc_events.clear(); - new_events - } else if $node == 0 { - let mut new_events = Vec::new(); - mem::swap(&mut new_events, &mut ab_events); - new_events - } else { - let mut new_events = Vec::new(); - mem::swap(&mut new_events, &mut cb_events); - new_events - }; - let mut new_events = Vec::new(); - if $limit_events != ProcessMessages::OnePendingMessage { - new_events = nodes[$node].get_and_clear_pending_msg_events(); - } - let mut had_events = false; - let mut events_iter = events.drain(..).chain(new_events.drain(..)); - let mut extra_ev = None; - for event in &mut events_iter { - had_events = true; + } + } + + fn clear_link(&mut self, link: &PeerLink) { + match (link.node_a, link.node_b) { + (0, 1) | (1, 0) => { + self.ab.clear(); + self.ba.clear(); + }, + (1, 2) | (2, 1) => { + self.bc.clear(); + self.cb.clear(); + }, + _ => panic!("unsupported link"), + } + } + + fn drain_on_disconnect( + &mut self, edge_node: usize, nodes: &[HarnessNode<'_>; 3], + close_tracker: &ChannelCloseTracker, + ) { + match edge_node { + 0 => { + for event in nodes[0].get_and_clear_pending_msg_events() { match event { - MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates: CommitmentUpdate { update_add_htlcs, update_fail_htlcs, update_fulfill_htlcs, update_fail_malformed_htlcs, update_fee, commitment_signed } } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == node_id { - for update_add in update_add_htlcs.iter() { - out.locked_write(format!("Delivering update_add_htlc from node {} to node {}.\n", $node, idx).as_bytes()); - if !$corrupt_forward { - dest.handle_update_add_htlc(nodes[$node].get_our_node_id(), update_add); - } else { - // Corrupt the update_add_htlc message so that its HMAC - // check will fail and we generate a - // update_fail_malformed_htlc instead of an - // update_fail_htlc as we do when we reject a payment. - let mut msg_ser = update_add.encode(); - msg_ser[1000] ^= 0xff; - let new_msg = UpdateAddHTLC::read_from_fixed_length_buffer(&mut &msg_ser[..]).unwrap(); - dest.handle_update_add_htlc(nodes[$node].get_our_node_id(), &new_msg); - } - } - let processed_change = !update_add_htlcs.is_empty() || !update_fulfill_htlcs.is_empty() || - !update_fail_htlcs.is_empty() || !update_fail_malformed_htlcs.is_empty(); - for update_fulfill in update_fulfill_htlcs { - out.locked_write(format!("Delivering update_fulfill_htlc from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_update_fulfill_htlc(nodes[$node].get_our_node_id(), update_fulfill); - } - for update_fail in update_fail_htlcs.iter() { - out.locked_write(format!("Delivering update_fail_htlc from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_update_fail_htlc(nodes[$node].get_our_node_id(), update_fail); - } - for update_fail_malformed in update_fail_malformed_htlcs.iter() { - out.locked_write(format!("Delivering update_fail_malformed_htlc from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_update_fail_malformed_htlc(nodes[$node].get_our_node_id(), update_fail_malformed); - } - if let Some(msg) = update_fee { - out.locked_write(format!("Delivering update_fee from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_update_fee(nodes[$node].get_our_node_id(), &msg); - } - if $limit_events != ProcessMessages::AllMessages && processed_change { - // If we only want to process some messages, don't deliver the CS until later. - extra_ev = Some(MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates: CommitmentUpdate { - update_add_htlcs: Vec::new(), - update_fail_htlcs: Vec::new(), - update_fulfill_htlcs: Vec::new(), - update_fail_malformed_htlcs: Vec::new(), - update_fee: None, - commitment_signed - } }); - break; - } - out.locked_write(format!("Delivering commitment_signed from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_commitment_signed_batch_test(nodes[$node].get_our_node_id(), &commitment_signed); - break; - } - } - }, - MessageSendEvent::SendRevokeAndACK { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering revoke_and_ack from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_revoke_and_ack(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendChannelReestablish { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering channel_reestablish from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_channel_reestablish(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendStfu { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering stfu from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_stfu(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendTxAddInput { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering tx_add_input from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_tx_add_input(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendTxAddOutput { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering tx_add_output from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_tx_add_output(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendTxRemoveInput { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering tx_remove_input from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_tx_remove_input(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendTxRemoveOutput { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering tx_remove_output from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_tx_remove_output(nodes[$node].get_our_node_id(), msg); - } - } + MessageSendEvent::UpdateHTLCs { .. } => {}, + MessageSendEvent::SendRevokeAndACK { .. } => {}, + MessageSendEvent::SendChannelReestablish { .. } => {}, + MessageSendEvent::SendStfu { .. } => {}, + MessageSendEvent::SendChannelReady { .. } => {}, + MessageSendEvent::SendAnnouncementSignatures { .. } => {}, + MessageSendEvent::BroadcastChannelUpdate { .. } => {}, + MessageSendEvent::SendChannelUpdate { .. } => {}, + MessageSendEvent::HandleError { ref action, .. } => { + assert_disconnect_action(action, close_tracker); }, - MessageSendEvent::SendTxComplete { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering tx_complete from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_tx_complete(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendTxAbort { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering tx_abort from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_tx_abort(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendSpliceInit { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering splice_init from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_splice_init(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendSpliceAck { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering splice_ack from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_splice_ack(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendSpliceLocked { ref node_id, ref msg } => { - for (idx, dest) in nodes.iter().enumerate() { - if dest.get_our_node_id() == *node_id { - out.locked_write(format!("Delivering splice_locked from node {} to node {}.\n", $node, idx).as_bytes()); - dest.handle_splice_locked(nodes[$node].get_our_node_id(), msg); - } - } - }, - MessageSendEvent::SendChannelReady { .. } => { - // Can be generated as a reestablish response - }, - MessageSendEvent::SendAnnouncementSignatures { .. } => { - // Can be generated as a reestablish response - }, - MessageSendEvent::SendChannelUpdate { ref msg, .. } => { - // When we reconnect we will resend a channel_update to make sure our - // counterparty has the latest parameters for receiving payments - // through us. We do, however, check that the message does not include - // the "disabled" bit, as we should never ever have a channel which is - // disabled when we send such an update (or it may indicate channel - // force-close which we should detect as an error). - assert_eq!(msg.contents.channel_flags & 2, 0); - }, - _ => if out.may_fail.load(atomic::Ordering::Acquire) { - return; - } else { - panic!("Unhandled message event {:?}", event) - }, - } - if $limit_events != ProcessMessages::AllMessages { - break; - } - } - if $node == 1 { - push_excess_b_events!(extra_ev.into_iter().chain(events_iter), None); - } else if $node == 0 { - if let Some(ev) = extra_ev { ab_events.push(ev); } - for event in events_iter { ab_events.push(event); } - } else { - if let Some(ev) = extra_ev { cb_events.push(ev); } - for event in events_iter { cb_events.push(event); } - } - had_events - } } - } - - macro_rules! process_msg_noret { - ($node: expr, $corrupt_forward: expr, $limit_events: expr) => {{ - process_msg_events!($node, $corrupt_forward, $limit_events); - }}; - } - - macro_rules! drain_msg_events_on_disconnect { - ($counterparty_id: expr) => {{ - if $counterparty_id == 0 { - for event in nodes[0].get_and_clear_pending_msg_events() { - match event { - MessageSendEvent::UpdateHTLCs { .. } => {}, - MessageSendEvent::SendRevokeAndACK { .. } => {}, - MessageSendEvent::SendChannelReestablish { .. } => {}, - MessageSendEvent::SendStfu { .. } => {}, - MessageSendEvent::SendChannelReady { .. } => {}, - MessageSendEvent::SendAnnouncementSignatures { .. } => {}, - MessageSendEvent::SendChannelUpdate { ref msg, .. } => { - assert_eq!(msg.contents.channel_flags & 2, 0); // The disable bit must never be set! - }, - _ => { - if out.may_fail.load(atomic::Ordering::Acquire) { - return; - } else { - panic!("Unhandled message event") - } - }, - } - } - push_excess_b_events!( - nodes[1].get_and_clear_pending_msg_events().drain(..), - Some(0) - ); - ab_events.clear(); - ba_events.clear(); - } else { - for event in nodes[2].get_and_clear_pending_msg_events() { - match event { - MessageSendEvent::UpdateHTLCs { .. } => {}, - MessageSendEvent::SendRevokeAndACK { .. } => {}, - MessageSendEvent::SendChannelReestablish { .. } => {}, - MessageSendEvent::SendStfu { .. } => {}, - MessageSendEvent::SendChannelReady { .. } => {}, - MessageSendEvent::SendAnnouncementSignatures { .. } => {}, - MessageSendEvent::SendChannelUpdate { ref msg, .. } => { - assert_eq!(msg.contents.channel_flags & 2, 0); // The disable bit must never be set! - }, - _ => { - if out.may_fail.load(atomic::Ordering::Acquire) { - return; - } else { - panic!("Unhandled message event") - } - }, - } + _ => panic!("Unhandled message event"), } - push_excess_b_events!( - nodes[1].get_and_clear_pending_msg_events().drain(..), - Some(2) - ); - bc_events.clear(); - cb_events.clear(); } - }}; - } - - macro_rules! process_events { - ($node: expr, $fail: expr) => {{ - // In case we get 256 payments we may have a hash collision, resulting in the - // second claim/fail call not finding the duplicate-hash HTLC, so we have to - // deduplicate the calls here. - let mut claim_set = new_hash_map(); - let mut events = nodes[$node].get_and_clear_pending_events(); - let had_events = !events.is_empty(); - let mut pending_payments = pending_payments.borrow_mut(); - let mut resolved_payments = resolved_payments.borrow_mut(); - for event in events.drain(..) { + self.route_from_middle( + nodes[1].get_and_clear_pending_msg_events(), + Some(0), + nodes, + close_tracker, + ); + }, + 2 => { + for event in nodes[2].get_and_clear_pending_msg_events() { match event { - events::Event::PaymentClaimable { payment_hash, .. } => { - if claim_set.insert(payment_hash.0, ()).is_none() { - if $fail { - nodes[$node].fail_htlc_backwards(&payment_hash); - } else { - nodes[$node].claim_funds(PaymentPreimage(payment_hash.0)); - } - } + MessageSendEvent::UpdateHTLCs { .. } => {}, + MessageSendEvent::SendRevokeAndACK { .. } => {}, + MessageSendEvent::SendChannelReestablish { .. } => {}, + MessageSendEvent::SendStfu { .. } => {}, + MessageSendEvent::SendChannelReady { .. } => {}, + MessageSendEvent::SendAnnouncementSignatures { .. } => {}, + MessageSendEvent::BroadcastChannelUpdate { .. } => {}, + MessageSendEvent::SendChannelUpdate { .. } => {}, + MessageSendEvent::HandleError { ref action, .. } => { + assert_disconnect_action(action, close_tracker); }, - events::Event::PaymentSent { payment_id, .. } => { - let sent_id = payment_id.unwrap(); - let idx_opt = - pending_payments[$node].iter().position(|id| *id == sent_id); - if let Some(idx) = idx_opt { - pending_payments[$node].remove(idx); - resolved_payments[$node].push(sent_id); - } else { - assert!(resolved_payments[$node].contains(&sent_id)); - } - }, - events::Event::PaymentFailed { payment_id, .. } => { - let idx_opt = - pending_payments[$node].iter().position(|id| *id == payment_id); - if let Some(idx) = idx_opt { - pending_payments[$node].remove(idx); - resolved_payments[$node].push(payment_id); - } else if !resolved_payments[$node].contains(&payment_id) { - // Payment failed immediately on send, so it was never added to - // pending_payments. Add it to resolved_payments to track it. - resolved_payments[$node].push(payment_id); - } - }, - events::Event::PaymentClaimed { .. } => {}, - events::Event::PaymentPathSuccessful { .. } => {}, - events::Event::PaymentPathFailed { .. } => {}, - events::Event::ProbeSuccessful { .. } - | events::Event::ProbeFailed { .. } => { - // Even though we don't explicitly send probes, because probes are - // detected based on hashing the payment hash+preimage, its rather - // trivial for the fuzzer to build payments that accidentally end up - // looking like probes. - }, - events::Event::PaymentForwarded { .. } if $node == 1 => {}, - events::Event::ChannelReady { .. } => {}, - events::Event::HTLCHandlingFailed { .. } => {}, - - events::Event::FundingTransactionReadyForSigning { - channel_id, - counterparty_node_id, - unsigned_transaction, - .. - } => { - let signed_tx = wallets[$node].sign_tx(unsigned_transaction).unwrap(); - nodes[$node] - .funding_transaction_signed( - &channel_id, - &counterparty_node_id, - signed_tx, + _ => panic!("Unhandled message event"), + } + } + self.route_from_middle( + nodes[1].get_and_clear_pending_msg_events(), + Some(2), + nodes, + close_tracker, + ); + }, + _ => panic!("unsupported disconnected edge"), + } + } +} + +struct PeerLink { + node_a: usize, + node_b: usize, + channel_ids: [ChannelId; 3], + disconnected: bool, +} + +impl PeerLink { + fn new(node_a: usize, node_b: usize, channel_ids: [ChannelId; 3]) -> Self { + Self { node_a, node_b, channel_ids, disconnected: false } + } + + fn first_channel_id(&self) -> ChannelId { + self.channel_ids[0] + } + + fn channel_ids(&self) -> &[ChannelId; 3] { + &self.channel_ids + } + + fn connects(&self, node_a: usize, node_b: usize) -> bool { + (self.node_a == node_a && self.node_b == node_b) + || (self.node_a == node_b && self.node_b == node_a) + } + + fn complete_all_monitor_updates(&self, nodes: &[HarnessNode<'_>; 3]) -> bool { + let mut completed_updates = false; + for id in &self.channel_ids { + completed_updates |= nodes[self.node_a].complete_all_monitor_updates(id); + completed_updates |= nodes[self.node_b].complete_all_monitor_updates(id); + } + completed_updates + } + + fn complete_monitor_updates_for_node( + &self, node_idx: usize, nodes: &[HarnessNode<'_>; 3], selector: MonitorUpdateSelector, + ) { + assert!(node_idx == self.node_a || node_idx == self.node_b); + for id in &self.channel_ids { + nodes[node_idx].complete_monitor_update(id, selector); + } + } + + fn assert_no_unexpected_disappeared_channels( + &self, nodes: &[HarnessNode<'_>; 3], close_tracker: &ChannelCloseTracker, + ) { + let node_a_channels = nodes[self.node_a].list_channels(); + let node_b_channels = nodes[self.node_b].list_channels(); + for channel_id in &self.channel_ids { + if close_tracker.is_closed_or_closing(channel_id) { + continue; + } + assert!( + node_a_channels.iter().any(|chan| chan.channel_id == *channel_id), + "Node {} no longer lists channel {:?} without an explicit force-close", + self.node_a, + channel_id, + ); + assert!( + node_b_channels.iter().any(|chan| chan.channel_id == *channel_id), + "Node {} no longer lists channel {:?} without an explicit force-close", + self.node_b, + channel_id, + ); + } + } + + fn disconnect( + &mut self, nodes: &[HarnessNode<'_>; 3], queues: &mut EventQueues, + close_tracker: &ChannelCloseTracker, + ) { + if self.disconnected { + return; + } + let node_a_id = nodes[self.node_a].get_our_node_id(); + let node_b_id = nodes[self.node_b].get_our_node_id(); + nodes[self.node_a].peer_disconnected(node_b_id); + nodes[self.node_b].peer_disconnected(node_a_id); + self.disconnected = true; + let edge_node = if self.node_a == 1 { + self.node_b + } else if self.node_b == 1 { + self.node_a + } else { + panic!("unsupported link topology") + }; + queues.drain_on_disconnect(edge_node, nodes, close_tracker); + queues.clear_link(self); + } + + fn reconnect(&mut self, nodes: &[HarnessNode<'_>; 3]) { + if !self.disconnected { + return; + } + let node_a_id = nodes[self.node_a].get_our_node_id(); + let node_b_id = nodes[self.node_b].get_our_node_id(); + let init_b = Init { + features: nodes[self.node_b].init_features(), + networks: None, + remote_network_address: None, + }; + nodes[self.node_a].peer_connected(node_b_id, &init_b, true).unwrap(); + let init_a = Init { + features: nodes[self.node_a].init_features(), + networks: None, + remote_network_address: None, + }; + nodes[self.node_b].peer_connected(node_a_id, &init_a, false).unwrap(); + self.disconnected = false; + } + + fn disconnect_for_reload( + &mut self, restarted_node: usize, nodes: &[HarnessNode<'_>; 3], queues: &mut EventQueues, + close_tracker: &ChannelCloseTracker, + ) { + if self.disconnected { + return; + } + assert!(restarted_node == self.node_a || restarted_node == self.node_b); + + let remaining_node = if restarted_node == self.node_a { self.node_b } else { self.node_a }; + let restarted_node_id = nodes[restarted_node].get_our_node_id(); + nodes[remaining_node].peer_disconnected(restarted_node_id); + self.disconnected = true; + + if remaining_node == 1 { + queues.route_from_middle( + nodes[1].get_and_clear_pending_msg_events(), + Some(restarted_node), + nodes, + close_tracker, + ); + } else { + nodes[remaining_node].get_and_clear_pending_msg_events(); + } + queues.clear_link(self); + } +} + +#[derive(Clone, Copy, PartialEq)] +enum PaymentExpectation { + MustSucceed, + MayFail, +} + +#[derive(Clone, Copy)] +struct PaymentHop { + channel_id: ChannelId, + amount_msat: u64, + short_channel_id: u64, +} + +type PaymentPath = Vec<PaymentHop>; + +struct PendingPayment { + payment_id: PaymentId, + payment_hash: PaymentHash, + first_persisted_manager_generation: u64, + paths: Vec<PaymentPath>, + min_final_cltv_expiry: u32, + expectation: PaymentExpectation, +} + +struct NodePayments { + pending: Vec<PendingPayment>, + resolved: HashMap<PaymentId, Option<PaymentHash>>, +} + +impl NodePayments { + fn new() -> Self { + Self { pending: Vec::new(), resolved: new_hash_map() } + } + + fn add_pending( + &mut self, payment_id: PaymentId, payment_hash: PaymentHash, + first_persisted_manager_generation: u64, paths: Vec<PaymentPath>, + min_final_cltv_expiry: u32, + ) { + assert!(!self.pending.iter().any(|pending| pending.payment_id == payment_id)); + assert!(!self.resolved.contains_key(&payment_id)); + assert!(!paths.is_empty(), "tracked payment must have at least one path"); + self.pending.push(PendingPayment { + payment_id, + payment_hash, + first_persisted_manager_generation, + paths, + min_final_cltv_expiry, + expectation: PaymentExpectation::MustSucceed, + }); + } + + fn resolve_pending( + &mut self, payment_id: PaymentId, payment_hash: Option<PaymentHash>, + ) -> PendingPayment { + assert!(!self.resolved.contains_key(&payment_id)); + let idx = self + .pending + .iter() + .position(|pending| pending.payment_id == payment_id) + .expect("resolved payment must be pending"); + let pending = self.pending.remove(idx); + if let Some(payment_hash) = payment_hash { + assert_eq!(pending.payment_hash, payment_hash); + } + assert!(self.resolved.insert(payment_id, payment_hash).is_none()); + pending + } + + fn allow_failure_for_id(&mut self, payment_id: PaymentId) { + for pending in &mut self.pending { + if pending.payment_id == payment_id { + pending.expectation = PaymentExpectation::MayFail; + } + } + } + + fn allow_failure_for_hash(&mut self, payment_hash: PaymentHash) { + for pending in &mut self.pending { + if pending.payment_hash == payment_hash { + pending.expectation = PaymentExpectation::MayFail; + } + } + } + + fn allow_failure_for_closed_channel(&mut self, channel_id: ChannelId) { + for pending in &mut self.pending { + let uses_channel = pending + .paths + .iter() + .any(|path| path.iter().any(|hop| hop.channel_id == channel_id)); + if uses_channel { + pending.expectation = PaymentExpectation::MayFail; + } + } + } + + fn allow_failure_for_receive_cltv_buffer(&mut self, current_height: u32) { + let unsafe_receive_height = + current_height.saturating_add(channelmonitor::HTLC_FAIL_BACK_BUFFER + 1); + for pending in &mut self.pending { + if pending.min_final_cltv_expiry <= unsafe_receive_height { + pending.expectation = PaymentExpectation::MayFail; + } + } + } + + fn mark_sent(&mut self, sent_id: PaymentId, payment_hash: PaymentHash) { + if self.pending.iter().any(|pending| pending.payment_id == sent_id) { + self.resolve_pending(sent_id, Some(payment_hash)); + } else if let Some(resolved_hash) = self.resolved.get_mut(&sent_id) { + if let Some(existing_hash) = *resolved_hash { + assert_eq!(existing_hash, payment_hash); + } else { + *resolved_hash = Some(payment_hash); + } + } else { + panic!("Payment {:?} sent without being tracked", sent_id); + } + } + fn mark_failed(&mut self, source_idx: usize, payment_id: PaymentId) { + if self.pending.iter().any(|pending| pending.payment_id == payment_id) { + let pending = self.resolve_pending(payment_id, None); + assert!( + pending.expectation == PaymentExpectation::MayFail, + "Payment {:?} from node {} failed without an expected failure source", + pending.payment_hash, + source_idx + ); + } else { + assert!( + self.resolved.contains_key(&payment_id), + "Payment {:?} from node {} failed without being tracked", + payment_id, + source_idx + ); + } + } + + fn mark_resolved_without_hash(&mut self, payment_id: PaymentId) { + if self.pending.iter().any(|pending| pending.payment_id == payment_id) { + self.resolve_pending(payment_id, None); + } else { + assert!(self.resolved.contains_key(&payment_id)); + } + } + + fn mark_successful_probe(&mut self, payment_id: PaymentId) { + self.mark_resolved_without_hash(payment_id); + } + + fn sync_pending_with_manager_generation( + &mut self, loaded_manager_generation: u64, + ) -> Vec<PaymentHash> { + let rolled_back_payments = self + .pending + .iter() + .filter(|pending| { + pending.first_persisted_manager_generation > loaded_manager_generation + }) + .map(|pending| (pending.payment_id, pending.payment_hash)) + .collect::<Vec<_>>(); + for (payment_id, _) in &rolled_back_payments { + self.resolve_pending(*payment_id, None); + } + rolled_back_payments.into_iter().map(|(_, payment_hash)| payment_hash).collect() + } +} + +struct PaymentTracker { + nodes: [NodePayments; 3], + claimed_payment_hashes: HashSet<PaymentHash>, + payment_preimages: HashMap<PaymentHash, PaymentPreimage>, + // Inbound HTLCs whose failures were received from downstream. + downstream_failed_inbound_htlcs: [HashSet<(ChannelId, u64, PaymentHash)>; 3], + payment_ctr: u64, +} + +impl PaymentTracker { + fn new() -> Self { + Self { + nodes: [NodePayments::new(), NodePayments::new(), NodePayments::new()], + claimed_payment_hashes: HashSet::new(), + payment_preimages: new_hash_map(), + downstream_failed_inbound_htlcs: [HashSet::new(), HashSet::new(), HashSet::new()], + payment_ctr: 0, + } + } + + fn payment_has_pending_work(source: &ChanMan, sent_payment_id: PaymentId) -> bool { + for payment in source.list_recent_payments() { + match payment { + RecentPaymentDetails::Pending { payment_id, .. } + if payment_id == sent_payment_id => + { + return true; + }, + RecentPaymentDetails::Abandoned { payment_id, payment_hash, .. } + if payment_id == sent_payment_id => + { + return Self::has_outbound_htlc(source, payment_hash); + }, + _ => {}, + } + } + return false; + } + + fn next_payment(&mut self, dest: &ChanMan) -> (PaymentSecret, PaymentHash, PaymentId) { + self.payment_ctr += 1; + let mut payment_preimage = PaymentPreimage([0; 32]); + payment_preimage.0[0..8].copy_from_slice(&self.payment_ctr.to_be_bytes()); + let hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); + let (secret, _no_metadata) = dest + .create_inbound_payment_for_hash(hash, None, 3600, None, None) + .expect("create_inbound_payment_for_hash failed"); + assert!(self.payment_preimages.insert(hash, payment_preimage).is_none()); + let mut id = PaymentId([0; 32]); + id.0[0..8].copy_from_slice(&self.payment_ctr.to_ne_bytes()); + (secret, hash, id) + } + + fn route_from_payment_paths( + payment_paths: &[PaymentPath], path_nodes: &[&HarnessNode<'_>], + route_params: RouteParameters, + ) -> Route { + let paths = payment_paths + .iter() + .map(|payment_path| { + assert_eq!(payment_path.len(), path_nodes.len()); + let hops = payment_path + .iter() + .enumerate() + .map(|(idx, hop)| { + let node = path_nodes[idx]; + let fee_msat = + payment_path.get(idx + 1).map_or(hop.amount_msat, |next_hop| { + hop.amount_msat.checked_sub(next_hop.amount_msat).expect( + "payment path amounts must not increase toward the recipient", ) - .unwrap(); - }, - events::Event::SplicePending { .. } => {}, - events::Event::SpliceFailed { .. } => {}, - - _ => { - if out.may_fail.load(atomic::Ordering::Acquire) { - return; - } else { - panic!("Unhandled event") - } - }, + }); + RouteHop { + pubkey: node.get_our_node_id(), + node_features: node.node_features(), + short_channel_id: hop.short_channel_id, + channel_features: node.channel_features(), + fee_msat, + cltv_expiry_delta: (idx as u32 + 1) * 100, + maybe_announced_channel: true, + } + }) + .collect(); + Path { hops, blinded_tail: None } + }) + .collect(); + Route { paths, route_params } + } + + fn allow_failure_for_hash(&mut self, payment_hash: PaymentHash) { + for node in &mut self.nodes { + node.allow_failure_for_hash(payment_hash); + } + } + + fn allow_failure_for_closed_channel(&mut self, channel_id: ChannelId) { + for node in &mut self.nodes { + node.allow_failure_for_closed_channel(channel_id); + } + } + + fn allow_failure_for_receive_cltv_buffer(&mut self, current_height: u32) { + for node in &mut self.nodes { + node.allow_failure_for_receive_cltv_buffer(current_height); + } + } + + fn record_downstream_failure( + &mut self, node_idx: usize, node: &HarnessNode<'_>, counterparty_node_id: &PublicKey, + channel_id: ChannelId, htlc_id: u64, + ) { + let Some(htlc) = node + .list_channels() + .into_iter() + .find(|chan| { + chan.counterparty.node_id == *counterparty_node_id && chan.channel_id == channel_id + }) + .and_then(|chan| { + chan.pending_outbound_htlcs.into_iter().find(|htlc| htlc.htlc_id == Some(htlc_id)) + }) + else { + return; + }; + let payment_hash = htlc.payment_hash; + match htlc.source { + Some(OutboundHTLCSource::Forwarded { inbound_htlc }) => { + self.downstream_failed_inbound_htlcs[node_idx].insert(( + inbound_htlc.channel_id, + inbound_htlc.htlc_id, + payment_hash, + )); + }, + Some(OutboundHTLCSource::TrampolineForwarded { inbound_htlcs }) => { + self.downstream_failed_inbound_htlcs[node_idx].extend( + inbound_htlcs + .into_iter() + .map(|htlc| (htlc.channel_id, htlc.htlc_id, payment_hash)), + ); + }, + Some(OutboundHTLCSource::Local { .. }) | None => {}, + } + } + + fn allow_failure_for_local_inbound_htlcs(&mut self, node_idx: usize, node: &HarnessNode<'_>) { + // Classify failures immediately after forwarding so implicit LDK-local + // policy or state failures are observed before their failure messages can + // reach the payer. Downstream-originated failures are already tracked by + // the failure message that caused them. + let failed_htlcs: Vec<_> = node + .list_channels() + .iter() + .flat_map(|chan| { + chan.pending_inbound_htlcs.iter().filter_map(|htlc| { + matches!( + htlc.state.as_ref(), + Some(InboundHTLCStateDetails::AwaitingRemoteRevokeToRemoveFail) + ) + .then_some((chan.channel_id, htlc.htlc_id, htlc.payment_hash)) + }) + }) + .collect(); + for (channel_id, htlc_id, payment_hash) in failed_htlcs { + // Failed inbound HTLCs may appear in multiple state snapshots, so keep downstream + // markers after matching them. + let htlc = (channel_id, htlc_id, payment_hash); + if !self.downstream_failed_inbound_htlcs[node_idx].contains(&htlc) { + self.allow_failure_for_hash(payment_hash); + } + } + } + + fn route_min_final_cltv_expiry(route: &Route, source_best_block_height: u32) -> u32 { + let cur_height = source_best_block_height.saturating_add(1); + route + .paths + .iter() + .map(|path| { + cur_height.saturating_add( + path.hops + .last() + .expect("payment path should contain at least one hop") + .cltv_expiry_delta, + ) + }) + .min() + .expect("payment route should contain at least one path") + } + + fn has_outbound_htlc(source: &ChanMan, payment_hash: PaymentHash) -> bool { + source.list_channels().iter().any(|chan| { + chan.pending_outbound_htlcs.iter().any(|htlc| htlc.payment_hash == payment_hash) + }) + } + fn payment_has_uncommitted_paths( + source: &HarnessNode<'_>, payment_hash: PaymentHash, path_count: usize, + ) -> bool { + let committed_htlc_count = source + .list_channels() + .iter() + .flat_map(|chan| chan.pending_outbound_htlcs.iter()) + .filter(|htlc| htlc.payment_hash == payment_hash && htlc.htlc_id.is_some()) + .count(); + committed_htlc_count < path_count + } + + fn record_send_result( + &mut self, source_idx: usize, source: &HarnessNode<'_>, payment_id: PaymentId, + payment_hash: PaymentHash, payment_paths: Vec<PaymentPath>, min_final_cltv_expiry: u32, + has_pending_work: bool, + ) { + let path_count = payment_paths.len(); + let has_uncommitted_paths = has_pending_work + && Self::payment_has_uncommitted_paths(source, payment_hash, path_count); + let node_payments = &mut self.nodes[source_idx]; + node_payments.add_pending( + payment_id, + payment_hash, + source.next_manager_persistence_generation(), + payment_paths, + min_final_cltv_expiry, + ); + if has_pending_work { + // Holding-cell HTLCs have no id, while paths that failed locally are absent. + // Either can make an otherwise tracked payment fail without a downstream cause. + if has_uncommitted_paths { + node_payments.allow_failure_for_id(payment_id); + } + } else { + node_payments.resolve_pending(payment_id, None); + } + } + + fn send( + &mut self, nodes: &[HarnessNode<'_>; 3], source_idx: usize, dest_idx: usize, + dest_chan_id: ChannelId, amt: u64, + ) -> bool { + let source = &nodes[source_idx]; + let dest = &nodes[dest_idx]; + let (secret, hash, id) = self.next_payment(dest); + let (min_value_sendable, max_value_sendable, dest_scid) = source + .list_usable_channels() + .iter() + .find(|chan| chan.channel_id == dest_chan_id) + .map(|chan| { + ( + chan.next_outbound_htlc_minimum_msat, + chan.next_outbound_htlc_limit_msat, + chan.short_channel_id.unwrap_or(0), + ) + }) + .unwrap_or((0, 0, 0)); + let payment_paths = vec![vec![PaymentHop { + channel_id: dest_chan_id, + amount_msat: amt, + short_channel_id: dest_scid, + }]]; + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(source.get_our_node_id(), TEST_FINAL_CLTV), + amt, + ); + let route = Self::route_from_payment_paths(&payment_paths, &[dest], route_params); + let min_final_cltv_expiry = + Self::route_min_final_cltv_expiry(&route, source.current_best_block().height); + let onion = RecipientOnionFields::secret_only(secret, amt); + let res = source.send_payment_with_route(route, hash, onion, id); + let has_pending_work = match res { + Err(err) => { + panic!("Errored with {:?} on initial payment send", err); + }, + Ok(()) => { + let expect_failure = amt < min_value_sendable || amt > max_value_sendable; + let has_pending_work = Self::payment_has_pending_work(source, id); + assert_eq!(has_pending_work, !expect_failure); + has_pending_work + }, + }; + self.record_send_result( + source_idx, + source, + id, + hash, + payment_paths, + min_final_cltv_expiry, + has_pending_work, + ); + has_pending_work + } + + fn send_hop( + &mut self, nodes: &[HarnessNode<'_>; 3], source_idx: usize, middle_idx: usize, + middle_chan_id: ChannelId, dest_idx: usize, dest_chan_id: ChannelId, amt: u64, + ) { + let source = &nodes[source_idx]; + let middle = &nodes[middle_idx]; + let dest = &nodes[dest_idx]; + let (secret, hash, id) = self.next_payment(dest); + let (min_value_sendable, max_value_sendable, middle_scid) = source + .list_usable_channels() + .iter() + .find(|chan| chan.channel_id == middle_chan_id) + .map(|chan| { + ( + chan.next_outbound_htlc_minimum_msat, + chan.next_outbound_htlc_limit_msat, + chan.short_channel_id.unwrap_or(0), + ) + }) + .unwrap_or((0, 0, 0)); + let dest_scid = dest + .list_channels() + .iter() + .find(|chan| chan.channel_id == dest_chan_id) + .and_then(|chan| chan.short_channel_id) + .unwrap_or(0); + let first_hop_fee = 50_000; + let payment_paths = vec![vec![ + PaymentHop { + channel_id: middle_chan_id, + amount_msat: amt + first_hop_fee, + short_channel_id: middle_scid, + }, + PaymentHop { channel_id: dest_chan_id, amount_msat: amt, short_channel_id: dest_scid }, + ]]; + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(source.get_our_node_id(), TEST_FINAL_CLTV), + amt, + ); + let route = Self::route_from_payment_paths(&payment_paths, &[middle, dest], route_params); + let min_final_cltv_expiry = + Self::route_min_final_cltv_expiry(&route, source.current_best_block().height); + let onion = RecipientOnionFields::secret_only(secret, amt); + let res = source.send_payment_with_route(route, hash, onion, id); + let has_pending_work = match res { + Err(err) => { + panic!("Errored with {:?} on initial payment send", err); + }, + Ok(()) => { + let sent_amt = amt + first_hop_fee; + let expect_failure = sent_amt < min_value_sendable || sent_amt > max_value_sendable; + let has_pending_work = Self::payment_has_pending_work(source, id); + assert_eq!(has_pending_work, !expect_failure); + has_pending_work + }, + }; + self.record_send_result( + source_idx, + source, + id, + hash, + payment_paths, + min_final_cltv_expiry, + has_pending_work, + ); + } + + fn send_noret( + &mut self, nodes: &[HarnessNode<'_>; 3], source_idx: usize, dest_idx: usize, + dest_chan_id: ChannelId, amt: u64, + ) { + self.send(nodes, source_idx, dest_idx, dest_chan_id, amt); + } + + // Direct MPP payment (no hop) + fn send_mpp_direct( + &mut self, nodes: &[HarnessNode<'_>; 3], source_idx: usize, dest_idx: usize, + dest_chan_ids: &[ChannelId], amt: u64, + ) { + let source = &nodes[source_idx]; + let dest = &nodes[dest_idx]; + let (secret, hash, id) = self.next_payment(dest); + let num_paths = dest_chan_ids.len(); + if num_paths == 0 { + return; + } + + let amt_per_path = amt / num_paths as u64; + + let dest_chans = dest.list_channels(); + let dest_scids: Vec<_> = dest_chan_ids + .iter() + .map(|chan_id| { + let scid = dest_chans + .iter() + .find(|chan| chan.channel_id == *chan_id) + .and_then(|chan| chan.short_channel_id) + .unwrap(); + (*chan_id, scid) + }) + .collect(); + + let payment_paths: Vec<PaymentPath> = dest_scids + .iter() + .enumerate() + .map(|(i, (chan_id, dest_scid))| { + let path_amt = if i == num_paths - 1 { + amt - amt_per_path * (num_paths as u64 - 1) + } else { + amt_per_path + }; + vec![PaymentHop { + channel_id: *chan_id, + amount_msat: path_amt, + short_channel_id: *dest_scid, + }] + }) + .collect(); + + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(dest.get_our_node_id(), TEST_FINAL_CLTV), + amt, + ); + let route = Self::route_from_payment_paths(&payment_paths, &[dest], route_params); + let min_final_cltv_expiry = + Self::route_min_final_cltv_expiry(&route, source.current_best_block().height); + let onion = RecipientOnionFields::secret_only(secret, amt); + let res = source.send_payment_with_route(route, hash, onion, id); + let has_pending_work = match res { + Err(_) => false, + Ok(()) => Self::payment_has_pending_work(source, id), + }; + self.record_send_result( + source_idx, + source, + id, + hash, + payment_paths, + min_final_cltv_expiry, + has_pending_work, + ); + } + + // MPP payment via hop - splits payment across multiple channels on either or both hops + fn send_mpp_hop( + &mut self, nodes: &[HarnessNode<'_>; 3], source_idx: usize, middle_idx: usize, + middle_chan_ids: &[ChannelId], dest_idx: usize, dest_chan_ids: &[ChannelId], amt: u64, + ) { + let source = &nodes[source_idx]; + let middle = &nodes[middle_idx]; + let dest = &nodes[dest_idx]; + let (secret, hash, id) = self.next_payment(dest); + // Create paths by pairing middle_scids with dest_scids. + let num_paths = middle_chan_ids.len().max(dest_chan_ids.len()); + if num_paths == 0 { + return; + } + + let first_hop_fee = 50_000; + let amt_per_path = amt / num_paths as u64; + let fee_per_path = first_hop_fee / num_paths as u64; + + let middle_chans = middle.list_channels(); + let middle_scids: Vec<_> = middle_chan_ids + .iter() + .map(|chan_id| { + let scid = middle_chans + .iter() + .find(|chan| chan.channel_id == *chan_id) + .and_then(|chan| chan.short_channel_id) + .unwrap(); + (*chan_id, scid) + }) + .collect(); + + let dest_chans = dest.list_channels(); + let dest_scids: Vec<_> = dest_chan_ids + .iter() + .map(|chan_id| { + let scid = dest_chans + .iter() + .find(|chan| chan.channel_id == *chan_id) + .and_then(|chan| chan.short_channel_id) + .unwrap(); + (*chan_id, scid) + }) + .collect(); + + let payment_paths: Vec<PaymentPath> = (0..num_paths) + .map(|i| { + let (middle_chan_id, middle_scid) = middle_scids[i % middle_scids.len()]; + let (dest_chan_id, dest_scid) = dest_scids[i % dest_scids.len()]; + let path_amt = if i == num_paths - 1 { + amt - amt_per_path * (num_paths as u64 - 1) + } else { + amt_per_path + }; + let path_fee = if i == num_paths - 1 { + first_hop_fee - fee_per_path * (num_paths as u64 - 1) + } else { + fee_per_path + }; + vec![ + PaymentHop { + channel_id: middle_chan_id, + amount_msat: path_amt + path_fee, + short_channel_id: middle_scid, + }, + PaymentHop { + channel_id: dest_chan_id, + amount_msat: path_amt, + short_channel_id: dest_scid, + }, + ] + }) + .collect(); + + let route_params = RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(dest.get_our_node_id(), TEST_FINAL_CLTV), + amt, + ); + let route = Self::route_from_payment_paths(&payment_paths, &[middle, dest], route_params); + let min_final_cltv_expiry = + Self::route_min_final_cltv_expiry(&route, source.current_best_block().height); + let onion = RecipientOnionFields::secret_only(secret, amt); + let res = source.send_payment_with_route(route, hash, onion, id); + let has_pending_work = match res { + Err(_) => false, + Ok(()) => Self::payment_has_pending_work(source, id), + }; + self.record_send_result( + source_idx, + source, + id, + hash, + payment_paths, + min_final_cltv_expiry, + has_pending_work, + ); + } + + fn claim_payment(&mut self, node: &HarnessNode<'_>, payment_hash: PaymentHash, fail: bool) { + if fail { + self.allow_failure_for_hash(payment_hash); + node.fail_htlc_backwards(&payment_hash); + } else { + let payment_preimage = *self + .payment_preimages + .get(&payment_hash) + .expect("PaymentClaimable for unknown payment hash"); + node.claim_funds(payment_preimage); + self.claimed_payment_hashes.insert(payment_hash); + } + } + + fn assert_all_resolved(&self) { + for (idx, node) in self.nodes.iter().enumerate() { + assert!( + node.pending.is_empty(), + "Node {} has {} stuck pending payments after settling all state", + idx, + node.pending.len() + ); + } + } + + fn assert_claims_reported(&self) { + for hash in self.claimed_payment_hashes.iter() { + let found = self + .nodes + .iter() + .any(|node| node.resolved.values().any(|h| h.as_ref() == Some(hash))); + assert!( + found, + "Payment {:?} was claimed by receiver but sender never got PaymentSent", + hash + ); + } + } +} + +struct Harness<'a, Out: Output + MaybeSend + MaybeSync> { + out: Out, + chan_type: ChanType, + chain_state: ChainState, + nodes: [HarnessNode<'a>; 3], + ab_link: PeerLink, + bc_link: PeerLink, + queues: EventQueues, + payments: PaymentTracker, + close_tracker: ChannelCloseTracker, +} + +fn build_node_config(chan_type: ChanType) -> UserConfig { + let mut config = UserConfig::default(); + config.channel_config.forwarding_fee_proportional_millionths = 0; + config.channel_handshake_config.announce_for_forwarding = true; + config.reject_inbound_splices = false; + match chan_type { + ChanType::Legacy => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + }, + ChanType::KeyedAnchors => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + }, + ChanType::ZeroFeeCommitments => { + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + }, + } + config +} + +fn connect_peers(source: &ChanMan<'_>, dest: &ChanMan<'_>) { + let init_dest = + Init { features: dest.init_features(), networks: None, remote_network_address: None }; + source.peer_connected(dest.get_our_node_id(), &init_dest, true).unwrap(); + let init_src = + Init { features: source.init_features(), networks: None, remote_network_address: None }; + dest.peer_connected(source.get_our_node_id(), &init_src, false).unwrap(); +} + +fn make_channel( + nodes: &mut [HarnessNode<'_>; 3], source_idx: usize, dest_idx: usize, chan_id: i32, + trusted_open: bool, trusted_accept: bool, chain_state: &mut ChainState, +) { + assert!(source_idx < dest_idx); + let (left, right) = nodes.split_at_mut(dest_idx); + let (source, dest) = (&mut left[source_idx], &mut right[0]); + if trusted_open { + source + .create_channel_to_trusted_peer_0reserve( + dest.get_our_node_id(), + 100_000, + 42, + 0, + None, + None, + ) + .unwrap(); + } else { + source.create_channel(dest.get_our_node_id(), 100_000, 42, 0, None, None).unwrap(); + } + let open_channel = { + let events = source.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + if let MessageSendEvent::SendOpenChannel { ref msg, .. } = events[0] { + msg.clone() + } else { + panic!("Wrong event type"); + } + }; + + dest.handle_open_channel(source.get_our_node_id(), &open_channel); + let accept_channel = { + let events = dest.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + if let events::Event::OpenChannelRequest { + ref temporary_channel_id, + ref counterparty_node_id, + .. + } = events[0] + { + let mut random_bytes = [0u8; 16]; + random_bytes.copy_from_slice(&dest.keys_manager.get_secure_random_bytes()[..16]); + let user_channel_id = u128::from_be_bytes(random_bytes); + if trusted_accept { + dest.accept_inbound_channel_from_trusted_peer( + temporary_channel_id, + counterparty_node_id, + user_channel_id, + TrustedChannelFeatures::ZeroReserve, + None, + ) + .unwrap(); + } else { + dest.accept_inbound_channel( + temporary_channel_id, + counterparty_node_id, + user_channel_id, + None, + ) + .unwrap(); + } + } else { + panic!("Wrong event type"); + } + let events = dest.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + if let MessageSendEvent::SendAcceptChannel { ref msg, .. } = events[0] { + msg.clone() + } else { + panic!("Wrong event type"); + } + }; + + source.handle_accept_channel(dest.get_our_node_id(), &accept_channel); + { + let mut events = source.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + if let events::Event::FundingGenerationReady { + temporary_channel_id, + channel_value_satoshis, + output_script, + .. + } = events.pop().unwrap() + { + let tx = Transaction { + version: Version(chan_id), + lock_time: LockTime::ZERO, + input: Vec::new(), + output: vec![TxOut { + value: Amount::from_sat(channel_value_satoshis), + script_pubkey: output_script, + }], + }; + source + .funding_transaction_generated( + temporary_channel_id, + dest.get_our_node_id(), + tx.clone(), + ) + .unwrap(); + chain_state.mine_setup_tx_to_depth(tx, ANTI_REORG_DELAY); + } else { + panic!("Wrong event type"); + } + } + + let funding_created = { + let events = source.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + if let MessageSendEvent::SendFundingCreated { ref msg, .. } = events[0] { + msg.clone() + } else { + panic!("Wrong event type"); + } + }; + dest.handle_funding_created(source.get_our_node_id(), &funding_created); + dest.checkpoint_manager_persistence(); + // Complete any monitor persistence callbacks made available for dest after watch_channel. + dest.complete_all_pending_monitor_updates(); + + let (funding_signed, channel_id) = { + let events = dest.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + if let MessageSendEvent::SendFundingSigned { ref msg, .. } = events[0] { + (msg.clone(), msg.channel_id) + } else { + panic!("Wrong event type"); + } + }; + let events = dest.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + if let events::Event::ChannelPending { ref counterparty_node_id, .. } = events[0] { + assert_eq!(counterparty_node_id, &source.get_our_node_id()); + } else { + panic!("Wrong event type"); + } + + source.handle_funding_signed(dest.get_our_node_id(), &funding_signed); + source.checkpoint_manager_persistence(); + // Complete any monitor persistence callbacks made available for source after watch_channel. + source.complete_all_pending_monitor_updates(); + + let events = source.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + if let events::Event::ChannelPending { + ref counterparty_node_id, + channel_id: ref event_channel_id, + .. + } = events[0] + { + assert_eq!(counterparty_node_id, &dest.get_our_node_id()); + assert_eq!(*event_channel_id, channel_id); + } else { + panic!("Wrong event type"); + } +} + +fn lock_fundings(nodes: &[HarnessNode<'_>; 3]) { + let mut node_events = Vec::new(); + for node in nodes.iter() { + node_events.push(node.get_and_clear_pending_msg_events()); + } + for (idx, node_event) in node_events.iter().enumerate() { + for event in node_event { + if let MessageSendEvent::SendChannelReady { ref node_id, ref msg } = event { + for node in nodes.iter() { + if node.get_our_node_id() == *node_id { + node.handle_channel_ready(nodes[idx].get_our_node_id(), msg); } } - while nodes[$node].needs_pending_htlc_processing() { - nodes[$node].process_pending_htlc_forwards(); + } else { + panic!("Wrong event type"); + } + } + } + + for node in nodes.iter() { + let events = node.get_and_clear_pending_msg_events(); + for event in events { + if let MessageSendEvent::SendAnnouncementSignatures { .. } = event { + } else { + panic!("Wrong event type"); + } + } + } +} + +impl<'a, Out: Output + MaybeSend + MaybeSync> Harness<'a, Out> { + fn new(config_byte: u8, out: Out, router: &'a FuzzRouter) -> Self { + let chan_type = match (config_byte >> 3) & 0b11 { + 0 => ChanType::Legacy, + 1 => ChanType::KeyedAnchors, + _ => ChanType::ZeroFeeCommitments, + }; + let persistence_styles = [ + if config_byte & 0b01 != 0 { + ChannelMonitorUpdateStatus::InProgress + } else { + ChannelMonitorUpdateStatus::Completed + }, + if config_byte & 0b10 != 0 { + ChannelMonitorUpdateStatus::InProgress + } else { + ChannelMonitorUpdateStatus::Completed + }, + if config_byte & 0b100 != 0 { + ChannelMonitorUpdateStatus::InProgress + } else { + ChannelMonitorUpdateStatus::Completed + }, + ]; + let deferred = [ + config_byte & 0b0010_0000 != 0, + config_byte & 0b0100_0000 != 0, + config_byte & 0b1000_0000 != 0, + ]; + + let wallet_a = Arc::new(TestWalletSource::new(SecretKey::from_slice(&[1; 32]).unwrap())); + let wallet_b = Arc::new(TestWalletSource::new(SecretKey::from_slice(&[2; 32]).unwrap())); + let wallet_c = Arc::new(TestWalletSource::new(SecretKey::from_slice(&[3; 32]).unwrap())); + let wallets = [wallet_a.as_ref(), wallet_b.as_ref(), wallet_c.as_ref()]; + let mut chain_state = ChainState::new(); + for wallet in wallets { + let coinbase_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn { ..Default::default() }], + output: (0..NUM_WALLET_UTXOS) + .map(|_| TxOut { + value: Amount::from_sat(100_000), + script_pubkey: wallet.get_change_script().unwrap(), + }) + .collect(), + }; + for vout in 0..NUM_WALLET_UTXOS { + wallet.add_utxo(coinbase_tx.clone(), vout); + } + chain_state.mine_setup_tx_to_depth(coinbase_tx, ANTI_REORG_DELAY); + } + + let fee_est_a = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); + let fee_est_b = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); + let fee_est_c = Arc::new(FuzzEstimator { ret_val: atomic::AtomicU32::new(253) }); + let broadcast_a = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); + let broadcast_b = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); + let broadcast_c = Arc::new(TestBroadcaster { txn_broadcasted: RefCell::new(Vec::new()) }); + + // 3 nodes is enough to hit all the possible cases, notably + // unknown-source-unknown-dest forwarding. + let mut nodes = [ + HarnessNode::new( + 0, + Arc::clone(&wallet_a), + Arc::clone(&fee_est_a), + Arc::clone(&broadcast_a), + persistence_styles[0], + deferred[0], + &out, + router, + chan_type, + ), + HarnessNode::new( + 1, + Arc::clone(&wallet_b), + Arc::clone(&fee_est_b), + Arc::clone(&broadcast_b), + persistence_styles[1], + deferred[1], + &out, + router, + chan_type, + ), + HarnessNode::new( + 2, + Arc::clone(&wallet_c), + Arc::clone(&fee_est_c), + Arc::clone(&broadcast_c), + persistence_styles[2], + deferred[2], + &out, + router, + chan_type, + ), + ]; + // Connect peers first, then create channels. + connect_peers(&nodes[0], &nodes[1]); + connect_peers(&nodes[1], &nodes[2]); + + let set_0reserve = chan_type != ChanType::Legacy; + // Create 3 channels between A-B and 3 channels between B-C (6 total). + // + // Use distinct version numbers for each funding transaction so each test + // channel gets its own txid and funding outpoint. + // A-B: channel 2 A and B have 0-reserve (trusted open + trusted accept), + // channel 3 A has 0-reserve (trusted accept), if channels are non-legacy. + make_channel(&mut nodes, 0, 1, 1, false, false, &mut chain_state); + make_channel(&mut nodes, 0, 1, 2, set_0reserve, set_0reserve, &mut chain_state); + make_channel(&mut nodes, 0, 1, 3, false, set_0reserve, &mut chain_state); + // B-C: channel 4 B has 0-reserve (via trusted accept), + // channel 5 C has 0-reserve (via trusted open), if channels are non-legacy. + make_channel(&mut nodes, 1, 2, 4, false, set_0reserve, &mut chain_state); + make_channel(&mut nodes, 1, 2, 5, set_0reserve, false, &mut chain_state); + make_channel(&mut nodes, 1, 2, 6, false, false, &mut chain_state); + + // Wipe the transactions-broadcasted set to make sure we don't broadcast + // any transactions during normal operation after setup. + nodes[0].broadcaster.txn_broadcasted.borrow_mut().clear(); + nodes[1].broadcaster.txn_broadcasted.borrow_mut().clear(); + nodes[2].broadcaster.txn_broadcasted.borrow_mut().clear(); + + // Sync all nodes to tip to lock the funding. + nodes[0].sync_with_chain_state(&chain_state, None); + nodes[1].sync_with_chain_state(&chain_state, None); + nodes[2].sync_with_chain_state(&chain_state, None); + + lock_fundings(&nodes); + + let chan_ab_ids = { + // Get channel IDs for all A-B channels (from node A's perspective). + let node_a_chans = nodes[0].list_usable_channels(); + [node_a_chans[0].channel_id, node_a_chans[1].channel_id, node_a_chans[2].channel_id] + }; + let chan_bc_ids = { + // Get channel IDs for all B-C channels (from node C's perspective). + let node_c_chans = nodes[2].list_usable_channels(); + [node_c_chans[0].channel_id, node_c_chans[1].channel_id, node_c_chans[2].channel_id] + }; + + for node in &mut nodes { + node.force_checkpoint_manager_persistence(); + } + + Self { + out, + chan_type, + chain_state, + nodes, + ab_link: PeerLink::new(0, 1, chan_ab_ids), + bc_link: PeerLink::new(1, 2, chan_bc_ids), + queues: EventQueues::new(), + payments: PaymentTracker::new(), + close_tracker: ChannelCloseTracker::new(), + } + } + + fn chan_a_id(&self) -> ChannelId { + self.ab_link.first_channel_id() + } + + fn chan_b_id(&self) -> ChannelId { + self.bc_link.first_channel_id() + } + + // Runs end-of-input cleanup by relaying and mining remaining broadcasts. + // Final invariants should not depend on the input ending with explicit relay + // and mining bytes. + fn finish(&mut self) { + self.mine_relayed_txs_until_quiet(); + self.assert_only_expected_channel_closes(); + + // All broadcasters should be empty. Broadcast transactions are handled explicitly. + for node in &self.nodes { + assert!(node.broadcaster.txn_broadcasted.borrow().is_empty()); + } + } + + fn assert_only_expected_channel_closes(&self) { + // A close may show up first as a missing list_channels entry rather + // than as an already-drained ChannelClosed event. + self.ab_link.assert_no_unexpected_disappeared_channels(&self.nodes, &self.close_tracker); + self.bc_link.assert_no_unexpected_disappeared_channels(&self.nodes, &self.close_tracker); + } + + fn link_between(&self, source_idx: usize, dest_idx: usize) -> &PeerLink { + if self.ab_link.connects(source_idx, dest_idx) { + &self.ab_link + } else if self.bc_link.connects(source_idx, dest_idx) { + &self.bc_link + } else { + panic!("invalid payment peers") + } + } + + fn channel_ids_between(&self, source_idx: usize, dest_idx: usize) -> [ChannelId; 3] { + self.link_between(source_idx, dest_idx).channel_ids().clone() + } + + fn first_channel_id_between(&self, source_idx: usize, dest_idx: usize) -> ChannelId { + self.link_between(source_idx, dest_idx).first_channel_id() + } + + // API calls are filtered before we make them if the harness knows they would + // target stale state. The open-channel filters below still handle tracked- + // closed channel ids after both peers have dropped them from list_channels. + fn has_stale_closed_channel_between(&self, source_idx: usize, dest_idx: usize) -> bool { + let channel_ids = self.channel_ids_between(source_idx, dest_idx); + let source_channels = self.nodes[source_idx].list_channels(); + let dest_channels = self.nodes[dest_idx].list_channels(); + channel_ids.iter().any(|channel_id| { + self.close_tracker.is_closed_or_closing(channel_id) + && (source_channels.iter().any(|chan| chan.channel_id == *channel_id) + || dest_channels.iter().any(|chan| chan.channel_id == *channel_id)) + }) + } + + fn send_on_channel( + &mut self, source_idx: usize, dest_idx: usize, dest_chan_id: ChannelId, amt: u64, + ) -> bool { + if !self.close_tracker.is_open(&dest_chan_id) { + return false; + } + self.payments.send(&self.nodes, source_idx, dest_idx, dest_chan_id, amt) + } + + fn send(&mut self, source_idx: usize, dest_idx: usize, amt: u64) { + let chan_ids = self.channel_ids_between(source_idx, dest_idx); + let dest_chan_id = match self.close_tracker.open_channels(&chan_ids).first().copied() { + Some(chan_id) => chan_id, + None => return, + }; + self.payments.send_noret(&self.nodes, source_idx, dest_idx, dest_chan_id, amt); + } + + fn send_hop(&mut self, source_idx: usize, middle_idx: usize, dest_idx: usize, amt: u64) { + // Even if we route over an open SCID, the middle node's non-strict + // forwarding can pick a parallel channel that the harness has already + // tracked closed but the node still lists. In that window, the downstream + // HTLC may never get committed, so close cleanup has nothing to fail back + // and the source payment can remain pending. + if self.has_stale_closed_channel_between(source_idx, middle_idx) + || self.has_stale_closed_channel_between(middle_idx, dest_idx) + { + return; + } + let middle_chan_id = self.first_channel_id_between(source_idx, middle_idx); + let dest_chan_id = self.first_channel_id_between(middle_idx, dest_idx); + if !self.close_tracker.is_open(&middle_chan_id) + || !self.close_tracker.is_open(&dest_chan_id) + { + return; + } + self.payments.send_hop( + &self.nodes, + source_idx, + middle_idx, + middle_chan_id, + dest_idx, + dest_chan_id, + amt, + ); + } + + fn send_mpp_direct( + &mut self, source_idx: usize, dest_idx: usize, channels: MppDirectChannels, amt: u64, + ) { + match channels { + MppDirectChannels::All => { + let dest_chan_ids = self + .close_tracker + .open_channels(&self.channel_ids_between(source_idx, dest_idx)); + self.payments.send_mpp_direct( + &self.nodes, + source_idx, + dest_idx, + &dest_chan_ids[..], + amt, + ); + }, + MppDirectChannels::RepeatedFirst => { + let dest_chan_id = self.first_channel_id_between(source_idx, dest_idx); + if !self.close_tracker.is_open(&dest_chan_id) { + return; + } + let dest_chan_ids = [dest_chan_id, dest_chan_id, dest_chan_id]; + self.payments.send_mpp_direct( + &self.nodes, + source_idx, + dest_idx, + &dest_chan_ids, + amt, + ); + }, + } + } + + fn send_mpp_hop( + &mut self, source_idx: usize, middle_idx: usize, dest_idx: usize, channels: MppHopChannels, + amt: u64, + ) { + // Even if we route over an open SCID, the middle node's non-strict + // forwarding can pick a parallel channel that the harness has already + // tracked closed but the node still lists. In that window, the downstream + // HTLC may never get committed, so close cleanup has nothing to fail back + // and the source payment can remain pending. + if self.has_stale_closed_channel_between(source_idx, middle_idx) + || self.has_stale_closed_channel_between(middle_idx, dest_idx) + { + return; + } + let middle_chan_ids = self.channel_ids_between(source_idx, middle_idx); + let dest_chan_ids = self.channel_ids_between(middle_idx, dest_idx); + let middle_first_chan_id = middle_chan_ids[0]; + let dest_first_chan_id = dest_chan_ids[0]; + match channels { + MppHopChannels::FirstHop => { + let middle_chan_ids = self.close_tracker.open_channels(&middle_chan_ids); + if !self.close_tracker.is_open(&dest_first_chan_id) { + return; + } + let dest_chan_ids = [dest_first_chan_id]; + self.payments.send_mpp_hop( + &self.nodes, + source_idx, + middle_idx, + &middle_chan_ids[..], + dest_idx, + &dest_chan_ids, + amt, + ); + }, + MppHopChannels::BothHops => { + let middle_chan_ids = self.close_tracker.open_channels(&middle_chan_ids); + let dest_chan_ids = self.close_tracker.open_channels(&dest_chan_ids); + self.payments.send_mpp_hop( + &self.nodes, + source_idx, + middle_idx, + &middle_chan_ids[..], + dest_idx, + &dest_chan_ids[..], + amt, + ); + }, + MppHopChannels::SecondHop => { + if !self.close_tracker.is_open(&middle_first_chan_id) { + return; + } + let dest_chan_ids = self.close_tracker.open_channels(&dest_chan_ids); + let middle_chan_ids = [middle_first_chan_id]; + self.payments.send_mpp_hop( + &self.nodes, + source_idx, + middle_idx, + &middle_chan_ids, + dest_idx, + &dest_chan_ids[..], + amt, + ); + }, + } + } + + fn process_msg_events( + &mut self, node_idx: usize, corrupt_forward: bool, limit_events: ProcessMessages, + ) -> bool { + fn find_destination_node(nodes: &[HarnessNode<'_>; 3], node_id: &PublicKey) -> usize { + nodes + .iter() + .position(|node| node.get_our_node_id() == *node_id) + .expect("message destination should be a known harness node") + } + + fn log_msg_delivery<Out: Output + MaybeSend + MaybeSync>( + node_idx: usize, dest_idx: usize, msg_name: &str, out: &Out, + ) { + out.locked_write( + format!("Delivering {} from node {} to node {}.\n", msg_name, node_idx, dest_idx) + .as_bytes(), + ); + } + + fn log_peer_message<Out: Output + MaybeSend + MaybeSync>( + node_idx: usize, node_id: &PublicKey, nodes: &[HarnessNode<'_>; 3], out: &Out, + msg_name: &str, + ) -> usize { + let dest_idx = find_destination_node(nodes, node_id); + log_msg_delivery(node_idx, dest_idx, msg_name, out); + dest_idx + } + + fn handle_update_add_htlc( + source_node_id: PublicKey, dest: &HarnessNode<'_>, update_add: &UpdateAddHTLC, + corrupt_forward: bool, + ) { + if !corrupt_forward { + dest.handle_update_add_htlc(source_node_id, update_add); + } else { + // Corrupt the update_add_htlc message so that its HMAC check will fail and we + // generate an update_fail_malformed_htlc instead of an update_fail_htlc as we do + // when we reject a payment. + let mut msg_ser = update_add.encode(); + msg_ser[1000] ^= 0xff; + let new_msg = + UpdateAddHTLC::read_from_fixed_length_buffer(&mut &msg_ser[..]).unwrap(); + dest.handle_update_add_htlc(source_node_id, &new_msg); + } + } + + fn handle_update_htlcs_event<Out: Output + MaybeSend + MaybeSync>( + node_idx: usize, source_node_id: PublicKey, node_id: PublicKey, channel_id: ChannelId, + updates: CommitmentUpdate, corrupt_forward: bool, limit_events: ProcessMessages, + nodes: &[HarnessNode<'_>; 3], payments: &mut PaymentTracker, out: &Out, + ) -> Option<MessageSendEvent> { + let dest_idx = find_destination_node(nodes, &node_id); + let dest = &nodes[dest_idx]; + let CommitmentUpdate { + update_add_htlcs, + update_fail_htlcs, + update_fulfill_htlcs, + update_fail_malformed_htlcs, + update_fee, + commitment_signed, + } = updates; + + for update_add in update_add_htlcs.iter() { + log_msg_delivery(node_idx, dest_idx, "update_add_htlc", out); + if corrupt_forward { + payments.allow_failure_for_hash(update_add.payment_hash); } - had_events - }}; + handle_update_add_htlc(source_node_id, dest, update_add, corrupt_forward); + } + let processed_change = !update_add_htlcs.is_empty() + || !update_fulfill_htlcs.is_empty() + || !update_fail_htlcs.is_empty() + || !update_fail_malformed_htlcs.is_empty(); + for update_fulfill in update_fulfill_htlcs { + log_msg_delivery(node_idx, dest_idx, "update_fulfill_htlc", out); + dest.handle_update_fulfill_htlc(source_node_id, update_fulfill); + } + for update_fail in update_fail_htlcs.iter() { + log_msg_delivery(node_idx, dest_idx, "update_fail_htlc", out); + payments.record_downstream_failure( + dest_idx, + dest, + &source_node_id, + update_fail.channel_id, + update_fail.htlc_id, + ); + dest.handle_update_fail_htlc(source_node_id, update_fail); + } + for update_fail_malformed in update_fail_malformed_htlcs.iter() { + log_msg_delivery(node_idx, dest_idx, "update_fail_malformed_htlc", out); + payments.record_downstream_failure( + dest_idx, + dest, + &source_node_id, + update_fail_malformed.channel_id, + update_fail_malformed.htlc_id, + ); + dest.handle_update_fail_malformed_htlc(source_node_id, update_fail_malformed); + } + if let Some(msg) = update_fee { + log_msg_delivery(node_idx, dest_idx, "update_fee", out); + dest.handle_update_fee(source_node_id, &msg); + } + if limit_events != ProcessMessages::AllMessages && processed_change { + // If we only want to process some messages, don't deliver the CS until later. + return Some(MessageSendEvent::UpdateHTLCs { + node_id, + channel_id, + updates: CommitmentUpdate { + update_add_htlcs: Vec::new(), + update_fail_htlcs: Vec::new(), + update_fulfill_htlcs: Vec::new(), + update_fail_malformed_htlcs: Vec::new(), + update_fee: None, + commitment_signed, + }, + }); + } + log_msg_delivery(node_idx, dest_idx, "commitment_signed", out); + dest.handle_commitment_signed_batch_test(source_node_id, &commitment_signed); + None + } + + fn process_msg_event<Out: Output + MaybeSend + MaybeSync>( + node_idx: usize, source_node_id: PublicKey, event: MessageSendEvent, + corrupt_forward: bool, limit_events: ProcessMessages, nodes: &[HarnessNode<'_>; 3], + payments: &mut PaymentTracker, close_tracker: &ChannelCloseTracker, out: &Out, + ) -> Option<MessageSendEvent> { + // Always deliver message events, even when the harness knows they are stale, + // so message handlers exercise their normal error paths. + match event { + MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates } => { + handle_update_htlcs_event( + node_idx, + source_node_id, + node_id, + channel_id, + updates, + corrupt_forward, + limit_events, + nodes, + payments, + out, + ) + }, + MessageSendEvent::SendRevokeAndACK { ref node_id, ref msg } => { + let dest_idx = + log_peer_message(node_idx, node_id, nodes, out, "revoke_and_ack"); + nodes[dest_idx].handle_revoke_and_ack(source_node_id, msg); + None + }, + MessageSendEvent::SendChannelReestablish { ref node_id, ref msg } => { + if close_tracker.is_closed_or_closing(&msg.channel_id) { + // A reestablish generated before an explicit close is stale once that + // close is tracked. Delivering it can keep generating closed-channel + // error messages and prevent settle_all from quiescing. + return None; + } + let dest_idx = + log_peer_message(node_idx, node_id, nodes, out, "channel_reestablish"); + nodes[dest_idx].handle_channel_reestablish(source_node_id, msg); + None + }, + MessageSendEvent::SendStfu { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "stfu"); + nodes[dest_idx].handle_stfu(source_node_id, msg); + None + }, + MessageSendEvent::SendTxAddInput { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_add_input"); + nodes[dest_idx].handle_tx_add_input(source_node_id, msg); + None + }, + MessageSendEvent::SendTxAddOutput { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_add_output"); + nodes[dest_idx].handle_tx_add_output(source_node_id, msg); + None + }, + MessageSendEvent::SendTxRemoveInput { ref node_id, ref msg } => { + let dest_idx = + log_peer_message(node_idx, node_id, nodes, out, "tx_remove_input"); + nodes[dest_idx].handle_tx_remove_input(source_node_id, msg); + None + }, + MessageSendEvent::SendTxRemoveOutput { ref node_id, ref msg } => { + let dest_idx = + log_peer_message(node_idx, node_id, nodes, out, "tx_remove_output"); + nodes[dest_idx].handle_tx_remove_output(source_node_id, msg); + None + }, + MessageSendEvent::SendTxComplete { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_complete"); + nodes[dest_idx].handle_tx_complete(source_node_id, msg); + None + }, + MessageSendEvent::SendTxAbort { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_abort"); + nodes[dest_idx].handle_tx_abort(source_node_id, msg); + None + }, + MessageSendEvent::SendTxInitRbf { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_init_rbf"); + nodes[dest_idx].handle_tx_init_rbf(source_node_id, msg); + None + }, + MessageSendEvent::SendTxAckRbf { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_ack_rbf"); + nodes[dest_idx].handle_tx_ack_rbf(source_node_id, msg); + None + }, + MessageSendEvent::SendTxSignatures { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "tx_signatures"); + nodes[dest_idx].handle_tx_signatures(source_node_id, msg); + None + }, + MessageSendEvent::SendSpliceInit { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "splice_init"); + nodes[dest_idx].handle_splice_init(source_node_id, msg); + None + }, + MessageSendEvent::SendSpliceAck { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "splice_ack"); + nodes[dest_idx].handle_splice_ack(source_node_id, msg); + None + }, + MessageSendEvent::SendSpliceLocked { ref node_id, ref msg } => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "splice_locked"); + nodes[dest_idx].handle_splice_locked(source_node_id, msg); + None + }, + MessageSendEvent::HandleError { ref action, ref node_id, .. } => { + match assert_disconnect_action(action, close_tracker) { + ExpectedControlAction::Warning(msg, is_quiescent) => { + let dest_idx = + log_peer_message(node_idx, node_id, nodes, out, "warning"); + if is_quiescent && !close_tracker.is_closed_or_closing(&msg.channel_id) + { + nodes[node_idx] + .node + .exit_quiescence(node_id, &msg.channel_id) + .unwrap(); + nodes[dest_idx] + .node + .exit_quiescence(&source_node_id, &msg.channel_id) + .unwrap(); + } + }, + ExpectedControlAction::Error(msg) => { + let dest_idx = log_peer_message(node_idx, node_id, nodes, out, "error"); + nodes[dest_idx].handle_error(source_node_id, msg); + }, + } + None + }, + MessageSendEvent::SendChannelReady { .. } + | MessageSendEvent::SendAnnouncementSignatures { .. } + | MessageSendEvent::SendChannelUpdate { .. } => { + // Can be generated as a reestablish response. + None + }, + MessageSendEvent::BroadcastChannelUpdate { .. } => { + // Can be generated as a result of calling `timer_tick_occurred` enough + // times while peers are disconnected. + None + }, + _ => panic!("Unhandled message event {:?}", event), + } + } + + let nodes = &self.nodes; + let payments = &mut self.payments; + let close_tracker = &self.close_tracker; + let out = &self.out; + let queues = &mut self.queues; + let mut events = queues.take_for_node(node_idx); + let mut new_events = Vec::new(); + if limit_events != ProcessMessages::OnePendingMessage { + new_events = nodes[node_idx].get_and_clear_pending_msg_events(); + } + let mut had_events = false; + let source_node_id = nodes[node_idx].get_our_node_id(); + let mut events_iter = events.drain(..).chain(new_events.drain(..)); + let mut extra_ev = None; + for event in &mut events_iter { + had_events = true; + extra_ev = process_msg_event( + node_idx, + source_node_id, + event, + corrupt_forward, + limit_events, + nodes, + payments, + close_tracker, + out, + ); + if limit_events != ProcessMessages::AllMessages { + break; + } + } + if node_idx == 1 { + let remaining = extra_ev.into_iter().chain(events_iter).collect::<Vec<_>>(); + queues.route_from_middle(remaining, None, nodes, close_tracker); + } else if node_idx == 0 { + if let Some(ev) = extra_ev { + queues.push_for_node(0, ev); + } + queues.extend_for_node(0, events_iter); + } else { + if let Some(ev) = extra_ev { + queues.push_for_node(2, ev); + } + queues.extend_for_node(2, events_iter); + } + had_events + } + + fn process_events(&mut self, node_idx: usize, fail: bool) -> bool { + let nodes = &self.nodes; + let payments = &mut self.payments; + let chain_state = &self.chain_state; + let close_tracker = &mut self.close_tracker; + // Multiple HTLCs can resolve for the same payment hash, so deduplicate + // claim/fail handling per event batch. + let mut claim_set = new_hash_map(); + let mut events = nodes[node_idx].get_and_clear_pending_events(); + let mut had_events = !events.is_empty(); + for event in events.drain(..) { + match event { + events::Event::PaymentClaimable { payment_hash, .. } => { + if claim_set.insert(payment_hash.0, ()).is_none() { + payments.claim_payment(&nodes[node_idx], payment_hash, fail); + } + }, + events::Event::PaymentSent { payment_id, payment_hash, .. } => { + payments.nodes[node_idx].mark_sent(payment_id.unwrap(), payment_hash); + }, + // Even though we don't explicitly send probes, because probes are detected based on + // hashing the payment hash+preimage, it is rather trivial for the fuzzer to build + // payments that accidentally end up looking like probes. + events::Event::ProbeSuccessful { payment_id, .. } => { + payments.nodes[node_idx].mark_successful_probe(payment_id); + }, + events::Event::PaymentFailed { payment_id, .. } => { + payments.nodes[node_idx].mark_failed(node_idx, payment_id); + }, + events::Event::ProbeFailed { payment_id, .. } => { + payments.nodes[node_idx].mark_resolved_without_hash(payment_id); + }, + events::Event::PaymentClaimed { .. } => {}, + events::Event::PaymentPathSuccessful { .. } => {}, + events::Event::PaymentPathFailed { .. } => {}, + events::Event::PaymentForwarded { .. } if node_idx == 1 => {}, + events::Event::ChannelReady { .. } => {}, + events::Event::HTLCHandlingFailed { .. } => {}, + events::Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } => { + if close_tracker.is_closed_or_closing(&channel_id) { + // The signing event was queued before an explicit close. + // Do not call splice funding APIs for a tracked-closed channel. + continue; + } + let wallet_script = nodes[node_idx].wallet.get_change_script().unwrap(); + let has_unknown_spent_input = unsigned_transaction.input.iter().any(|input| { + !chain_state.is_unspent(&input.previous_output) + && chain_state.confirmed_output(&input.previous_output).is_none() + }); + assert!( + !has_unknown_spent_input, + "funding transaction referenced an unmodeled input: {:?}", + unsigned_transaction, + ); + let has_spent_wallet_input = unsigned_transaction.input.iter().any(|input| { + !chain_state.is_unspent(&input.previous_output) + && chain_state + .confirmed_output(&input.previous_output) + .map_or(false, |output| output.script_pubkey == wallet_script) + }); + if has_spent_wallet_input { + // A queued RBF signing request can lose the race against a + // transaction confirming with one of its wallet inputs. + match nodes[node_idx] + .cancel_funding_contributed(&channel_id, &counterparty_node_id) + { + Ok(()) => {}, + Err(APIError::APIMisuseError { ref err }) + if err.contains("does not have a pending splice negotiation") => {}, + Err(e) => panic!("{e:?}"), + } + } else { + let signed_tx = + nodes[node_idx].wallet.sign_tx(unsigned_transaction).unwrap(); + match nodes[node_idx].funding_transaction_signed( + &channel_id, + &counterparty_node_id, + signed_tx, + ) { + Ok(()) => {}, + Err(APIError::APIMisuseError { ref err }) + if err.contains("not expecting funding signatures") => + { + // A queued signing event can be invalidated by a later `tx_abort` + // before the application handles it. + }, + Err(e) => panic!("{e:?}"), + } + } + }, + events::Event::SpliceNegotiated { .. } => {}, + events::Event::SpliceNegotiationFailed { .. } => {}, + events::Event::ChannelClosed { channel_id, reason, .. } => { + close_tracker.verify_channel_closed_event(channel_id, &reason); + }, + events::Event::DiscardFunding { + funding_info: + events::FundingInfo::Contribution { .. } | events::FundingInfo::Tx { .. }, + .. + } => {}, + events::Event::SpendableOutputs { .. } => { + // The harness does not model an external sweeper wallet. + }, + events::Event::BumpTransaction(bump) => { + nodes[node_idx].bump_tx_handler.handle_event(&bump); + }, + _ => panic!("Unhandled event: {:?}", event), + } + } + // Chain monitor events are processed together with manager events, + // mirroring how a node's background processor polls both queues. + had_events |= nodes[node_idx].process_monitor_pending_events(); + while nodes[node_idx].needs_pending_htlc_processing() { + nodes[node_idx].process_pending_htlc_forwards(); + payments.allow_failure_for_local_inbound_htlcs(node_idx, &nodes[node_idx]); + had_events = true; + } + had_events + } + + fn process_msg_noret( + &mut self, node_idx: usize, corrupt_forward: bool, limit_events: ProcessMessages, + ) { + self.process_msg_events(node_idx, corrupt_forward, limit_events); + } + + fn process_ev_noret(&mut self, node_idx: usize, fail: bool) { + self.process_events(node_idx, fail); + } + + fn process_all_events(&mut self) { + let mut last_pass_no_updates = false; + for i in 0..std::usize::MAX { + if i == MAX_SETTLE_ITERATIONS { + panic!( + "It may take many iterations to settle the state, but it should not take forever" + ); + } + let mut made_progress = self.checkpoint_manager_persistences(); + // Next, make sure no monitor completion callbacks are pending. + made_progress |= self.ab_link.complete_all_monitor_updates(&self.nodes); + made_progress |= self.bc_link.complete_all_monitor_updates(&self.nodes); + // Then, make sure any current forwards make their way to their destination. + if self.process_msg_events(0, false, ProcessMessages::AllMessages) { + last_pass_no_updates = false; + continue; + } + if self.process_msg_events(1, false, ProcessMessages::AllMessages) { + last_pass_no_updates = false; + continue; + } + if self.process_msg_events(2, false, ProcessMessages::AllMessages) { + last_pass_no_updates = false; + continue; + } + // ...making sure any payments are claimed. + if self.process_events(0, false) { + last_pass_no_updates = false; + continue; + } + if self.process_events(1, false) { + last_pass_no_updates = false; + continue; + } + if self.process_events(2, false) { + last_pass_no_updates = false; + continue; + } + if made_progress { + last_pass_no_updates = false; + continue; + } + if last_pass_no_updates { + // In some cases, we may generate a message to send in + // `process_msg_events`, but block sending until + // `complete_all_monitor_updates` gets called on the next + // iteration. + // + // Thus, we only exit if we manage two iterations with no messages + // or events to process. + break; + } + last_pass_no_updates = true; + } + } + + fn disconnect_ab(&mut self) { + self.ab_link.disconnect(&self.nodes, &mut self.queues, &self.close_tracker); + } + + fn disconnect_bc(&mut self) { + self.bc_link.disconnect(&self.nodes, &mut self.queues, &self.close_tracker); + } + + fn reconnect_ab(&mut self) { + self.ab_link.reconnect(&self.nodes); + } + + fn reconnect_bc(&mut self) { + self.bc_link.reconnect(&self.nodes); + } + + fn channel_has_pending_htlcs(&self, channel_id: ChannelId) -> bool { + self.nodes.iter().any(|node| { + node.list_channels().iter().any(|chan| { + chan.channel_id == channel_id + && (!chan.pending_inbound_htlcs.is_empty() + || !chan.pending_outbound_htlcs.is_empty()) + }) + }) + } + + fn force_close(&mut self, closer_idx: usize, channel_id: ChannelId, counterparty_idx: usize) { + if self.close_tracker.is_closed_or_closing(&channel_id) + || self.channel_has_pending_htlcs(channel_id) + { + // This opcode only models closes whose target channel has no + // pending HTLCs. Other channels may still carry HTLCs that later + // fail back through normal peer messages during settlement. + return; + } + assert!( + self.nodes[closer_idx].list_channels().iter().any(|chan| chan.channel_id == channel_id), + "force-close target channel {:?} missing before explicit close", + channel_id, + ); + let reason = + format!("chanmon harness force-close by node {} on {:?}", closer_idx, channel_id); + match self.nodes[closer_idx].node.force_close_broadcasting_latest_txn( + &channel_id, + &self.nodes[counterparty_idx].get_our_node_id(), + reason.clone(), + ) { + Ok(()) => { + self.payments.allow_failure_for_closed_channel(channel_id); + self.close_tracker.expect_channel_close(channel_id, reason); + }, + Err(e) => panic!("{e:?}"), } + } - macro_rules! process_ev_noret { - ($node: expr, $fail: expr) => {{ - process_events!($node, $fail); - }}; + fn splice_in(&self, node_idx: usize, channel_id: ChannelId, counterparty_idx: usize) { + if self.close_tracker.is_closed_or_closing(&channel_id) { + return; } + let cp_node_id = self.nodes[counterparty_idx].get_our_node_id(); + self.nodes[node_idx].splice_in(&cp_node_id, &channel_id); + } - let complete_first = |v: &mut Vec<_>| if !v.is_empty() { Some(v.remove(0)) } else { None }; - let complete_second = |v: &mut Vec<_>| if v.len() > 1 { Some(v.remove(1)) } else { None }; - let complete_monitor_update = - |monitor: &Arc<TestChainMonitor>, - chan_funding, - compl_selector: &dyn Fn(&mut Vec<(u64, Vec<u8>)>) -> Option<(u64, Vec<u8>)>| { - if let Some(state) = monitor.latest_monitors.lock().unwrap().get_mut(chan_funding) { - assert!( - state.pending_monitors.windows(2).all(|pair| pair[0].0 < pair[1].0), - "updates should be sorted by id" - ); - if let Some((id, data)) = compl_selector(&mut state.pending_monitors) { - monitor.chain_monitor.channel_monitor_updated(*chan_funding, id).unwrap(); - if id > state.persisted_monitor_id { - state.persisted_monitor_id = id; - state.persisted_monitor = data; - } - } - } - }; + fn splice_out(&self, node_idx: usize, channel_id: ChannelId, counterparty_idx: usize) { + if self.close_tracker.is_closed_or_closing(&channel_id) { + return; + } + let cp_node_id = self.nodes[counterparty_idx].get_our_node_id(); + self.nodes[node_idx].splice_out(&cp_node_id, &channel_id); + } - let complete_all_monitor_updates = |monitor: &Arc<TestChainMonitor>, chan_id| { - if let Some(state) = monitor.latest_monitors.lock().unwrap().get_mut(chan_id) { - assert!( - state.pending_monitors.windows(2).all(|pair| pair[0].0 < pair[1].0), - "updates should be sorted by id" + fn restart_node(&mut self, node_idx: usize, v: u8, router: &'a FuzzRouter) { + if !self.nodes[node_idx].deferred { + self.nodes[node_idx].checkpoint_manager_persistence(); + } + match node_idx { + 0 => { + self.ab_link.disconnect_for_reload( + 0, + &self.nodes, + &mut self.queues, + &self.close_tracker, ); - for (id, data) in state.pending_monitors.drain(..) { - monitor.chain_monitor.channel_monitor_updated(*chan_id, id).unwrap(); - if id > state.persisted_monitor_id { - state.persisted_monitor_id = id; - state.persisted_monitor = data; - } + }, + 1 => { + self.ab_link.disconnect_for_reload( + 1, + &self.nodes, + &mut self.queues, + &self.close_tracker, + ); + self.bc_link.disconnect_for_reload( + 1, + &self.nodes, + &mut self.queues, + &self.close_tracker, + ); + }, + 2 => { + self.bc_link.disconnect_for_reload( + 2, + &self.nodes, + &mut self.queues, + &self.close_tracker, + ); + }, + _ => panic!("invalid node index"), + } + let loaded_manager_generation = + self.nodes[node_idx].reload(v, &self.out, router, self.chan_type); + // Startup sync is part of LDK's deserialization contract. Monitors and + // the manager can be loaded at different heights, so sync each monitor + // from its own best block rather than driving them all from the oldest + // one, which would look like a reorg to the monitors already ahead. + let manager_start_height = self.nodes[node_idx].manager_height(); + let tip_height = self.chain_state.tip_height(); + self.nodes[node_idx].sync_monitors_to_tip(&self.chain_state); + self.nodes[node_idx].connect_chain_range( + &self.chain_state, + manager_start_height, + tip_height, + false, + ); + assert_eq!( + self.nodes[node_idx].manager_height(), + self.chain_state.tip_height(), + "reloaded node {} must sync to the harness tip before normal operation resumes", + node_idx + ); + let rolled_back_payment_hashes = self.payments.nodes[node_idx] + .sync_pending_with_manager_generation(loaded_manager_generation); + for payment_hash in rolled_back_payment_hashes { + self.payments.claimed_payment_hashes.remove(&payment_hash); + } + } + + fn settle_all(&mut self) { + let chain_state = &self.chain_state; + for node in &mut self.nodes { + node.sync_with_chain_state(chain_state, None); + } + + // First, make sure peers are all connected to each other + self.reconnect_ab(); + self.reconnect_bc(); + + for op in SUPPORTED_SIGNER_OPS { + self.nodes[0].keys_manager.enable_op_for_all_signers(op); + self.nodes[1].keys_manager.enable_op_for_all_signers(op); + self.nodes[2].keys_manager.enable_op_for_all_signers(op); + } + // Live-channel signer work retries through the manager, while + // on-chain holder claims retry through the chain monitor. + self.nodes[0].signer_unblocked(None); + self.nodes[1].signer_unblocked(None); + self.nodes[2].signer_unblocked(None); + self.nodes[0].monitor.signer_unblocked(None); + self.nodes[1].monitor.signer_unblocked(None); + self.nodes[2].monitor.signer_unblocked(None); + + self.process_all_events(); + + // Since MPP payments are supported, we wait until we fully settle the state of all + // channels to see if we have any committed HTLC parts of an MPP payment that need + // to be failed back. + for node in self.nodes.iter() { + node.timer_tick_occurred(); + } + self.process_all_events(); + + if self.close_tracker.has_closed_channels() { + self.settle_force_close_onchain(); + } + + // Verify no payments are stuck - all should have resolved + self.payments.assert_all_resolved(); + // Verify that every payment claimed by a receiver resulted in a + // PaymentSent event at the sender. + self.payments.assert_claims_reported(); + + // All HTLCs should have been claimed or failed once we reach quiescence. + for (idx, node) in self.nodes.iter().enumerate() { + for chan in node.list_channels() { + if !self.close_tracker.is_open(&chan.channel_id) { + continue; } + assert!( + chan.pending_inbound_htlcs.is_empty() && chan.pending_outbound_htlcs.is_empty(), + "Node {} channel {:?} has stuck HTLCs after settling all state: \ + {} inbound {:?}, {} outbound {:?}", + idx, + chan.channel_id, + chan.pending_inbound_htlcs.len(), + chan.pending_inbound_htlcs, + chan.pending_outbound_htlcs.len(), + chan.pending_outbound_htlcs + ); } - }; + } + + self.assert_only_expected_channel_closes(); + + // Finally, make sure that at least one end of each live channel can make + // a substantial payment. + let chan_ab_ids = self.ab_link.channel_ids().clone(); + let chan_bc_ids = self.bc_link.channel_ids().clone(); + for chan_id in self.close_tracker.open_channels(&chan_ab_ids) { + assert!( + self.send_on_channel(0, 1, chan_id, 10_000_000) + || self.send_on_channel(1, 0, chan_id, 10_000_000) + ); + } + for chan_id in self.close_tracker.open_channels(&chan_bc_ids) { + assert!( + self.send_on_channel(1, 2, chan_id, 10_000_000) + || self.send_on_channel(2, 1, chan_id, 10_000_000) + ); + } + + self.nodes[0].record_last_htlc_clear_fee(); + self.nodes[1].record_last_htlc_clear_fee(); + self.nodes[2].record_last_htlc_clear_fee(); + } + + fn checkpoint_manager_persistences(&mut self) -> bool { + let mut made_progress = false; + for node in &mut self.nodes { + made_progress |= node.checkpoint_manager_persistence(); + } + made_progress + } + + // Relays one node's broadcasts into the mempool. Per-node relay lets fuzz + // inputs model partial propagation before a block is mined. + fn relay_broadcasts_for_node(&mut self, node_idx: usize) { + let txs = self.nodes[node_idx] + .broadcaster + .txn_broadcasted + .borrow_mut() + .drain(..) + .collect::<Vec<_>>(); + self.chain_state.relay_transactions(txs); + } + + fn relay_all_broadcasts(&mut self) { + let mut txs = Vec::new(); + for node in &self.nodes { + txs.extend(node.broadcaster.txn_broadcasted.borrow_mut().drain(..)); + } + self.chain_state.relay_transactions(txs); + } - let send = - |source_idx: usize, dest_idx: usize, dest_chan_id, amt, payment_ctr: &mut u64| { - let source = &nodes[source_idx]; - let dest = &nodes[dest_idx]; - let (secret, hash) = get_payment_secret_hash(dest, payment_ctr); - let mut id = PaymentId([0; 32]); - id.0[0..8].copy_from_slice(&payment_ctr.to_ne_bytes()); - let succeeded = send_payment(source, dest, dest_chan_id, amt, secret, hash, id); - if succeeded { - pending_payments.borrow_mut()[source_idx].push(id); + fn earliest_pending_htlc_expiry(&self) -> Option<u32> { + let mut earliest_expiry: Option<u32> = None; + for node in &self.nodes { + for chan in node.list_channels() { + for htlc in &chan.pending_inbound_htlcs { + earliest_expiry = Some( + earliest_expiry + .map_or(htlc.cltv_expiry, |expiry| expiry.min(htlc.cltv_expiry)), + ); } - succeeded - }; - let send_noret = |source_idx, dest_idx, dest_chan_id, amt, payment_ctr: &mut u64| { - send(source_idx, dest_idx, dest_chan_id, amt, payment_ctr); - }; + for htlc in &chan.pending_outbound_htlcs { + earliest_expiry = Some( + earliest_expiry + .map_or(htlc.cltv_expiry, |expiry| expiry.min(htlc.cltv_expiry)), + ); + } + } + } + earliest_expiry + } - let send_hop_noret = |source_idx: usize, - middle_idx: usize, - middle_scid: u64, - dest_idx: usize, - dest_scid: u64, - amt: u64, - payment_ctr: &mut u64| { - let source = &nodes[source_idx]; - let middle = &nodes[middle_idx]; - let dest = &nodes[dest_idx]; - let (secret, hash) = get_payment_secret_hash(dest, payment_ctr); - let mut id = PaymentId([0; 32]); - id.0[0..8].copy_from_slice(&payment_ctr.to_ne_bytes()); - let succeeded = send_hop_payment( - source, - middle, - middle_scid, - dest, - dest_scid, - amt, - secret, - hash, - id, + fn safe_mine_block_count(&self, count: u32) -> u32 { + if let Some(expiry) = self.earliest_pending_htlc_expiry() { + let current_tip = self.chain_state.tip_height(); + // LDK may close to protect a pending HTLC before its raw CLTV + // expiry. Keep mining outside that fail-back window so fuzzed block + // production does not force an on-chain timeout path. + let timeout_deadline = expiry.saturating_sub(channelmonitor::HTLC_FAIL_BACK_BUFFER); + assert!( + current_tip < timeout_deadline, + "pending HTLC with expiry {} and timeout deadline {} is already unsafe at tip {}", + expiry, + timeout_deadline, + current_tip ); - if succeeded { - pending_payments.borrow_mut()[source_idx].push(id); + // Stop before the deadline block itself, since connecting it is + // enough for ChannelMonitor timeout handling to run. + count.min(timeout_deadline - current_tip - 1) + } else { + count + } + } + + // Mines blocks through ChainState, then applies confirmed transactions to + // the wallets and syncs node chain listeners. + fn mine_blocks(&mut self, count: u32) -> u32 { + assert!(count > 0, "mining zero blocks should not be requested"); + + let count = self.safe_mine_block_count(count); + if count == 0 { + return 0; + } + let confirmed_txs = self.chain_state.mine_blocks(count); + self.payments.allow_failure_for_receive_cltv_buffer(self.chain_state.tip_height()); + let wallets = [ + self.nodes[0].wallet.as_ref(), + self.nodes[1].wallet.as_ref(), + self.nodes[2].wallet.as_ref(), + ]; + for tx in &confirmed_txs { + for wallet in wallets.iter().copied() { + let change_script = wallet.get_change_script().unwrap(); + for input in &tx.input { + // The test wallet is a simple UTXO source. When one of its + // outputs is spent by a confirmed transaction, remove it so + // later funding attempts cannot double-spend it. + wallet.remove_utxo(input.previous_output); + } + for (vout, output) in tx.output.iter().enumerate() { + if output.script_pubkey == change_script { + // Add outputs to whichever test wallet owns the script. + // This lets splice flows recycle wallet change through + // later fuzz commands. + wallet.add_utxo(tx.clone(), vout as u32); + } + } } - }; + } + let chain_state = &self.chain_state; + for node in &mut self.nodes { + node.sync_with_chain_state(chain_state, None); + } + count + } - // Direct MPP payment (no hop) - let send_mpp_direct = |source_idx: usize, - dest_idx: usize, - dest_scids: &[u64], - amt: u64, - payment_ctr: &mut u64| { - let source = &nodes[source_idx]; - let dest = &nodes[dest_idx]; - let (secret, hash) = get_payment_secret_hash(dest, payment_ctr); - let mut id = PaymentId([0; 32]); - id.0[0..8].copy_from_slice(&payment_ctr.to_ne_bytes()); - let succeeded = send_mpp_payment(source, dest, dest_scids, amt, secret, hash, id); - if succeeded { - pending_payments.borrow_mut()[source_idx].push(id); + fn mine_relayed_txs_until_quiet(&mut self) { + for _ in 0..MAX_FINISH_RELAY_MINE_ROUNDS { + self.relay_all_broadcasts(); + if self.chain_state.pending_txs.is_empty() { + return; } - }; + if self.mine_blocks(ANTI_REORG_DELAY) == 0 { + // Pending mempool transactions remain, but no safe block is + // left before an HTLC fail-back window. Leave them unconfirmed + // rather than advancing the chain past that boundary. + return; + } + } + assert!( + !self.nodes.iter().any(|node| !node.broadcaster.txn_broadcasted.borrow().is_empty()) + && self.chain_state.pending_txs.is_empty(), + "tx mining loop failed to quiesce", + ); + } - // MPP payment via hop - splits payment across multiple channels on either or both hops - let send_mpp_hop = |source_idx: usize, - middle_idx: usize, - middle_scids: &[u64], - dest_idx: usize, - dest_scids: &[u64], - amt: u64, - payment_ctr: &mut u64| { - let source = &nodes[source_idx]; - let middle = &nodes[middle_idx]; - let dest = &nodes[dest_idx]; - let (secret, hash) = get_payment_secret_hash(dest, payment_ctr); - let mut id = PaymentId([0; 32]); - id.0[0..8].copy_from_slice(&payment_ctr.to_ne_bytes()); - let succeeded = send_mpp_hop_payment( - source, - middle, - middle_scids, - dest, - dest_scids, - amt, - secret, - hash, - id, - ); - if succeeded { - pending_payments.borrow_mut()[source_idx].push(id); + fn settle_force_close_onchain(&mut self) { + // Alternate event processing, relay, and mining until all tracked + // closed-channel on-chain balances have resolved. + let deadline_blocked = "force-close cleanup was blocked by an HTLC fail-back deadline"; + for _ in 0..FORCE_CLOSE_CLEANUP_ROUNDS { + self.process_all_events(); + self.relay_all_broadcasts(); + if !self.chain_state.pending_txs.is_empty() { + assert!(self.mine_blocks(ANTI_REORG_DELAY) > 0, "{}", deadline_blocked); + continue; } - }; + let has_claimable_balance = self.nodes.iter().any(|node| { + // get_claimable_balances ignores the channels passed in. Pass + // each node's own live channels so closed-channel balances stay + // visible. + let open_channels = node.node.list_channels(); + let open_refs: Vec<_> = open_channels.iter().collect(); + !node.monitor.get_claimable_balances(&open_refs).is_empty() + }); + if !has_claimable_balance { + return; + } + assert!(self.mine_blocks(1) > 0, "{}", deadline_blocked); + } + panic!("force-close cleanup loop failed to quiesce"); + } +} - let v = get_slice!(1)[0]; - out.locked_write(format!("READ A BYTE! HANDLING INPUT {:x}...........\n", v).as_bytes()); +#[inline] +pub fn do_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) { + let router = FuzzRouter {}; + // Read initial monitor styles, channel type, and deferred write mode from fuzz input byte 0: + // bits 0-2: monitor styles (1 bit per node) + // bits 3-4: channel type (0=Legacy, 1=KeyedAnchors, 2=ZeroFeeCommitments) + // bits 5-7: deferred monitor write mode (1 bit per node) + let config_byte = if !data.is_empty() { data[0] } else { 0 }; + let mut harness = Harness::new(config_byte, out, &router); + let mut read_pos = 1; // First byte was consumed for initial config. + + 'fuzz_loop: loop { + if data.len() < read_pos + 1 { + break 'fuzz_loop; + } + let v = data[read_pos]; + read_pos += 1; + harness + .out + .locked_write(format!("READ A BYTE! HANDLING INPUT {:x}...........\n", v).as_bytes()); match v { // In general, we keep related message groups close together in binary form, allowing // bit-twiddling mutations to have similar effects. This is probably overkill, but no // harm in doing so. - 0x00 => { - *mon_style[0].borrow_mut() = ChannelMonitorUpdateStatus::InProgress; - }, - 0x01 => { - *mon_style[1].borrow_mut() = ChannelMonitorUpdateStatus::InProgress; - }, - 0x02 => { - *mon_style[2].borrow_mut() = ChannelMonitorUpdateStatus::InProgress; - }, - 0x04 => { - *mon_style[0].borrow_mut() = ChannelMonitorUpdateStatus::Completed; - }, - 0x05 => { - *mon_style[1].borrow_mut() = ChannelMonitorUpdateStatus::Completed; - }, - 0x06 => { - *mon_style[2].borrow_mut() = ChannelMonitorUpdateStatus::Completed; - }, + 0x00 => harness.nodes[0].set_persistence_style(ChannelMonitorUpdateStatus::InProgress), + 0x01 => harness.nodes[1].set_persistence_style(ChannelMonitorUpdateStatus::InProgress), + 0x02 => harness.nodes[2].set_persistence_style(ChannelMonitorUpdateStatus::InProgress), + 0x04 => harness.nodes[0].set_persistence_style(ChannelMonitorUpdateStatus::Completed), + 0x05 => harness.nodes[1].set_persistence_style(ChannelMonitorUpdateStatus::Completed), + 0x06 => harness.nodes[2].set_persistence_style(ChannelMonitorUpdateStatus::Completed), 0x08 => { - for id in &chan_ab_ids { - complete_all_monitor_updates(&monitor_a, id); + for id in harness.ab_link.channel_ids() { + harness.nodes[0].complete_all_monitor_updates(id); } }, 0x09 => { - for id in &chan_ab_ids { - complete_all_monitor_updates(&monitor_b, id); + for id in harness.ab_link.channel_ids() { + harness.nodes[1].complete_all_monitor_updates(id); } }, 0x0a => { - for id in &chan_bc_ids { - complete_all_monitor_updates(&monitor_b, id); + for id in harness.bc_link.channel_ids() { + harness.nodes[1].complete_all_monitor_updates(id); } }, 0x0b => { - for id in &chan_bc_ids { - complete_all_monitor_updates(&monitor_c, id); - } - }, - - 0x0c => { - if !peers_ab_disconnected { - nodes[0].peer_disconnected(nodes[1].get_our_node_id()); - nodes[1].peer_disconnected(nodes[0].get_our_node_id()); - peers_ab_disconnected = true; - drain_msg_events_on_disconnect!(0); - } - }, - 0x0d => { - if !peers_bc_disconnected { - nodes[1].peer_disconnected(nodes[2].get_our_node_id()); - nodes[2].peer_disconnected(nodes[1].get_our_node_id()); - peers_bc_disconnected = true; - drain_msg_events_on_disconnect!(2); - } - }, - 0x0e => { - if peers_ab_disconnected { - let init_1 = Init { - features: nodes[1].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[0].peer_connected(nodes[1].get_our_node_id(), &init_1, true).unwrap(); - let init_0 = Init { - features: nodes[0].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[1].peer_connected(nodes[0].get_our_node_id(), &init_0, false).unwrap(); - peers_ab_disconnected = false; + for id in harness.bc_link.channel_ids() { + harness.nodes[2].complete_all_monitor_updates(id); } }, - 0x0f => { - if peers_bc_disconnected { - let init_2 = Init { - features: nodes[2].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[1].peer_connected(nodes[2].get_our_node_id(), &init_2, true).unwrap(); - let init_1 = Init { - features: nodes[1].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[2].peer_connected(nodes[1].get_our_node_id(), &init_1, false).unwrap(); - peers_bc_disconnected = false; - } - }, - - 0x10 => process_msg_noret!(0, true, ProcessMessages::AllMessages), - 0x11 => process_msg_noret!(0, false, ProcessMessages::AllMessages), - 0x12 => process_msg_noret!(0, true, ProcessMessages::OneMessage), - 0x13 => process_msg_noret!(0, false, ProcessMessages::OneMessage), - 0x14 => process_msg_noret!(0, true, ProcessMessages::OnePendingMessage), - 0x15 => process_msg_noret!(0, false, ProcessMessages::OnePendingMessage), - - 0x16 => process_ev_noret!(0, true), - 0x17 => process_ev_noret!(0, false), - 0x18 => process_msg_noret!(1, true, ProcessMessages::AllMessages), - 0x19 => process_msg_noret!(1, false, ProcessMessages::AllMessages), - 0x1a => process_msg_noret!(1, true, ProcessMessages::OneMessage), - 0x1b => process_msg_noret!(1, false, ProcessMessages::OneMessage), - 0x1c => process_msg_noret!(1, true, ProcessMessages::OnePendingMessage), - 0x1d => process_msg_noret!(1, false, ProcessMessages::OnePendingMessage), - - 0x1e => process_ev_noret!(1, true), - 0x1f => process_ev_noret!(1, false), - - 0x20 => process_msg_noret!(2, true, ProcessMessages::AllMessages), - 0x21 => process_msg_noret!(2, false, ProcessMessages::AllMessages), - 0x22 => process_msg_noret!(2, true, ProcessMessages::OneMessage), - 0x23 => process_msg_noret!(2, false, ProcessMessages::OneMessage), - 0x24 => process_msg_noret!(2, true, ProcessMessages::OnePendingMessage), - 0x25 => process_msg_noret!(2, false, ProcessMessages::OnePendingMessage), - - 0x26 => process_ev_noret!(2, true), - 0x27 => process_ev_noret!(2, false), + 0x0c => harness.disconnect_ab(), + 0x0d => harness.disconnect_bc(), + 0x0e => harness.reconnect_ab(), + 0x0f => harness.reconnect_bc(), + + 0x10 => harness.process_msg_noret(0, true, ProcessMessages::AllMessages), + 0x11 => harness.process_msg_noret(0, false, ProcessMessages::AllMessages), + 0x12 => harness.process_msg_noret(0, true, ProcessMessages::OneMessage), + 0x13 => harness.process_msg_noret(0, false, ProcessMessages::OneMessage), + 0x14 => harness.process_msg_noret(0, true, ProcessMessages::OnePendingMessage), + 0x15 => harness.process_msg_noret(0, false, ProcessMessages::OnePendingMessage), + + 0x16 => harness.process_ev_noret(0, true), + 0x17 => harness.process_ev_noret(0, false), + + 0x18 => harness.process_msg_noret(1, true, ProcessMessages::AllMessages), + 0x19 => harness.process_msg_noret(1, false, ProcessMessages::AllMessages), + 0x1a => harness.process_msg_noret(1, true, ProcessMessages::OneMessage), + 0x1b => harness.process_msg_noret(1, false, ProcessMessages::OneMessage), + 0x1c => harness.process_msg_noret(1, true, ProcessMessages::OnePendingMessage), + 0x1d => harness.process_msg_noret(1, false, ProcessMessages::OnePendingMessage), + + 0x1e => harness.process_ev_noret(1, true), + 0x1f => harness.process_ev_noret(1, false), + + 0x20 => harness.process_msg_noret(2, true, ProcessMessages::AllMessages), + 0x21 => harness.process_msg_noret(2, false, ProcessMessages::AllMessages), + 0x22 => harness.process_msg_noret(2, true, ProcessMessages::OneMessage), + 0x23 => harness.process_msg_noret(2, false, ProcessMessages::OneMessage), + 0x24 => harness.process_msg_noret(2, true, ProcessMessages::OnePendingMessage), + 0x25 => harness.process_msg_noret(2, false, ProcessMessages::OnePendingMessage), + + 0x26 => harness.process_ev_noret(2, true), + 0x27 => harness.process_ev_noret(2, false), // 1/10th the channel size: - 0x30 => send_noret(0, 1, chan_a, 10_000_000, &mut p_ctr), - 0x31 => send_noret(1, 0, chan_a, 10_000_000, &mut p_ctr), - 0x32 => send_noret(1, 2, chan_b, 10_000_000, &mut p_ctr), - 0x33 => send_noret(2, 1, chan_b, 10_000_000, &mut p_ctr), - 0x34 => send_hop_noret(0, 1, chan_a, 2, chan_b, 10_000_000, &mut p_ctr), - 0x35 => send_hop_noret(2, 1, chan_b, 0, chan_a, 10_000_000, &mut p_ctr), - - 0x38 => send_noret(0, 1, chan_a, 1_000_000, &mut p_ctr), - 0x39 => send_noret(1, 0, chan_a, 1_000_000, &mut p_ctr), - 0x3a => send_noret(1, 2, chan_b, 1_000_000, &mut p_ctr), - 0x3b => send_noret(2, 1, chan_b, 1_000_000, &mut p_ctr), - 0x3c => send_hop_noret(0, 1, chan_a, 2, chan_b, 1_000_000, &mut p_ctr), - 0x3d => send_hop_noret(2, 1, chan_b, 0, chan_a, 1_000_000, &mut p_ctr), - - 0x40 => send_noret(0, 1, chan_a, 100_000, &mut p_ctr), - 0x41 => send_noret(1, 0, chan_a, 100_000, &mut p_ctr), - 0x42 => send_noret(1, 2, chan_b, 100_000, &mut p_ctr), - 0x43 => send_noret(2, 1, chan_b, 100_000, &mut p_ctr), - 0x44 => send_hop_noret(0, 1, chan_a, 2, chan_b, 100_000, &mut p_ctr), - 0x45 => send_hop_noret(2, 1, chan_b, 0, chan_a, 100_000, &mut p_ctr), - - 0x48 => send_noret(0, 1, chan_a, 10_000, &mut p_ctr), - 0x49 => send_noret(1, 0, chan_a, 10_000, &mut p_ctr), - 0x4a => send_noret(1, 2, chan_b, 10_000, &mut p_ctr), - 0x4b => send_noret(2, 1, chan_b, 10_000, &mut p_ctr), - 0x4c => send_hop_noret(0, 1, chan_a, 2, chan_b, 10_000, &mut p_ctr), - 0x4d => send_hop_noret(2, 1, chan_b, 0, chan_a, 10_000, &mut p_ctr), - - 0x50 => send_noret(0, 1, chan_a, 1_000, &mut p_ctr), - 0x51 => send_noret(1, 0, chan_a, 1_000, &mut p_ctr), - 0x52 => send_noret(1, 2, chan_b, 1_000, &mut p_ctr), - 0x53 => send_noret(2, 1, chan_b, 1_000, &mut p_ctr), - 0x54 => send_hop_noret(0, 1, chan_a, 2, chan_b, 1_000, &mut p_ctr), - 0x55 => send_hop_noret(2, 1, chan_b, 0, chan_a, 1_000, &mut p_ctr), - - 0x58 => send_noret(0, 1, chan_a, 100, &mut p_ctr), - 0x59 => send_noret(1, 0, chan_a, 100, &mut p_ctr), - 0x5a => send_noret(1, 2, chan_b, 100, &mut p_ctr), - 0x5b => send_noret(2, 1, chan_b, 100, &mut p_ctr), - 0x5c => send_hop_noret(0, 1, chan_a, 2, chan_b, 100, &mut p_ctr), - 0x5d => send_hop_noret(2, 1, chan_b, 0, chan_a, 100, &mut p_ctr), - - 0x60 => send_noret(0, 1, chan_a, 10, &mut p_ctr), - 0x61 => send_noret(1, 0, chan_a, 10, &mut p_ctr), - 0x62 => send_noret(1, 2, chan_b, 10, &mut p_ctr), - 0x63 => send_noret(2, 1, chan_b, 10, &mut p_ctr), - 0x64 => send_hop_noret(0, 1, chan_a, 2, chan_b, 10, &mut p_ctr), - 0x65 => send_hop_noret(2, 1, chan_b, 0, chan_a, 10, &mut p_ctr), - - 0x68 => send_noret(0, 1, chan_a, 1, &mut p_ctr), - 0x69 => send_noret(1, 0, chan_a, 1, &mut p_ctr), - 0x6a => send_noret(1, 2, chan_b, 1, &mut p_ctr), - 0x6b => send_noret(2, 1, chan_b, 1, &mut p_ctr), - 0x6c => send_hop_noret(0, 1, chan_a, 2, chan_b, 1, &mut p_ctr), - 0x6d => send_hop_noret(2, 1, chan_b, 0, chan_a, 1, &mut p_ctr), + 0x30 => harness.send(0, 1, 10_000_000), + 0x31 => harness.send(1, 0, 10_000_000), + 0x32 => harness.send(1, 2, 10_000_000), + 0x33 => harness.send(2, 1, 10_000_000), + 0x34 => harness.send_hop(0, 1, 2, 10_000_000), + 0x35 => harness.send_hop(2, 1, 0, 10_000_000), + + 0x38 => harness.send(0, 1, 1_000_000), + 0x39 => harness.send(1, 0, 1_000_000), + 0x3a => harness.send(1, 2, 1_000_000), + 0x3b => harness.send(2, 1, 1_000_000), + 0x3c => harness.send_hop(0, 1, 2, 1_000_000), + 0x3d => harness.send_hop(2, 1, 0, 1_000_000), + + 0x40 => harness.send(0, 1, 100_000), + 0x41 => harness.send(1, 0, 100_000), + 0x42 => harness.send(1, 2, 100_000), + 0x43 => harness.send(2, 1, 100_000), + 0x44 => harness.send_hop(0, 1, 2, 100_000), + 0x45 => harness.send_hop(2, 1, 0, 100_000), + + 0x48 => harness.send(0, 1, 10_000), + 0x49 => harness.send(1, 0, 10_000), + 0x4a => harness.send(1, 2, 10_000), + 0x4b => harness.send(2, 1, 10_000), + 0x4c => harness.send_hop(0, 1, 2, 10_000), + 0x4d => harness.send_hop(2, 1, 0, 10_000), + + 0x50 => harness.send(0, 1, 1_000), + 0x51 => harness.send(1, 0, 1_000), + 0x52 => harness.send(1, 2, 1_000), + 0x53 => harness.send(2, 1, 1_000), + 0x54 => harness.send_hop(0, 1, 2, 1_000), + 0x55 => harness.send_hop(2, 1, 0, 1_000), + + 0x58 => harness.send(0, 1, 100), + 0x59 => harness.send(1, 0, 100), + 0x5a => harness.send(1, 2, 100), + 0x5b => harness.send(2, 1, 100), + 0x5c => harness.send_hop(0, 1, 2, 100), + 0x5d => harness.send_hop(2, 1, 0, 100), + + 0x60 => harness.send(0, 1, 10), + 0x61 => harness.send(1, 0, 10), + 0x62 => harness.send(1, 2, 10), + 0x63 => harness.send(2, 1, 10), + 0x64 => harness.send_hop(0, 1, 2, 10), + 0x65 => harness.send_hop(2, 1, 0, 10), + + 0x68 => harness.send(0, 1, 1), + 0x69 => harness.send(1, 0, 1), + 0x6a => harness.send(1, 2, 1), + 0x6b => harness.send(2, 1, 1), + 0x6c => harness.send_hop(0, 1, 2, 1), + 0x6d => harness.send_hop(2, 1, 0, 1), // MPP payments // 0x70: direct MPP from 0 to 1 (multi A-B channels) - 0x70 => send_mpp_direct(0, 1, &chan_ab_scids, 1_000_000, &mut p_ctr), + 0x70 => harness.send_mpp_direct(0, 1, MppDirectChannels::All, 1_000_000), // 0x71: MPP 0->1->2, multi channels on first hop (A-B) - 0x71 => send_mpp_hop(0, 1, &chan_ab_scids, 2, &[chan_b], 1_000_000, &mut p_ctr), + 0x71 => harness.send_mpp_hop(0, 1, 2, MppHopChannels::FirstHop, 1_000_000), // 0x72: MPP 0->1->2, multi channels on both hops (A-B and B-C) - 0x72 => send_mpp_hop(0, 1, &chan_ab_scids, 2, &chan_bc_scids, 1_000_000, &mut p_ctr), + 0x72 => harness.send_mpp_hop(0, 1, 2, MppHopChannels::BothHops, 1_000_000), // 0x73: MPP 0->1->2, multi channels on second hop (B-C) - 0x73 => send_mpp_hop(0, 1, &[chan_a], 2, &chan_bc_scids, 1_000_000, &mut p_ctr), + 0x73 => harness.send_mpp_hop(0, 1, 2, MppHopChannels::SecondHop, 1_000_000), // 0x74: direct MPP from 0 to 1, multi parts over single channel - 0x74 => send_mpp_direct(0, 1, &[chan_a, chan_a, chan_a], 1_000_000, &mut p_ctr), + 0x74 => harness.send_mpp_direct(0, 1, MppDirectChannels::RepeatedFirst, 1_000_000), - 0x80 => { - let mut max_feerate = last_htlc_clear_fee_a; - if !anchors { - max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; - } - if fee_est_a.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate { - fee_est_a.ret_val.store(max_feerate, atomic::Ordering::Release); - } - nodes[0].maybe_update_chan_fees(); - }, - 0x81 => { - fee_est_a.ret_val.store(253, atomic::Ordering::Release); - nodes[0].maybe_update_chan_fees(); - }, + 0x80 => harness.nodes[0].bump_fee_estimate(harness.chan_type), + 0x81 => harness.nodes[0].reset_fee_estimate(), + 0x84 => harness.nodes[1].bump_fee_estimate(harness.chan_type), + 0x85 => harness.nodes[1].reset_fee_estimate(), + 0x88 => harness.nodes[2].bump_fee_estimate(harness.chan_type), + 0x89 => harness.nodes[2].reset_fee_estimate(), - 0x84 => { - let mut max_feerate = last_htlc_clear_fee_b; - if !anchors { - max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; - } - if fee_est_b.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate { - fee_est_b.ret_val.store(max_feerate, atomic::Ordering::Release); - } - nodes[1].maybe_update_chan_fees(); - }, - 0x85 => { - fee_est_b.ret_val.store(253, atomic::Ordering::Release); - nodes[1].maybe_update_chan_fees(); + 0x90 => { + harness.nodes[0].checkpoint_manager_persistence(); }, - - 0x88 => { - let mut max_feerate = last_htlc_clear_fee_c; - if !anchors { - max_feerate *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; - } - if fee_est_c.ret_val.fetch_add(250, atomic::Ordering::AcqRel) + 250 > max_feerate { - fee_est_c.ret_val.store(max_feerate, atomic::Ordering::Release); - } - nodes[2].maybe_update_chan_fees(); + 0x91 => { + harness.nodes[1].checkpoint_manager_persistence(); }, - 0x89 => { - fee_est_c.ret_val.store(253, atomic::Ordering::Release); - nodes[2].maybe_update_chan_fees(); + 0x92 => { + harness.nodes[2].checkpoint_manager_persistence(); }, - 0xa0 => { - let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); - let contribution = - SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); - let funding_feerate_sat_per_kw = fee_est_a.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[0].splice_channel( - &chan_a_id, - &nodes[1].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } - }, - 0xa1 => { - let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); - let contribution = - SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); - let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[1].splice_channel( - &chan_a_id, - &nodes[0].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } + 0xa0 => harness.splice_in(0, harness.chan_a_id(), 1), + 0xa1 => harness.splice_in(1, harness.chan_a_id(), 0), + 0xa2 => harness.splice_in(1, harness.chan_b_id(), 2), + 0xa3 => harness.splice_in(2, harness.chan_b_id(), 1), + + 0xa4 => harness.splice_out(0, harness.chan_a_id(), 1), + 0xa5 => harness.splice_out(1, harness.chan_a_id(), 0), + 0xa6 => harness.splice_out(1, harness.chan_b_id(), 2), + 0xa7 => harness.splice_out(2, harness.chan_b_id(), 1), + + // Sync node by 1 block. + 0xa8 => harness.nodes[0].sync_with_chain_state(&harness.chain_state, Some(1)), + 0xa9 => harness.nodes[1].sync_with_chain_state(&harness.chain_state, Some(1)), + 0xaa => harness.nodes[2].sync_with_chain_state(&harness.chain_state, Some(1)), + // Sync node to chain tip. + 0xab => harness.nodes[0].sync_with_chain_state(&harness.chain_state, None), + 0xac => harness.nodes[1].sync_with_chain_state(&harness.chain_state, None), + 0xad => harness.nodes[2].sync_with_chain_state(&harness.chain_state, None), + + 0xb0 | 0xb1 | 0xb2 => { + // Restart node A, picking among persisted and in-flight `ChannelMonitor` + // candidates based on the value of `v` we're matching. + harness.restart_node(0, v, &router); }, - 0xa2 => { - let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap(); - let contribution = - SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); - let funding_feerate_sat_per_kw = fee_est_b.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[1].splice_channel( - &chan_b_id, - &nodes[2].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } + 0xb3..=0xbb => { + // Restart node B, picking among persisted and in-flight `ChannelMonitor` + // candidates based on the value of `v` we're matching. + harness.restart_node(1, v, &router); }, - 0xa3 => { - let input = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 1).unwrap(); - let contribution = - SpliceContribution::splice_in(Amount::from_sat(10_000), vec![input], None); - let funding_feerate_sat_per_kw = fee_est_c.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[2].splice_channel( - &chan_b_id, - &nodes[1].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } + 0xbc | 0xbd | 0xbe => { + // Restart node C, picking among persisted and in-flight `ChannelMonitor` + // candidates based on the value of `v` we're matching. + harness.restart_node(2, v, &router); }, - // We conditionally splice out `MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS` only when the node - // has double the balance required to send a payment upon a `0xff` byte. We do this to - // ensure there's always liquidity available for a payment to succeed then. - 0xa4 => { - let outbound_capacity_msat = nodes[0] - .list_channels() - .iter() - .find(|chan| chan.channel_id == chan_a_id) - .map(|chan| chan.outbound_capacity_msat) - .unwrap(); - if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[0].script_pubkey.clone(), - }]); - let funding_feerate_sat_per_kw = - fee_est_a.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[0].splice_channel( - &chan_a_id, - &nodes[1].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } - } - }, - 0xa5 => { - let outbound_capacity_msat = nodes[1] - .list_channels() - .iter() - .find(|chan| chan.channel_id == chan_a_id) - .map(|chan| chan.outbound_capacity_msat) - .unwrap(); - if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), - }]); - let funding_feerate_sat_per_kw = - fee_est_b.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[1].splice_channel( - &chan_a_id, - &nodes[0].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } - } - }, - 0xa6 => { - let outbound_capacity_msat = nodes[1] - .list_channels() - .iter() - .find(|chan| chan.channel_id == chan_b_id) - .map(|chan| chan.outbound_capacity_msat) - .unwrap(); - if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[1].script_pubkey.clone(), - }]); - let funding_feerate_sat_per_kw = - fee_est_b.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[1].splice_channel( - &chan_b_id, - &nodes[2].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } - } + 0xc0 => harness.nodes[0].keys_manager.disable_supported_ops_for_all_signers(), + 0xc1 => harness.nodes[1].keys_manager.disable_supported_ops_for_all_signers(), + 0xc2 => harness.nodes[2].keys_manager.disable_supported_ops_for_all_signers(), + 0xc3 => { + harness.nodes[0] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignCounterpartyCommitment); + harness.nodes[0].signer_unblocked(None); }, - 0xa7 => { - let outbound_capacity_msat = nodes[2] - .list_channels() - .iter() - .find(|chan| chan.channel_id == chan_b_id) - .map(|chan| chan.outbound_capacity_msat) - .unwrap(); - if outbound_capacity_msat >= 20_000_000 { - let contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS), - script_pubkey: coinbase_tx.output[2].script_pubkey.clone(), - }]); - let funding_feerate_sat_per_kw = - fee_est_c.ret_val.load(atomic::Ordering::Acquire); - if let Err(e) = nodes[2].splice_channel( - &chan_b_id, - &nodes[1].get_our_node_id(), - contribution, - funding_feerate_sat_per_kw, - None, - ) { - assert!( - matches!(e, APIError::APIMisuseError { ref err } if err.contains("splice pending")), - "{:?}", - e - ); - } - } + 0xc4 => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignCounterpartyCommitment); + let filter = Some((harness.nodes[0].get_our_node_id(), harness.chan_a_id())); + harness.nodes[1].signer_unblocked(filter); }, - - 0xb0 | 0xb1 | 0xb2 => { - // Restart node A, picking among the in-flight `ChannelMonitor`s to use based on - // the value of `v` we're matching. - if !peers_ab_disconnected { - nodes[1].peer_disconnected(nodes[0].get_our_node_id()); - peers_ab_disconnected = true; - push_excess_b_events!( - nodes[1].get_and_clear_pending_msg_events().drain(..), - Some(0) - ); - ab_events.clear(); - ba_events.clear(); - } - let (new_node_a, new_monitor_a) = - reload_node(&node_a_ser, 0, &monitor_a, v, &keys_manager_a, &fee_est_a); - nodes[0] = new_node_a; - monitor_a = new_monitor_a; + 0xc5 => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignCounterpartyCommitment); + let filter = Some((harness.nodes[2].get_our_node_id(), harness.chan_b_id())); + harness.nodes[1].signer_unblocked(filter); }, - 0xb3..=0xbb => { - // Restart node B, picking among the in-flight `ChannelMonitor`s to use based on - // the value of `v` we're matching. - if !peers_ab_disconnected { - nodes[0].peer_disconnected(nodes[1].get_our_node_id()); - peers_ab_disconnected = true; - nodes[0].get_and_clear_pending_msg_events(); - ab_events.clear(); - ba_events.clear(); - } - if !peers_bc_disconnected { - nodes[2].peer_disconnected(nodes[1].get_our_node_id()); - peers_bc_disconnected = true; - nodes[2].get_and_clear_pending_msg_events(); - bc_events.clear(); - cb_events.clear(); - } - let (new_node_b, new_monitor_b) = - reload_node(&node_b_ser, 1, &monitor_b, v, &keys_manager_b, &fee_est_b); - nodes[1] = new_node_b; - monitor_b = new_monitor_b; + 0xc6 => { + harness.nodes[2] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignCounterpartyCommitment); + harness.nodes[2].signer_unblocked(None); }, - 0xbc | 0xbd | 0xbe => { - // Restart node C, picking among the in-flight `ChannelMonitor`s to use based on - // the value of `v` we're matching. - if !peers_bc_disconnected { - nodes[1].peer_disconnected(nodes[2].get_our_node_id()); - peers_bc_disconnected = true; - push_excess_b_events!( - nodes[1].get_and_clear_pending_msg_events().drain(..), - Some(2) - ); - bc_events.clear(); - cb_events.clear(); - } - let (new_node_c, new_monitor_c) = - reload_node(&node_c_ser, 2, &monitor_c, v, &keys_manager_c, &fee_est_c); - nodes[2] = new_node_c; - monitor_c = new_monitor_c; + 0xc7 => { + harness.nodes[0] + .keys_manager + .enable_op_for_all_signers(SignerOp::GetPerCommitmentPoint); + harness.nodes[0].signer_unblocked(None); }, - - 0xf0 => { - for id in &chan_ab_ids { - complete_monitor_update(&monitor_a, id, &complete_first); - } + 0xc8 => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::GetPerCommitmentPoint); + let filter = Some((harness.nodes[0].get_our_node_id(), harness.chan_a_id())); + harness.nodes[1].signer_unblocked(filter); }, - 0xf1 => { - for id in &chan_ab_ids { - complete_monitor_update(&monitor_a, id, &complete_second); - } + 0xc9 => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::GetPerCommitmentPoint); + let filter = Some((harness.nodes[2].get_our_node_id(), harness.chan_b_id())); + harness.nodes[1].signer_unblocked(filter); }, - 0xf2 => { - for id in &chan_ab_ids { - complete_monitor_update(&monitor_a, id, &Vec::pop); - } + 0xca => { + harness.nodes[2] + .keys_manager + .enable_op_for_all_signers(SignerOp::GetPerCommitmentPoint); + harness.nodes[2].signer_unblocked(None); }, - - 0xf4 => { - for id in &chan_ab_ids { - complete_monitor_update(&monitor_b, id, &complete_first); - } + 0xcb => { + harness.nodes[0] + .keys_manager + .enable_op_for_all_signers(SignerOp::ReleaseCommitmentSecret); + harness.nodes[0].signer_unblocked(None); }, - 0xf5 => { - for id in &chan_ab_ids { - complete_monitor_update(&monitor_b, id, &complete_second); - } + 0xcc => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::ReleaseCommitmentSecret); + let filter = Some((harness.nodes[0].get_our_node_id(), harness.chan_a_id())); + harness.nodes[1].signer_unblocked(filter); }, - 0xf6 => { - for id in &chan_ab_ids { - complete_monitor_update(&monitor_b, id, &Vec::pop); - } + 0xcd => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::ReleaseCommitmentSecret); + let filter = Some((harness.nodes[2].get_our_node_id(), harness.chan_b_id())); + harness.nodes[1].signer_unblocked(filter); }, - - 0xf8 => { - for id in &chan_bc_ids { - complete_monitor_update(&monitor_b, id, &complete_first); - } + 0xce => { + harness.nodes[2] + .keys_manager + .enable_op_for_all_signers(SignerOp::ReleaseCommitmentSecret); + harness.nodes[2].signer_unblocked(None); }, - 0xf9 => { - for id in &chan_bc_ids { - complete_monitor_update(&monitor_b, id, &complete_second); - } + 0xcf => { + harness.nodes[0] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignSpliceSharedInput); + harness.nodes[0].signer_unblocked(None); }, - 0xfa => { - for id in &chan_bc_ids { - complete_monitor_update(&monitor_b, id, &Vec::pop); - } + 0xd0 => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignSpliceSharedInput); + let filter = Some((harness.nodes[0].get_our_node_id(), harness.chan_a_id())); + harness.nodes[1].signer_unblocked(filter); }, - - 0xfc => { - for id in &chan_bc_ids { - complete_monitor_update(&monitor_c, id, &complete_first); - } + 0xd1 => { + harness.nodes[1] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignSpliceSharedInput); + let filter = Some((harness.nodes[2].get_our_node_id(), harness.chan_b_id())); + harness.nodes[1].signer_unblocked(filter); }, - 0xfd => { - for id in &chan_bc_ids { - complete_monitor_update(&monitor_c, id, &complete_second); - } + 0xd2 => { + harness.nodes[2] + .keys_manager + .enable_op_for_all_signers(SignerOp::SignSpliceSharedInput); + harness.nodes[2].signer_unblocked(None); }, - 0xfe => { - for id in &chan_bc_ids { - complete_monitor_update(&monitor_c, id, &Vec::pop); - } + // The harness toggles signer availability at node granularity, not + // per channel, so each byte re-enables both holder claim ops and + // asks that node's monitors to retry. + 0xd3 => harness.nodes[0].enable_holder_signer_ops(), + 0xd4 => harness.nodes[1].enable_holder_signer_ops(), + 0xd5 => harness.nodes[2].enable_holder_signer_ops(), + 0xd6 => harness.relay_broadcasts_for_node(0), + 0xd7 => harness.relay_broadcasts_for_node(1), + 0xd8 => harness.relay_broadcasts_for_node(2), + 0xd9..=0xe0 => { + let count = MINE_BLOCK_COUNTS[(v - 0xd9) as usize]; + harness.mine_blocks(count); }, + 0xe1 => harness.force_close(0, harness.chan_a_id(), 1), + 0xe2 => harness.force_close(1, harness.chan_b_id(), 2), + 0xe3 => harness.force_close(1, harness.chan_a_id(), 0), + 0xe4 => harness.force_close(2, harness.chan_b_id(), 1), + + 0xf0 => harness.ab_link.complete_monitor_updates_for_node( + 0, + &harness.nodes, + MonitorUpdateSelector::First, + ), + 0xf1 => harness.ab_link.complete_monitor_updates_for_node( + 0, + &harness.nodes, + MonitorUpdateSelector::Second, + ), + 0xf2 => harness.ab_link.complete_monitor_updates_for_node( + 0, + &harness.nodes, + MonitorUpdateSelector::Last, + ), + + 0xf4 => harness.ab_link.complete_monitor_updates_for_node( + 1, + &harness.nodes, + MonitorUpdateSelector::First, + ), + 0xf5 => harness.ab_link.complete_monitor_updates_for_node( + 1, + &harness.nodes, + MonitorUpdateSelector::Second, + ), + 0xf6 => harness.ab_link.complete_monitor_updates_for_node( + 1, + &harness.nodes, + MonitorUpdateSelector::Last, + ), + + 0xf8 => harness.bc_link.complete_monitor_updates_for_node( + 1, + &harness.nodes, + MonitorUpdateSelector::First, + ), + 0xf9 => harness.bc_link.complete_monitor_updates_for_node( + 1, + &harness.nodes, + MonitorUpdateSelector::Second, + ), + 0xfa => harness.bc_link.complete_monitor_updates_for_node( + 1, + &harness.nodes, + MonitorUpdateSelector::Last, + ), + + 0xfc => harness.bc_link.complete_monitor_updates_for_node( + 2, + &harness.nodes, + MonitorUpdateSelector::First, + ), + 0xfd => harness.bc_link.complete_monitor_updates_for_node( + 2, + &harness.nodes, + MonitorUpdateSelector::Second, + ), + 0xfe => harness.bc_link.complete_monitor_updates_for_node( + 2, + &harness.nodes, + MonitorUpdateSelector::Last, + ), 0xff => { // Test that no channel is in a stuck state where neither party can send funds even // after we resolve all pending events. - - // First, make sure peers are all connected to each other - if peers_ab_disconnected { - let init_1 = Init { - features: nodes[1].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[0].peer_connected(nodes[1].get_our_node_id(), &init_1, true).unwrap(); - let init_0 = Init { - features: nodes[0].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[1].peer_connected(nodes[0].get_our_node_id(), &init_0, false).unwrap(); - peers_ab_disconnected = false; - } - if peers_bc_disconnected { - let init_2 = Init { - features: nodes[2].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[1].peer_connected(nodes[2].get_our_node_id(), &init_2, true).unwrap(); - let init_1 = Init { - features: nodes[1].init_features(), - networks: None, - remote_network_address: None, - }; - nodes[2].peer_connected(nodes[1].get_our_node_id(), &init_1, false).unwrap(); - peers_bc_disconnected = false; - } - - macro_rules! process_all_events { - () => { { - let mut last_pass_no_updates = false; - for i in 0..std::usize::MAX { - if i == 100 { - panic!("It may take may iterations to settle the state, but it should not take forever"); - } - // Next, make sure no monitor updates are pending - for id in &chan_ab_ids { - complete_all_monitor_updates(&monitor_a, id); - complete_all_monitor_updates(&monitor_b, id); - } - for id in &chan_bc_ids { - complete_all_monitor_updates(&monitor_b, id); - complete_all_monitor_updates(&monitor_c, id); - } - // Then, make sure any current forwards make their way to their destination - if process_msg_events!(0, false, ProcessMessages::AllMessages) { - last_pass_no_updates = false; - continue; - } - if process_msg_events!(1, false, ProcessMessages::AllMessages) { - last_pass_no_updates = false; - continue; - } - if process_msg_events!(2, false, ProcessMessages::AllMessages) { - last_pass_no_updates = false; - continue; - } - // ...making sure any payments are claimed. - if process_events!(0, false) { - last_pass_no_updates = false; - continue; - } - if process_events!(1, false) { - last_pass_no_updates = false; - continue; - } - if process_events!(2, false) { - last_pass_no_updates = false; - continue; - } - if last_pass_no_updates { - // In some cases, we may generate a message to send in - // `process_msg_events`, but block sending until - // `complete_all_monitor_updates` gets called on the next - // iteration. - // - // Thus, we only exit if we manage two iterations with no messages - // or events to process. - break; - } - last_pass_no_updates = true; - } - } }; - } - - process_all_events!(); - - // Verify no payments are stuck - all should have resolved - for (idx, pending) in pending_payments.borrow().iter().enumerate() { - assert!( - pending.is_empty(), - "Node {} has {} stuck pending payments after settling all state", - idx, - pending.len() - ); - } - - // Finally, make sure that at least one end of each channel can make a substantial payment - for &scid in &chan_ab_scids { - assert!( - send(0, 1, scid, 10_000_000, &mut p_ctr) - || send(1, 0, scid, 10_000_000, &mut p_ctr) - ); - } - for &scid in &chan_bc_scids { - assert!( - send(1, 2, scid, 10_000_000, &mut p_ctr) - || send(2, 1, scid, 10_000_000, &mut p_ctr) - ); - } - - last_htlc_clear_fee_a = fee_est_a.ret_val.load(atomic::Ordering::Acquire); - last_htlc_clear_fee_b = fee_est_b.ret_val.load(atomic::Ordering::Acquire); - last_htlc_clear_fee_c = fee_est_c.ret_val.load(atomic::Ordering::Acquire); + harness.settle_all(); }, - _ => test_return!(), - } - - if nodes[0].get_and_clear_needs_persistence() == true { - node_a_ser = nodes[0].encode(); - } - if nodes[1].get_and_clear_needs_persistence() == true { - node_b_ser = nodes[1].encode(); + _ => break 'fuzz_loop, } - if nodes[2].get_and_clear_needs_persistence() == true { - node_c_ser = nodes[2].encode(); - } - } -} -/// We actually have different behavior based on if a certain log string has been seen, so we have -/// to do a bit more tracking. -#[derive(Clone)] -struct SearchingOutput<O: Output> { - output: O, - may_fail: Arc<atomic::AtomicBool>, -} -impl<O: Output> Output for SearchingOutput<O> { - fn locked_write(&self, data: &[u8]) { - // We hit a design limitation of LN state machine (see CONCURRENT_INBOUND_HTLC_FEE_BUFFER) - if std::str::from_utf8(data).unwrap().contains("Outbound update_fee HTLC buffer overflow - counterparty should force-close this channel") { - self.may_fail.store(true, atomic::Ordering::Release); + // Compute `ChannelDetails` for every channel after each step (ignoring the result) so the + // fuzzer exercises the splice-details derivation in `to_details` across as many states as + // possible. + for node in harness.nodes.iter() { + let _ = node.list_channels(); } - self.output.locked_write(data) - } -} -impl<O: Output> SearchingOutput<O> { - pub fn new(output: O) -> Self { - Self { output, may_fail: Arc::new(atomic::AtomicBool::new(false)) } } + harness.finish(); } -pub fn chanmon_consistency_test<Out: Output>(data: &[u8], out: Out) { - do_test(data, out.clone(), false); - do_test(data, out, true); +pub fn chanmon_consistency_test<Out: Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) { + do_test(data, out); } #[no_mangle] pub extern "C" fn chanmon_consistency_run(data: *const u8, datalen: usize) { - do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}, false); - do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}, true); + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); } diff --git a/fuzz/src/chanmon_deser.rs b/fuzz/src/chanmon_deser.rs index 4a4e79c83c1..3206db0b143 100644 --- a/fuzz/src/chanmon_deser.rs +++ b/fuzz/src/chanmon_deser.rs @@ -1,9 +1,7 @@ // This file is auto-generated by gen_target.sh based on msg_target_template.txt // To modify it, modify msg_target_template.txt and run gen_target.sh instead. -use bitcoin::hash_types::BlockHash; - -use lightning::chain::channelmonitor; +use lightning::chain::{channelmonitor, BlockLocator}; use lightning::util::ser::{ReadableArgs, Writeable, Writer}; use lightning::util::test_channel_signer::TestChannelSigner; use lightning::util::test_utils::OnlyReadsKeysInterface; @@ -23,14 +21,14 @@ impl Writer for VecWriter { #[inline] pub fn do_test<Out: test_logger::Output>(data: &[u8], _out: Out) { if let Ok((latest_block_hash, monitor)) = - <(BlockHash, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( + <(BlockLocator, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( &mut Cursor::new(data), (&OnlyReadsKeysInterface {}, &OnlyReadsKeysInterface {}), ) { let mut w = VecWriter(Vec::new()); monitor.write(&mut w).unwrap(); let deserialized_copy = - <(BlockHash, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( + <(BlockLocator, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( &mut Cursor::new(&w.0), (&OnlyReadsKeysInterface {}, &OnlyReadsKeysInterface {}), ) diff --git a/fuzz/src/fs_store.rs b/fuzz/src/fs_store.rs index 821439f390e..4d86ffce2e6 100644 --- a/fuzz/src/fs_store.rs +++ b/fuzz/src/fs_store.rs @@ -1,6 +1,6 @@ use core::hash::{BuildHasher, Hasher}; use lightning::util::persist::{KVStore, KVStoreSync}; -use lightning_persister::fs_store::FilesystemStore; +use lightning_persister::fs_store::v1::FilesystemStore; use std::fs; use tokio::runtime::Runtime; diff --git a/fuzz/src/full_stack.rs b/fuzz/src/full_stack.rs index 39588bcdc50..d57bec8ac74 100644 --- a/fuzz/src/full_stack.rs +++ b/fuzz/src/full_stack.rs @@ -22,6 +22,7 @@ use bitcoin::opcodes; use bitcoin::script::{Builder, ScriptBuf}; use bitcoin::transaction::Version; use bitcoin::transaction::{Transaction, TxIn, TxOut}; +use bitcoin::FeeRate; use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::hashes::sha256::Hash as Sha256; @@ -30,18 +31,15 @@ use bitcoin::hashes::Hash as _; use bitcoin::hex::FromHex; use bitcoin::WPubkeyHash; -use lightning::ln::funding::{FundingTxInput, SpliceContribution}; - use lightning::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode}; use lightning::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; use lightning::chain; use lightning::chain::chaininterface::{ - TransactionType, BroadcasterInterface, ConfirmationTarget, FeeEstimator, + BroadcasterInterface, ConfirmationTarget, FeeEstimator, TransactionType, }; use lightning::chain::chainmonitor; use lightning::chain::transaction::OutPoint; -use lightning::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen}; -use lightning::events::bump_transaction::sync::WalletSourceSync; +use lightning::chain::{BlockLocator, ChannelMonitorUpdateStatus, Confirm, Listen}; use lightning::events::Event; use lightning::ln::channel_state::ChannelDetails; use lightning::ln::channelmanager::{ChainParameters, ChannelManager, InterceptId, PaymentId}; @@ -68,9 +66,11 @@ use lightning::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning::util::config::{ChannelConfig, UserConfig}; use lightning::util::hash_tables::*; use lightning::util::logger::Logger; +use lightning::util::native_async::{MaybeSend, MaybeSync}; use lightning::util::ser::{Readable, Writeable}; use lightning::util::test_channel_signer::{EnforcementState, TestChannelSigner}; use lightning::util::test_utils::TestWalletSource; +use lightning::util::wallet_utils::{WalletSourceSync, WalletSync}; use lightning_invoice::RawBolt11Invoice; @@ -227,7 +227,7 @@ type ChannelMan<'a> = ChannelManager< Arc<dyn chain::Filter>, Arc<TestBroadcaster>, Arc<FuzzEstimator>, - Arc<dyn Logger>, + Arc<dyn Logger + MaybeSend + MaybeSync>, Arc<TestPersister>, Arc<KeyProvider>, >, @@ -239,14 +239,20 @@ type ChannelMan<'a> = ChannelManager< Arc<FuzzEstimator>, &'a FuzzRouter, &'a FuzzRouter, - Arc<dyn Logger>, + Arc<dyn Logger + MaybeSend + MaybeSync>, >; type PeerMan<'a> = PeerManager< Peer<'a>, Arc<ChannelMan<'a>>, - Arc<P2PGossipSync<Arc<NetworkGraph<Arc<dyn Logger>>>, Arc<dyn UtxoLookup>, Arc<dyn Logger>>>, + Arc< + P2PGossipSync< + Arc<NetworkGraph<Arc<dyn Logger + MaybeSend + MaybeSync>>>, + Arc<dyn UtxoLookup>, + Arc<dyn Logger + MaybeSend + MaybeSync>, + >, + >, IgnoringMessageHandler, - Arc<dyn Logger>, + Arc<dyn Logger + MaybeSend + MaybeSync>, IgnoringMessageHandler, Arc<KeyProvider>, IgnoringMessageHandler, @@ -260,7 +266,7 @@ struct MoneyLossDetector<'a> { Arc<dyn chain::Filter>, Arc<TestBroadcaster>, Arc<FuzzEstimator>, - Arc<dyn Logger>, + Arc<dyn Logger + MaybeSend + MaybeSync>, Arc<TestPersister>, Arc<KeyProvider>, >, @@ -285,7 +291,7 @@ impl<'a> MoneyLossDetector<'a> { Arc<dyn chain::Filter>, Arc<TestBroadcaster>, Arc<FuzzEstimator>, - Arc<dyn Logger>, + Arc<dyn Logger + MaybeSend + MaybeSync>, Arc<TestPersister>, Arc<KeyProvider>, >, @@ -348,7 +354,7 @@ impl<'a> MoneyLossDetector<'a> { self.header_hashes[self.height - 1].0, self.header_hashes[self.height].1, ); - let best_block = BestBlock::new(header.prev_blockhash, self.height as u32 - 1); + let best_block = BlockLocator::new(header.prev_blockhash, self.height as u32 - 1); self.manager.blocks_disconnected(best_block); self.monitor.blocks_disconnected(best_block); self.height -= 1; @@ -451,8 +457,6 @@ impl NodeSigner for KeyProvider { impl SignerProvider for KeyProvider { type EcdsaSigner = TestChannelSigner; - #[cfg(taproot)] - type TaprootSigner = TestChannelSigner; fn generate_channel_keys_id(&self, inbound: bool, _user_channel_id: u128) -> [u8; 32] { let ctr = self.counter.fetch_add(1, Ordering::Relaxed) as u8; @@ -520,7 +524,7 @@ impl SignerProvider for KeyProvider { } #[inline] -pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { +pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger + MaybeSend + MaybeSync>) { if data.len() < 32 { return; } @@ -597,11 +601,12 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { Arc::new(TestPersister { update_ret: Mutex::new(ChannelMonitorUpdateStatus::Completed) }), Arc::clone(&keys_manager), keys_manager.get_peer_storage_key(), + false, )); let network = Network::Bitcoin; let best_block_timestamp = genesis_block(network).header.time; - let params = ChainParameters { network, best_block: BestBlock::from_network(network) }; + let params = ChainParameters { network, best_block: BlockLocator::from_network(network) }; let channelmanager = Arc::new(ChannelManager::new( fee_est.clone(), monitor.clone(), @@ -668,9 +673,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { script_pubkey: wallet.get_change_script().unwrap(), }], }; - let coinbase_txid = coinbase_tx.compute_txid(); - wallet - .add_utxo(bitcoin::OutPoint { txid: coinbase_txid, vout: 0 }, Amount::from_sat(1_000_000)); + wallet.add_utxo(coinbase_tx.clone(), 0); loop { match get_slice!(1)[0] { @@ -739,7 +742,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { payments_sent += 1; let _ = channelmanager.send_payment( payment_hash, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(final_value_msat), PaymentId(payment_hash.0), params, Retry::Attempts(2), @@ -761,7 +764,7 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { payments_sent += 1; let _ = channelmanager.send_payment( payment_hash, - RecipientOnionFields::secret_only(payment_secret), + RecipientOnionFields::secret_only(payment_secret, final_value_msat), PaymentId(payment_hash.0), params, Retry::Attempts(2), @@ -834,11 +837,10 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { }, 16 => { let payment_preimage = PaymentPreimage(keys_manager.get_secure_random_bytes()); - let payment_hash = - PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); + let hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); // Note that this may fail - our hashes may collide and we'll end up trying to // double-register the same payment_hash. - let _ = channelmanager.create_inbound_payment_for_hash(payment_hash, None, 1, None); + let _ = channelmanager.create_inbound_payment_for_hash(hash, None, 1, None, None); }, 9 => { for payment in payments_received.drain(..) { @@ -1026,20 +1028,27 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { if splice_in_sats == 0 { continue; } - // Create a funding input from the coinbase transaction - if let Ok(input) = FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0) { - let contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_in_sats.min(900_000)), // Cap at available funds minus fees - vec![input], - Some(wallet.get_change_script().unwrap()), - ); - let _ = channelmanager.splice_channel( - &chan.channel_id, - &chan.counterparty.node_id, - contribution, - 253, // funding_feerate_per_kw - None, - ); + let chan_id = chan.channel_id; + let counterparty = chan.counterparty.node_id; + if let Ok(funding_template) = channelmanager.splice_channel(&chan_id, &counterparty) + { + let feerate = funding_template + .min_rbf_feerate() + .unwrap_or(FeeRate::from_sat_per_kwu(253)); + let wallet_sync = WalletSync::new(&wallet, Arc::clone(&logger)); + if let Ok(contribution) = funding_template.splice_in_sync( + Amount::from_sat(splice_in_sats.min(900_000)), + feerate, + FeeRate::MAX, + &wallet_sync, + ) { + let _ = channelmanager.funding_contributed( + &chan_id, + &counterparty, + contribution, + None, + ); + } } }, // Splice-out: remove funds from a channel @@ -1062,17 +1071,28 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { // Cap splice-out at a reasonable portion of channel capacity let max_splice_out = chan.channel_value_satoshis / 4; let splice_out_sats = splice_out_sats.min(max_splice_out).max(546); // At least dust limit - let contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(splice_out_sats), - script_pubkey: wallet.get_change_script().unwrap(), - }]); - let _ = channelmanager.splice_channel( - &chan.channel_id, - &chan.counterparty.node_id, - contribution, - 253, // funding_feerate_per_kw - None, - ); + let chan_id = chan.channel_id; + let counterparty = chan.counterparty.node_id; + if let Ok(funding_template) = channelmanager.splice_channel(&chan_id, &counterparty) + { + let feerate = funding_template + .min_rbf_feerate() + .unwrap_or(FeeRate::from_sat_per_kwu(253)); + let outputs = vec![TxOut { + value: Amount::from_sat(splice_out_sats), + script_pubkey: wallet.get_change_script().unwrap(), + }]; + if let Ok(contribution) = + funding_template.splice_out(outputs, feerate, FeeRate::MAX) + { + let _ = channelmanager.funding_contributed( + &chan_id, + &counterparty, + contribution, + None, + ); + } + } }, _ => return, } @@ -1116,10 +1136,10 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { signed_tx, ); }, - Event::SplicePending { .. } => { + Event::SpliceNegotiated { .. } => { // Splice negotiation completed, waiting for confirmation }, - Event::SpliceFailed { .. } => { + Event::SpliceNegotiationFailed { .. } => { // Splice failed, inputs can be re-spent }, Event::OpenChannelRequest { @@ -1139,14 +1159,15 @@ pub fn do_test(mut data: &[u8], logger: &Arc<dyn Logger>) { } } -pub fn full_stack_test<Out: test_logger::Output>(data: &[u8], out: Out) { - let logger: Arc<dyn Logger> = Arc::new(test_logger::TestLogger::new("".to_owned(), out)); +pub fn full_stack_test<Out: test_logger::Output + MaybeSend + MaybeSync>(data: &[u8], out: Out) { + let logger: Arc<dyn Logger + MaybeSend + MaybeSync> = + Arc::new(test_logger::TestLogger::new("".to_owned(), out)); do_test(data, &logger); } #[no_mangle] pub extern "C" fn full_stack_run(data: *const u8, datalen: usize) { - let logger: Arc<dyn Logger> = + let logger: Arc<dyn Logger + MaybeSend + MaybeSync> = Arc::new(test_logger::TestLogger::new("".to_owned(), test_logger::DevNull {})); do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, &logger); } @@ -1170,7 +1191,7 @@ fn two_peer_forwarding_seed() -> Vec<u8> { // our network key ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test); // config - ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test); + ext_from_hex("000000000090000000000000000064000100000000000100ffff00000000000000ffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test); // new outbound connection with id 0 ext_from_hex("00", &mut test); @@ -1624,7 +1645,7 @@ fn gossip_exchange_seed() -> Vec<u8> { // our network key ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test); // config - ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test); + ext_from_hex("000000000090000000000000000064000100000000000100ffff00000000000000ffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test); // new outbound connection with id 0 ext_from_hex("00", &mut test); @@ -1671,11 +1692,11 @@ fn gossip_exchange_seed() -> Vec<u8> { // inbound read from peer id 0 of len 255 ext_from_hex("0300ff", &mut test); // First part of channel_announcement (type 256) - ext_from_hex("0100 00000000000000000000000000000000000000000000000000000000000000b20303030303030303030303030303030303030303030303030303030303030303 00000000000000000000000000000000000000000000000000000000000000b20202020202020202020202020202020202020202020202020202020202020202 00000000000000000000000000000000000000000000000000000000000000b20303030303030303030303030303030303030303030303030303030303030303 00000000000000000000000000000000000000000000000000000000000000b20202020202020202020202020202020202020202020202020202020202", &mut test); + ext_from_hex("0100 00000000000000000000000000000000000000000000000000000000000000b20202020202020202020202020202020202020202020202020202020202020202 00000000000000000000000000000000000000000000000000000000000000b20303030303030303030303030303030303030303030303030303030303030303 00000000000000000000000000000000000000000000000000000000000000b20202020202020202020202020202020202020202020202020202020202020202 00000000000000000000000000000000000000000000000000000000000000b20303030303030303030303030303030303030303030303030303030303", &mut test); // inbound read from peer id 0 of len 193 ext_from_hex("0300c1", &mut test); // Last part of channel_announcement and mac - ext_from_hex("020202 00006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000000000000000002a030303030303030303030303030303030303030303030303030303030303030303020202020202020202020202020202020202020202020202020202020202020202030303030303030303030303030303030303030303030303030303030303030303020202020202020202020202020202020202020202020202020202020202020202 03000000000000000000000000000000", &mut test); + ext_from_hex("030303 00006fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000000000000000002a020202020202020202020202020202020202020202020202020202020202020202030303030303030303030303030303030303030303030303030303030303030303020202020202020202020202020202020202020202020202020202020202020202030303030303030303030303030303030303030303030303030303030303030303 03000000000000000000000000000000", &mut test); // inbound read from peer id 0 of len 18 ext_from_hex("030012", &mut test); @@ -1684,7 +1705,7 @@ fn gossip_exchange_seed() -> Vec<u8> { // inbound read from peer id 0 of len 154 ext_from_hex("03009a", &mut test); // channel_update (type 258) and mac - ext_from_hex("0102 00000000000000000000000000000000000000000000000000000000000000a60303030303030303030303030303030303030303030303030303030303030303 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000 000000000000002a0000002c01000028000000000000000000000000000000000000000005f5e100 03000000000000000000000000000000", &mut test); + ext_from_hex("0102 00000000000000000000000000000000000000000000000000000000000000a60202020202020202020202020202020202020202020202020202020202020202 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000 000000000000002a0000002c01000028000000000000000000000000000000000000000005f5e100 03000000000000000000000000000000", &mut test); // inbound read from peer id 0 of len 18 ext_from_hex("030012", &mut test); @@ -1706,7 +1727,7 @@ fn splice_seed() -> Vec<u8> { // our network key ext_from_hex("0100000000000000000000000000000000000000000000000000000000000000", &mut test); // config - ext_from_hex("000000000090000000000000000064000100000000000100ffff0000000000000000ffffffffffffffffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test); + ext_from_hex("000000000090000000000000000064000100000000000100ffff00000000000000ffffffffffffffffff0000000000000000ffffffffffffffff000000ffffffff00ffff1a000400010000020400000000040200000a08ffffffffffffffff0001000000000000", &mut test); // new outbound connection with id 0 ext_from_hex("00", &mut test); @@ -1864,8 +1885,8 @@ fn splice_seed() -> Vec<u8> { // CommitmentSigned message with proper signature (r=f7, s=01...) and funding_txid TLV // signature r encodes sighash first byte f7, s follows the pattern from funding_created // TLV type 1 (odd/optional) for funding_txid as per impl_writeable_msg!(CommitmentSigned, ...) - // Note: txid is encoded in reverse byte order (Bitcoin standard), so to get display 0000...0033, encode 3300...0000 - ext_from_hex("0084 c000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000f7 0100000000000000000000000000000000000000000000000000000000000000 0000 01 20 3300000000000000000000000000000000000000000000000000000000000000 03000000000000000000000000000000", &mut test); + // Note: txid is encoded in reverse byte order (Bitcoin standard), so to get display 0000...0032, encode 3200...0000 + ext_from_hex("0084 c000000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000000000000000000000000000000f7 0100000000000000000000000000000000000000000000000000000000000000 0000 01 20 3200000000000000000000000000000000000000000000000000000000000000 03000000000000000000000000000000", &mut test); // After commitment_signed exchange, we need to exchange tx_signatures. // Message type IDs: TxSignatures = 71 (0x0047) @@ -1878,19 +1899,19 @@ fn splice_seed() -> Vec<u8> { // inbound read from peer id 0 of len 150 (134 message + 16 MAC) ext_from_hex("030096", &mut test); // TxSignatures message with shared_input_signature TLV (type 0) - // txid must match the splice funding txid (0x33 in reverse byte order) + // txid must match the splice funding txid (0x32 in reverse byte order) // shared_input_signature: 64-byte fuzz signature for the shared input - ext_from_hex("0047 c000000000000000000000000000000000000000000000000000000000000000 3300000000000000000000000000000000000000000000000000000000000000 0000 00 40 00000000000000000000000000000000000000000000000000000000000000dc 0100000000000000000000000000000000000000000000000000000000000000 03000000000000000000000000000000", &mut test); + ext_from_hex("0047 c000000000000000000000000000000000000000000000000000000000000000 3200000000000000000000000000000000000000000000000000000000000000 0000 00 40 00000000000000000000000000000000000000000000000000000000000000dc 0100000000000000000000000000000000000000000000000000000000000000 03000000000000000000000000000000", &mut test); // Connect a block with the splice funding transaction to confirm it // The splice funding tx: version(4) + input_count(1) + txid(32) + vout(4) + script_len(1) + sequence(4) // + output_count(1) + value(8) + script_len(1) + script(34) + locktime(4) = 94 bytes = 0x5e // Transaction structure from FundingTransactionReadyForSigning: // - Input: spending c000...00:0 with sequence 0xfffffffd - // - Output: 115536 sats to OP_0 PUSH32 6e00...00 + // - Output: 115537 sats to OP_0 PUSH32 6e00...00 // - Locktime: 13 ext_from_hex("0c005e", &mut test); - ext_from_hex("02000000 01 c000000000000000000000000000000000000000000000000000000000000000 00000000 00 fdffffff 01 50c3010000000000 22 00206e00000000000000000000000000000000000000000000000000000000000000 0d000000", &mut test); + ext_from_hex("02000000 01 c000000000000000000000000000000000000000000000000000000000000000 00000000 00 fdffffff 01 51c3010000000000 22 00206e00000000000000000000000000000000000000000000000000000000000000 0d000000", &mut test); // Connect additional blocks to reach minimum_depth confirmations for _ in 0..5 { @@ -1907,8 +1928,8 @@ fn splice_seed() -> Vec<u8> { // inbound read from peer id 0 of len 82 (66 message + 16 MAC) ext_from_hex("030052", &mut test); // SpliceLocked message (type 77 = 0x004d): channel_id + splice_txid + mac - // splice_txid must match the splice funding txid (0x33 in reverse byte order) - ext_from_hex("004d c000000000000000000000000000000000000000000000000000000000000000 3300000000000000000000000000000000000000000000000000000000000000 03000000000000000000000000000000", &mut test); + // splice_txid must match the splice funding txid (0x32 in reverse byte order) + ext_from_hex("004d c000000000000000000000000000000000000000000000000000000000000000 3200000000000000000000000000000000000000000000000000000000000000 03000000000000000000000000000000", &mut test); test } @@ -1933,6 +1954,7 @@ pub fn write_fst_seeds(path: &str) { #[cfg(test)] mod tests { use lightning::util::logger::{Logger, Record}; + use lightning::util::native_async::{MaybeSend, MaybeSync}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -1963,7 +1985,7 @@ mod tests { let test = super::two_peer_forwarding_seed(); let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) }); - super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>)); + super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>)); let log_entries = logger.lines.lock().unwrap(); // 1 @@ -1998,11 +2020,11 @@ mod tests { let test = super::gossip_exchange_seed(); let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) }); - super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>)); + super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>)); let log_entries = logger.lines.lock().unwrap(); - assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Sending message to all peers except Some(PublicKey(0000000000000000000000000000000000000000000000000000000000000002ff00000000000000000000000000000000000000000000000000000000000002)) or the announced channel's counterparties: ChannelAnnouncement { node_signature_1: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, node_signature_2: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, bitcoin_signature_1: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, bitcoin_signature_2: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, contents: UnsignedChannelAnnouncement { features: [], chain_hash: 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000, short_channel_id: 42, node_id_1: NodeId(030303030303030303030303030303030303030303030303030303030303030303), node_id_2: NodeId(020202020202020202020202020202020202020202020202020202020202020202), bitcoin_key_1: NodeId(030303030303030303030303030303030303030303030303030303030303030303), bitcoin_key_2: NodeId(020202020202020202020202020202020202020202020202020202020202020202), excess_data: [] } }".to_string())), Some(&1)); - assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Sending message to all peers except Some(PublicKey(0000000000000000000000000000000000000000000000000000000000000002ff00000000000000000000000000000000000000000000000000000000000002)): ChannelUpdate { signature: 3026020200a602200303030303030303030303030303030303030303030303030303030303030303, contents: UnsignedChannelUpdate { chain_hash: 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000, short_channel_id: 42, timestamp: 44, message_flags: 1, channel_flags: 0, cltv_expiry_delta: 40, htlc_minimum_msat: 0, htlc_maximum_msat: 100000000, fee_base_msat: 0, fee_proportional_millionths: 0, excess_data: [] } }".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Sending message to all peers except Some(PublicKey(0000000000000000000000000000000000000000000000000000000000000002ff00000000000000000000000000000000000000000000000000000000000002)) or the announced channel's counterparties: ChannelAnnouncement { node_signature_1: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, node_signature_2: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, bitcoin_signature_1: 3026020200b202200202020202020202020202020202020202020202020202020202020202020202, bitcoin_signature_2: 3026020200b202200303030303030303030303030303030303030303030303030303030303030303, contents: UnsignedChannelAnnouncement { features: [], chain_hash: 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000, short_channel_id: 42, node_id_1: NodeId(020202020202020202020202020202020202020202020202020202020202020202), node_id_2: NodeId(030303030303030303030303030303030303030303030303030303030303030303), bitcoin_key_1: NodeId(020202020202020202020202020202020202020202020202020202020202020202), bitcoin_key_2: NodeId(030303030303030303030303030303030303030303030303030303030303030303), excess_data: [] } }".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Sending message to all peers except Some(PublicKey(0000000000000000000000000000000000000000000000000000000000000002ff00000000000000000000000000000000000000000000000000000000000002)): ChannelUpdate { signature: 3026020200a602200202020202020202020202020202020202020202020202020202020202020202, contents: UnsignedChannelUpdate { chain_hash: 6fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d6190000000000, short_channel_id: 42, timestamp: 44, message_flags: 1, channel_flags: 0, cltv_expiry_delta: 40, htlc_minimum_msat: 0, htlc_maximum_msat: 100000000, fee_base_msat: 0, fee_proportional_millionths: 0, excess_data: [] } }".to_string())), Some(&1)); assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Sending message to all peers except Some(PublicKey(0000000000000000000000000000000000000000000000000000000000000002ff00000000000000000000000000000000000000000000000000000000000002)) or the announced node: NodeAnnouncement { signature: 302502012802200303030303030303030303030303030303030303030303030303030303030303, contents: UnsignedNodeAnnouncement { features: [], timestamp: 43, node_id: NodeId(030303030303030303030303030303030303030303030303030303030303030303), rgb: [0, 0, 0], alias: NodeAlias([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), addresses: [], excess_address_data: [], excess_data: [] } }".to_string())), Some(&1)); } @@ -2011,7 +2033,7 @@ mod tests { let test = super::splice_seed(); let logger = Arc::new(TrackingLogger { lines: Mutex::new(HashMap::new()) }); - super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger>)); + super::do_test(&test, &(Arc::clone(&logger) as Arc<dyn Logger + MaybeSend + MaybeSync>)); let log_entries = logger.lines.lock().unwrap(); @@ -2037,6 +2059,6 @@ mod tests { // Splice locked assert_eq!(log_entries.get(&("lightning::ln::peer_handler".to_string(), "Handling SendSpliceLocked event in peer_handler for node 030000000000000000000000000000000000000000000000000000000000000002 for channel c000000000000000000000000000000000000000000000000000000000000000".to_string())), Some(&1)); - assert_eq!(log_entries.get(&("lightning::ln::channel".to_string(), "Promoting splice funding txid 0000000000000000000000000000000000000000000000000000000000000033".to_string())), Some(&1)); + assert_eq!(log_entries.get(&("lightning::ln::channel".to_string(), "Promoting splice funding txid 0000000000000000000000000000000000000000000000000000000000000032".to_string())), Some(&1)); } } diff --git a/fuzz/src/gossip_discovery.rs b/fuzz/src/gossip_discovery.rs new file mode 100644 index 00000000000..8eee8dc482b --- /dev/null +++ b/fuzz/src/gossip_discovery.rs @@ -0,0 +1,265 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Test that no series of gossip messages received from peers can result in a crash. We do this +//! by standing up a `P2PGossipSync` with a `NetworkGraph` and a mock UTXO lookup, then reading +//! bytes from the fuzz input to denote actions such as feeding channel announcements, node +//! announcements, channel updates, query messages, and pruning channels and nodes. Both valid +//! and malformed messages are generated to exercise error paths. + +use bitcoin::amount::Amount; +use bitcoin::constants::ChainHash; +use bitcoin::network::Network; +use bitcoin::secp256k1::PublicKey; +use bitcoin::TxOut; + +use lightning::ln::chan_utils::make_funding_redeemscript; +use lightning::ln::msgs::{self, BaseMessageHandler, MessageSendEvent, RoutingMessageHandler}; +use lightning::routing::gossip::{NetworkGraph, NetworkUpdate, NodeId, P2PGossipSync}; +use lightning::routing::utxo::{UtxoLookup, UtxoLookupError, UtxoResult}; +use lightning::util::ser::LengthReadable; +use lightning::util::wakers::Notifier; + +use crate::utils::test_logger; + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +struct FuzzUtxoLookup { + utxos: Mutex<HashMap<u64, TxOut>>, +} + +impl FuzzUtxoLookup { + fn new() -> Arc<Self> { + Arc::new(Self { utxos: Mutex::new(HashMap::new()) }) + } + + fn register(&self, scid: u64, txout: TxOut) { + self.utxos.lock().unwrap().insert(scid, txout); + } +} + +impl UtxoLookup for FuzzUtxoLookup { + fn get_utxo( + &self, _chain_hash: &ChainHash, short_channel_id: u64, + _async_completion_notifier: Arc<Notifier>, + ) -> UtxoResult { + let utxos = self.utxos.lock().unwrap(); + match utxos.get(&short_channel_id) { + Some(txout) => UtxoResult::Sync(Ok(txout.clone())), + None => UtxoResult::Sync(Err(UtxoLookupError::UnknownTx)), + } + } +} + +#[inline] +fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) { + let logger = Arc::new(test_logger::TestLogger::new("".to_owned(), out)); + + let network = Network::Bitcoin; + let network_graph = Arc::new(NetworkGraph::new(network, Arc::clone(&logger))); + let utxo_lookup = FuzzUtxoLookup::new(); + let gossip = Arc::new(P2PGossipSync::new( + Arc::clone(&network_graph), + Some(Arc::clone(&utxo_lookup)), + Arc::clone(&logger), + )); + + let mut read_pos = 0; + macro_rules! get_slice { + ($len: expr) => {{ + let slice_len = $len as usize; + if data.len() < read_pos + slice_len { + return; + } + read_pos += slice_len; + &data[read_pos - slice_len..read_pos] + }}; + } + + macro_rules! get_pubkey { + () => { + match PublicKey::from_slice(get_slice!(33)) { + Ok(key) => key, + Err(_) => continue, + } + }; + } + + macro_rules! decode_msg { + ($MsgType: path) => {{ + let len_bytes = get_slice!(2); + let msg_len = u16::from_be_bytes(len_bytes.try_into().unwrap()) as usize; + if msg_len == 0 { + continue; + } + let msg_data = get_slice!(msg_len); + let mut reader = &msg_data[..]; + match <$MsgType>::read_from_fixed_length_buffer(&mut reader) { + Ok(msg) => { + assert!(reader.is_empty()); + msg + }, + Err(e) => match e { + msgs::DecodeError::UnknownVersion => continue, + msgs::DecodeError::UnknownRequiredFeature => continue, + msgs::DecodeError::InvalidValue => continue, + msgs::DecodeError::BadLengthDescriptor => continue, + msgs::DecodeError::ShortRead => continue, + msgs::DecodeError::Io(e) => panic!("{:?}", e), + msgs::DecodeError::UnsupportedCompression => continue, + msgs::DecodeError::DangerousValue => continue, + }, + } + }}; + } + + loop { + match get_slice!(1)[0] % 7 { + // Handle a node announcement. + 0 => { + let node_ann = decode_msg!(msgs::NodeAnnouncement); + let Ok(peer_node_id) = node_ann.contents.node_id.as_pubkey() else { + continue; + }; + + match gossip.handle_node_announcement(Some(peer_node_id), &node_ann) { + Ok(_) => { + let graph = network_graph.read_only(); + let node = graph.node(&node_ann.contents.node_id).unwrap(); + let info = node.announcement_info.as_ref().unwrap(); + assert_eq!(info.last_update(), node_ann.contents.timestamp); + }, + Err(_) => {}, + } + }, + // Handle a channel announcement. + 1 => { + let chan_ann = decode_msg!(msgs::ChannelAnnouncement); + let scid = chan_ann.contents.short_channel_id; + let Ok(peer_node_id) = chan_ann.contents.node_id_1.as_pubkey() else { + continue; + }; + let Ok(btc_key1) = chan_ann.contents.bitcoin_key_1.as_pubkey() else { + continue; + }; + let Ok(btc_key2) = chan_ann.contents.bitcoin_key_2.as_pubkey() else { + continue; + }; + + // We conditionally register the funding script in the UTXO set so that valid funding + // script cases are also validated. + if (get_slice!(1)[0] & 1) != 0 { + let script_pubkey = make_funding_redeemscript(&btc_key1, &btc_key2).to_p2wsh(); + utxo_lookup.register( + scid, + TxOut { value: Amount::from_sat(1_000_000), script_pubkey }, + ); + } + + match gossip.handle_channel_announcement(Some(peer_node_id), &chan_ann) { + Ok(_) => { + let graph = network_graph.read_only(); + let chan = graph.channel(scid).unwrap(); + assert_eq!(chan.node_one, chan_ann.contents.node_id_1); + assert_eq!(chan.node_two, chan_ann.contents.node_id_2); + + assert!(graph.node(&chan_ann.contents.node_id_1).is_some()); + assert!(graph.node(&chan_ann.contents.node_id_2).is_some()); + }, + Err(_) => {}, + } + }, + // Handle a channel update. + 2 => { + let chan_upd = decode_msg!(msgs::ChannelUpdate); + let peer_node_id = get_pubkey!(); + + match gossip.handle_channel_update(Some(peer_node_id), &chan_upd) { + Ok(_) => { + let graph = network_graph.read_only(); + let chan = graph.channel(chan_upd.contents.short_channel_id).unwrap(); + let info = + chan.get_directional_info(chan_upd.contents.channel_flags).unwrap(); + assert_eq!(info.last_update, chan_upd.contents.timestamp); + }, + Err(_) => {}, + } + }, + // Handle query channel range. + 3 => { + let query = decode_msg!(msgs::QueryChannelRange); + let peer_node_id = get_pubkey!(); + + let _ = gossip.handle_query_channel_range(peer_node_id, query); + + // handle_query_channel_range always enqueues at least one + // SendReplyChannelRange event regardless of success or failure. + let events = gossip.get_and_clear_pending_msg_events(); + assert!(!events.is_empty()); + for event in &events { + match event { + MessageSendEvent::SendReplyChannelRange { node_id, msg } => { + assert_eq!(*node_id, peer_node_id); + assert!(msg.sync_complete || events.len() > 1); + }, + _ => panic!("Expected SendReplyChannelRange event"), + } + } + // The last reply must have sync_complete set. + match events.last().unwrap() { + MessageSendEvent::SendReplyChannelRange { msg, .. } => { + assert!(msg.sync_complete); + }, + _ => panic!("Expected SendReplyChannelRange event"), + } + }, + // Handle channel failure network update. + 4 => { + let scid = u64::from_be_bytes(get_slice!(8).try_into().unwrap()); + + network_graph.handle_network_update(&NetworkUpdate::ChannelFailure { + short_channel_id: scid, + is_permanent: true, + }); + + assert!(network_graph.read_only().channel(scid).is_none()); + }, + // Handle node failure network update. + 5 => { + let peer_node_id = get_pubkey!(); + + network_graph.handle_network_update(&NetworkUpdate::NodeFailure { + node_id: peer_node_id, + is_permanent: true, + }); + + assert!(network_graph + .read_only() + .node(&NodeId::from_pubkey(&peer_node_id)) + .is_none()); + }, + // Remove stale channels and tracking. + 6 => { + let time_unix = u64::from_be_bytes(get_slice!(8).try_into().unwrap()); + network_graph.remove_stale_channels_and_tracking_with_time(time_unix); + }, + _ => unreachable!(), + } + } +} + +pub fn gossip_discovery_test<Out: test_logger::Output>(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn gossip_discovery_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/invoice_request_deser.rs b/fuzz/src/invoice_request_deser.rs index a21303debd7..c4b31942843 100644 --- a/fuzz/src/invoice_request_deser.rs +++ b/fuzz/src/invoice_request_deser.rs @@ -104,6 +104,7 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>( let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: OfferId([42; 32]), invoice_request: invoice_request_fields, + payment_metadata: None, }); let payee_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([42; 32]), diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 582fa346c54..be5b34acbc7 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -15,9 +15,6 @@ extern crate lightning_rapid_gossip_sync; #[cfg(not(fuzzing))] compile_error!("Fuzz targets need cfg=fuzzing"); -#[cfg(not(hashes_fuzz))] -compile_error!("Fuzz targets need cfg=hashes_fuzz"); - #[cfg(not(secp256k1_fuzz))] compile_error!("Fuzz targets need cfg=secp256k1_fuzz"); @@ -31,6 +28,7 @@ pub mod chanmon_deser; pub mod feature_flags; pub mod fromstr_to_netaddress; pub mod full_stack; +pub mod gossip_discovery; pub mod indexedmap; pub mod invoice_deser; pub mod invoice_request_deser; @@ -38,6 +36,7 @@ pub mod lsps_message; pub mod offer_deser; pub mod onion_hop_data; pub mod onion_message; +pub mod payer_proof_deser; pub mod peer_crypt; pub mod process_network_graph; pub mod process_onion_failure; diff --git a/fuzz/src/lsps_message.rs b/fuzz/src/lsps_message.rs index 547a27b70ee..7a3cb0cf7e0 100644 --- a/fuzz/src/lsps_message.rs +++ b/fuzz/src/lsps_message.rs @@ -5,8 +5,7 @@ use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use bitcoin::Network; -use lightning::chain::Filter; -use lightning::chain::{chainmonitor, BestBlock}; +use lightning::chain::{chainmonitor, BlockLocator}; use lightning::ln::channelmanager::{ChainParameters, ChannelManager}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; @@ -59,8 +58,9 @@ pub fn do_test(data: &[u8]) { Arc::clone(&kv_store), Arc::clone(&keys_manager), keys_manager.get_peer_storage_key(), + false, )); - let best_block = BestBlock::from_network(network); + let best_block = BlockLocator::from_network(network); let params = ChainParameters { network, best_block }; let manager = Arc::new(ChannelManager::new( Arc::clone(&fee_estimator), @@ -77,17 +77,18 @@ pub fn do_test(data: &[u8]) { genesis_block.header.time, )); - let liquidity_manager = Arc::new(LiquidityManagerSync::new( - Arc::clone(&keys_manager), - Arc::clone(&keys_manager), - Arc::clone(&manager), - None::<Arc<dyn Filter + Send + Sync>>, - None, - kv_store, - Arc::clone(&tx_broadcaster), - None, - None, - ).unwrap()); + let liquidity_manager = Arc::new( + LiquidityManagerSync::new( + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&manager), + kv_store, + Arc::clone(&tx_broadcaster), + None, + None, + ) + .unwrap(), + ); let mut reader = data; if let Ok(Some(msg)) = liquidity_manager.read(LSPS_MESSAGE_TYPE_ID, &mut reader) { let secp = Secp256k1::signing_only(); diff --git a/fuzz/src/onion_message.rs b/fuzz/src/onion_message.rs index 09634a1c373..4859f7379fb 100644 --- a/fuzz/src/onion_message.rs +++ b/fuzz/src/onion_message.rs @@ -260,7 +260,7 @@ impl NodeSigner for KeyProvider { } fn get_expanded_key(&self) -> ExpandedKey { - unreachable!() + ExpandedKey::new([42; 32]) } fn sign_invoice( @@ -296,8 +296,6 @@ impl NodeSigner for KeyProvider { impl SignerProvider for KeyProvider { type EcdsaSigner = TestChannelSigner; - #[cfg(taproot)] - type TaprootSigner = TestChannelSigner; fn generate_channel_keys_id(&self, _inbound: bool, _user_channel_id: u128) -> [u8; 32] { unreachable!() diff --git a/fuzz/src/payer_proof_deser.rs b/fuzz/src/payer_proof_deser.rs new file mode 100644 index 00000000000..adccbe5f1bc --- /dev/null +++ b/fuzz/src/payer_proof_deser.rs @@ -0,0 +1,31 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or 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::utils::test_logger; +use core::convert::TryFrom; +use lightning::offers::payer_proof::PayerProof; +use lightning::util::ser::Writeable; + +#[inline] +pub fn do_test<Out: test_logger::Output>(data: &[u8], _out: Out) { + if let Ok(payer_proof) = PayerProof::try_from(data.to_vec()) { + let mut bytes = Vec::with_capacity(data.len()); + payer_proof.write(&mut bytes).unwrap(); + assert_eq!(data, bytes); + } +} + +pub fn payer_proof_deser_test<Out: test_logger::Output>(data: &[u8], out: Out) { + do_test(data, out); +} + +#[no_mangle] +pub extern "C" fn payer_proof_deser_run(data: *const u8, datalen: usize) { + do_test(unsafe { std::slice::from_raw_parts(data, datalen) }, test_logger::DevNull {}); +} diff --git a/fuzz/src/refund_deser.rs b/fuzz/src/refund_deser.rs index 446ac704455..c705bda1a2f 100644 --- a/fuzz/src/refund_deser.rs +++ b/fuzz/src/refund_deser.rs @@ -69,7 +69,8 @@ fn build_response<T: secp256k1::Signing + secp256k1::Verification>( ) -> Result<UnsignedBolt12Invoice, Bolt12SemanticError> { let entropy_source = Randomness {}; let receive_auth_key = ReceiveAuthKey([41; 32]); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = + PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let payee_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([42; 32]), payment_constraints: PaymentConstraints { diff --git a/fuzz/src/router.rs b/fuzz/src/router.rs index 2e5b15fc7f4..aa3d274dac2 100644 --- a/fuzz/src/router.rs +++ b/fuzz/src/router.rs @@ -248,6 +248,7 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) { outbound_capacity_msat: capacity.saturating_mul(1000), next_outbound_htlc_limit_msat: capacity.saturating_mul(1000), next_outbound_htlc_minimum_msat: 0, + next_splice_out_maximum_sat: capacity, inbound_htlc_minimum_msat: None, inbound_htlc_maximum_msat: None, config: None, @@ -255,6 +256,8 @@ pub fn do_test<Out: test_logger::Output>(data: &[u8], out: Out) { channel_shutdown_state: Some(ChannelShutdownState::NotShuttingDown), pending_inbound_htlcs: Vec::new(), pending_outbound_htlcs: Vec::new(), + current_dust_exposure_msat: None, + splice_details: None, }); } Some(&$first_hops_vec[..]) diff --git a/fuzz/src/utils/test_logger.rs b/fuzz/src/utils/test_logger.rs index f8369879447..e629a7f486b 100644 --- a/fuzz/src/utils/test_logger.rs +++ b/fuzz/src/utils/test_logger.rs @@ -8,6 +8,7 @@ // licenses. use lightning::util::logger::{Logger, Record}; +use std::any::TypeId; use std::io::Write; use std::sync::{Arc, Mutex}; @@ -21,6 +22,13 @@ impl Output for DevNull { fn locked_write(&self, _data: &[u8]) {} } #[derive(Clone)] +pub struct Stdout {} +impl Output for Stdout { + fn locked_write(&self, data: &[u8]) { + std::io::stdout().write_all(data).unwrap(); + } +} +#[derive(Clone)] pub struct StringBuffer(Arc<Mutex<String>>); impl Output for StringBuffer { fn locked_write(&self, data: &[u8]) { @@ -59,6 +67,9 @@ impl<'a, Out: Output> Write for LockedWriteAdapter<'a, Out> { impl<Out: Output> Logger for TestLogger<Out> { fn log(&self, record: Record) { - write!(LockedWriteAdapter(&self.out), "{:<6} {}", self.id, record).unwrap(); + if TypeId::of::<Out>() == TypeId::of::<DevNull>() { + return; + } + writeln!(LockedWriteAdapter(&self.out), "{:<6} {}", self.id, record).unwrap(); } } diff --git a/fuzz/targets.h b/fuzz/targets.h index 921439836af..3a0699d2dac 100644 --- a/fuzz/targets.h +++ b/fuzz/targets.h @@ -12,6 +12,7 @@ void onion_message_run(const unsigned char* data, size_t data_len); void peer_crypt_run(const unsigned char* data, size_t data_len); void process_network_graph_run(const unsigned char* data, size_t data_len); void process_onion_failure_run(const unsigned char* data, size_t data_len); +void payer_proof_deser_run(const unsigned char* data, size_t data_len); void refund_deser_run(const unsigned char* data, size_t data_len); void router_run(const unsigned char* data, size_t data_len); void zbase32_run(const unsigned char* data, size_t data_len); @@ -22,6 +23,7 @@ void fromstr_to_netaddress_run(const unsigned char* data, size_t data_len); void feature_flags_run(const unsigned char* data, size_t data_len); void lsps_message_run(const unsigned char* data, size_t data_len); void fs_store_run(const unsigned char* data, size_t data_len); +void gossip_discovery_run(const unsigned char* data, size_t data_len); void msg_accept_channel_run(const unsigned char* data, size_t data_len); void msg_announcement_signatures_run(const unsigned char* data, size_t data_len); void msg_channel_reestablish_run(const unsigned char* data, size_t data_len); diff --git a/fuzz/test_cases/base32/smoke b/fuzz/test_cases/base32/smoke new file mode 100644 index 00000000000..573541ac970 --- /dev/null +++ b/fuzz/test_cases/base32/smoke @@ -0,0 +1 @@ +0 diff --git a/fuzz/test_cases/bech32_parse/smoke b/fuzz/test_cases/bech32_parse/smoke new file mode 100644 index 00000000000..573541ac970 --- /dev/null +++ b/fuzz/test_cases/bech32_parse/smoke @@ -0,0 +1 @@ +0 diff --git a/fuzz/test_cases/chanmon_consistency/smoke b/fuzz/test_cases/chanmon_consistency/smoke new file mode 100644 index 00000000000..573541ac970 --- /dev/null +++ b/fuzz/test_cases/chanmon_consistency/smoke @@ -0,0 +1 @@ +0 diff --git a/fuzz/write-seeds/Cargo.toml b/fuzz/write-seeds/Cargo.toml index 6e1952ea8a3..1c5acb7919f 100644 --- a/fuzz/write-seeds/Cargo.toml +++ b/fuzz/write-seeds/Cargo.toml @@ -9,7 +9,3 @@ edition = "2021" [dependencies] lightning-fuzz = { path = "../" } - -# Prevent this from interfering with workspaces -[workspace] -members = ["."] diff --git a/lightning-background-processor/src/lib.rs b/lightning-background-processor/src/lib.rs index f052f3d8d4c..f2b3cdd1831 100644 --- a/lightning-background-processor/src/lib.rs +++ b/lightning-background-processor/src/lib.rs @@ -55,9 +55,9 @@ use lightning::routing::utxo::UtxoLookup; #[cfg(not(c_bindings))] use lightning::sign::EntropySource; use lightning::sign::{ChangeDestinationSource, ChangeDestinationSourceSync, OutputSpender}; -#[cfg(not(c_bindings))] -use lightning::util::async_poll::MaybeSend; use lightning::util::logger::Logger; +#[cfg(not(c_bindings))] +use lightning::util::native_async::MaybeSend; use lightning::util::persist::{ KVStore, KVStoreSync, KVStoreSyncWrapper, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, @@ -378,18 +378,11 @@ type DynMessageRouter = lightning::onion_message::messenger::DefaultMessageRoute &'static (dyn EntropySource + Send + Sync), >; -#[cfg(all(not(c_bindings), not(taproot)))] +#[cfg(not(c_bindings))] type DynSignerProvider = dyn lightning::sign::SignerProvider<EcdsaSigner = lightning::sign::InMemorySigner> + Send + Sync; -#[cfg(all(not(c_bindings), taproot))] -type DynSignerProvider = (dyn lightning::sign::SignerProvider< - EcdsaSigner = lightning::sign::InMemorySigner, - TaprootSigner = lightning::sign::InMemorySigner, -> + Send - + Sync); - #[cfg(not(c_bindings))] type DynChannelManager = lightning::ln::channelmanager::ChannelManager< &'static (dyn chain::Watch<lightning::sign::InMemorySigner> + Send + Sync), @@ -464,7 +457,6 @@ pub const NO_LIQUIDITY_MANAGER: Option< NodeSigner = &(dyn lightning::sign::NodeSigner + Send + Sync), AChannelManager = DynChannelManager, CM = &DynChannelManager, - C = &(dyn chain::Filter + Send + Sync), K = &DummyKVStore, TimeProvider = dyn lightning_liquidity::utils::time::TimeProvider + Send + Sync, TP = &(dyn lightning_liquidity::utils::time::TimeProvider + Send + Sync), @@ -486,7 +478,6 @@ pub const NO_LIQUIDITY_MANAGER_SYNC: Option< NodeSigner = &(dyn lightning::sign::NodeSigner + Send + Sync), AChannelManager = DynChannelManager, CM = &DynChannelManager, - C = &(dyn chain::Filter + Send + Sync), KVStoreSync = dyn lightning::util::persist::KVStoreSync + Send + Sync, KS = &(dyn lightning::util::persist::KVStoreSync + Send + Sync), TimeProvider = dyn lightning_liquidity::utils::time::TimeProvider + Send + Sync, @@ -775,6 +766,17 @@ use futures_util::{dummy_waker, Joiner, OptionalSelector, Selector, SelectorOutp /// The `fetch_time` parameter should return the current wall clock time, if one is available. If /// no time is available, some features may be disabled, however the node will still operate fine. /// +/// Note that when deferred monitor writes are enabled on [`ChainMonitor`], this function flushes +/// pending writes after persisting the [`ChannelManager`]. If the [`Persist`] implementation +/// performs blocking I/O and returns [`Completed`] synchronously rather than returning +/// [`InProgress`], this will block the async executor. +/// +/// [`ChainMonitor`]: lightning::chain::chainmonitor::ChainMonitor +/// [`Persist`]: lightning::chain::chainmonitor::Persist +/// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager +/// [`Completed`]: lightning::chain::ChannelMonitorUpdateStatus::Completed +/// [`InProgress`]: lightning::chain::ChannelMonitorUpdateStatus::InProgress +/// /// For example, in order to process background events in a [Tokio](https://tokio.rs/) task, you /// could setup `process_events_async` like this: /// ``` @@ -829,7 +831,7 @@ use futures_util::{dummy_waker, Joiner, OptionalSelector, Selector, SelectorOutp /// # type P2PGossipSync<UL> = lightning::routing::gossip::P2PGossipSync<Arc<NetworkGraph>, Arc<UL>, Arc<Logger>>; /// # type ChannelManager<B, F, FE> = lightning::ln::channelmanager::SimpleArcChannelManager<ChainMonitor<B, F, FE>, B, FE, Logger>; /// # type OnionMessenger<B, F, FE> = lightning::onion_message::messenger::OnionMessenger<Arc<lightning::sign::KeysManager>, Arc<lightning::sign::KeysManager>, Arc<Logger>, Arc<ChannelManager<B, F, FE>>, Arc<lightning::onion_message::messenger::DefaultMessageRouter<Arc<NetworkGraph>, Arc<Logger>, Arc<lightning::sign::KeysManager>>>, Arc<ChannelManager<B, F, FE>>, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler, lightning::ln::peer_handler::IgnoringMessageHandler>; -/// # type LiquidityManager<B, F, FE> = lightning_liquidity::LiquidityManager<Arc<lightning::sign::KeysManager>, Arc<lightning::sign::KeysManager>, Arc<ChannelManager<B, F, FE>>, Arc<F>, Arc<Store>, Arc<DefaultTimeProvider>, Arc<B>>; +/// # type LiquidityManager<B, F, FE> = lightning_liquidity::LiquidityManager<Arc<lightning::sign::KeysManager>, Arc<lightning::sign::KeysManager>, Arc<ChannelManager<B, F, FE>>, Arc<Store>, Arc<DefaultTimeProvider>, Arc<B>>; /// # type Scorer = RwLock<lightning::routing::scoring::ProbabilisticScorer<Arc<NetworkGraph>, Arc<Logger>>>; /// # type PeerManager<B, F, FE, UL> = lightning::ln::peer_handler::SimpleArcPeerManager<SocketDescriptor, ChainMonitor<B, F, FE>, B, FE, Arc<UL>, Logger, F, StoreSync>; /// # type OutputSweeper<B, D, FE, F, O> = lightning::util::sweep::OutputSweeper<Arc<B>, Arc<D>, Arc<FE>, Arc<F>, Arc<Store>, Arc<Logger>, Arc<O>>; @@ -1120,10 +1122,12 @@ where let mut futures = Joiner::new(); - if channel_manager.get_cm().get_and_clear_needs_persistence() { - log_trace!(logger, "Persisting ChannelManager..."); + let needs_cm_persist = channel_manager.get_cm().get_and_clear_needs_persistence(); + let mut cm_fut = core::pin::pin!(async { + if needs_cm_persist { + // Capture the monitor operations pending before we persist the ChannelManager. + let pending_monitor_writes = chain_monitor.get_cm().pending_operation_count(); - let fut = async { kv_store .write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1131,22 +1135,29 @@ where CHANNEL_MANAGER_PERSISTENCE_KEY, channel_manager.get_cm().encode(), ) - .await - }; - // TODO: Once our MSRV is 1.68 we should be able to drop the Box - let mut fut = Box::pin(fut); - - // Because persisting the ChannelManager is important to avoid accidental - // force-closures, go ahead and poll the future once before we do slightly more - // CPU-intensive tasks in the form of NetworkGraph pruning or scorer time-stepping - // below. This will get it moving but won't block us for too long if the underlying - // future is actually async. + .await?; + + // Flush monitor operations that were pending before we persisted. New updates + // that arrived after are left for the next iteration. + chain_monitor.get_cm().flush(pending_monitor_writes, &logger); + } + Ok(()) + }); + + // Because persisting the ChannelManager is important to avoid accidental force-closures, + // go ahead and poll the future once before we do slightly more CPU-intensive tasks in the + // form of NetworkGraph pruning or scorer time-stepping below. This will get it moving but + // won't block us for too long if the underlying future is actually async. We stash the + // outcome and feed it into the `Joiner` once it is constructed. + if needs_cm_persist { + log_trace!(logger, "Persisting ChannelManager..."); + use core::future::Future; let mut waker = dummy_waker(); let mut ctx = task::Context::from_waker(&mut waker); - match core::pin::Pin::new(&mut fut).poll(&mut ctx) { + match cm_fut.as_mut().poll(&mut ctx) { task::Poll::Ready(res) => futures.set_a_res(res), - task::Poll::Pending => futures.set_a(fut), + task::Poll::Pending => futures.set_a(cm_fut), } log_trace!(logger, "Done persisting ChannelManager."); @@ -1194,7 +1205,8 @@ where GossipSync::Rapid(_) => !have_pruned || prune_timer_elapsed, _ => prune_timer_elapsed, }; - if should_prune { + + let network_graph_to_persist = if should_prune { // The network graph must not be pruned while rapid sync completion is pending if let Some(network_graph) = gossip_sync.prunable_network_graph() { if let Some(duration_since_epoch) = fetch_time() { @@ -1206,28 +1218,15 @@ where log_warn!(logger, "Not pruning network graph, consider implementing the fetch_time argument or calling remove_stale_channels_and_tracking_with_time manually."); log_trace!(logger, "Persisting network graph."); } - let fut = async { - if let Err(e) = kv_store - .write( - NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, - NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, - NETWORK_GRAPH_PERSISTENCE_KEY, - network_graph.encode(), - ) - .await - { - log_error!(logger, "Error: Failed to persist network graph, check your disk and permissions {}",e); - } - - Ok(()) - }; - - // TODO: Once our MSRV is 1.68 we should be able to drop the Box - futures.set_b(Box::pin(fut)); have_pruned = true; + Some(network_graph) + } else { + None } - } + } else { + None + }; if !have_decayed_scorer { if let Some(ref scorer) = scorer { if let Some(duration_since_epoch) = fetch_time() { @@ -1237,7 +1236,9 @@ where } have_decayed_scorer = true; } - match check_and_reset_sleeper(&mut last_scorer_persist_call, || { + // Step the scorer forward synchronously here, deferring the actual write to the + // future built below. + let persist_scorer = match check_and_reset_sleeper(&mut last_scorer_persist_call, || { sleeper(SCORER_PERSIST_TIMER) }) { Some(false) => { @@ -1248,7 +1249,46 @@ where } else { log_trace!(logger, "Persisting scorer"); } - let fut = async { + true + } else { + false + } + }, + Some(true) => break, + None => false, + }; + let persist_sweeper = + match check_and_reset_sleeper(&mut last_sweeper_call, || sleeper(SWEEPER_TIMER)) { + Some(false) => { + log_trace!(logger, "Regenerating sweeper spends if necessary"); + true + }, + Some(true) => break, + None => false, + }; + + let network_graph_fut = core::pin::pin!(async { + if let Some(network_graph) = network_graph_to_persist { + if let Err(e) = kv_store + .write( + NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, + NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, + NETWORK_GRAPH_PERSISTENCE_KEY, + network_graph.encode(), + ) + .await + { + log_error!(logger, "Error: Failed to persist network graph, check your disk and permissions {}",e); + } + } + Ok(()) + }); + futures.set_b(network_graph_fut); + + let scorer_fut = + core::pin::pin!(async { + if persist_scorer { + if let Some(ref scorer) = scorer { if let Err(e) = kv_store .write( SCORER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1258,43 +1298,26 @@ where ) .await { - log_error!( - logger, - "Error: Failed to persist scorer, check your disk and permissions {}", - e - ); + log_error!(logger, "Error: Failed to persist scorer, check your disk and permissions {}", e); } - - Ok(()) - }; - - // TODO: Once our MSRV is 1.68 we should be able to drop the Box - futures.set_c(Box::pin(fut)); + } } - }, - Some(true) => break, - None => {}, - } - match check_and_reset_sleeper(&mut last_sweeper_call, || sleeper(SWEEPER_TIMER)) { - Some(false) => { - log_trace!(logger, "Regenerating sweeper spends if necessary"); - if let Some(ref sweeper) = sweeper { - let fut = async { - let _ = sweeper.regenerate_and_broadcast_spend_if_necessary().await; - - Ok(()) - }; + Ok(()) + }); + futures.set_c(scorer_fut); - // TODO: Once our MSRV is 1.68 we should be able to drop the Box - futures.set_d(Box::pin(fut)); + let sweeper_fut = core::pin::pin!(async { + if persist_sweeper { + if let Some(ref sweeper) = sweeper { + let _ = sweeper.regenerate_and_broadcast_spend_if_necessary().await; } - }, - Some(true) => break, - None => {}, - } + } + Ok(()) + }); + futures.set_d(sweeper_fut); - if let Some(liquidity_manager) = liquidity_manager.as_ref() { - let fut = async { + let lm_fut = core::pin::pin!(async { + if let Some(liquidity_manager) = liquidity_manager.as_ref() { liquidity_manager .get_lm() .persist() @@ -1308,9 +1331,11 @@ where log_error!(logger, "Persisting LiquidityManager failed: {}", e); e }) - }; - futures.set_e(Box::pin(fut)); - } + } else { + Ok(()) + } + }); + futures.set_e(lm_fut); // Run persistence tasks in parallel and exit if any of them returns an error. for res in futures.await { @@ -1373,6 +1398,7 @@ where // After we exit, ensure we persist the ChannelManager one final time - this avoids // some races where users quit while channel updates were in-flight, with // ChannelMonitor update(s) persisted without a corresponding ChannelManager update. + let pending_monitor_writes = chain_monitor.get_cm().pending_operation_count(); kv_store .write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1381,6 +1407,10 @@ where channel_manager.get_cm().encode(), ) .await?; + + // Flush monitor operations that were pending before final persistence. + chain_monitor.get_cm().flush(pending_monitor_writes, &logger); + if let Some(ref scorer) = scorer { kv_store .write( @@ -1684,7 +1714,15 @@ impl BackgroundProcessor { channel_manager.get_cm().timer_tick_occurred(); last_freshness_call = Instant::now(); } + if channel_manager.get_cm().get_and_clear_needs_persistence() { + // We capture pending_operation_count inside the persistence + // branch to avoid a race: ChannelManager handlers queue + // deferred monitor ops before the persistence flag is set. + // Capturing outside would let us observe pending ops while + // the flag is still unset, causing us to flush monitor + // writes without persisting the ChannelManager. + let pending_monitor_writes = chain_monitor.get_cm().pending_operation_count(); log_trace!(logger, "Persisting ChannelManager..."); (kv_store.write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1693,6 +1731,10 @@ impl BackgroundProcessor { channel_manager.get_cm().encode(), ))?; log_trace!(logger, "Done persisting ChannelManager."); + + // Flush monitor operations that were pending before we persisted. + // New updates that arrived after are left for the next iteration. + chain_monitor.get_cm().flush(pending_monitor_writes, &logger); } if let Some(liquidity_manager) = liquidity_manager.as_ref() { @@ -1809,12 +1851,17 @@ impl BackgroundProcessor { // After we exit, ensure we persist the ChannelManager one final time - this avoids // some races where users quit while channel updates were in-flight, with // ChannelMonitor update(s) persisted without a corresponding ChannelManager update. + let pending_monitor_writes = chain_monitor.get_cm().pending_operation_count(); kv_store.write( CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_KEY, channel_manager.get_cm().encode(), )?; + + // Flush monitor operations that were pending before final persistence. + chain_monitor.get_cm().flush(pending_monitor_writes, &logger); + if let Some(ref scorer) = scorer { kv_store.write( SCORER_PERSISTENCE_PRIMARY_NAMESPACE, @@ -1896,9 +1943,10 @@ mod tests { use bitcoin::transaction::{Transaction, TxOut}; use bitcoin::{Amount, ScriptBuf, Txid}; use core::sync::atomic::{AtomicBool, Ordering}; + use lightning::chain::chainmonitor; use lightning::chain::channelmonitor::ANTI_REORG_DELAY; use lightning::chain::transaction::OutPoint; - use lightning::chain::{chainmonitor, BestBlock, Confirm, Filter}; + use lightning::chain::{BlockLocator, Confirm}; use lightning::events::{Event, PathFailure, ReplayEvent}; use lightning::ln::channelmanager; use lightning::ln::channelmanager::{ @@ -1934,7 +1982,7 @@ mod tests { use lightning::{get_event, get_event_msg}; use lightning_liquidity::utils::time::DefaultTimeProvider; use lightning_liquidity::{ALiquidityManagerSync, LiquidityManager, LiquidityManagerSync}; - use lightning_persister::fs_store::FilesystemStore; + use lightning_persister::fs_store::v1::FilesystemStore; use lightning_rapid_gossip_sync::RapidGossipSync; use std::collections::VecDeque; use std::path::PathBuf; @@ -2054,7 +2102,6 @@ mod tests { Arc<KeysManager>, Arc<KeysManager>, Arc<ChannelManager>, - Arc<dyn Filter + Sync + Send>, Arc<Persister>, DefaultTimeProvider, Arc<test_utils::TestBroadcaster>, @@ -2083,7 +2130,7 @@ mod tests { tx_broadcaster: Arc<test_utils::TestBroadcaster>, network_graph: Arc<NetworkGraph<Arc<test_utils::TestLogger>>>, logger: Arc<test_utils::TestLogger>, - best_block: BestBlock, + best_block: BlockLocator, scorer: Arc<LockingWrapper<TestScorer>>, sweeper: Arc< OutputSweeperSync< @@ -2444,8 +2491,9 @@ mod tests { Arc::clone(&kv_store), Arc::clone(&keys_manager), keys_manager.get_peer_storage_key(), + true, )); - let best_block = BestBlock::from_network(network); + let best_block = BlockLocator::from_network(network); let params = ChainParameters { network, best_block }; let mut config = UserConfig::default(); config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; @@ -2513,8 +2561,6 @@ mod tests { Arc::clone(&keys_manager), Arc::clone(&keys_manager), Arc::clone(&manager), - None, - None, Arc::clone(&kv_store), Arc::clone(&tx_broadcaster), None, @@ -2567,6 +2613,8 @@ mod tests { (persist_dir, nodes) } + /// Opens a channel between two nodes without a running `BackgroundProcessor`, + /// so deferred monitor operations are flushed manually at each step. macro_rules! open_channel { ($node_a: expr, $node_b: expr, $channel_value: expr) => {{ begin_open_channel!($node_a, $node_b, $channel_value); @@ -2582,12 +2630,19 @@ mod tests { tx.clone(), ) .unwrap(); + // funding_transaction_generated does not call watch_channel, so no + // deferred op is queued and FundingCreated is available immediately. let msg_a = get_event_msg!( $node_a, MessageSendEvent::SendFundingCreated, $node_b.node.get_our_node_id() ); $node_b.node.handle_funding_created($node_a.node.get_our_node_id(), &msg_a); + // Flush node_b's new monitor (watch_channel) so it releases the + // FundingSigned message. + $node_b + .chain_monitor + .flush($node_b.chain_monitor.pending_operation_count(), &$node_b.logger); get_event!($node_b, Event::ChannelPending); let msg_b = get_event_msg!( $node_b, @@ -2595,6 +2650,11 @@ mod tests { $node_a.node.get_our_node_id() ); $node_a.node.handle_funding_signed($node_b.node.get_our_node_id(), &msg_b); + // Flush node_a's new monitor (watch_channel) queued by + // handle_funding_signed. + $node_a + .chain_monitor + .flush($node_a.chain_monitor.pending_operation_count(), &$node_a.logger); get_event!($node_a, Event::ChannelPending); tx }}; @@ -2675,7 +2735,7 @@ mod tests { let height = node.best_block.height + 1; let header = create_dummy_header(prev_blockhash, height); let txdata = vec![(0, tx)]; - node.best_block = BestBlock::new(header.block_hash(), height); + node.best_block = BlockLocator::new(header.block_hash(), height); match i { 1 => { node.node.transactions_confirmed(&header, &txdata, height); @@ -2702,7 +2762,7 @@ mod tests { let prev_blockhash = node.best_block.block_hash; let height = node.best_block.height + 1; let header = create_dummy_header(prev_blockhash, height); - node.best_block = BestBlock::new(header.block_hash(), height); + node.best_block = BlockLocator::new(header.block_hash(), height); if i == num_blocks { // We need the TestBroadcaster to know about the new height so that it doesn't think // we're violating the time lock requirements of transactions broadcasted at that @@ -2720,6 +2780,20 @@ mod tests { confirm_transaction_depth(node, tx, ANTI_REORG_DELAY); } + /// Waits until the background processor has flushed all pending deferred monitor + /// operations for the given node. Panics if the pending count does not reach zero + /// within `EVENT_DEADLINE`. + fn wait_for_flushed(chain_monitor: &ChainMonitor) { + let start = std::time::Instant::now(); + while chain_monitor.pending_operation_count() > 0 { + assert!( + start.elapsed() < EVENT_DEADLINE, + "Pending monitor operations were not flushed within deadline" + ); + std::thread::sleep(Duration::from_millis(10)); + } + } + #[test] fn test_background_processor() { // Test that when a new channel is created, the ChannelManager needs to be re-persisted with @@ -2910,10 +2984,10 @@ mod tests { let kv_store = KVStoreSyncWrapper(kv_store_sync); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) @@ -3060,11 +3134,21 @@ mod tests { .node .funding_transaction_generated(temporary_channel_id, node_1_id, funding_tx.clone()) .unwrap(); + // funding_transaction_generated does not call watch_channel, so no deferred op is + // queued and the FundingCreated message is available immediately. let msg_0 = get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_1_id); nodes[1].node.handle_funding_created(node_0_id, &msg_0); + // Node 1 has no bg processor, flush its new monitor (watch_channel) manually so + // events and FundingSigned are released. + nodes[1] + .chain_monitor + .flush(nodes[1].chain_monitor.pending_operation_count(), &nodes[1].logger); get_event!(nodes[1], Event::ChannelPending); let msg_1 = get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_0_id); nodes[0].node.handle_funding_signed(node_1_id, &msg_1); + // Wait for the bg processor to flush the new monitor (watch_channel) queued by + // handle_funding_signed. + wait_for_flushed(&nodes[0].chain_monitor); channel_pending_recv .recv_timeout(EVENT_DEADLINE) .expect("ChannelPending not handled within deadline"); @@ -3125,6 +3209,9 @@ mod tests { error_message.to_string(), ) .unwrap(); + // Wait for the bg processor to flush the monitor update triggered by force close + // so the commitment tx is broadcast. + wait_for_flushed(&nodes[0].chain_monitor); let commitment_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().pop().unwrap(); confirm_transaction_depth(&mut nodes[0], &commitment_tx, BREAKDOWN_TIMEOUT as u32); @@ -3435,10 +3522,10 @@ mod tests { let kv_store = KVStoreSyncWrapper(kv_store_sync); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) @@ -3662,10 +3749,10 @@ mod tests { let (exit_sender, exit_receiver) = tokio::sync::watch::channel(()); // Yes, you can unsafe { turn off the borrow checker } - let lm_async: &'static LiquidityManager<_, _, _, _, _, _, _> = unsafe { + let lm_async: &'static LiquidityManager<_, _, _, _, _, _> = unsafe { &*(nodes[0].liquidity_manager.get_lm_async() - as *const LiquidityManager<_, _, _, _, _, _, _>) - as &'static LiquidityManager<_, _, _, _, _, _, _> + as *const LiquidityManager<_, _, _, _, _, _>) + as &'static LiquidityManager<_, _, _, _, _, _> }; let sweeper_async: &'static OutputSweeper<_, _, _, _, _, _, _> = unsafe { &*(nodes[0].sweeper.sweeper_async() as *const OutputSweeper<_, _, _, _, _, _, _>) diff --git a/lightning-block-sync/Cargo.toml b/lightning-block-sync/Cargo.toml index 97f199963ac..d8d71da3fae 100644 --- a/lightning-block-sync/Cargo.toml +++ b/lightning-block-sync/Cargo.toml @@ -16,15 +16,16 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -rest-client = [ "serde_json", "chunked_transfer" ] -rpc-client = [ "serde_json", "chunked_transfer" ] +rest-client = [ "serde_json", "dep:bitreq" ] +rpc-client = [ "serde_json", "dep:bitreq" ] +tokio = [ "dep:tokio", "bitreq?/async" ] [dependencies] bitcoin = "0.32.2" lightning = { version = "0.3.0", path = "../lightning" } tokio = { version = "1.35", features = [ "io-util", "net", "time", "rt" ], optional = true } serde_json = { version = "1.0", optional = true } -chunked_transfer = { version = "1.4", optional = true } +bitreq = { version = "0.3", default-features = false, features = ["std"], optional = true } [dev-dependencies] lightning = { version = "0.3.0", path = "../lightning", features = ["_test_utils"] } diff --git a/lightning-block-sync/src/async_poll.rs b/lightning-block-sync/src/async_poll.rs new file mode 120000 index 00000000000..eb85cdac697 --- /dev/null +++ b/lightning-block-sync/src/async_poll.rs @@ -0,0 +1 @@ +../../lightning/src/util/async_poll.rs \ No newline at end of file diff --git a/lightning-block-sync/src/convert.rs b/lightning-block-sync/src/convert.rs index a31b329a5af..48a80c8cbf1 100644 --- a/lightning-block-sync/src/convert.rs +++ b/lightning-block-sync/src/convert.rs @@ -1,4 +1,6 @@ -use crate::http::{BinaryResponse, JsonResponse}; +use crate::http::{BinaryResponse, HttpClientError, JsonResponse}; +#[cfg(feature = "rpc-client")] +use crate::rpc::RpcClientError; use crate::utils::hex_to_work; use crate::{BlockHeaderData, BlockSourceError}; @@ -11,15 +13,15 @@ use bitcoin::Transaction; use serde_json; use bitcoin::hashes::Hash; -use std::convert::From; +use std::convert::Infallible; use std::convert::TryFrom; use std::convert::TryInto; use std::io; use std::str::FromStr; impl TryInto<serde_json::Value> for JsonResponse { - type Error = io::Error; - fn try_into(self) -> Result<serde_json::Value, io::Error> { + type Error = Infallible; + fn try_into(self) -> Result<serde_json::Value, Infallible> { Ok(self.0) } } @@ -35,51 +37,106 @@ impl From<io::Error> for BlockSourceError { } } +/// Conversion from `HttpClientError` into `BlockSourceError`. +impl From<HttpClientError> for BlockSourceError { + fn from(e: HttpClientError) -> BlockSourceError { + match e { + // Transport errors (connection, timeout, etc.) are transient + HttpClientError::Transport(err) => { + BlockSourceError::transient(HttpClientError::Transport(err)) + }, + // 5xx errors are transient (server issues), others are persistent (client errors) + HttpClientError::Http(http_err) => { + if (500..600).contains(&http_err.status_code) { + BlockSourceError::transient(HttpClientError::Http(http_err)) + } else { + BlockSourceError::persistent(HttpClientError::Http(http_err)) + } + }, + // Parse errors are persistent (invalid data) + HttpClientError::Parse(msg) => { + BlockSourceError::persistent(HttpClientError::Parse(msg)) + }, + } + } +} + +/// Conversion from `RpcClientError` into `BlockSourceError`. +#[cfg(feature = "rpc-client")] +impl From<RpcClientError> for BlockSourceError { + fn from(e: RpcClientError) -> BlockSourceError { + match e { + RpcClientError::Http(http_err) => match http_err { + // Transport errors (connection, timeout, etc.) are transient + HttpClientError::Transport(err) => BlockSourceError::transient( + RpcClientError::Http(HttpClientError::Transport(err)), + ), + // 5xx errors are transient (server issues), others are persistent (client errors) + HttpClientError::Http(http) => { + if (500..600).contains(&http.status_code) { + BlockSourceError::transient(RpcClientError::Http(HttpClientError::Http( + http, + ))) + } else { + BlockSourceError::persistent(RpcClientError::Http(HttpClientError::Http( + http, + ))) + } + }, + HttpClientError::Parse(msg) => { + BlockSourceError::persistent(RpcClientError::Http(HttpClientError::Parse(msg))) + }, + }, + // RPC errors (e.g. "block not found") are transient + RpcClientError::Rpc(rpc_err) => { + BlockSourceError::transient(RpcClientError::Rpc(rpc_err)) + }, + // Malformed response data is persistent + RpcClientError::InvalidData(msg) => { + BlockSourceError::persistent(RpcClientError::InvalidData(msg)) + }, + } + } +} + /// Parses binary data as a block. impl TryInto<Block> for BinaryResponse { - type Error = io::Error; + type Error = (); - fn try_into(self) -> io::Result<Block> { - match encode::deserialize(&self.0) { - Err(_) => return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid block data")), - Ok(block) => Ok(block), - } + fn try_into(self) -> Result<Block, ()> { + encode::deserialize(&self.0).map_err(|_| ()) } } /// Parses binary data as a block hash. impl TryInto<BlockHash> for BinaryResponse { - type Error = io::Error; + type Error = (); - fn try_into(self) -> io::Result<BlockHash> { - BlockHash::from_slice(&self.0) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad block hash length")) + fn try_into(self) -> Result<BlockHash, ()> { + BlockHash::from_slice(&self.0).map_err(|_| ()) } } /// Converts a JSON value into block header data. The JSON value may be an object representing a /// block header or an array of such objects. In the latter case, the first object is converted. impl TryInto<BlockHeaderData> for JsonResponse { - type Error = io::Error; + type Error = &'static str; - fn try_into(self) -> io::Result<BlockHeaderData> { + fn try_into(self) -> Result<BlockHeaderData, &'static str> { let header = match self.0 { serde_json::Value::Array(mut array) if !array.is_empty() => { array.drain(..).next().unwrap() }, serde_json::Value::Object(_) => self.0, - _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "unexpected JSON type")), + _ => return Err("unexpected JSON type"), }; if !header.is_object() { - return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON object")); + return Err("expected JSON object"); } // Add an empty previousblockhash for the genesis block. - match header.try_into() { - Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid header data")), - Ok(header) => Ok(header), - } + header.try_into().map_err(|_| "invalid header data") } } @@ -119,15 +176,15 @@ impl TryFrom<serde_json::Value> for BlockHeaderData { /// Converts a JSON value into a block. Assumes the block is hex-encoded in a JSON string. impl TryInto<Block> for JsonResponse { - type Error = io::Error; + type Error = &'static str; - fn try_into(self) -> io::Result<Block> { + fn try_into(self) -> Result<Block, &'static str> { match self.0.as_str() { - None => Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")), + None => Err("expected JSON string"), Some(hex_data) => match Vec::<u8>::from_hex(hex_data) { - Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hex data")), + Err(_) => Err("invalid hex data"), Ok(block_data) => match encode::deserialize(&block_data) { - Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid block data")), + Err(_) => Err("invalid block data"), Ok(block) => Ok(block), }, }, @@ -137,35 +194,31 @@ impl TryInto<Block> for JsonResponse { /// Converts a JSON value into the best block hash and optional height. impl TryInto<(BlockHash, Option<u32>)> for JsonResponse { - type Error = io::Error; + type Error = &'static str; - fn try_into(self) -> io::Result<(BlockHash, Option<u32>)> { + fn try_into(self) -> Result<(BlockHash, Option<u32>), &'static str> { if !self.0.is_object() { - return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON object")); + return Err("expected JSON object"); } let hash = match &self.0["bestblockhash"] { serde_json::Value::String(hex_data) => match BlockHash::from_str(&hex_data) { - Err(_) => { - return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hex data")) - }, + Err(_) => return Err("invalid hex data"), Ok(block_hash) => block_hash, }, - _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")), + _ => return Err("expected JSON string"), }; let height = match &self.0["blocks"] { serde_json::Value::Null => None, serde_json::Value::Number(height) => match height.as_u64() { - None => return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid height")), + None => return Err("invalid height"), Some(height) => match height.try_into() { - Err(_) => { - return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid height")) - }, + Err(_) => return Err("invalid height"), Ok(height) => Some(height), }, }, - _ => return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON number")), + _ => return Err("expected JSON number"), }; Ok((hash, height)) @@ -173,22 +226,18 @@ impl TryInto<(BlockHash, Option<u32>)> for JsonResponse { } impl TryInto<Txid> for JsonResponse { - type Error = io::Error; - fn try_into(self) -> io::Result<Txid> { - let hex_data = self - .0 - .as_str() - .ok_or(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string"))?; - Txid::from_str(hex_data) - .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string())) + type Error = String; + fn try_into(self) -> Result<Txid, String> { + let hex_data = self.0.as_str().ok_or_else(|| "expected JSON string".to_string())?; + Txid::from_str(hex_data).map_err(|err| err.to_string()) } } /// Converts a JSON value into a transaction. WATCH OUT! this cannot be used for zero-input transactions /// (e.g. createrawtransaction). See <https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/197> impl TryInto<Transaction> for JsonResponse { - type Error = io::Error; - fn try_into(self) -> io::Result<Transaction> { + type Error = String; + fn try_into(self) -> Result<Transaction, String> { let hex_tx = if self.0.is_object() { // result is json encoded match &self.0["hex"] { @@ -202,10 +251,7 @@ impl TryInto<Transaction> for JsonResponse { _ => "Unknown error", }; - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("transaction couldn't be signed. {}", reason), - )); + return Err(format!("transaction couldn't be signed. {}", reason)); } else { hex_data } @@ -214,7 +260,7 @@ impl TryInto<Transaction> for JsonResponse { _ => hex_data, }, _ => { - return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")); + return Err("expected JSON string".to_string()); }, } } else { @@ -222,15 +268,15 @@ impl TryInto<Transaction> for JsonResponse { match self.0.as_str() { Some(hex_tx) => hex_tx, None => { - return Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")); + return Err("expected JSON string".to_string()); }, } }; match Vec::<u8>::from_hex(hex_tx) { - Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hex data")), + Err(_) => Err("invalid hex data".to_string()), Ok(tx_data) => match encode::deserialize(&tx_data) { - Err(_) => Err(io::Error::new(io::ErrorKind::InvalidData, "invalid transaction")), + Err(_) => Err("invalid transaction".to_string()), Ok(tx) => Ok(tx), }, } @@ -238,16 +284,13 @@ impl TryInto<Transaction> for JsonResponse { } impl TryInto<BlockHash> for JsonResponse { - type Error = io::Error; + type Error = &'static str; - fn try_into(self) -> io::Result<BlockHash> { + fn try_into(self) -> Result<BlockHash, &'static str> { match self.0.as_str() { - None => Err(io::Error::new(io::ErrorKind::InvalidData, "expected JSON string")), - Some(hex_data) if hex_data.len() != 64 => { - Err(io::Error::new(io::ErrorKind::InvalidData, "invalid hash length")) - }, - Some(hex_data) => BlockHash::from_str(hex_data) - .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid hex data")), + None => Err("expected JSON string"), + Some(hex_data) if hex_data.len() != 64 => Err("invalid hash length"), + Some(hex_data) => BlockHash::from_str(hex_data).map_err(|_| "invalid hex data"), } } } @@ -262,24 +305,21 @@ pub(crate) struct GetUtxosResponse { #[cfg(feature = "rest-client")] impl TryInto<GetUtxosResponse> for JsonResponse { - type Error = io::Error; + type Error = &'static str; - fn try_into(self) -> io::Result<GetUtxosResponse> { - let obj_err = || io::Error::new(io::ErrorKind::InvalidData, "expected an object"); - let bitmap_err = || io::Error::new(io::ErrorKind::InvalidData, "missing bitmap field"); - let bitstr_err = || io::Error::new(io::ErrorKind::InvalidData, "bitmap should be an str"); + fn try_into(self) -> Result<GetUtxosResponse, &'static str> { let bitmap_str = self .0 .as_object() - .ok_or_else(obj_err)? + .ok_or("expected an object")? .get("bitmap") - .ok_or_else(bitmap_err)? + .ok_or("missing bitmap field")? .as_str() - .ok_or_else(bitstr_err)?; + .ok_or("bitmap should be an str")?; let mut hit_bitmap_nonempty = false; for c in bitmap_str.chars() { if c < '0' || c > '9' { - return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid byte")); + return Err("invalid byte"); } if c > '0' { hit_bitmap_nonempty = true; @@ -321,8 +361,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!(42)); match TryInto::<BlockHeaderData>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "unexpected JSON type"); + assert_eq!(e, "unexpected JSON type"); }, Ok(_) => panic!("Expected error"), } @@ -333,8 +372,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!([42])); match TryInto::<BlockHeaderData>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object"); + assert_eq!(e, "expected JSON object"); }, Ok(_) => panic!("Expected error"), } @@ -351,8 +389,7 @@ pub(crate) mod tests { match TryInto::<BlockHeaderData>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid header data"); + assert_eq!(e, "invalid header data"); }, Ok(_) => panic!("Expected error"), } @@ -369,8 +406,7 @@ pub(crate) mod tests { match TryInto::<BlockHeaderData>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid header data"); + assert_eq!(e, "invalid header data"); }, Ok(_) => panic!("Expected error"), } @@ -464,8 +500,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!({ "result": "foo" })); match TryInto::<Block>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string"); + assert_eq!(e, "expected JSON string"); }, Ok(_) => panic!("Expected error"), } @@ -476,8 +511,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!("foobar")); match TryInto::<Block>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data"); + assert_eq!(e, "invalid hex data"); }, Ok(_) => panic!("Expected error"), } @@ -488,8 +522,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!("abcd")); match TryInto::<Block>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid block data"); + assert_eq!(e, "invalid block data"); }, Ok(_) => panic!("Expected error"), } @@ -510,8 +543,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!("foo")); match TryInto::<(BlockHash, Option<u32>)>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object"); + assert_eq!(e, "expected JSON object"); }, Ok(_) => panic!("Expected error"), } @@ -522,8 +554,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!({ "bestblockhash": 42 })); match TryInto::<(BlockHash, Option<u32>)>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string"); + assert_eq!(e, "expected JSON string"); }, Ok(_) => panic!("Expected error"), } @@ -534,8 +565,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!({ "bestblockhash": "foobar"} )); match TryInto::<(BlockHash, Option<u32>)>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data"); + assert_eq!(e, "invalid hex data"); }, Ok(_) => panic!("Expected error"), } @@ -565,8 +595,7 @@ pub(crate) mod tests { })); match TryInto::<(BlockHash, Option<u32>)>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON number"); + assert_eq!(e, "expected JSON number"); }, Ok(_) => panic!("Expected error"), } @@ -581,8 +610,7 @@ pub(crate) mod tests { })); match TryInto::<(BlockHash, Option<u32>)>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid height"); + assert_eq!(e, "invalid height"); }, Ok(_) => panic!("Expected error"), } @@ -609,8 +637,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!({ "result": "foo" })); match TryInto::<Txid>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string"); + assert_eq!(e, "expected JSON string"); }, Ok(_) => panic!("Expected error"), } @@ -621,8 +648,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!("foobar")); match TryInto::<Txid>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "failed to parse hex"); + assert_eq!(e, "failed to parse hex"); }, Ok(_) => panic!("Expected error"), } @@ -633,8 +659,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!("abcd")); match TryInto::<Txid>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "failed to parse hex"); + assert_eq!(e, "failed to parse hex"); }, Ok(_) => panic!("Expected error"), } @@ -654,9 +679,8 @@ pub(crate) mod tests { fn into_txid_from_bitcoind_rpc_json_response() { let mut rpc_response = serde_json::json!( {"error": "", "id": "770", "result": "7934f775149929a8b742487129a7c3a535dfb612f0b726cc67bc10bc2628f906"} - ); - let r: io::Result<Txid> = + let r: Result<Txid, String> = JsonResponse(rpc_response.get_mut("result").unwrap().take()).try_into(); assert_eq!( r.unwrap().to_string(), @@ -676,8 +700,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!("foobar")); match TryInto::<Transaction>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid hex data"); + assert_eq!(e, "invalid hex data"); }, Ok(_) => panic!("Expected error"), } @@ -688,8 +711,7 @@ pub(crate) mod tests { let response = JsonResponse(Value::Number(Number::from_f64(1.0).unwrap())); match TryInto::<Transaction>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string"); + assert_eq!(e, "expected JSON string"); }, Ok(_) => panic!("Expected error"), } @@ -700,8 +722,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!("abcd")); match TryInto::<Transaction>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "invalid transaction"); + assert_eq!(e, "invalid transaction"); }, Ok(_) => panic!("Expected error"), } @@ -737,8 +758,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!({ "error": "foo" })); match TryInto::<Transaction>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON string"); + assert_eq!(e, "expected JSON string"); }, Ok(_) => panic!("Expected error"), } @@ -749,12 +769,7 @@ pub(crate) mod tests { let response = JsonResponse(serde_json::json!({ "hex": "foo", "complete": false })); match TryInto::<Transaction>::try_into(response) { Err(e) => { - assert_eq!(e.kind(), io::ErrorKind::InvalidData); - assert!(e - .get_ref() - .unwrap() - .to_string() - .contains("transaction couldn't be signed")); + assert!(e.contains("transaction couldn't be signed")); }, Ok(_) => panic!("Expected error"), } diff --git a/lightning-block-sync/src/http.rs b/lightning-block-sync/src/http.rs index 0fb82b4acde..f473849226f 100644 --- a/lightning-block-sync/src/http.rs +++ b/lightning-block-sync/src/http.rs @@ -1,399 +1,191 @@ //! Simple HTTP implementation which supports both async and traditional execution environments //! with minimal dependencies. This is used as the basis for REST and RPC clients. -use chunked_transfer; use serde_json; +#[cfg(feature = "tokio")] +use bitreq::RequestExt; + +use std::convert::Infallible; use std::convert::TryFrom; use std::fmt; -#[cfg(not(feature = "tokio"))] -use std::io::Write; -use std::net::{SocketAddr, ToSocketAddrs}; -use std::time::Duration; -#[cfg(feature = "tokio")] -use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt}; -#[cfg(feature = "tokio")] -use tokio::net::TcpStream; +/// Trait for converting parse errors into a String message. +pub trait ToParseErrorMessage { + /// Converts a parse error into a human-readable message. + fn to_parse_error_message(self) -> String; +} + +impl ToParseErrorMessage for Infallible { + fn to_parse_error_message(self) -> String { + match self {} + } +} -#[cfg(not(feature = "tokio"))] -use std::io::BufRead; -use std::io::Read; -#[cfg(not(feature = "tokio"))] -use std::net::TcpStream; +impl ToParseErrorMessage for () { + fn to_parse_error_message(self) -> String { + "invalid data".to_string() + } +} -/// Timeout for operations on TCP streams. -const TCP_STREAM_TIMEOUT: Duration = Duration::from_secs(5); +impl ToParseErrorMessage for &'static str { + fn to_parse_error_message(self) -> String { + self.to_string() + } +} -/// Timeout for reading the first byte of a response. This is separate from the general read -/// timeout as it is not uncommon for Bitcoin Core to be blocked waiting on UTXO cache flushes for -/// upwards of 10 minutes on slow devices (e.g. RPis with SSDs over USB). Note that we always retry -/// once when we time out, so the maximum time we allow Bitcoin Core to block for is twice this -/// value. -const TCP_STREAM_RESPONSE_TIMEOUT: Duration = Duration::from_secs(300); +impl ToParseErrorMessage for String { + fn to_parse_error_message(self) -> String { + self + } +} -/// Maximum HTTP message header size in bytes. -const MAX_HTTP_MESSAGE_HEADER_SIZE: usize = 8192; +/// Timeout for requests in seconds. This is set to a high value as it is not uncommon for Bitcoin +/// Core to be blocked waiting on UTXO cache flushes for upwards of 10 minutes on slow devices +/// (e.g. RPis with SSDs over USB). +const TCP_STREAM_RESPONSE_TIMEOUT: u64 = 300; /// Maximum HTTP message body size in bytes. Enough for a hex-encoded block in JSON format and any /// overhead for HTTP chunked transfer encoding. const MAX_HTTP_MESSAGE_BODY_SIZE: usize = 2 * 4_000_000 + 32_000; -/// Endpoint for interacting with an HTTP-based API. +/// Error type for HTTP client operations. #[derive(Debug)] -pub struct HttpEndpoint { - host: String, - port: Option<u16>, - path: String, +pub enum HttpClientError { + /// transport-level error (connection, timeout, protocol parsing, etc.) + Transport(bitreq::Error), + /// HTTP error response (non-2xx status code) + Http(HttpError), + /// Response parsing/conversion error + Parse(String), } -impl HttpEndpoint { - /// Creates an endpoint for the given host and default HTTP port. - pub fn for_host(host: String) -> Self { - Self { host, port: None, path: String::from("/") } - } - - /// Specifies a port to use with the endpoint. - pub fn with_port(mut self, port: u16) -> Self { - self.port = Some(port); - self - } - - /// Specifies a path to use with the endpoint. - pub fn with_path(mut self, path: String) -> Self { - self.path = path; - self - } - - /// Returns the endpoint host. - pub fn host(&self) -> &str { - &self.host +impl std::error::Error for HttpClientError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + HttpClientError::Transport(e) => Some(e), + HttpClientError::Http(e) => Some(e), + HttpClientError::Parse(_) => None, + } } +} - /// Returns the endpoint port. - pub fn port(&self) -> u16 { - match self.port { - None => 80, - Some(port) => port, +impl fmt::Display for HttpClientError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + HttpClientError::Transport(e) => write!(f, "transport error: {}", e), + HttpClientError::Http(e) => write!(f, "HTTP error: {}", e), + HttpClientError::Parse(e) => write!(f, "response parsing error: {}", e), } } +} - /// Returns the endpoint path. - pub fn path(&self) -> &str { - &self.path +impl From<bitreq::Error> for HttpClientError { + fn from(e: bitreq::Error) -> Self { + HttpClientError::Transport(e) } } -impl<'a> std::net::ToSocketAddrs for &'a HttpEndpoint { - type Iter = <(&'a str, u16) as std::net::ToSocketAddrs>::Iter; - - fn to_socket_addrs(&self) -> std::io::Result<Self::Iter> { - (self.host(), self.port()).to_socket_addrs() +impl From<HttpError> for HttpClientError { + fn from(e: HttpError) -> Self { + HttpClientError::Http(e) } } +/// Maximum number of cached connections in the connection pool. +#[cfg(feature = "tokio")] +const MAX_CONNECTIONS: usize = 10; + /// Client for making HTTP requests. pub(crate) struct HttpClient { - address: SocketAddr, - stream: TcpStream, + base_url: String, + #[cfg(feature = "tokio")] + client: bitreq::Client, } impl HttpClient { - /// Opens a connection to an HTTP endpoint. - pub fn connect<E: ToSocketAddrs>(endpoint: E) -> std::io::Result<Self> { - let address = match endpoint.to_socket_addrs()?.next() { - None => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "could not resolve to any addresses", - )); - }, - Some(address) => address, - }; - let stream = std::net::TcpStream::connect_timeout(&address, TCP_STREAM_TIMEOUT)?; - stream.set_read_timeout(Some(TCP_STREAM_TIMEOUT))?; - stream.set_write_timeout(Some(TCP_STREAM_TIMEOUT))?; - - #[cfg(feature = "tokio")] - let stream = { - stream.set_nonblocking(true)?; - TcpStream::from_std(stream)? - }; - - Ok(Self { address, stream }) + /// Creates a new HTTP client for the given base URL. + /// + /// The base URL should include the scheme, host, and port (e.g., "http://127.0.0.1:8332"). + /// DNS resolution is deferred until the first request is made. + pub fn new(base_url: String) -> Self { + Self { + base_url, + #[cfg(feature = "tokio")] + client: bitreq::Client::new(MAX_CONNECTIONS), + } } - /// Sends a `GET` request for a resource identified by `uri` at the `host`. + /// Sends a `GET` request for a resource identified by `uri`. /// /// Returns the response body in `F` format. #[allow(dead_code)] - pub async fn get<F>(&mut self, uri: &str, host: &str) -> std::io::Result<F> + pub async fn get<F>(&self, uri: &str) -> Result<F, HttpClientError> where - F: TryFrom<Vec<u8>, Error = std::io::Error>, + F: TryFrom<Vec<u8>>, + <F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage, { - let request = format!( - "GET {} HTTP/1.1\r\n\ - Host: {}\r\n\ - Connection: keep-alive\r\n\ - \r\n", - uri, host - ); - let response_body = self.send_request_with_retry(&request).await?; - F::try_from(response_body) + let url = format!("{}{}", self.base_url, uri); + let request = bitreq::get(url) + .with_timeout(TCP_STREAM_RESPONSE_TIMEOUT) + .with_max_body_size(Some(MAX_HTTP_MESSAGE_BODY_SIZE)); + #[cfg(feature = "tokio")] + let request = request.with_pipelining(); + let response_body = self.send_request(request).await?; + F::try_from(response_body).map_err(|e| HttpClientError::Parse(e.to_parse_error_message())) } - /// Sends a `POST` request for a resource identified by `uri` at the `host` using the given HTTP + /// Sends a `POST` request for a resource identified by `uri` using the given HTTP /// authentication credentials. /// /// The request body consists of the provided JSON `content`. Returns the response body in `F` /// format. #[allow(dead_code)] pub async fn post<F>( - &mut self, uri: &str, host: &str, auth: &str, content: serde_json::Value, - ) -> std::io::Result<F> + &self, uri: &str, auth: &str, content: serde_json::Value, + ) -> Result<F, HttpClientError> where - F: TryFrom<Vec<u8>, Error = std::io::Error>, + F: TryFrom<Vec<u8>>, + <F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage, { - let content = content.to_string(); - let request = format!( - "POST {} HTTP/1.1\r\n\ - Host: {}\r\n\ - Authorization: {}\r\n\ - Connection: keep-alive\r\n\ - Content-Type: application/json\r\n\ - Content-Length: {}\r\n\ - \r\n\ - {}", - uri, - host, - auth, - content.len(), - content - ); - let response_body = self.send_request_with_retry(&request).await?; - F::try_from(response_body) - } - - /// Sends an HTTP request message and reads the response, returning its body. Attempts to - /// reconnect and retry if the connection has been closed. - async fn send_request_with_retry(&mut self, request: &str) -> std::io::Result<Vec<u8>> { - match self.send_request(request).await { - Ok(bytes) => Ok(bytes), - Err(_) => { - // Reconnect and retry on fail. This can happen if the connection was closed after - // the keep-alive limits are reached, or generally if the request timed out due to - // Bitcoin Core being stuck on a long-running operation or its RPC queue being - // full. - // Block 100ms before retrying the request as in many cases the source of the error - // may be persistent for some time. - #[cfg(feature = "tokio")] - tokio::time::sleep(Duration::from_millis(100)).await; - #[cfg(not(feature = "tokio"))] - std::thread::sleep(Duration::from_millis(100)); - *self = Self::connect(self.address)?; - self.send_request(request).await - }, - } - } - - /// Sends an HTTP request message and reads the response, returning its body. - async fn send_request(&mut self, request: &str) -> std::io::Result<Vec<u8>> { - self.write_request(request).await?; - self.read_response().await - } - - /// Writes an HTTP request message. - async fn write_request(&mut self, request: &str) -> std::io::Result<()> { + let url = format!("{}{}", self.base_url, uri); + let request = bitreq::post(url) + .with_header("Authorization", auth) + .with_header("Content-Type", "application/json") + .with_timeout(TCP_STREAM_RESPONSE_TIMEOUT) + .with_max_body_size(Some(MAX_HTTP_MESSAGE_BODY_SIZE)) + .with_body(content.to_string()); #[cfg(feature = "tokio")] - { - self.stream.write_all(request.as_bytes()).await?; - self.stream.flush().await - } - #[cfg(not(feature = "tokio"))] - { - self.stream.write_all(request.as_bytes())?; - self.stream.flush() - } + let request = request.with_pipelining(); + let response_body = self.send_request(request).await?; + F::try_from(response_body).map_err(|e| HttpClientError::Parse(e.to_parse_error_message())) } - /// Reads an HTTP response message. - async fn read_response(&mut self) -> std::io::Result<Vec<u8>> { - #[cfg(feature = "tokio")] - let stream = self.stream.split().0; - #[cfg(not(feature = "tokio"))] - let stream = std::io::Read::by_ref(&mut self.stream); - - let limited_stream = stream.take(MAX_HTTP_MESSAGE_HEADER_SIZE as u64); - + /// Sends an HTTP request message and reads the response, returning its body. + async fn send_request(&self, request: bitreq::Request) -> Result<Vec<u8>, HttpClientError> { #[cfg(feature = "tokio")] - let mut reader = tokio::io::BufReader::new(limited_stream); + let response = request.send_async_with_client(&self.client).await?; #[cfg(not(feature = "tokio"))] - let mut reader = std::io::BufReader::new(limited_stream); - - macro_rules! read_line { - () => { - read_line!(0) - }; - ($retry_count: expr) => {{ - let mut line = String::new(); - let mut timeout_count: u64 = 0; - let bytes_read = loop { - #[cfg(feature = "tokio")] - let read_res = reader.read_line(&mut line).await; - #[cfg(not(feature = "tokio"))] - let read_res = reader.read_line(&mut line); - match read_res { - Ok(bytes_read) => break bytes_read, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { - timeout_count += 1; - if timeout_count > $retry_count { - return Err(e); - } else { - continue; - } - }, - Err(e) => return Err(e), - } - }; - - match bytes_read { - 0 => None, - _ => { - // Remove trailing CRLF - if line.ends_with('\n') { - line.pop(); - if line.ends_with('\r') { - line.pop(); - } - } - Some(line) - }, - } - }}; - } - - // Read and parse status line - // Note that we allow retrying a few times to reach TCP_STREAM_RESPONSE_TIMEOUT. - let status_line = - read_line!(TCP_STREAM_RESPONSE_TIMEOUT.as_secs() / TCP_STREAM_TIMEOUT.as_secs()) - .ok_or(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "no status line"))?; - let status = HttpStatus::parse(&status_line)?; - - // Read and parse relevant headers - let mut message_length = HttpMessageLength::Empty; - loop { - let line = read_line!() - .ok_or(std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "no headers"))?; - if line.is_empty() { - break; - } - - let header = HttpHeader::parse(&line)?; - if header.has_name("Content-Length") { - let length = header - .value - .parse() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - if let HttpMessageLength::Empty = message_length { - message_length = HttpMessageLength::ContentLength(length); - } - continue; - } - - if header.has_name("Transfer-Encoding") { - message_length = HttpMessageLength::TransferEncoding(header.value.into()); - continue; - } - } + let response = request.send()?; - // Read message body - let read_limit = MAX_HTTP_MESSAGE_BODY_SIZE - reader.buffer().len(); - reader.get_mut().set_limit(read_limit as u64); - let contents = match message_length { - HttpMessageLength::Empty => Vec::new(), - HttpMessageLength::ContentLength(length) => { - if length == 0 || length > MAX_HTTP_MESSAGE_BODY_SIZE { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("invalid response length: {} bytes", length), - )); - } else { - let mut content = vec![0; length]; - #[cfg(feature = "tokio")] - reader.read_exact(&mut content[..]).await?; - #[cfg(not(feature = "tokio"))] - reader.read_exact(&mut content[..])?; - content - } - }, - HttpMessageLength::TransferEncoding(coding) => { - if !coding.eq_ignore_ascii_case("chunked") { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "unsupported transfer coding", - )); - } else { - let mut content = Vec::new(); - #[cfg(feature = "tokio")] - { - // Since chunked_transfer doesn't have an async interface, only use it to - // determine the size of each chunk to read. - // - // TODO: Replace with an async interface when available. - // https://github.com/frewsxcv/rust-chunked-transfer/issues/7 - loop { - // Read the chunk header which contains the chunk size. - let mut chunk_header = String::new(); - reader.read_line(&mut chunk_header).await?; - if chunk_header == "0\r\n" { - // Read the terminator chunk since the decoder consumes the CRLF - // immediately when this chunk is encountered. - reader.read_line(&mut chunk_header).await?; - } - - // Decode the chunk header to obtain the chunk size. - let mut buffer = Vec::new(); - let mut decoder = - chunked_transfer::Decoder::new(chunk_header.as_bytes()); - decoder.read_to_end(&mut buffer)?; - - // Read the chunk body. - let chunk_size = match decoder.remaining_chunks_size() { - None => break, - Some(chunk_size) => chunk_size, - }; - let chunk_offset = content.len(); - content.resize(chunk_offset + chunk_size + "\r\n".len(), 0); - reader.read_exact(&mut content[chunk_offset..]).await?; - content.resize(chunk_offset + chunk_size, 0); - } - content - } - #[cfg(not(feature = "tokio"))] - { - let mut decoder = chunked_transfer::Decoder::new(reader); - decoder.read_to_end(&mut content)?; - content - } - } - }, - }; + let status_code = response.status_code; + let body = response.into_bytes(); - if !status.is_ok() { - // TODO: Handle 3xx redirection responses. - let error = HttpError { status_code: status.code.to_string(), contents }; - return Err(std::io::Error::new(std::io::ErrorKind::Other, error)); + if !(200..300).contains(&status_code) { + return Err(HttpError { status_code, contents: body }.into()); } - Ok(contents) + Ok(body) } } /// HTTP error consisting of a status code and body contents. #[derive(Debug)] -pub(crate) struct HttpError { - pub(crate) status_code: String, - pub(crate) contents: Vec<u8>, +pub struct HttpError { + /// The HTTP status code. + pub status_code: i32, + /// The response body contents. + pub contents: Vec<u8>, } impl std::error::Error for HttpError {} @@ -405,94 +197,6 @@ impl fmt::Display for HttpError { } } -/// HTTP response status code as defined by [RFC 7231]. -/// -/// [RFC 7231]: https://tools.ietf.org/html/rfc7231#section-6 -struct HttpStatus<'a> { - code: &'a str, -} - -impl<'a> HttpStatus<'a> { - /// Parses an HTTP status line as defined by [RFC 7230]. - /// - /// [RFC 7230]: https://tools.ietf.org/html/rfc7230#section-3.1.2 - fn parse(line: &'a String) -> std::io::Result<HttpStatus<'a>> { - let mut tokens = line.splitn(3, ' '); - - let http_version = tokens - .next() - .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "no HTTP-Version"))?; - if !http_version.eq_ignore_ascii_case("HTTP/1.1") - && !http_version.eq_ignore_ascii_case("HTTP/1.0") - { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid HTTP-Version", - )); - } - - let code = tokens - .next() - .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "no Status-Code"))?; - if code.len() != 3 || !code.chars().all(|c| c.is_ascii_digit()) { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "invalid Status-Code", - )); - } - - let _reason = tokens - .next() - .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "no Reason-Phrase"))?; - - Ok(Self { code }) - } - - /// Returns whether the status is successful (i.e., 2xx status class). - fn is_ok(&self) -> bool { - self.code.starts_with('2') - } -} - -/// HTTP response header as defined by [RFC 7231]. -/// -/// [RFC 7231]: https://tools.ietf.org/html/rfc7231#section-7 -struct HttpHeader<'a> { - name: &'a str, - value: &'a str, -} - -impl<'a> HttpHeader<'a> { - /// Parses an HTTP header field as defined by [RFC 7230]. - /// - /// [RFC 7230]: https://tools.ietf.org/html/rfc7230#section-3.2 - fn parse(line: &'a String) -> std::io::Result<HttpHeader<'a>> { - let mut tokens = line.splitn(2, ':'); - let name = tokens - .next() - .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "no header name"))?; - let value = tokens - .next() - .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidData, "no header value"))? - .trim_start(); - Ok(Self { name, value }) - } - - /// Returns whether the header field has the given name. - fn has_name(&self, name: &str) -> bool { - self.name.eq_ignore_ascii_case(name) - } -} - -/// HTTP message body length as defined by [RFC 7230]. -/// -/// [RFC 7230]: https://tools.ietf.org/html/rfc7230#section-3.3.3 -enum HttpMessageLength { - Empty, - ContentLength(usize), - TransferEncoding(String), -} - /// An HTTP response body in binary format. pub struct BinaryResponse(pub Vec<u8>); @@ -501,98 +205,61 @@ pub struct JsonResponse(pub serde_json::Value); /// Interprets bytes from an HTTP response body as binary data. impl TryFrom<Vec<u8>> for BinaryResponse { - type Error = std::io::Error; + type Error = Infallible; - fn try_from(bytes: Vec<u8>) -> std::io::Result<Self> { + fn try_from(bytes: Vec<u8>) -> Result<Self, Infallible> { Ok(BinaryResponse(bytes)) } } /// Interprets bytes from an HTTP response body as a JSON value. impl TryFrom<Vec<u8>> for JsonResponse { - type Error = std::io::Error; - - fn try_from(bytes: Vec<u8>) -> std::io::Result<Self> { - Ok(JsonResponse(serde_json::from_slice(&bytes)?)) - } -} - -#[cfg(test)] -mod endpoint_tests { - use super::HttpEndpoint; - - #[test] - fn with_default_port() { - let endpoint = HttpEndpoint::for_host("foo.com".into()); - assert_eq!(endpoint.host(), "foo.com"); - assert_eq!(endpoint.port(), 80); - } - - #[test] - fn with_custom_port() { - let endpoint = HttpEndpoint::for_host("foo.com".into()).with_port(8080); - assert_eq!(endpoint.host(), "foo.com"); - assert_eq!(endpoint.port(), 8080); - } - - #[test] - fn with_uri_path() { - let endpoint = HttpEndpoint::for_host("foo.com".into()).with_path("/path".into()); - assert_eq!(endpoint.host(), "foo.com"); - assert_eq!(endpoint.path(), "/path"); - } + type Error = String; - #[test] - fn without_uri_path() { - let endpoint = HttpEndpoint::for_host("foo.com".into()); - assert_eq!(endpoint.host(), "foo.com"); - assert_eq!(endpoint.path(), "/"); - } - - #[test] - fn convert_to_socket_addrs() { - let endpoint = HttpEndpoint::for_host("localhost".into()); - let host = endpoint.host(); - let port = endpoint.port(); - - use std::net::ToSocketAddrs; - match (&endpoint).to_socket_addrs() { - Err(e) => panic!("Unexpected error: {:?}", e), - Ok(socket_addrs) => { - let mut std_addrs = (host, port).to_socket_addrs().unwrap(); - for addr in socket_addrs { - assert_eq!(addr, std_addrs.next().unwrap()); - } - assert!(std_addrs.next().is_none()); - }, - } + fn try_from(bytes: Vec<u8>) -> Result<Self, String> { + serde_json::from_slice(&bytes).map(JsonResponse).map_err(|e| e.to_string()) } } #[cfg(test)] pub(crate) mod client_tests { use super::*; - use std::io::BufRead; - use std::io::Write; + use std::io::{BufRead, Read, Write}; + use std::time::Duration; /// Server for handling HTTP client requests with a stock response. pub struct HttpServer { address: std::net::SocketAddr, - handler: std::thread::JoinHandle<()>, + handler: Option<std::thread::JoinHandle<()>>, shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>, } + impl Drop for HttpServer { + fn drop(&mut self) { + self.shutdown.store(true, std::sync::atomic::Ordering::SeqCst); + // Make a connection to unblock the listener's accept() call + let _ = std::net::TcpStream::connect(self.address); + if let Some(handler) = self.handler.take() { + let _ = handler.join(); + } + } + } + /// Body of HTTP response messages. pub enum MessageBody<T: ToString> { Empty, Content(T), - ChunkedContent(T), } impl HttpServer { fn responding_with_body<T: ToString>(status: &str, body: MessageBody<T>) -> Self { let response = match body { - MessageBody::Empty => format!("{}\r\n\r\n", status), + MessageBody::Empty => format!( + "{}\r\n\ + Content-Length: 0\r\n\ + \r\n", + status + ), MessageBody::Content(body) => { let body = body.to_string(); format!( @@ -605,22 +272,6 @@ pub(crate) mod client_tests { body ) }, - MessageBody::ChunkedContent(body) => { - let mut chuncked_body = Vec::new(); - { - use chunked_transfer::Encoder; - let mut encoder = Encoder::with_chunks_size(&mut chuncked_body, 8); - encoder.write_all(body.to_string().as_bytes()).unwrap(); - } - format!( - "{}\r\n\ - Transfer-Encoding: chunked\r\n\ - \r\n\ - {}", - status, - String::from_utf8(chuncked_body).unwrap() - ) - }, }; HttpServer::responding_with(response) } @@ -645,179 +296,90 @@ pub(crate) mod client_tests { let shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let shutdown_signaled = std::sync::Arc::clone(&shutdown); let handler = std::thread::spawn(move || { + let timeout = Duration::from_secs(5); for stream in listener.incoming() { - let mut stream = stream.unwrap(); - stream.set_write_timeout(Some(TCP_STREAM_TIMEOUT)).unwrap(); - - let lines_read = std::io::BufReader::new(&stream) - .lines() - .take_while(|line| !line.as_ref().unwrap().is_empty()) - .count(); - if lines_read == 0 { - continue; + if shutdown_signaled.load(std::sync::atomic::Ordering::SeqCst) { + return; } - for chunk in response.as_bytes().chunks(16) { + let stream = stream.unwrap(); + stream.set_write_timeout(Some(timeout)).unwrap(); + stream.set_read_timeout(Some(timeout)).unwrap(); + + let mut reader = std::io::BufReader::new(stream); + + // Handle multiple requests on the same connection (keep-alive) + loop { if shutdown_signaled.load(std::sync::atomic::Ordering::SeqCst) { return; - } else { - if let Err(_) = stream.write(chunk) { + } + + // Read request headers + let mut lines_read = 0; + let mut content_length: usize = 0; + loop { + let mut line = String::new(); + match reader.read_line(&mut line) { + Ok(0) => break, // eof + Ok(_) => { + if line == "\r\n" || line == "\n" { + break; // end of headers + } + // Parse content_length for POST body handling + if let Some(value) = line.strip_prefix("Content-Length:") { + content_length = value.trim().parse().unwrap_or(0); + } + lines_read += 1; + }, + Err(_) => break, // Read error or timeout + } + } + + if lines_read == 0 { + break; // No request received, connection closed + } + + // Consume request body if present (needed for POST keep-alive) + if content_length > 0 { + let mut body = vec![0u8; content_length]; + if reader.read_exact(&mut body).is_err() { break; } - if let Err(_) = stream.flush() { + } + + // Send response + let stream = reader.get_mut(); + let mut write_error = false; + for chunk in response.as_bytes().chunks(16) { + if shutdown_signaled.load(std::sync::atomic::Ordering::SeqCst) { + return; + } + if stream.write(chunk).is_err() || stream.flush().is_err() { + write_error = true; break; } } + if write_error { + break; + } } } }); - Self { address, handler, shutdown } - } - - fn shutdown(self) { - self.shutdown.store(true, std::sync::atomic::Ordering::SeqCst); - self.handler.join().unwrap(); - } - - pub fn endpoint(&self) -> HttpEndpoint { - HttpEndpoint::for_host(self.address.ip().to_string()).with_port(self.address.port()) - } - } - - #[test] - fn connect_to_unresolvable_host() { - match HttpClient::connect(("example.invalid", 80)) { - Err(e) => { - assert!( - e.to_string().contains("failed to lookup address information") - || e.to_string().contains("No such host"), - "{:?}", - e - ); - }, - Ok(_) => panic!("Expected error"), - } - } - - #[test] - fn connect_with_no_socket_address() { - match HttpClient::connect(&vec![][..]) { - Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::InvalidInput), - Ok(_) => panic!("Expected error"), - } - } - - #[test] - fn connect_with_unknown_server() { - // get an unused port by binding to port 0 - let port = { - let t = std::net::TcpListener::bind(("127.0.0.1", 0)).unwrap(); - t.local_addr().unwrap().port() - }; - - match HttpClient::connect(("::", port)) { - #[cfg(target_os = "windows")] - Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::AddrNotAvailable), - #[cfg(not(target_os = "windows"))] - Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::ConnectionRefused), - Ok(_) => panic!("Expected error"), - } - } - - #[tokio::test] - async fn connect_with_valid_endpoint() { - let server = HttpServer::responding_with_ok::<String>(MessageBody::Empty); - - match HttpClient::connect(&server.endpoint()) { - Err(e) => panic!("Unexpected error: {:?}", e), - Ok(_) => {}, - } - } - - #[tokio::test] - async fn read_empty_message() { - let server = HttpServer::responding_with("".to_string()); - - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof); - assert_eq!(e.get_ref().unwrap().to_string(), "no status line"); - }, - Ok(_) => panic!("Expected error"), + Self { address, handler: Some(handler), shutdown } } - } - #[tokio::test] - async fn read_incomplete_message() { - let server = HttpServer::responding_with("HTTP/1.1 200 OK".to_string()); - - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof); - assert_eq!(e.get_ref().unwrap().to_string(), "no headers"); - }, - Ok(_) => panic!("Expected error"), + pub fn endpoint(&self) -> String { + format!("http://{}:{}", self.address.ip(), self.address.port()) } } #[tokio::test] - async fn read_too_large_message_headers() { - let response = format!( - "HTTP/1.1 302 Found\r\n\ - Location: {}\r\n\ - \r\n", - "Z".repeat(MAX_HTTP_MESSAGE_HEADER_SIZE) - ); - let server = HttpServer::responding_with(response); - - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof); - assert_eq!(e.get_ref().unwrap().to_string(), "no headers"); - }, - Ok(_) => panic!("Expected error"), - } - } - - #[tokio::test] - async fn read_too_large_message_body() { - let body = "Z".repeat(MAX_HTTP_MESSAGE_BODY_SIZE + 1); - let server = HttpServer::responding_with_ok::<String>(MessageBody::Content(body)); - - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); - assert_eq!( - e.get_ref().unwrap().to_string(), - "invalid response length: 8032001 bytes" - ); - }, - Ok(_) => panic!("Expected error"), - } - server.shutdown(); - } - - #[tokio::test] - async fn read_message_with_unsupported_transfer_coding() { - let response = String::from( - "HTTP/1.1 200 OK\r\n\ - Transfer-Encoding: gzip\r\n\ - \r\n\ - foobar", - ); - let server = HttpServer::responding_with(response); - - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::InvalidInput); - assert_eq!(e.get_ref().unwrap().to_string(), "unsupported transfer coding"); - }, + async fn connect_with_invalid_host() { + let client = HttpClient::new("http://invalid.host.example:80".to_string()); + match client.get::<JsonResponse>("/foo").await { + Err(HttpClientError::Transport(_)) => {}, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } @@ -826,50 +388,25 @@ pub(crate) mod client_tests { async fn read_error() { let server = HttpServer::responding_with_server_error("foo"); - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<JsonResponse>("/foo", "foo.com").await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::Other); - let http_error = e.into_inner().unwrap().downcast::<HttpError>().unwrap(); - assert_eq!(http_error.status_code, "500"); + let client = HttpClient::new(server.endpoint()); + match client.get::<JsonResponse>("/foo").await { + Err(HttpClientError::Http(http_error)) => { + assert_eq!(http_error.status_code, 500); assert_eq!(http_error.contents, "foo".as_bytes()); }, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } #[tokio::test] - async fn read_empty_message_body() { - let server = HttpServer::responding_with_ok::<String>(MessageBody::Empty); - - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { - Err(e) => panic!("Unexpected error: {:?}", e), - Ok(bytes) => assert_eq!(bytes.0, Vec::<u8>::new()), - } - } - - #[tokio::test] - async fn read_message_body_with_length() { + async fn read_message_body() { let body = "foo bar baz qux".repeat(32); let content = MessageBody::Content(body.clone()); let server = HttpServer::responding_with_ok::<String>(content); - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { - Err(e) => panic!("Unexpected error: {:?}", e), - Ok(bytes) => assert_eq!(bytes.0, body.as_bytes()), - } - } - - #[tokio::test] - async fn read_chunked_message_body() { - let body = "foo bar baz qux".repeat(32); - let chunked_content = MessageBody::ChunkedContent(body.clone()); - let server = HttpServer::responding_with_ok::<String>(chunked_content); - - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - match client.get::<BinaryResponse>("/foo", "foo.com").await { + let client = HttpClient::new(server.endpoint()); + match client.get::<BinaryResponse>("/foo").await { Err(e) => panic!("Unexpected error: {:?}", e), Ok(bytes) => assert_eq!(bytes.0, body.as_bytes()), } @@ -879,9 +416,9 @@ pub(crate) mod client_tests { async fn reconnect_closed_connection() { let server = HttpServer::responding_with_ok::<String>(MessageBody::Empty); - let mut client = HttpClient::connect(&server.endpoint()).unwrap(); - assert!(client.get::<BinaryResponse>("/foo", "foo.com").await.is_ok()); - match client.get::<BinaryResponse>("/foo", "foo.com").await { + let client = HttpClient::new(server.endpoint()); + assert!(client.get::<BinaryResponse>("/foo").await.is_ok()); + match client.get::<BinaryResponse>("/foo").await { Err(e) => panic!("Unexpected error: {:?}", e), Ok(bytes) => assert_eq!(bytes.0, Vec::<u8>::new()), } diff --git a/lightning-block-sync/src/init.rs b/lightning-block-sync/src/init.rs index a870f8ca88c..b41489e0a28 100644 --- a/lightning-block-sync/src/init.rs +++ b/lightning-block-sync/src/init.rs @@ -1,15 +1,15 @@ //! Utilities to assist in the initial sync required to initialize or reload Rust-Lightning objects //! from disk. -use crate::poll::{ChainPoller, Validate, ValidatedBlockHeader}; -use crate::{BlockSource, BlockSourceResult, Cache, ChainNotifier}; +use crate::async_poll::{MultiResultFuturePoller, ResultFuture}; +use crate::poll::{ChainPoller, Poll, Validate, ValidatedBlockHeader}; +use crate::{BlockData, BlockSource, BlockSourceResult, ChainNotifier, HeaderCache}; use bitcoin::block::Header; -use bitcoin::hash_types::BlockHash; use bitcoin::network::Network; use lightning::chain; -use lightning::chain::BestBlock; +use lightning::chain::BlockLocator; use std::ops::Deref; @@ -32,19 +32,21 @@ where /// Performs a one-time sync of chain listeners using a single *trusted* block source, bringing each /// listener's view of the chain from its paired block hash to `block_source`'s best chain tip. /// -/// Upon success, the returned header can be used to initialize [`SpvClient`]. In the case of -/// failure, each listener may be left at a different block hash than the one it was originally -/// paired with. +/// Upon success, the returned header and header cache can be used to initialize [`SpvClient`]. In +/// the case of failure, *each listener may be left at a different block hash than the one it was +/// originally paired with*. +/// +/// Thus, in case of errors you likely need to reload each object via deserialization or check its +/// current tip directly via accessors on the object before trying again. /// /// Useful during startup to bring the [`ChannelManager`] and each [`ChannelMonitor`] in sync before /// switching to [`SpvClient`]. For example: /// /// ``` -/// use bitcoin::hash_types::BlockHash; /// use bitcoin::network::Network; /// /// use lightning::chain; -/// use lightning::chain::Watch; +/// use lightning::chain::{BlockLocator, Watch}; /// use lightning::chain::chainmonitor; /// use lightning::chain::chainmonitor::ChainMonitor; /// use lightning::chain::channelmonitor::ChannelMonitor; @@ -89,14 +91,14 @@ where /// logger: &L, /// persister: &P, /// ) { -/// // Read a serialized channel monitor paired with the block hash when it was persisted. +/// // Read a serialized channel monitor paired with the best block when it was persisted. /// let serialized_monitor = "..."; -/// let (monitor_block_hash, mut monitor) = <(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>::read( +/// let (monitor_best_block, mut monitor) = <(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>::read( /// &mut Cursor::new(&serialized_monitor), (entropy_source, signer_provider)).unwrap(); /// -/// // Read the channel manager paired with the block hash when it was persisted. +/// // Read the channel manager paired with the best block when it was persisted. /// let serialized_manager = "..."; -/// let (manager_block_hash, mut manager) = { +/// let (manager_best_block, mut manager) = { /// let read_args = ChannelManagerReadArgs::new( /// entropy_source, /// node_signer, @@ -110,19 +112,18 @@ where /// config, /// vec![&mut monitor], /// ); -/// <(BlockHash, ChannelManager<&ChainMonitor<SP::EcdsaSigner, &C, &T, &F, &L, &P, &ES>, &T, &ES, &NS, &SP, &F, &R, &MR, &L>)>::read( +/// <(BlockLocator, ChannelManager<&ChainMonitor<SP::EcdsaSigner, &C, &T, &F, &L, &P, &ES>, &T, &ES, &NS, &SP, &F, &R, &MR, &L>)>::read( /// &mut Cursor::new(&serialized_manager), read_args).unwrap() /// }; /// /// // Synchronize any channel monitors and the channel manager to be on the best block. -/// let mut cache = UnboundedCache::new(); /// let mut monitor_listener = (monitor, &*tx_broadcaster, &*fee_estimator, &*logger); /// let listeners = vec![ -/// (monitor_block_hash, &monitor_listener as &dyn chain::Listen), -/// (manager_block_hash, &manager as &dyn chain::Listen), +/// (monitor_best_block, &monitor_listener as &dyn chain::Listen), +/// (manager_best_block, &manager as &dyn chain::Listen), /// ]; -/// let chain_tip = init::synchronize_listeners( -/// block_source, Network::Bitcoin, &mut cache, listeners).await.unwrap(); +/// let (chain_cache, chain_tip) = init::synchronize_listeners( +/// block_source, Network::Bitcoin, listeners).await.unwrap(); /// /// // Allow the chain monitor to watch any channels. /// let monitor = monitor_listener.0; @@ -131,94 +132,104 @@ where /// // Create an SPV client to notify the chain monitor and channel manager of block events. /// let chain_poller = poll::ChainPoller::new(block_source, Network::Bitcoin); /// let mut chain_listener = (chain_monitor, &manager); -/// let spv_client = SpvClient::new(chain_tip, chain_poller, &mut cache, &chain_listener); +/// let spv_client = SpvClient::new(chain_tip, chain_poller, chain_cache, &chain_listener); /// } /// ``` /// /// [`SpvClient`]: crate::SpvClient /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager /// [`ChannelMonitor`]: lightning::chain::channelmonitor::ChannelMonitor -pub async fn synchronize_listeners< - B: Deref + Sized + Send + Sync, - C: Cache, - L: chain::Listen + ?Sized, ->( - block_source: B, network: Network, header_cache: &mut C, - mut chain_listeners: Vec<(BlockHash, &L)>, -) -> BlockSourceResult<ValidatedBlockHeader> +pub async fn synchronize_listeners<B: Deref + Sized + Send + Sync, L: chain::Listen + ?Sized>( + block_source: B, network: Network, mut chain_listeners: Vec<(BlockLocator, &L)>, +) -> BlockSourceResult<(HeaderCache, ValidatedBlockHeader)> where B::Target: BlockSource, { let best_header = validate_best_block_header(&*block_source).await?; - // Fetch the header for the block hash paired with each listener. - let mut chain_listeners_with_old_headers = Vec::new(); - for (old_block_hash, chain_listener) in chain_listeners.drain(..) { - let old_header = match header_cache.look_up(&old_block_hash) { - Some(header) => *header, - None => { - block_source.get_header(&old_block_hash, None).await?.validate(old_block_hash)? - }, - }; - chain_listeners_with_old_headers.push((old_header, chain_listener)) - } - // Find differences and disconnect blocks for each listener individually. let mut chain_poller = ChainPoller::new(block_source, network); let mut chain_listeners_at_height = Vec::new(); - let mut most_common_ancestor = None; let mut most_connected_blocks = Vec::new(); - for (old_header, chain_listener) in chain_listeners_with_old_headers.drain(..) { + let mut header_cache = HeaderCache::new(); + header_cache.retain_on_disconnect = true; + for (old_best_block, chain_listener) in chain_listeners.drain(..) { // Disconnect any stale blocks, but keep them in the cache for the next iteration. - let header_cache = &mut ReadOnlyCache(header_cache); let (common_ancestor, connected_blocks) = { let chain_listener = &DynamicChainListener(chain_listener); - let mut chain_notifier = ChainNotifier { header_cache, chain_listener }; - let difference = - chain_notifier.find_difference(best_header, &old_header, &mut chain_poller).await?; - chain_notifier.disconnect_blocks(difference.disconnected_blocks); + let mut chain_notifier = + ChainNotifier { header_cache: &mut header_cache, chain_listener }; + let difference = chain_notifier + .find_difference_from_best_block(best_header, old_best_block, &mut chain_poller) + .await?; + if difference.common_ancestor.block_hash != old_best_block.block_hash { + chain_notifier.disconnect_blocks(difference.common_ancestor); + } (difference.common_ancestor, difference.connected_blocks) }; // Keep track of the most common ancestor and all blocks connected across all listeners. chain_listeners_at_height.push((common_ancestor.height, chain_listener)); if connected_blocks.len() > most_connected_blocks.len() { - most_common_ancestor = Some(common_ancestor); most_connected_blocks = connected_blocks; } } - // Connect new blocks for all listeners at once to avoid re-fetching blocks. - if let Some(common_ancestor) = most_common_ancestor { - let chain_listener = &ChainListenerSet(chain_listeners_at_height); - let mut chain_notifier = ChainNotifier { header_cache, chain_listener }; - chain_notifier - .connect_blocks(common_ancestor, most_connected_blocks, &mut chain_poller) - .await - .map_err(|(e, _)| e)?; - } - - Ok(best_header) -} - -/// A wrapper to make a cache read-only. -/// -/// Used to prevent losing headers that may be needed to disconnect blocks common to more than one -/// listener. -struct ReadOnlyCache<'a, C: Cache>(&'a mut C); - -impl<'a, C: Cache> Cache for ReadOnlyCache<'a, C> { - fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader> { - self.0.look_up(block_hash) - } + while !most_connected_blocks.is_empty() { + #[cfg(not(test))] + const MAX_BLOCKS_AT_ONCE: usize = 6 * 6; // Six hours of blocks, 144MiB encoded + #[cfg(test)] + const MAX_BLOCKS_AT_ONCE: usize = 2; + + let mut fetch_block_futures = + Vec::with_capacity(core::cmp::min(MAX_BLOCKS_AT_ONCE, most_connected_blocks.len())); + for header in most_connected_blocks.iter().rev().take(MAX_BLOCKS_AT_ONCE) { + let fetch_future = chain_poller.fetch_block(header); + fetch_block_futures + .push(ResultFuture::Pending(Box::pin(async move { (header, fetch_future.await) }))); + } + let results = MultiResultFuturePoller::new(fetch_block_futures).await.into_iter(); + + const NO_BLOCK: Option<(u32, crate::poll::ValidatedBlock)> = None; + let mut fetched_blocks = [NO_BLOCK; MAX_BLOCKS_AT_ONCE]; + for ((header, block_res), result) in results.into_iter().zip(fetched_blocks.iter_mut()) { + let block = block_res?; + header_cache.block_connected(header.block_hash, *header); + *result = Some((header.height, block)); + } + debug_assert!(fetched_blocks.iter().take(most_connected_blocks.len()).all(|r| r.is_some())); + // TODO: When our MSRV is 1.82, use is_sorted_by_key + debug_assert!(fetched_blocks.windows(2).all(|blocks| { + if let (Some(a), Some(b)) = (&blocks[0], &blocks[1]) { + a.0 < b.0 + } else { + // Any non-None blocks have to come before any None entries + blocks[1].is_none() + } + })); + + for (listener_height, listener) in chain_listeners_at_height.iter() { + // Connect blocks for this listener. + for (height, block_data) in fetched_blocks.iter().flatten() { + if *height > *listener_height { + match &**block_data { + BlockData::FullBlock(block) => { + listener.block_connected(&block, *height); + }, + BlockData::HeaderOnly(header_data) => { + listener.filtered_block_connected(&header_data, &[], *height); + }, + } + } + } + } - fn block_connected(&mut self, _block_hash: BlockHash, _block_header: ValidatedBlockHeader) { - unreachable!() + most_connected_blocks + .truncate(most_connected_blocks.len().saturating_sub(MAX_BLOCKS_AT_ONCE)); } - fn block_disconnected(&mut self, _block_hash: &BlockHash) -> Option<ValidatedBlockHeader> { - None - } + header_cache.retain_on_disconnect = false; + Ok((header_cache, best_header)) } /// Wrapper for supporting dynamically sized chain listeners. @@ -231,38 +242,11 @@ impl<'a, L: chain::Listen + ?Sized> chain::Listen for DynamicChainListener<'a, L unreachable!() } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { self.0.blocks_disconnected(fork_point) } } -/// A set of dynamically sized chain listeners, each paired with a starting block height. -struct ChainListenerSet<'a, L: chain::Listen + ?Sized>(Vec<(u32, &'a L)>); - -impl<'a, L: chain::Listen + ?Sized> chain::Listen for ChainListenerSet<'a, L> { - fn block_connected(&self, block: &bitcoin::Block, height: u32) { - for (starting_height, chain_listener) in self.0.iter() { - if height > *starting_height { - chain_listener.block_connected(block, height); - } - } - } - - fn filtered_block_connected( - &self, header: &Header, txdata: &chain::transaction::TransactionData, height: u32, - ) { - for (starting_height, chain_listener) in self.0.iter() { - if height > *starting_height { - chain_listener.filtered_block_connected(header, txdata, height); - } - } - } - - fn blocks_disconnected(&self, _fork_point: BestBlock) { - unreachable!() - } -} - #[cfg(test)] mod tests { use super::*; @@ -282,13 +266,18 @@ mod tests { let listener_3 = MockChainListener::new().expect_block_connected(*chain.at_height(4)); let listeners = vec![ - (chain.at_height(1).block_hash, &listener_1 as &dyn chain::Listen), - (chain.at_height(2).block_hash, &listener_2 as &dyn chain::Listen), - (chain.at_height(3).block_hash, &listener_3 as &dyn chain::Listen), + (chain.block_locator_at_height(1), &listener_1 as &dyn chain::Listen), + (chain.block_locator_at_height(2), &listener_2 as &dyn chain::Listen), + (chain.block_locator_at_height(3), &listener_3 as &dyn chain::Listen), ]; - let mut cache = chain.header_cache(0..=4); - match synchronize_listeners(&chain, Network::Bitcoin, &mut cache, listeners).await { - Ok(header) => assert_eq!(header, chain.tip()), + match synchronize_listeners(&chain, Network::Bitcoin, listeners).await { + Ok((cache, header)) => { + assert_eq!(header, chain.tip()); + assert!(cache.look_up(&chain.at_height(1).block_hash).is_some()); + assert!(cache.look_up(&chain.at_height(2).block_hash).is_some()); + assert!(cache.look_up(&chain.at_height(3).block_hash).is_some()); + assert!(cache.look_up(&chain.at_height(4).block_hash).is_some()); + }, Err(e) => panic!("Unexpected error: {:?}", e), } } @@ -314,15 +303,20 @@ mod tests { .expect_block_connected(*main_chain.at_height(4)); let listeners = vec![ - (fork_chain_1.tip().block_hash, &listener_1 as &dyn chain::Listen), - (fork_chain_2.tip().block_hash, &listener_2 as &dyn chain::Listen), - (fork_chain_3.tip().block_hash, &listener_3 as &dyn chain::Listen), + (fork_chain_1.best_block(), &listener_1 as &dyn chain::Listen), + (fork_chain_2.best_block(), &listener_2 as &dyn chain::Listen), + (fork_chain_3.best_block(), &listener_3 as &dyn chain::Listen), ]; - let mut cache = fork_chain_1.header_cache(2..=4); - cache.extend(fork_chain_2.header_cache(3..=4)); - cache.extend(fork_chain_3.header_cache(4..=4)); - match synchronize_listeners(&main_chain, Network::Bitcoin, &mut cache, listeners).await { - Ok(header) => assert_eq!(header, main_chain.tip()), + match synchronize_listeners(&main_chain, Network::Bitcoin, listeners).await { + Ok((cache, header)) => { + assert_eq!(header, main_chain.tip()); + assert!(cache.look_up(&main_chain.at_height(1).block_hash).is_some()); + assert!(cache.look_up(&main_chain.at_height(2).block_hash).is_some()); + assert!(cache.look_up(&main_chain.at_height(3).block_hash).is_some()); + assert!(cache.look_up(&fork_chain_1.at_height(2).block_hash).is_none()); + assert!(cache.look_up(&fork_chain_2.at_height(3).block_hash).is_none()); + assert!(cache.look_up(&fork_chain_3.at_height(4).block_hash).is_none()); + }, Err(e) => panic!("Unexpected error: {:?}", e), } } @@ -351,36 +345,20 @@ mod tests { .expect_block_connected(*main_chain.at_height(4)); let listeners = vec![ - (fork_chain_1.tip().block_hash, &listener_1 as &dyn chain::Listen), - (fork_chain_2.tip().block_hash, &listener_2 as &dyn chain::Listen), - (fork_chain_3.tip().block_hash, &listener_3 as &dyn chain::Listen), + (fork_chain_1.best_block(), &listener_1 as &dyn chain::Listen), + (fork_chain_2.best_block(), &listener_2 as &dyn chain::Listen), + (fork_chain_3.best_block(), &listener_3 as &dyn chain::Listen), ]; - let mut cache = fork_chain_1.header_cache(2..=4); - cache.extend(fork_chain_2.header_cache(3..=4)); - cache.extend(fork_chain_3.header_cache(4..=4)); - match synchronize_listeners(&main_chain, Network::Bitcoin, &mut cache, listeners).await { - Ok(header) => assert_eq!(header, main_chain.tip()), - Err(e) => panic!("Unexpected error: {:?}", e), - } - } - - #[tokio::test] - async fn cache_connected_and_keep_disconnected_blocks() { - let main_chain = Blockchain::default().with_height(2); - let fork_chain = main_chain.fork_at_height(1); - let new_tip = main_chain.tip(); - let old_tip = fork_chain.tip(); - - let listener = MockChainListener::new() - .expect_blocks_disconnected(*fork_chain.at_height(1)) - .expect_block_connected(*new_tip); - - let listeners = vec![(old_tip.block_hash, &listener as &dyn chain::Listen)]; - let mut cache = fork_chain.header_cache(2..=2); - match synchronize_listeners(&main_chain, Network::Bitcoin, &mut cache, listeners).await { - Ok(_) => { - assert!(cache.contains_key(&new_tip.block_hash)); - assert!(cache.contains_key(&old_tip.block_hash)); + match synchronize_listeners(&main_chain, Network::Bitcoin, listeners).await { + Ok((cache, header)) => { + assert_eq!(header, main_chain.tip()); + assert!(cache.look_up(&main_chain.at_height(1).block_hash).is_some()); + assert!(cache.look_up(&main_chain.at_height(2).block_hash).is_some()); + assert!(cache.look_up(&main_chain.at_height(3).block_hash).is_some()); + assert!(cache.look_up(&main_chain.at_height(4).block_hash).is_some()); + assert!(cache.look_up(&fork_chain_1.at_height(2).block_hash).is_none()); + assert!(cache.look_up(&fork_chain_1.at_height(3).block_hash).is_none()); + assert!(cache.look_up(&fork_chain_1.at_height(4).block_hash).is_none()); }, Err(e) => panic!("Unexpected error: {:?}", e), } diff --git a/lightning-block-sync/src/lib.rs b/lightning-block-sync/src/lib.rs index 02593047658..b5d76e3bd06 100644 --- a/lightning-block-sync/src/lib.rs +++ b/lightning-block-sync/src/lib.rs @@ -16,9 +16,11 @@ #![deny(rustdoc::broken_intra_doc_links)] #![deny(rustdoc::private_intra_doc_links)] #![deny(missing_docs)] -#![deny(unsafe_code)] #![cfg_attr(docsrs, feature(doc_cfg))] +extern crate alloc; +extern crate core; + #[cfg(any(feature = "rest-client", feature = "rpc-client"))] pub mod http; @@ -42,6 +44,9 @@ mod test_utils; #[cfg(any(feature = "rest-client", feature = "rpc-client"))] mod utils; +#[allow(unused)] +mod async_poll; + use crate::poll::{ChainTip, Poll, ValidatedBlockHeader}; use bitcoin::block::{Block, Header}; @@ -49,7 +54,7 @@ use bitcoin::hash_types::BlockHash; use bitcoin::pow::Work; use lightning::chain; -use lightning::chain::{BestBlock, Listen}; +use lightning::chain::BlockLocator; use std::future::Future; use std::ops::Deref; @@ -170,61 +175,78 @@ pub enum BlockData { /// sources for the best chain tip. During this process it detects any chain forks, determines which /// constitutes the best chain, and updates the listener accordingly with any blocks that were /// connected or disconnected since the last poll. -/// -/// Block headers for the best chain are maintained in the parameterized cache, allowing for a -/// custom cache eviction policy. This offers flexibility to those sensitive to resource usage. -/// Hence, there is a trade-off between a lower memory footprint and potentially increased network -/// I/O as headers are re-fetched during fork detection. -pub struct SpvClient<'a, P: Poll, C: Cache, L: Deref> +pub struct SpvClient<P: Poll, L: Deref> where L::Target: chain::Listen, { chain_tip: ValidatedBlockHeader, chain_poller: P, - chain_notifier: ChainNotifier<'a, C, L>, + header_cache: HeaderCache, + chain_listener: L, } -/// The `Cache` trait defines behavior for managing a block header cache, where block headers are -/// keyed by block hash. -/// -/// Used by [`ChainNotifier`] to store headers along the best chain, which is important for ensuring -/// that blocks can be disconnected if they are no longer accessible from a block source (e.g., if -/// the block source does not store stale forks indefinitely). +/// The maximum number of [`ValidatedBlockHeader`]s stored in a [`HeaderCache`]. +pub const HEADER_CACHE_LIMIT: u32 = 6 * 24 * 7; + +/// Bounded cache of block headers keyed by block hash. /// -/// Implementations may define how long to retain headers such that it's unlikely they will ever be -/// needed to disconnect a block. In cases where block sources provide access to headers on stale -/// forks reliably, caches may be entirely unnecessary. -pub trait Cache { +/// Retains only the latest [`HEADER_CACHE_LIMIT`] block headers based on height. +pub struct HeaderCache { + headers: std::collections::HashMap<BlockHash, ValidatedBlockHeader>, + /// When set, [`Self::blocks_disconnected`] will not evict headers above the fork point. + /// This is used during initial sync to retain headers across multiple listeners. + retain_on_disconnect: bool, +} + +impl HeaderCache { + /// Creates a new empty header cache. + pub fn new() -> Self { + Self { headers: std::collections::HashMap::new(), retain_on_disconnect: false } + } + /// Retrieves the block header keyed by the given block hash. - fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader>; + pub fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader> { + self.headers.get(block_hash) + } /// Called when a block has been connected to the best chain to ensure it is available to be /// disconnected later if needed. - fn block_connected(&mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader); - - /// Called when a block has been disconnected from the best chain. Once disconnected, a block's - /// header is no longer needed and thus can be removed. - fn block_disconnected(&mut self, block_hash: &BlockHash) -> Option<ValidatedBlockHeader>; -} - -/// Unbounded cache of block headers keyed by block hash. -pub type UnboundedCache = std::collections::HashMap<BlockHash, ValidatedBlockHeader>; - -impl Cache for UnboundedCache { - fn look_up(&self, block_hash: &BlockHash) -> Option<&ValidatedBlockHeader> { - self.get(block_hash) + pub(crate) fn block_connected( + &mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader, + ) { + self.headers.insert(block_hash, block_header); + + // Remove headers older than a week. + let cutoff_height = block_header.height.saturating_sub(HEADER_CACHE_LIMIT); + self.headers.retain(|_, header| header.height >= cutoff_height); } - fn block_connected(&mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader) { - self.insert(block_hash, block_header); + /// Inserts the given block header during a find_difference operation, implying it might not be + /// the best header. + pub(crate) fn insert_during_diff( + &mut self, block_hash: BlockHash, block_header: ValidatedBlockHeader, + ) { + self.headers.insert(block_hash, block_header); + + // Remove headers older than our newest header minus a week. + let best_height = self.headers.iter().map(|(_, header)| header.height).max().unwrap_or(0); + let cutoff_height = best_height.saturating_sub(HEADER_CACHE_LIMIT); + self.headers.retain(|_, header| header.height >= cutoff_height); } - fn block_disconnected(&mut self, block_hash: &BlockHash) -> Option<ValidatedBlockHeader> { - self.remove(block_hash) + /// Called when blocks have been disconnected from the best chain. Only the fork point + /// (best common ancestor) is provided. + /// + /// Once disconnected, unless [`Self::retain_on_disconnect`] is set, a block's header is no + /// longer needed and thus can be removed. + pub(crate) fn blocks_disconnected(&mut self, fork_point: &ValidatedBlockHeader) { + if !self.retain_on_disconnect { + self.headers.retain(|_, block_info| block_info.height <= fork_point.height); + } } } -impl<'a, P: Poll, C: Cache, L: Deref> SpvClient<'a, P, C, L> +impl<P: Poll, L: Deref> SpvClient<P, L> where L::Target: chain::Listen, { @@ -239,11 +261,10 @@ where /// /// [`poll_best_tip`]: SpvClient::poll_best_tip pub fn new( - chain_tip: ValidatedBlockHeader, chain_poller: P, header_cache: &'a mut C, + chain_tip: ValidatedBlockHeader, chain_poller: P, header_cache: HeaderCache, chain_listener: L, ) -> Self { - let chain_notifier = ChainNotifier { header_cache, chain_listener }; - Self { chain_tip, chain_poller, chain_notifier } + Self { chain_tip, chain_poller, header_cache, chain_listener } } /// Polls for the best tip and updates the chain listener with any connected or disconnected @@ -272,8 +293,11 @@ where /// Updates the chain tip, syncing the chain listener with any connected or disconnected /// blocks. Returns whether there were any such blocks. async fn update_chain_tip(&mut self, best_chain_tip: ValidatedBlockHeader) -> bool { - match self - .chain_notifier + let mut chain_notifier = ChainNotifier { + header_cache: &mut self.header_cache, + chain_listener: &*self.chain_listener, + }; + match chain_notifier .synchronize_listener(best_chain_tip, &self.chain_tip, &mut self.chain_poller) .await { @@ -293,15 +317,12 @@ where /// Notifies [listeners] of blocks that have been connected or disconnected from the chain. /// /// [listeners]: lightning::chain::Listen -pub struct ChainNotifier<'a, C: Cache, L: Deref> -where - L::Target: chain::Listen, -{ +pub(crate) struct ChainNotifier<'a, L: chain::Listen + ?Sized> { /// Cache for looking up headers before fetching from a block source. - header_cache: &'a mut C, + pub(crate) header_cache: &'a mut HeaderCache, /// Listener that will be notified of connected or disconnected blocks. - chain_listener: L, + pub(crate) chain_listener: &'a L, } /// Changes made to the chain between subsequent polls that transformed it from having one chain tip @@ -315,17 +336,11 @@ struct ChainDifference { /// If there are any disconnected blocks, this is where the chain forked. common_ancestor: ValidatedBlockHeader, - /// Blocks that were disconnected from the chain since the last poll. - disconnected_blocks: Vec<ValidatedBlockHeader>, - /// Blocks that were connected to the chain since the last poll. connected_blocks: Vec<ValidatedBlockHeader>, } -impl<'a, C: Cache, L: Deref> ChainNotifier<'a, C, L> -where - L::Target: chain::Listen, -{ +impl<'a, L: chain::Listen + ?Sized> ChainNotifier<'a, L> { /// Finds the first common ancestor between `new_header` and `old_header`, disconnecting blocks /// from `old_header` to get to that point and then connecting blocks until `new_header`. /// @@ -338,23 +353,71 @@ where chain_poller: &mut P, ) -> Result<(), (BlockSourceError, Option<ValidatedBlockHeader>)> { let difference = self - .find_difference(new_header, old_header, chain_poller) + .find_difference_from_header(new_header, old_header, chain_poller) .await .map_err(|e| (e, None))?; - self.disconnect_blocks(difference.disconnected_blocks); + if difference.common_ancestor != *old_header { + self.disconnect_blocks(difference.common_ancestor); + } self.connect_blocks(difference.common_ancestor, difference.connected_blocks, chain_poller) .await } + /// Returns the changes needed to produce the chain with `current_header` as its tip from the + /// chain with `prev_best_block` as its tip. + /// + /// First resolves `prev_best_block` to a `ValidatedBlockHeader` using the `previous_blocks` + /// field as fallback if needed, then finds the common ancestor. + /// + /// Updates the header cache as it goes, tracking headers needed to find the diff to reuse for + /// other objects that might need similar headers. + async fn find_difference_from_best_block<P: Poll>( + &mut self, current_header: ValidatedBlockHeader, prev_best_block: BlockLocator, + chain_poller: &mut P, + ) -> BlockSourceResult<ChainDifference> { + // Try to resolve the header for the previous best block. First try the block_hash, + // then fall back to previous_blocks if that fails. + let cur_tip = core::iter::once((0, &prev_best_block.block_hash)); + let prev_tips = + prev_best_block.previous_blocks.iter().enumerate().filter_map(|(idx, hash_opt)| { + if let Some(block_hash) = hash_opt { + Some((idx as u32 + 1, block_hash)) + } else { + None + } + }); + let mut found_header = None; + for (height_diff, block_hash) in cur_tip.chain(prev_tips) { + if let Some(header) = self.header_cache.look_up(block_hash) { + found_header = Some(*header); + break; + } + let height = prev_best_block.height.checked_sub(height_diff).ok_or( + BlockSourceError::persistent( + "BlockLocator had more previous_blocks than its height", + ), + )?; + if let Ok(header) = chain_poller.get_header(block_hash, Some(height)).await { + found_header = Some(header); + self.header_cache.insert_during_diff(*block_hash, header); + break; + } + } + let found_header = found_header.ok_or_else(|| { + BlockSourceError::persistent("could not resolve any block from BlockLocator") + })?; + + self.find_difference_from_header(current_header, &found_header, chain_poller).await + } + /// Returns the changes needed to produce the chain with `current_header` as its tip from the /// chain with `prev_header` as its tip. /// /// Walks backwards from `current_header` and `prev_header`, finding the common ancestor. - async fn find_difference<P: Poll>( + async fn find_difference_from_header<P: Poll>( &self, current_header: ValidatedBlockHeader, prev_header: &ValidatedBlockHeader, chain_poller: &mut P, ) -> BlockSourceResult<ChainDifference> { - let mut disconnected_blocks = Vec::new(); let mut connected_blocks = Vec::new(); let mut current = current_header; let mut previous = *prev_header; @@ -369,7 +432,6 @@ where let current_height = current.height; let previous_height = previous.height; if current_height <= previous_height { - disconnected_blocks.push(previous); previous = self.look_up_previous_header(chain_poller, &previous).await?; } if current_height >= previous_height { @@ -379,7 +441,7 @@ where } let common_ancestor = current; - Ok(ChainDifference { common_ancestor, disconnected_blocks, connected_blocks }) + Ok(ChainDifference { common_ancestor, connected_blocks }) } /// Returns the previous header for the given header, either by looking it up in the cache or @@ -394,16 +456,10 @@ where } /// Notifies the chain listeners of disconnected blocks. - fn disconnect_blocks(&mut self, disconnected_blocks: Vec<ValidatedBlockHeader>) { - for header in disconnected_blocks.iter() { - if let Some(cached_header) = self.header_cache.block_disconnected(&header.block_hash) { - assert_eq!(cached_header, *header); - } - } - if let Some(block) = disconnected_blocks.last() { - let fork_point = BestBlock::new(block.header.prev_blockhash, block.height - 1); - self.chain_listener.blocks_disconnected(fork_point); - } + fn disconnect_blocks(&mut self, fork_point: ValidatedBlockHeader) { + self.header_cache.blocks_disconnected(&fork_point); + let best_block = BlockLocator::new(fork_point.block_hash, fork_point.height); + self.chain_listener.blocks_disconnected(best_block); } /// Notifies the chain listeners of connected blocks. @@ -447,9 +503,9 @@ mod spv_client_tests { let best_tip = chain.at_height(1); let poller = poll::ChainPoller::new(&mut chain, Network::Testnet); - let mut cache = UnboundedCache::new(); + let cache = HeaderCache::new(); let mut listener = NullChainListener {}; - let mut client = SpvClient::new(best_tip, poller, &mut cache, &mut listener); + let mut client = SpvClient::new(best_tip, poller, cache, &mut listener); match client.poll_best_tip().await { Err(e) => { assert_eq!(e.kind(), BlockSourceErrorKind::Persistent); @@ -466,9 +522,9 @@ mod spv_client_tests { let common_tip = chain.tip(); let poller = poll::ChainPoller::new(&mut chain, Network::Testnet); - let mut cache = UnboundedCache::new(); + let cache = HeaderCache::new(); let mut listener = NullChainListener {}; - let mut client = SpvClient::new(common_tip, poller, &mut cache, &mut listener); + let mut client = SpvClient::new(common_tip, poller, cache, &mut listener); match client.poll_best_tip().await { Err(e) => panic!("Unexpected error: {:?}", e), Ok((chain_tip, blocks_connected)) => { @@ -486,9 +542,9 @@ mod spv_client_tests { let old_tip = chain.at_height(1); let poller = poll::ChainPoller::new(&mut chain, Network::Testnet); - let mut cache = UnboundedCache::new(); + let cache = HeaderCache::new(); let mut listener = NullChainListener {}; - let mut client = SpvClient::new(old_tip, poller, &mut cache, &mut listener); + let mut client = SpvClient::new(old_tip, poller, cache, &mut listener); match client.poll_best_tip().await { Err(e) => panic!("Unexpected error: {:?}", e), Ok((chain_tip, blocks_connected)) => { @@ -506,9 +562,9 @@ mod spv_client_tests { let old_tip = chain.at_height(1); let poller = poll::ChainPoller::new(&mut chain, Network::Testnet); - let mut cache = UnboundedCache::new(); + let cache = HeaderCache::new(); let mut listener = NullChainListener {}; - let mut client = SpvClient::new(old_tip, poller, &mut cache, &mut listener); + let mut client = SpvClient::new(old_tip, poller, cache, &mut listener); match client.poll_best_tip().await { Err(e) => panic!("Unexpected error: {:?}", e), Ok((chain_tip, blocks_connected)) => { @@ -526,9 +582,9 @@ mod spv_client_tests { let old_tip = chain.at_height(1); let poller = poll::ChainPoller::new(&mut chain, Network::Testnet); - let mut cache = UnboundedCache::new(); + let cache = HeaderCache::new(); let mut listener = NullChainListener {}; - let mut client = SpvClient::new(old_tip, poller, &mut cache, &mut listener); + let mut client = SpvClient::new(old_tip, poller, cache, &mut listener); match client.poll_best_tip().await { Err(e) => panic!("Unexpected error: {:?}", e), Ok((chain_tip, blocks_connected)) => { @@ -547,9 +603,9 @@ mod spv_client_tests { let worse_tip = chain.tip(); let poller = poll::ChainPoller::new(&mut chain, Network::Testnet); - let mut cache = UnboundedCache::new(); + let cache = HeaderCache::new(); let mut listener = NullChainListener {}; - let mut client = SpvClient::new(best_tip, poller, &mut cache, &mut listener); + let mut client = SpvClient::new(best_tip, poller, cache, &mut listener); match client.poll_best_tip().await { Err(e) => panic!("Unexpected error: {:?}", e), Ok((chain_tip, blocks_connected)) => { diff --git a/lightning-block-sync/src/poll.rs b/lightning-block-sync/src/poll.rs index 13e0403c3b6..5637be174cc 100644 --- a/lightning-block-sync/src/poll.rs +++ b/lightning-block-sync/src/poll.rs @@ -4,7 +4,7 @@ use crate::{BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSour use bitcoin::hash_types::BlockHash; use bitcoin::network::Network; -use lightning::chain::BestBlock; +use lightning::chain::BlockLocator; use std::future::Future; use std::ops::Deref; @@ -31,6 +31,11 @@ pub trait Poll { fn fetch_block<'a>( &'a self, header: &'a ValidatedBlockHeader, ) -> impl Future<Output = BlockSourceResult<ValidatedBlock>> + Send + 'a; + + /// Returns the header for a given hash and optional height hint. + fn get_header<'a>( + &'a self, block_hash: &'a BlockHash, height_hint: Option<u32>, + ) -> impl Future<Output = BlockSourceResult<ValidatedBlockHeader>> + Send + 'a; } /// A chain tip relative to another chain tip in terms of block hash and chainwork. @@ -155,7 +160,7 @@ impl ValidatedBlockHeader { Ok(()) } - /// Returns the [`BestBlock`] corresponding to this validated block header, which can be passed + /// Returns the [`BlockLocator`] corresponding to this validated block header, which can be passed /// into [`ChannelManager::new`] as part of its [`ChainParameters`]. Useful for ensuring that /// the [`SpvClient`] and [`ChannelManager`] are initialized to the same block during a fresh /// start. @@ -164,8 +169,8 @@ impl ValidatedBlockHeader { /// [`ChainParameters`]: lightning::ln::channelmanager::ChainParameters /// [`ChannelManager`]: lightning::ln::channelmanager::ChannelManager /// [`ChannelManager::new`]: lightning::ln::channelmanager::ChannelManager::new - pub fn to_best_block(&self) -> BestBlock { - BestBlock::new(self.block_hash, self.inner.height) + pub fn to_block_locator(&self) -> BlockLocator { + BlockLocator::new(self.block_hash, self.inner.height) } } @@ -258,6 +263,14 @@ impl<B: Deref<Target = T> + Sized + Send + Sync, T: BlockSource + ?Sized> Poll ) -> impl Future<Output = BlockSourceResult<ValidatedBlock>> + Send + 'a { async move { self.block_source.get_block(&header.block_hash).await?.validate(header.block_hash) } } + + fn get_header<'a>( + &'a self, block_hash: &'a BlockHash, height_hint: Option<u32>, + ) -> impl Future<Output = BlockSourceResult<ValidatedBlockHeader>> + Send + 'a { + Box::pin(async move { + self.block_source.get_header(block_hash, height_hint).await?.validate(*block_hash) + }) + } } #[cfg(test)] diff --git a/lightning-block-sync/src/rest.rs b/lightning-block-sync/src/rest.rs index 619981bb4d0..cdcf8424d2a 100644 --- a/lightning-block-sync/src/rest.rs +++ b/lightning-block-sync/src/rest.rs @@ -3,7 +3,7 @@ use crate::convert::GetUtxosResponse; use crate::gossip::UtxoSource; -use crate::http::{BinaryResponse, HttpClient, HttpEndpoint, JsonResponse}; +use crate::http::{BinaryResponse, HttpClient, HttpClientError, JsonResponse, ToParseErrorMessage}; use crate::{BlockData, BlockHeaderData, BlockSource, BlockSourceResult}; use bitcoin::hash_types::BlockHash; @@ -12,38 +12,30 @@ use bitcoin::OutPoint; use std::convert::TryFrom; use std::convert::TryInto; use std::future::Future; -use std::sync::Mutex; /// A simple REST client for requesting resources using HTTP `GET`. pub struct RestClient { - endpoint: HttpEndpoint, - client: Mutex<Option<HttpClient>>, + client: HttpClient, } impl RestClient { /// Creates a new REST client connected to the given endpoint. /// - /// The endpoint should contain the REST path component (e.g., http://127.0.0.1:8332/rest). - pub fn new(endpoint: HttpEndpoint) -> Self { - Self { endpoint, client: Mutex::new(None) } + /// The base URL should include the REST path component (e.g., "http://127.0.0.1:8332/rest"). + pub fn new(base_url: String) -> Self { + Self { client: HttpClient::new(base_url) } } /// Requests a resource encoded in `F` format and interpreted as type `T`. - pub async fn request_resource<F, T>(&self, resource_path: &str) -> std::io::Result<T> + pub async fn request_resource<F, T>(&self, resource_path: &str) -> Result<T, HttpClientError> where - F: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error>, + F: TryFrom<Vec<u8>> + TryInto<T>, + <F as TryFrom<Vec<u8>>>::Error: ToParseErrorMessage, + <F as TryInto<T>>::Error: ToParseErrorMessage, { - let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port()); - let uri = format!("{}/{}", self.endpoint.path().trim_end_matches("/"), resource_path); - let reserved_client = self.client.lock().unwrap().take(); - let mut client = if let Some(client) = reserved_client { - client - } else { - HttpClient::connect(&self.endpoint)? - }; - let res = client.get::<F>(&uri, &host).await?.try_into(); - *self.client.lock().unwrap() = Some(client); - res + let uri = format!("/{}", resource_path); + let response = self.client.get::<F>(&uri).await?; + response.try_into().map_err(|e| HttpClientError::Parse(e.to_parse_error_message())) } } @@ -102,21 +94,15 @@ impl UtxoSource for RestClient { mod tests { use super::*; use crate::http::client_tests::{HttpServer, MessageBody}; - use crate::http::BinaryResponse; use bitcoin::hashes::Hash; /// Parses binary data as a string-encoded `u32`. impl TryInto<u32> for BinaryResponse { - type Error = std::io::Error; - - fn try_into(self) -> std::io::Result<u32> { - match std::str::from_utf8(&self.0) { - Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)), - Ok(s) => match u32::from_str_radix(s, 10) { - Err(e) => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)), - Ok(n) => Ok(n), - }, - } + type Error = String; + + fn try_into(self) -> Result<u32, String> { + let s = std::str::from_utf8(&self.0).map_err(|e| e.to_string())?; + u32::from_str_radix(s, 10).map_err(|e| e.to_string()) } } @@ -126,7 +112,8 @@ mod tests { let client = RestClient::new(server.endpoint()); match client.request_resource::<BinaryResponse, u32>("/").await { - Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other), + Err(HttpClientError::Http(e)) => assert_eq!(e.status_code, 404), + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } @@ -137,7 +124,8 @@ mod tests { let client = RestClient::new(server.endpoint()); match client.request_resource::<BinaryResponse, u32>("/").await { - Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::InvalidData), + Err(HttpClientError::Parse(_)) => {}, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } diff --git a/lightning-block-sync/src/rpc.rs b/lightning-block-sync/src/rpc.rs index d851ba2ccf0..bfa1b31c84a 100644 --- a/lightning-block-sync/src/rpc.rs +++ b/lightning-block-sync/src/rpc.rs @@ -2,14 +2,12 @@ //! endpoint. use crate::gossip::UtxoSource; -use crate::http::{HttpClient, HttpEndpoint, HttpError, JsonResponse}; +use crate::http::{HttpClient, HttpClientError, JsonResponse, ToParseErrorMessage}; use crate::{BlockData, BlockHeaderData, BlockSource, BlockSourceResult}; use bitcoin::hash_types::BlockHash; use bitcoin::OutPoint; -use std::sync::Mutex; - use serde_json; use std::convert::TryFrom; @@ -36,14 +34,56 @@ impl fmt::Display for RpcError { impl Error for RpcError {} +/// Error type for RPC client operations. +#[derive(Debug)] +pub enum RpcClientError { + /// An HTTP client error (transport or HTTP error). + Http(HttpClientError), + /// An RPC error returned by the server. + Rpc(RpcError), + /// Invalid data in the response. + InvalidData(String), +} + +impl std::error::Error for RpcClientError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + RpcClientError::Http(e) => Some(e), + RpcClientError::Rpc(e) => Some(e), + RpcClientError::InvalidData(_) => None, + } + } +} + +impl fmt::Display for RpcClientError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + RpcClientError::Http(e) => write!(f, "HTTP error: {}", e), + RpcClientError::Rpc(e) => write!(f, "{}", e), + RpcClientError::InvalidData(msg) => write!(f, "invalid data: {}", msg), + } + } +} + +impl From<HttpClientError> for RpcClientError { + fn from(e: HttpClientError) -> Self { + RpcClientError::Http(e) + } +} + +impl From<RpcError> for RpcClientError { + fn from(e: RpcError) -> Self { + RpcClientError::Rpc(e) + } +} + /// A simple RPC client for calling methods using HTTP `POST`. /// /// Implements [`BlockSource`] and may return an `Err` containing [`RpcError`]. See /// [`RpcClient::call_method`] for details. pub struct RpcClient { basic_auth: String, - endpoint: HttpEndpoint, - client: Mutex<Option<HttpClient>>, + client: HttpClient, id: AtomicUsize, } @@ -51,85 +91,65 @@ impl RpcClient { /// Creates a new RPC client connected to the given endpoint with the provided credentials. The /// credentials should be a base64 encoding of a user name and password joined by a colon, as is /// required for HTTP basic access authentication. - pub fn new(credentials: &str, endpoint: HttpEndpoint) -> Self { + /// + /// The base URL should include the scheme, host, and port (e.g., "http://127.0.0.1:8332"). + pub fn new(credentials: &str, base_url: String) -> Self { Self { basic_auth: "Basic ".to_string() + credentials, - endpoint, - client: Mutex::new(None), + client: HttpClient::new(base_url), id: AtomicUsize::new(0), } } /// Calls a method with the response encoded in JSON format and interpreted as type `T`. - /// - /// When an `Err` is returned, [`std::io::Error::into_inner`] may contain an [`RpcError`] if - /// [`std::io::Error::kind`] is [`std::io::ErrorKind::Other`]. pub async fn call_method<T>( &self, method: &str, params: &[serde_json::Value], - ) -> std::io::Result<T> + ) -> Result<T, RpcClientError> where - JsonResponse: TryFrom<Vec<u8>, Error = std::io::Error> + TryInto<T, Error = std::io::Error>, + JsonResponse: TryInto<T>, + <JsonResponse as TryInto<T>>::Error: ToParseErrorMessage, { - let host = format!("{}:{}", self.endpoint.host(), self.endpoint.port()); - let uri = self.endpoint.path(); let content = serde_json::json!({ "method": method, "params": params, "id": &self.id.fetch_add(1, Ordering::AcqRel).to_string() }); - let reserved_client = self.client.lock().unwrap().take(); - let mut client = if let Some(client) = reserved_client { - client - } else { - HttpClient::connect(&self.endpoint)? - }; - let http_response = - client.post::<JsonResponse>(&uri, &host, &self.basic_auth, content).await; - *self.client.lock().unwrap() = Some(client); + let http_response = self.client.post::<JsonResponse>("/", &self.basic_auth, content).await; let mut response = match http_response { Ok(JsonResponse(response)) => response, - Err(e) if e.kind() == std::io::ErrorKind::Other => { - match e.get_ref().unwrap().downcast_ref::<HttpError>() { - Some(http_error) => match JsonResponse::try_from(http_error.contents.clone()) { - Ok(JsonResponse(response)) => response, - Err(_) => Err(e)?, - }, - None => Err(e)?, + Err(HttpClientError::Http(http_error)) => { + // Try to parse the error body as JSON-RPC response + match JsonResponse::try_from(http_error.contents.clone()) { + Ok(JsonResponse(response)) => response, + Err(_) => return Err(HttpClientError::Http(http_error).into()), } }, - Err(e) => Err(e)?, + Err(e) => return Err(e.into()), }; if !response.is_object() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "expected JSON object", - )); + return Err(RpcClientError::InvalidData("expected JSON object".to_string())); } let error = &response["error"]; if !error.is_null() { - // TODO: Examine error code for a more precise std::io::ErrorKind. let rpc_error = RpcError { code: error["code"].as_i64().unwrap_or(-1), message: error["message"].as_str().unwrap_or("unknown error").to_string(), }; - return Err(std::io::Error::new(std::io::ErrorKind::Other, rpc_error)); + return Err(rpc_error.into()); } let result = match response.get_mut("result") { Some(result) => result.take(), - None => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "expected JSON result", - )) - }, + None => return Err(RpcClientError::InvalidData("expected JSON result".to_string())), }; - JsonResponse(result).try_into() + JsonResponse(result) + .try_into() + .map_err(|e| RpcClientError::InvalidData(e.to_parse_error_message())) } } @@ -196,11 +216,11 @@ mod tests { /// Converts a JSON value into `u64`. impl TryInto<u64> for JsonResponse { - type Error = std::io::Error; + type Error = &'static str; - fn try_into(self) -> std::io::Result<u64> { + fn try_into(self) -> Result<u64, &'static str> { match self.0.as_u64() { - None => Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "not a number")), + None => Err("not a number"), Some(n) => Ok(n), } } @@ -212,7 +232,10 @@ mod tests { let client = RpcClient::new(CREDENTIALS, server.endpoint()); match client.call_method::<u64>("getblockcount", &[]).await { - Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::Other), + Err(RpcClientError::Http(HttpClientError::Http(e))) => { + assert_eq!(e.status_code, 404); + }, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } @@ -224,10 +247,10 @@ mod tests { let client = RpcClient::new(CREDENTIALS, server.endpoint()); match client.call_method::<u64>("getblockcount", &[]).await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON object"); + Err(RpcClientError::InvalidData(msg)) => { + assert_eq!(msg, "expected JSON object"); }, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } @@ -242,12 +265,11 @@ mod tests { let invalid_block_hash = serde_json::json!("foo"); match client.call_method::<u64>("getblock", &[invalid_block_hash]).await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::Other); - let rpc_error: Box<RpcError> = e.into_inner().unwrap().downcast().unwrap(); + Err(RpcClientError::Rpc(rpc_error)) => { assert_eq!(rpc_error.code, -8); assert_eq!(rpc_error.message, "invalid parameter"); }, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } @@ -259,10 +281,10 @@ mod tests { let client = RpcClient::new(CREDENTIALS, server.endpoint()); match client.call_method::<u64>("getblockcount", &[]).await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "expected JSON result"); + Err(RpcClientError::InvalidData(msg)) => { + assert_eq!(msg, "expected JSON result"); }, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } @@ -274,10 +296,10 @@ mod tests { let client = RpcClient::new(CREDENTIALS, server.endpoint()); match client.call_method::<u64>("getblockcount", &[]).await { - Err(e) => { - assert_eq!(e.kind(), std::io::ErrorKind::InvalidData); - assert_eq!(e.get_ref().unwrap().to_string(), "not a number"); + Err(RpcClientError::InvalidData(msg)) => { + assert!(msg.contains("not a number")); }, + Err(e) => panic!("Unexpected error type: {:?}", e), Ok(_) => panic!("Expected error"), } } diff --git a/lightning-block-sync/src/test_utils.rs b/lightning-block-sync/src/test_utils.rs index 40788e4d08c..20ed6f0545e 100644 --- a/lightning-block-sync/src/test_utils.rs +++ b/lightning-block-sync/src/test_utils.rs @@ -1,6 +1,6 @@ use crate::poll::{Validate, ValidatedBlockHeader}; use crate::{ - BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult, UnboundedCache, + BlockData, BlockHeaderData, BlockSource, BlockSourceError, BlockSourceResult, HeaderCache, }; use bitcoin::block::{Block, Header, Version}; @@ -12,7 +12,7 @@ use bitcoin::transaction; use bitcoin::Transaction; use lightning::chain; -use lightning::chain::BestBlock; +use lightning::chain::BlockLocator; use std::cell::RefCell; use std::collections::VecDeque; @@ -104,6 +104,18 @@ impl Blockchain { block_header.validate(block_hash).unwrap() } + pub fn block_locator_at_height(&self, height: usize) -> BlockLocator { + let mut previous_blocks = [None; 12]; + for (i, height) in (0..height).rev().take(12).enumerate() { + previous_blocks[i] = Some(self.blocks[height].block_hash()); + } + BlockLocator { + height: height as u32, + block_hash: self.blocks[height].block_hash(), + previous_blocks, + } + } + fn at_height_unvalidated(&self, height: usize) -> BlockHeaderData { assert!(!self.blocks.is_empty()); assert!(height < self.blocks.len()); @@ -123,16 +135,21 @@ impl Blockchain { self.at_height(self.blocks.len() - 1) } + pub fn best_block(&self) -> BlockLocator { + assert!(!self.blocks.is_empty()); + self.block_locator_at_height(self.blocks.len() - 1) + } + pub fn disconnect_tip(&mut self) -> Option<Block> { self.blocks.pop() } - pub fn header_cache(&self, heights: std::ops::RangeInclusive<usize>) -> UnboundedCache { - let mut cache = UnboundedCache::new(); + pub fn header_cache(&self, heights: std::ops::RangeInclusive<usize>) -> HeaderCache { + let mut cache = HeaderCache::new(); for i in heights { let value = self.at_height(i); let key = value.header.block_hash(); - assert!(cache.insert(key, value).is_none()); + cache.block_connected(key, value); } cache } @@ -206,7 +223,7 @@ impl chain::Listen for NullChainListener { &self, _header: &Header, _txdata: &chain::transaction::TransactionData, _height: u32, ) { } - fn blocks_disconnected(&self, _fork_point: BestBlock) {} + fn blocks_disconnected(&self, _fork_point: BlockLocator) {} } pub struct MockChainListener { @@ -267,7 +284,7 @@ impl chain::Listen for MockChainListener { } } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { match self.expected_blocks_disconnected.borrow_mut().pop_front() { None => { panic!( diff --git a/lightning-custom-message/src/lib.rs b/lightning-custom-message/src/lib.rs index 32d5a9e4389..06e57b47b84 100644 --- a/lightning-custom-message/src/lib.rs +++ b/lightning-custom-message/src/lib.rs @@ -312,13 +312,25 @@ macro_rules! composite_custom_message_handler { } fn peer_connected(&self, their_node_id: $crate::bitcoin::secp256k1::PublicKey, msg: &$crate::lightning::ln::msgs::Init, inbound: bool) -> Result<(), ()> { - let mut result = Ok(()); + // Per the `CustomMessageHandler::peer_connected` contract, `peer_disconnected` + // will not be called by `PeerManager` if we return `Err`. To avoid leaking + // per-peer state in sub-handlers that already returned `Ok` when a later one + // errors, record each sub-handler's result and roll back the successful ones + // ourselves before propagating the failure. $( - if let Err(e) = self.$field.peer_connected(their_node_id, msg, inbound) { - result = Err(e); - } + let $field = self.$field.peer_connected(their_node_id, msg, inbound); )* - result + let any_err = false $( || $field.is_err() )*; + if any_err { + $( + if $field.is_ok() { + self.$field.peer_disconnected(their_node_id); + } + )* + Err(()) + } else { + Ok(()) + } } fn provided_node_features(&self) -> $crate::lightning::types::features::NodeFeatures { @@ -346,7 +358,12 @@ macro_rules! composite_custom_message_handler { match message_type { $( $pattern => match <$type>::read(&self.$field, message_type, buffer)? { - None => unreachable!(), + // A sub-handler returns `None` for a `message_type` it doesn't + // recognize. The composite's pattern can be broader than the types + // the sub-handler decodes (e.g. a range), and `message_type` is + // peer-provided, so report the message as unknown rather than + // treating this as unreachable and panicking. + None => Ok(None), Some(message) => Ok(Some($message::$variant(message))), }, )* @@ -376,3 +393,208 @@ macro_rules! composite_custom_message_handler { } } } + +#[cfg(test)] +mod tests { + use bitcoin::secp256k1::PublicKey; + use core::sync::atomic::{AtomicUsize, Ordering}; + use lightning::io; + use lightning::ln::msgs::{DecodeError, Init, LightningError}; + use lightning::ln::peer_handler::CustomMessageHandler; + use lightning::ln::wire::{CustomMessageReader, Type}; + use lightning::types::features::{InitFeatures, NodeFeatures}; + use lightning::util::ser::{LengthLimitedRead, Writeable, Writer}; + + #[derive(Debug)] + pub struct Foo; + impl Type for Foo { + fn type_id(&self) -> u16 { + 32768 + } + } + impl Writeable for Foo { + fn write<W: Writer>(&self, _: &mut W) -> Result<(), io::Error> { + Ok(()) + } + } + + pub struct CountingHandler { + pub connect_count: AtomicUsize, + } + impl CustomMessageReader for CountingHandler { + type CustomMessage = Foo; + fn read<R: LengthLimitedRead>( + &self, _t: u16, _b: &mut R, + ) -> Result<Option<Foo>, DecodeError> { + Ok(None) + } + } + impl CustomMessageHandler for CountingHandler { + fn handle_custom_message(&self, _msg: Foo, _: PublicKey) -> Result<(), LightningError> { + Ok(()) + } + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Foo)> { + vec![] + } + fn peer_disconnected(&self, _: PublicKey) { + self.connect_count.fetch_sub(1, Ordering::SeqCst); + } + fn peer_connected(&self, _: PublicKey, _: &Init, _: bool) -> Result<(), ()> { + self.connect_count.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + fn provided_node_features(&self) -> NodeFeatures { + NodeFeatures::empty() + } + fn provided_init_features(&self, _: PublicKey) -> InitFeatures { + InitFeatures::empty() + } + } + + #[derive(Debug)] + pub struct Bar; + impl Type for Bar { + fn type_id(&self) -> u16 { + 32769 + } + } + impl Writeable for Bar { + fn write<W: Writer>(&self, _: &mut W) -> Result<(), io::Error> { + Ok(()) + } + } + + pub struct ErroringHandler; + impl CustomMessageReader for ErroringHandler { + type CustomMessage = Bar; + fn read<R: LengthLimitedRead>( + &self, _t: u16, _b: &mut R, + ) -> Result<Option<Bar>, DecodeError> { + Ok(None) + } + } + impl CustomMessageHandler for ErroringHandler { + fn handle_custom_message(&self, _msg: Bar, _: PublicKey) -> Result<(), LightningError> { + Ok(()) + } + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Bar)> { + vec![] + } + fn peer_disconnected(&self, _: PublicKey) { + debug_assert!(false); + } + fn peer_connected(&self, _: PublicKey, _: &Init, _: bool) -> Result<(), ()> { + Err(()) + } + fn provided_node_features(&self) -> NodeFeatures { + NodeFeatures::empty() + } + fn provided_init_features(&self, _: PublicKey) -> InitFeatures { + InitFeatures::empty() + } + } + + composite_custom_message_handler!( + pub struct CompositeHandler { + counting: CountingHandler, + erroring: ErroringHandler, + } + + pub enum CompositeMessage { + Foo(32768), + Bar(32769), + } + ); + + struct ReservedBlockHandler; + impl CustomMessageReader for ReservedBlockHandler { + type CustomMessage = Foo; + fn read<R: LengthLimitedRead>( + &self, message_type: u16, _b: &mut R, + ) -> Result<Option<Foo>, DecodeError> { + // This build defines only the message at 32768; the rest of the block its + // protocol reserved (32768..=32777) is for types future versions may add. + // A not-yet-defined type is unknown to this build, so per the + // `CustomMessageReader` contract it returns `Ok(None)` -- a newer peer can + // send one and this older node will treat it as an unknown message. + match message_type { + 32768 => Ok(Some(Foo)), + _ => Ok(None), + } + } + } + impl CustomMessageHandler for ReservedBlockHandler { + fn handle_custom_message(&self, _msg: Foo, _: PublicKey) -> Result<(), LightningError> { + Ok(()) + } + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Foo)> { + vec![] + } + fn peer_disconnected(&self, _: PublicKey) {} + fn peer_connected(&self, _: PublicKey, _: &Init, _: bool) -> Result<(), ()> { + Ok(()) + } + fn provided_node_features(&self) -> NodeFeatures { + NodeFeatures::empty() + } + fn provided_init_features(&self, _: PublicKey) -> InitFeatures { + InitFeatures::empty() + } + } + + composite_custom_message_handler!( + struct ReservedBlockComposite { + proto: ReservedBlockHandler, + } + + enum ReservedBlockMessage { + Proto(32768..=32777), + } + ); + + #[test] + fn read_treats_a_reserved_in_range_type_as_unknown() { + // A sub-handler may own a block of type ids (declared here as a range) yet only + // decode the subset its build defines, returning `Ok(None)` for reserved or + // not-yet-defined types in the block -- exactly what a node does on receiving a + // newer peer's message. `read` must surface that as an unknown message, not + // panic. + let composite = ReservedBlockComposite { proto: ReservedBlockHandler }; + let mut buffer: &[u8] = &[]; + // The message this build defines decodes to its variant. + assert!(matches!( + composite.read(32768, &mut buffer), + Ok(Some(ReservedBlockMessage::Proto(_))) + )); + // A reserved type from the same block is reported unknown, not panicked + // (pre-fix the matched arm hit `unreachable!()`). + assert!(matches!(composite.read(32770, &mut buffer), Ok(None))); + } + + #[test] + fn peer_connected_failure_does_not_leak_subhandler_state() { + let composite = CompositeHandler { + counting: CountingHandler { connect_count: AtomicUsize::new(0) }, + erroring: ErroringHandler, + }; + let pk_bytes = [ + 0x02, 0x79, 0xBE, 0x66, 0x7E, 0xF9, 0xDC, 0xBB, 0xAC, 0x55, 0xA0, 0x62, 0x95, 0xCE, + 0x87, 0x0B, 0x07, 0x02, 0x9B, 0xFC, 0xDB, 0x2D, 0xCE, 0x28, 0xD9, 0x59, 0xF2, 0x81, + 0x5B, 0x16, 0xF8, 0x17, 0x98, + ]; + let pk = PublicKey::from_slice(&pk_bytes).unwrap(); + let init = + Init { features: InitFeatures::empty(), networks: None, remote_network_address: None }; + + let result = composite.peer_connected(pk, &init, true); + assert!(result.is_err(), "Composite must propagate the inner Err"); + + let leaked = composite.counting.connect_count.load(Ordering::SeqCst); + assert_eq!( + leaked, 0, + "CountingHandler tracked {leaked} connected peer(s) after the composite \ + returned Err; this state will never be cleaned up because per the trait \ + contract peer_disconnected won't be called when peer_connected returns Err.", + ); + } +} diff --git a/lightning-dns-resolver/src/lib.rs b/lightning-dns-resolver/src/lib.rs index e9578844cf8..b2af1b8e942 100644 --- a/lightning-dns-resolver/src/lib.rs +++ b/lightning-dns-resolver/src/lib.rs @@ -9,12 +9,12 @@ use std::net::SocketAddr; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; -use dnssec_prover::query::build_txt_proof_async; +use dnssec_prover::query::{build_txt_proof_async, ProofBuildingError}; use lightning::blinded_path::message::DNSResolverContext; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::onion_message::dns_resolution::{ - DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, + DNSResolverMessage, DNSResolverMessageHandler, DNSSECError, DNSSECProof, DNSSECQuery, }; use lightning::onion_message::messenger::{ MessageSendInstructions, Responder, ResponseInstruction, @@ -103,6 +103,12 @@ impl<PH: DNSResolverMessageHandler> DNSResolverMessageHandler for OMDomainResolv } } + fn handle_dnssec_error(&self, error: DNSSECError, context: DNSResolverContext) { + if let Some(proof_handler) = &self.proof_handler { + proof_handler.handle_dnssec_error(error, context); + } + } + fn handle_dnssec_query( &self, q: DNSSECQuery, responder_opt: Option<Responder>, ) -> Option<(DNSResolverMessage, ResponseInstruction)> { @@ -121,12 +127,26 @@ impl<PH: DNSResolverMessageHandler> DNSResolverMessageHandler for OMDomainResolv } let us = Arc::clone(&self.state); runtime.spawn(async move { - if let Ok((proof, _ttl)) = build_txt_proof_async(us.resolver, &q.0).await { - let contents = DNSResolverMessage::DNSSECProof(DNSSECProof { name: q.0, proof }); - let instructions = responder.respond().into_instructions(); - us.pending_replies.lock().unwrap().push((contents, instructions)); - us.pending_query_count.fetch_sub(1, Ordering::Relaxed); - } + let contents = match build_txt_proof_async(us.resolver, &q.0).await { + Ok((proof, _ttl)) => { + DNSResolverMessage::DNSSECProof(DNSSECProof { name: q.0, proof }) + }, + Err(e) => { + // We might get an Unauthenticated error if the DNS resolver does not support + // DNSSEC, so we only set `definitely_unresolvable` if we get an NXDOMAIN. + let definitely_unresolvable = matches!( + e.get_ref().and_then(|e| e.downcast_ref::<ProofBuildingError>()), + Some(ProofBuildingError::NoSuchName) + ); + DNSResolverMessage::DNSSECError(DNSSECError { + name: q.0, + definitely_unresolvable, + }) + }, + }; + let instructions = responder.respond().into_instructions(); + us.pending_replies.lock().unwrap().push((contents, instructions)); + us.pending_query_count.fetch_sub(1, Ordering::Relaxed); }); None } @@ -147,32 +167,21 @@ mod test { use super::*; use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; - use bitcoin::Block; use lightning::blinded_path::message::{ BlindedMessagePath, MessageContext, MessageForwardNode, }; use lightning::blinded_path::NodeIdLookUp; - use lightning::events::{Event, PaymentPurpose}; - use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId}; - use lightning::ln::functional_test_utils::*; - use lightning::ln::msgs::{ - BaseMessageHandler, ChannelMessageHandler, Init, OnionMessageHandler, - }; - use lightning::offers::offer::Offer; + use lightning::ln::channelmanager::PaymentId; + use lightning::ln::msgs::{BaseMessageHandler, Init, OnionMessageHandler}; use lightning::onion_message::dns_resolution::{HumanReadableName, OMNameResolver}; use lightning::onion_message::messenger::{ AOnionMessenger, Destination, MessageRouter, OnionMessagePath, OnionMessenger, }; - use lightning::routing::router::DEFAULT_PAYMENT_DUMMY_HOPS; use lightning::sign::{KeysManager, NodeSigner, ReceiveAuthKey, Recipient}; use lightning::types::features::InitFeatures; - use lightning::types::payment::PaymentHash; use lightning::util::logger::Logger; - use lightning::expect_payment_claimed; - use lightning_types::string::UntrustedString; - use std::sync::Mutex; use std::time::{Duration, Instant, SystemTime}; @@ -222,6 +231,7 @@ mod test { struct URIResolver { resolved_uri: Mutex<Option<(HumanReadableName, PaymentId, String)>>, + resolved_error: Mutex<Option<(HumanReadableName, PaymentId, bool)>>, resolver: OMNameResolver, pending_messages: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>, } @@ -240,6 +250,15 @@ mod test { core::mem::swap(&mut *self.resolved_uri.lock().unwrap(), &mut result); assert!(result.is_none()); } + fn handle_dnssec_error(&self, msg: DNSSECError, context: DNSResolverContext) { + let definitely_unresolvable = msg.definitely_unresolvable; + let mut failed = self.resolver.handle_dnssec_error(msg, context); + assert_eq!(failed.len(), 1); + let (name, payment_id) = failed.pop().unwrap(); + let mut result = Some((name, payment_id, definitely_unresolvable)); + core::mem::swap(&mut *self.resolved_error.lock().unwrap(), &mut result); + assert!(result.is_none()); + } fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { core::mem::take(&mut *self.pending_messages.lock().unwrap()) } @@ -275,8 +294,6 @@ mod test { #[tokio::test] async fn resolution_test() { - let secp_ctx = Secp256k1::new(); - let (resolver_messenger, resolver_id) = create_resolver(); let resolver_dest = Destination::Node(resolver_id); @@ -290,6 +307,7 @@ mod test { let payer_id = payer_keys.get_node_id(Recipient::Node).unwrap(); let payer = Arc::new(URIResolver { resolved_uri: Mutex::new(None), + resolved_error: Mutex::new(None), resolver: OMNameResolver::new(now as u32, 1), pending_messages: Mutex::new(Vec::new()), }); @@ -309,25 +327,11 @@ mod test { payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap(); - let (msg, context) = - payer.resolver.resolve_name(payment_id, name.clone(), &*payer_keys).unwrap(); - let query_context = MessageContext::DNSResolver(context); - let receive_key = payer_keys.get_receive_auth_key(); - let reply_path = BlindedMessagePath::one_hop( - payer_id, - receive_key, - query_context, - false, - &*payer_keys, - &secp_ctx, - ); - payer.pending_messages.lock().unwrap().push(( - DNSResolverMessage::DNSSECQuery(msg), - MessageSendInstructions::WithSpecifiedReplyPath { - destination: resolver_dest, - reply_path, - }, - )); + let messages = payer + .resolver + .initiate_resolution(payment_id, name.clone(), vec![resolver_dest], &*payer_keys) + .unwrap(); + payer.pending_messages.lock().unwrap().extend(messages); let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap(); resolver_messenger.get_om().handle_onion_message(payer_id, &query); @@ -349,25 +353,54 @@ mod test { assert!(resolution.2[.."bitcoin:".len()].eq_ignore_ascii_case("bitcoin:")); } - async fn pay_offer_flow<'a, 'b, 'c>( - nodes: &[Node<'a, 'b, 'c>], resolver_messenger: &impl AOnionMessenger, - resolver_id: PublicKey, payer_id: PublicKey, payee_id: PublicKey, offer: Offer, - name: HumanReadableName, payment_id: PaymentId, payer_note: Option<String>, - resolvers: Vec<Destination>, - ) { - // Override contents to offer provided - let proof_override = &nodes[0].node.testing_dnssec_proof_offer_resolution_override; - proof_override.lock().unwrap().insert(name.clone(), offer); - let amt = 42_000; - let mut opts = OptionalOfferPaymentParams::default(); - opts.payer_note = payer_note.clone(); - #[allow(deprecated)] - nodes[0] - .node - .pay_for_offer_from_human_readable_name(name, amt, payment_id, opts, resolvers) + #[tokio::test] + async fn resolution_failure_test() { + // Test that querying for a name which does not exist results in a `DNSSECError` with + // `definitely_unresolvable` set being returned (rather than a `DNSSECProof`). + + let (resolver_messenger, resolver_id) = create_resolver(); + + let resolver_dest = Destination::Node(resolver_id); + let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); + + let payment_id = PaymentId([43; 32]); + // `mattcorallo.com` is DNSSEC-signed, so a name which does not exist under it will result in + // an authenticated NXDOMAIN, i.e. a definitely-unresolvable name. + let name = + HumanReadableName::from_encoded("nonexistent-user-ldk-test@mattcorallo.com").unwrap(); + + let payer_keys = Arc::new(KeysManager::new(&[3; 32], 42, 43, true)); + let payer_logger = TestLogger { node: "payer" }; + let payer_id = payer_keys.get_node_id(Recipient::Node).unwrap(); + let payer = Arc::new(URIResolver { + resolved_uri: Mutex::new(None), + resolved_error: Mutex::new(None), + resolver: OMNameResolver::new(now as u32, 1), + pending_messages: Mutex::new(Vec::new()), + }); + let payer_messenger = Arc::new(OnionMessenger::new( + Arc::clone(&payer_keys), + Arc::clone(&payer_keys), + payer_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&payer), + IgnoringMessageHandler {}, + )); + + let init_msg = get_om_init(); + payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); + resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap(); + + let messages = payer + .resolver + .initiate_resolution(payment_id, name.clone(), vec![resolver_dest], &*payer_keys) .unwrap(); + payer.pending_messages.lock().unwrap().extend(messages); - let query = nodes[0].onion_messenger.next_onion_message_for_peer(resolver_id).unwrap(); + let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap(); resolver_messenger.get_om().handle_onion_message(payer_id, &query); assert!(resolver_messenger.get_om().next_onion_message_for_peer(payer_id).is_none()); @@ -380,121 +413,88 @@ mod test { assert!(start.elapsed() < Duration::from_secs(10), "Resolution took too long"); }; - nodes[0].onion_messenger.handle_onion_message(resolver_id, &response); - - let invreq = nodes[0].onion_messenger.next_onion_message_for_peer(payee_id).unwrap(); - nodes[1].onion_messenger.handle_onion_message(payer_id, &invreq); - - let inv = nodes[1].onion_messenger.next_onion_message_for_peer(payer_id).unwrap(); - nodes[0].onion_messenger.handle_onion_message(payee_id, &inv); - - check_added_monitors(&nodes[0], 1); - let updates = get_htlc_update_msgs(&nodes[0], &payee_id); - nodes[1].node.handle_update_add_htlc(payer_id, &updates.update_add_htlcs[0]); - do_commitment_signed_dance(&nodes[1], &nodes[0], &updates.commitment_signed, false, false); - - for _ in 0..DEFAULT_PAYMENT_DUMMY_HOPS { - assert!(nodes[1].node.needs_pending_htlc_processing()); - nodes[1].node.process_pending_htlc_forwards(); - } - - expect_and_process_pending_htlcs(&nodes[1], false); - - let claimable_events = nodes[1].node.get_and_clear_pending_events(); - assert_eq!(claimable_events.len(), 1); - let our_payment_preimage; - if let Event::PaymentClaimable { purpose, amount_msat, .. } = &claimable_events[0] { - assert_eq!(*amount_msat, amt); - if let PaymentPurpose::Bolt12OfferPayment { - payment_preimage, payment_context, .. - } = purpose - { - our_payment_preimage = payment_preimage.unwrap(); - nodes[1].node.claim_funds(our_payment_preimage); - let payment_hash: PaymentHash = our_payment_preimage.into(); - expect_payment_claimed!(nodes[1], payment_hash, amt); - if let Some(note) = payer_note { - assert_eq!( - payment_context.invoice_request.payer_note_truncated, - Some(UntrustedString(note.into())) - ); - } else { - assert_eq!(payment_context.invoice_request.payer_note_truncated, None); - } - } else { - panic!(); - } - } else { - panic!(); - } - - check_added_monitors(&nodes[1], 1); - let mut updates = get_htlc_update_msgs(&nodes[1], &payer_id); - nodes[0].node.handle_update_fulfill_htlc(payee_id, updates.update_fulfill_htlcs.remove(0)); - do_commitment_signed_dance(&nodes[0], &nodes[1], &updates.commitment_signed, false, false); - - expect_payment_sent(&nodes[0], our_payment_preimage, None, true, true); + payer_messenger.handle_onion_message(resolver_id, &response); + let (failed_name, failed_payment_id, definitely_unresolvable) = + payer.resolved_error.lock().unwrap().take().unwrap(); + assert_eq!(failed_name, name); + assert_eq!(failed_payment_id, payment_id); + assert!(definitely_unresolvable); + assert!(payer.resolved_uri.lock().unwrap().is_none()); } #[tokio::test] - async fn end_to_end_test() { - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs_with_node_id_message_router(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + async fn failed_query_does_not_leak_pending_counter() { + use std::sync::atomic::Ordering; - create_announced_chan_between_nodes(&nodes, 0, 1); + // Resolver points at a port that should refuse TCP, so build_txt_proof_async + // returns Err quickly. + let resolver_keys = Arc::new(KeysManager::new(&[99; 32], 42, 43, true)); + let resolver_logger = TestLogger { node: "resolver" }; + let resolver = + Arc::new(OMDomainResolver::<IgnoringMessageHandler>::ignoring_incoming_proofs( + "127.0.0.1:1".parse().unwrap(), + )); + let resolver_state = Arc::clone(&resolver.state); + let resolver_messenger = OnionMessenger::new( + Arc::clone(&resolver_keys), + Arc::clone(&resolver_keys), + resolver_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&resolver), + IgnoringMessageHandler {}, + ); + let resolver_id = resolver_keys.get_node_id(Recipient::Node).unwrap(); - // The DNSSEC validation will only work with the current time, so set the time on the - // resolver. + let resolver_dest = Destination::Node(resolver_id); let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs(); - let block = Block { - header: create_dummy_header(nodes[0].best_block_hash(), now as u32), - txdata: Vec::new(), - }; - connect_block(&nodes[0], &block); - connect_block(&nodes[1], &block); - let payer_id = nodes[0].node.get_our_node_id(); - let payee_id = nodes[1].node.get_our_node_id(); + let payment_id = PaymentId([42; 32]); + let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); + + let payer_keys = Arc::new(KeysManager::new(&[2; 32], 42, 43, true)); + let payer_logger = TestLogger { node: "payer" }; + let payer_id = payer_keys.get_node_id(Recipient::Node).unwrap(); + let payer = Arc::new(URIResolver { + resolved_uri: Mutex::new(None), + resolved_error: Mutex::new(None), + resolver: OMNameResolver::new(now as u32, 1), + pending_messages: Mutex::new(Vec::new()), + }); + let payer_messenger = Arc::new(OnionMessenger::new( + Arc::clone(&payer_keys), + Arc::clone(&payer_keys), + payer_logger, + DummyNodeLookup {}, + DirectlyConnectedRouter {}, + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + Arc::clone(&payer), + IgnoringMessageHandler {}, + )); - let (resolver_messenger, resolver_id) = create_resolver(); let init_msg = get_om_init(); - nodes[0].onion_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); - resolver_messenger.get_om().peer_connected(payer_id, &init_msg, false).unwrap(); + payer_messenger.peer_connected(resolver_id, &init_msg, true).unwrap(); + resolver_messenger.peer_connected(payer_id, &init_msg, false).unwrap(); - let name = HumanReadableName::from_encoded("matt@mattcorallo.com").unwrap(); + let messages = payer + .resolver + .initiate_resolution(payment_id, name.clone(), vec![resolver_dest], &*payer_keys) + .unwrap(); + payer.pending_messages.lock().unwrap().extend(messages); - let bs_offer = nodes[1].node.create_offer_builder().unwrap().build().unwrap(); - let resolvers = vec![Destination::Node(resolver_id)]; - - pay_offer_flow( - &nodes, - &resolver_messenger, - resolver_id, - payer_id, - payee_id, - bs_offer.clone(), - name.clone(), - PaymentId([42; 32]), - None, - resolvers.clone(), - ) - .await; - - // Pay offer with payer_note - pay_offer_flow( - &nodes, - &resolver_messenger, - resolver_id, - payer_id, - payee_id, - bs_offer, - name, - PaymentId([21; 32]), - Some("foo".into()), - resolvers, - ) - .await; + let query = payer_messenger.next_onion_message_for_peer(resolver_id).unwrap(); + resolver_messenger.handle_onion_message(payer_id, &query); + + let start = Instant::now(); + while resolver_state.pending_query_count.load(Ordering::Relaxed) != 0 { + tokio::time::sleep(Duration::from_millis(50)).await; + assert!( + start.elapsed() < Duration::from_secs(10), + "pending_query_count not decremented after failed proof: counter leaks" + ); + } } } diff --git a/lightning-invoice/Cargo.toml b/lightning-invoice/Cargo.toml index 2b5d570f43f..8efe3833351 100644 --- a/lightning-invoice/Cargo.toml +++ b/lightning-invoice/Cargo.toml @@ -22,7 +22,7 @@ std = [] bech32 = { version = "0.11.0", default-features = false } lightning-types = { version = "0.4.0", path = "../lightning-types", default-features = false } serde = { version = "1.0", optional = true, default-features = false, features = ["alloc"] } -bitcoin = { version = "0.32.4", default-features = false, features = ["secp-recovery"] } +bitcoin = { version = "0.32.7", default-features = false, features = ["secp-recovery"] } [dev-dependencies] serde_json = { version = "1"} diff --git a/lightning-invoice/src/lib.rs b/lightning-invoice/src/lib.rs index 4ee9acb5f27..2dfd752bc81 100644 --- a/lightning-invoice/src/lib.rs +++ b/lightning-invoice/src/lib.rs @@ -159,6 +159,10 @@ pub const DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA: u64 = 18; /// consistency is more important. pub const MAX_LENGTH: usize = 7089; +/// The maximum length of a tagged field in a BOLT11 invoice. This is 1023 * 5 bits (i.e., 639 +/// bytes). +pub const MAX_TAGGED_FIELD_DATA_BYTES: usize = 639; + /// The [`bech32::Bech32`] checksum algorithm, with extended max length suitable /// for BOLT11 invoices. pub enum Bolt11Bech32 {} @@ -880,14 +884,17 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> { /// Sets the payment metadata. /// - /// By default features are set to *optionally* allow the sender to include the payment metadata. - /// If you wish to require that the sender include the metadata (and fail to parse the invoice if - /// they don't support payment metadata fields), you need to call - /// [`InvoiceBuilder::require_payment_metadata`] after this. - pub fn payment_metadata( + /// This marks the payment metadata as optional, allowing a legacy sender that doesn't + /// understand payment metadata to ignore it. Note that LDK by default commits to the payment + /// metadata in its payment secret, implicitly making it required. + pub fn optional_payment_metadata( mut self, payment_metadata: Vec<u8>, ) -> InvoiceBuilder<D, H, T, C, S, tb::True> { - self.tagged_fields.push(TaggedField::PaymentMetadata(payment_metadata)); + if payment_metadata.len() > MAX_TAGGED_FIELD_DATA_BYTES { + self.error = Some(CreationError::PaymentMetadataTooLong); + } else { + self.tagged_fields.push(TaggedField::PaymentMetadata(payment_metadata)); + } let mut found_features = false; for field in self.tagged_fields.iter_mut() { if let TaggedField::Features(f) = field { @@ -902,20 +909,23 @@ impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> } self.set_flags() } -} -impl<D: tb::Bool, H: tb::Bool, T: tb::Bool, C: tb::Bool, S: tb::Bool> - InvoiceBuilder<D, H, T, C, S, tb::True> -{ - /// Sets forwarding of payment metadata as required. A reader of the invoice which does not - /// support sending payment metadata will fail to read the invoice. - pub fn require_payment_metadata(mut self) -> InvoiceBuilder<D, H, T, C, S, tb::True> { - for field in self.tagged_fields.iter_mut() { + /// Sets the payment metadata. + /// + /// By default features are set to *require* the sender to include the payment metadata. + /// If you wish to support legacy senders that ignore the metadata, you can call + /// [`InvoiceBuilder::optional_payment_metadata`] instead. Note that LDK by default commits to + /// the payment metadata in its payment secret, implicitly making it required. + pub fn payment_metadata( + self, payment_metadata: Vec<u8>, + ) -> InvoiceBuilder<D, H, T, C, S, tb::True> { + let mut res = self.optional_payment_metadata(payment_metadata); + for field in res.tagged_fields.iter_mut() { if let TaggedField::Features(f) = field { f.set_payment_metadata_required(); } } - self + res } } @@ -1476,7 +1486,7 @@ impl Bolt11Invoice { unreachable!("ensured by constructor"); } - /// Get the payee's public key if one was included in the invoice + /// Get the payee's public key if one was explicitly included in the invoice's `n` field. pub fn payee_pub_key(&self) -> Option<&PublicKey> { self.signed_invoice.payee_pub_key().map(|x| &x.0) } @@ -1496,17 +1506,21 @@ impl Bolt11Invoice { self.signed_invoice.features() } - /// 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.signed_invoice.recover_payee_pub_key().expect("was checked by constructor").0 + /// Recover the payee's public key from the invoice signature. + /// + /// This attempts signature recovery regardless of whether a payee public key was explicitly + /// included in the invoice's `n` field. Recovery can fail for a valid invoice with an included + /// `n` field, so [`Self::get_payee_pub_key`] should be used to obtain the invoice's payee key. + pub fn recover_payee_pub_key(&self) -> Option<PublicKey> { + self.signed_invoice.recover_payee_pub_key().ok().map(|p| p.0) } - /// Recover the payee's public key if one was included in the invoice, otherwise return the - /// recovered public key from the signature + /// Get the invoice's payee public key, preferring an explicitly included payee public key and + /// falling back to recovering the key from the signature. pub fn get_payee_pub_key(&self) -> PublicKey { match self.payee_pub_key() { Some(pk) => *pk, - None => self.recover_payee_pub_key(), + None => self.recover_payee_pub_key().expect("was checked by constructor"), } } @@ -1670,12 +1684,12 @@ impl TaggedField { } impl Description { - /// Creates a new `Description` if `description` is at most 1023 * 5 bits (i.e., 639 bytes) + /// Creates a new `Description` if `description` is at most [`MAX_TAGGED_FIELD_DATA_BYTES`] /// long, and returns [`CreationError::DescriptionTooLong`] otherwise. /// /// Please note that single characters may use more than one byte due to UTF8 encoding. pub fn new(description: String) -> Result<Description, CreationError> { - if description.len() > 639 { + if description.len() > MAX_TAGGED_FIELD_DATA_BYTES { Err(CreationError::DescriptionTooLong) } else { Ok(Description(UntrustedString(description))) @@ -1792,6 +1806,9 @@ pub enum CreationError { /// The supplied description string was longer than 639 __bytes__ (see [`Description::new`]) DescriptionTooLong, + /// The supplied payment metadata was longer than 639 __bytes__ + PaymentMetadataTooLong, + /// The specified route has too many hops and can't be encoded RouteTooLong, @@ -1814,6 +1831,7 @@ impl Display for CreationError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { CreationError::DescriptionTooLong => f.write_str("The supplied description string was longer than 639 bytes"), + CreationError::PaymentMetadataTooLong => f.write_str("The supplied payment metadata was longer than 639 bytes"), CreationError::RouteTooLong => f.write_str("The specified route has too many hops and can't be encoded"), CreationError::TimestampOutOfBounds => f.write_str("The Unix timestamp of the supplied date is less than zero or greater than 35-bits"), CreationError::InvalidAmount => f.write_str("The supplied millisatoshi amount was greater than the total bitcoin supply"), @@ -2055,6 +2073,58 @@ mod test { assert!(new_signed.check_signature()); } + #[test] + fn recover_payee_pub_key_returns_signature_recovery_result() { + use crate::{ + Bolt11Invoice, Bolt11InvoiceSignature, Currency, InvoiceBuilder, PaymentHash, + PaymentSecret, SignedRawBolt11Invoice, + }; + use bitcoin::secp256k1::ecdsa::{RecoverableSignature, RecoveryId}; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use core::time::Duration; + + let secp_ctx = Secp256k1::new(); + let private_key = SecretKey::from_slice(&[42; 32]).unwrap(); + let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); + + let invoice_without_payee_pub_key = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".to_string()) + .payment_hash(PaymentHash([0; 32])) + .payment_secret(PaymentSecret([21; 32])) + .min_final_cltv_expiry_delta(144) + .duration_since_epoch(Duration::from_secs(1234567)) + .build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key)) + .unwrap(); + assert_eq!(invoice_without_payee_pub_key.recover_payee_pub_key(), Some(public_key)); + assert_eq!(invoice_without_payee_pub_key.get_payee_pub_key(), public_key); + + let invoice_with_payee_pub_key = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".to_string()) + .payment_hash(PaymentHash([1; 32])) + .payment_secret(PaymentSecret([21; 32])) + .payee_pub_key(public_key) + .min_final_cltv_expiry_delta(144) + .duration_since_epoch(Duration::from_secs(1234567)) + .build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key)) + .unwrap(); + + let signed_raw = invoice_with_payee_pub_key.into_signed_raw(); + let (raw_invoice, hash, signature) = signed_raw.into_parts(); + let (_orig_rid, sig_bytes) = signature.0.serialize_compact(); + let bad_rid = RecoveryId::from_i32(2).unwrap(); + let bad_sig = RecoverableSignature::from_compact(&sig_bytes, bad_rid).unwrap(); + let bad_signed_raw = SignedRawBolt11Invoice { + raw_invoice, + hash, + signature: Bolt11InvoiceSignature(bad_sig), + }; + let bad_invoice = Bolt11Invoice::from_signed(bad_signed_raw).unwrap(); + + assert_eq!(bad_invoice.payee_pub_key(), Some(&public_key)); + assert_eq!(bad_invoice.recover_payee_pub_key(), None); + assert_eq!(bad_invoice.get_payee_pub_key(), public_key); + } + #[test] fn test_check_feature_bits() { use crate::TaggedField::*; @@ -2218,6 +2288,10 @@ mod test { let long_desc_res = builder.clone().description(too_long_string).build_raw(); assert_eq!(long_desc_res, Err(CreationError::DescriptionTooLong)); + let long_metadata_res = + builder.clone().description("Test".into()).payment_metadata(vec![0u8; 640]).build_raw(); + assert_eq!(long_metadata_res, Err(CreationError::PaymentMetadataTooLong)); + let route_hop = RouteHintHop { src_node_id: PublicKey::from_slice( &[ diff --git a/lightning-invoice/tests/ser_de.rs b/lightning-invoice/tests/ser_de.rs index 353878a9c52..be173912a78 100644 --- a/lightning-invoice/tests/ser_de.rs +++ b/lightning-invoice/tests/ser_de.rs @@ -418,7 +418,6 @@ fn get_test_tuples() -> Vec<(String, SignedRawBolt11Invoice, bool, bool)> { )) .description("payment metadata inside".to_owned()) .payment_metadata(<Vec<u8>>::from_hex("01fafaf0").unwrap()) - .require_payment_metadata() .payee_pub_key(PublicKey::from_slice(&<Vec<u8>>::from_hex( "03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad" ).unwrap()).unwrap()) @@ -450,7 +449,6 @@ fn get_test_tuples() -> Vec<(String, SignedRawBolt11Invoice, bool, bool)> { )) .description("payment metadata inside".to_owned()) .payment_metadata(<Vec<u8>>::from_hex("01fafaf0").unwrap()) - .require_payment_metadata() .payment_secret(PaymentSecret([0x11; 32])) .build_raw() .unwrap() diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml index 61f41c15d38..9b8114e47aa 100644 --- a/lightning-liquidity/Cargo.toml +++ b/lightning-liquidity/Cargo.toml @@ -33,6 +33,7 @@ chrono = { version = "0.4", default-features = false, features = ["serde", "allo serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } backtrace = { version = "0.3", optional = true } +bitreq = { version = "0.3.2", default-features = false } [dev-dependencies] lightning = { version = "0.3.0", path = "../lightning", default-features = false, features = ["_test_utils"] } @@ -46,7 +47,6 @@ parking_lot = { version = "0.12", default-features = false } level = "forbid" # When adding a new cfg attribute, ensure that it is added to this list. check-cfg = [ - "cfg(lsps1_service)", "cfg(c_bindings)", "cfg(backtrace)", "cfg(ldk_bench)", diff --git a/lightning-liquidity/src/events/mod.rs b/lightning-liquidity/src/events/mod.rs index c39b8b9fd59..3d9587a058a 100644 --- a/lightning-liquidity/src/events/mod.rs +++ b/lightning-liquidity/src/events/mod.rs @@ -33,7 +33,6 @@ pub enum LiquidityEvent { /// An LSPS1 (Channel Request) client event. LSPS1Client(lsps1::event::LSPS1ClientEvent), /// An LSPS1 (Channel Request) server event. - #[cfg(lsps1_service)] LSPS1Service(lsps1::event::LSPS1ServiceEvent), /// An LSPS2 (JIT Channel) client event. LSPS2Client(lsps2::event::LSPS2ClientEvent), @@ -57,7 +56,6 @@ impl From<lsps1::event::LSPS1ClientEvent> for LiquidityEvent { } } -#[cfg(lsps1_service)] impl From<lsps1::event::LSPS1ServiceEvent> for LiquidityEvent { fn from(event: lsps1::event::LSPS1ServiceEvent) -> Self { Self::LSPS1Service(event) diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs index 70649fe0f50..bbd3100e4ed 100644 --- a/lightning-liquidity/src/lsps0/ser.rs +++ b/lightning-liquidity/src/lsps0/ser.rs @@ -234,7 +234,7 @@ impl Readable for LSPSRequestId { } /// An object representing datetimes as described in bLIP-50 / LSPS0. -#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[derive(Clone, Debug, Copy, PartialEq, Eq, Hash, Serialize)] #[serde(transparent)] pub struct LSPSDateTime(pub chrono::DateTime<chrono::Utc>); @@ -256,10 +256,9 @@ impl LSPSDateTime { now_seconds_since_epoch > datetime_seconds_since_epoch } - /// Returns the absolute difference between two datetimes as a `Duration`. + /// Returns the elapsed duration from `other` to `self`, or zero if `other` is later. pub fn duration_since(&self, other: &Self) -> Duration { - let diff_secs = self.0.timestamp().abs_diff(other.0.timestamp()); - Duration::from_secs(diff_secs) + self.0.signed_duration_since(other.0).to_std().unwrap_or(Duration::ZERO) } /// Returns the time in seconds since the unix epoch. @@ -271,8 +270,23 @@ impl LSPSDateTime { impl FromStr for LSPSDateTime { type Err = (); fn from_str(s: &str) -> Result<Self, Self::Err> { - let datetime = chrono::DateTime::parse_from_rfc3339(s).map_err(|_| ())?; - Ok(Self(datetime.into())) + let datetime: chrono::DateTime<chrono::Utc> = + chrono::DateTime::parse_from_rfc3339(s).map_err(|_| ())?.into(); + // Reject pre-epoch datetimes here so peer-controlled `valid_until` / + // `expires_at` fields can never produce an `LSPSDateTime` with a negative + // UNIX timestamp, which would otherwise panic the `i64 -> u64` cast in + // `is_past`. + if datetime.timestamp() < 0 { + return Err(()); + } + Ok(Self(datetime)) + } +} + +impl<'de> Deserialize<'de> for LSPSDateTime { + fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { + let s = String::deserialize(deserializer)?; + Self::from_str(&s).map_err(|()| de::Error::custom("invalid LSPSDateTime")) } } @@ -971,6 +985,8 @@ pub(crate) mod u32_fee_rate { mod tests { use super::*; + use core::time::Duration; + use lightning::io::Cursor; #[test] @@ -981,4 +997,27 @@ mod tests { let decoded_datetime: LSPSDateTime = Readable::read(&mut Cursor::new(buf)).unwrap(); assert_eq!(expected_datetime, decoded_datetime); } + + #[test] + fn datetime_duration_since_is_directional() { + let earlier = LSPSDateTime::new_from_duration_since_epoch(Duration::from_secs(30)); + let later = LSPSDateTime::new_from_duration_since_epoch(Duration::from_secs(90)); + let later_with_millis = + LSPSDateTime::new_from_duration_since_epoch(Duration::from_millis(90_100)); + + assert_eq!(later.duration_since(&earlier), Duration::from_secs(60)); + assert_eq!(later_with_millis.duration_since(&later), Duration::from_millis(100)); + assert_eq!(earlier.duration_since(&later), Duration::ZERO); + } + + #[test] + fn is_past_handles_pre_epoch_datetime() { + // A peer-controlled RFC3339 datetime before 1970 must be rejected at parse + // time, so it can never reach `is_past` (or any other consumer) and panic. + assert!(LSPSDateTime::from_str("1900-01-01T00:00:00Z").is_err()); + + // JSON deserialization (the path peer messages take) must reject it too. + let json = "\"1900-01-01T00:00:00Z\""; + assert!(serde_json::from_str::<LSPSDateTime>(json).is_err()); + } } diff --git a/lightning-liquidity/src/lsps1/event.rs b/lightning-liquidity/src/lsps1/event.rs index fdf3fc57b0d..8868790da31 100644 --- a/lightning-liquidity/src/lsps1/event.rs +++ b/lightning-liquidity/src/lsps1/event.rs @@ -15,6 +15,7 @@ use super::msgs::{LSPS1ChannelInfo, LSPS1Options, LSPS1OrderParams, LSPS1Payment use crate::lsps0::ser::{LSPSRequestId, LSPSResponseError}; use bitcoin::secp256k1::PublicKey; +use bitcoin::Address; /// An event which an bLIP-51 / LSPS1 client should take some action in response to. #[derive(Clone, Debug, PartialEq, Eq)] @@ -142,7 +143,6 @@ pub enum LSPS1ClientEvent { } /// An event which an LSPS1 server should take some action in response to. -#[cfg(lsps1_service)] #[derive(Clone, Debug, PartialEq, Eq)] pub enum LSPS1ServiceEvent { /// A client has selected the parameters to use from the supported options of the LSP @@ -152,9 +152,13 @@ pub enum LSPS1ServiceEvent { /// send order parameters including the details regarding the /// payment and order id for this order for the client. /// + /// You should call [`LSPS1ServiceHandler::invalid_token_provided`] if the token provided as + /// part of the order parameters is invalid. + /// /// **Note: ** This event will *not* be persisted across restarts. /// /// [`LSPS1ServiceHandler::send_payment_details`]: crate::lsps1::service::LSPS1ServiceHandler::send_payment_details + /// [`LSPS1ServiceHandler::invalid_token_provided`]: crate::lsps1::service::LSPS1ServiceHandler::invalid_token_provided RequestForPaymentDetails { /// An identifier that must be passed to [`LSPS1ServiceHandler::send_payment_details`]. /// @@ -164,36 +168,12 @@ pub enum LSPS1ServiceEvent { counterparty_node_id: PublicKey, /// The order requested by the client. order: LSPS1OrderParams, - }, - /// A request from client to check the status of the payment. - /// - /// An event to poll for checking payment status either onchain or lightning. - /// - /// You must call [`LSPS1ServiceHandler::update_order_status`] to update the client - /// regarding the status of the payment and order. - /// - /// **Note: ** This event will *not* be persisted across restarts. - /// - /// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status - CheckPaymentConfirmation { - /// An identifier that must be passed to [`LSPS1ServiceHandler::update_order_status`]. + /// The address we need to send onchain refunds to in case channel opening fails. /// - /// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status - request_id: LSPSRequestId, - /// The node id of the client making the information request. - counterparty_node_id: PublicKey, - /// The order id of order with pending payment. - order_id: LSPS1OrderId, - }, - /// If error is encountered, refund the amount if paid by the client. - /// - /// **Note: ** This event will *not* be persisted across restarts. - Refund { - /// An identifier. - request_id: LSPSRequestId, - /// The node id of the client making the information request. - counterparty_node_id: PublicKey, - /// The order id of the refunded order. - order_id: LSPS1OrderId, + /// If this is `None` and you *require* onchain payment, you should call + /// [`LSPS1ServiceHandler::onchain_payments_required`] to reject the request. + /// + /// [`LSPS1ServiceHandler::onchain_payments_required`]: crate::lsps1::service::LSPS1ServiceHandler::onchain_payments_required + refund_onchain_address: Option<Address>, }, } diff --git a/lightning-liquidity/src/lsps1/mod.rs b/lightning-liquidity/src/lsps1/mod.rs index b068b186610..5f7f554dfb0 100644 --- a/lightning-liquidity/src/lsps1/mod.rs +++ b/lightning-liquidity/src/lsps1/mod.rs @@ -12,5 +12,5 @@ pub mod client; pub mod event; pub mod msgs; -#[cfg(lsps1_service)] +pub(crate) mod peer_state; pub mod service; diff --git a/lightning-liquidity/src/lsps1/msgs.rs b/lightning-liquidity/src/lsps1/msgs.rs index 8402827a4a6..9d0d54e2daf 100644 --- a/lightning-liquidity/src/lsps1/msgs.rs +++ b/lightning-liquidity/src/lsps1/msgs.rs @@ -19,8 +19,9 @@ use crate::lsps0::ser::{ }; use bitcoin::{Address, FeeRate, OutPoint}; - use lightning::offers::offer::Offer; +use lightning::util::ser::{Readable, Writeable}; +use lightning::{impl_ser_tlv_based, impl_ser_tlv_based_enum}; use lightning_invoice::Bolt11Invoice; use serde::{Deserialize, Serialize}; @@ -30,13 +31,31 @@ pub(crate) const LSPS1_CREATE_ORDER_METHOD_NAME: &str = "lsps1.create_order"; pub(crate) const LSPS1_GET_ORDER_METHOD_NAME: &str = "lsps1.get_order"; pub(crate) const _LSPS1_CREATE_ORDER_REQUEST_INVALID_PARAMS_ERROR_CODE: i32 = -32602; -#[cfg(lsps1_service)] -pub(crate) const LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE: i32 = 100; +pub(crate) const LSPS1_CREATE_ORDER_REQUEST_OPTION_MISMATCH_ERROR_CODE: i32 = 100; +pub(crate) const LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE: i32 = 101; +pub(crate) const LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE: i32 = 102; /// The identifier of an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] pub struct LSPS1OrderId(pub String); +impl Writeable for LSPS1OrderId { + fn write<W: lightning::util::ser::Writer>( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + self.0.write(writer) + } +} + +impl Readable for LSPS1OrderId { + fn read<R: bitcoin::io::Read>( + reader: &mut R, + ) -> Result<Self, lightning::ln::msgs::DecodeError> { + let inner = Readable::read(reader)?; + Ok(Self(inner)) + } +} + /// A request made to an LSP to retrieve the supported options. /// /// Please refer to the [bLIP-51 / LSPS1 @@ -126,6 +145,16 @@ pub struct LSPS1OrderParams { pub announce_channel: bool, } +impl_ser_tlv_based!(LSPS1OrderParams, { + (0, lsp_balance_sat, required), + (2, client_balance_sat, required), + (4, required_channel_confirmations, required), + (6, funding_confirms_within_blocks, required), + (8, channel_expiry_blocks, required), + (10, token, option), + (12, announce_channel, required), +}); + /// A response to a [`LSPS1CreateOrderRequest`]. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1CreateOrderResponse { @@ -156,6 +185,12 @@ pub enum LSPS1OrderState { Failed, } +impl_ser_tlv_based_enum!(LSPS1OrderState, + (0, Created) => {}, + (2, Completed) => {}, + (4, Failed) => {} +); + /// Details regarding how to pay for an order. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1PaymentInfo { @@ -167,6 +202,12 @@ pub struct LSPS1PaymentInfo { pub onchain: Option<LSPS1OnchainPaymentInfo>, } +impl_ser_tlv_based!(LSPS1PaymentInfo, { + (0, bolt11, option), + (2, bolt12, option), + (4, onchain, option), +}); + /// A Lightning payment using BOLT 11. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1Bolt11PaymentInfo { @@ -184,6 +225,14 @@ pub struct LSPS1Bolt11PaymentInfo { pub invoice: Bolt11Invoice, } +impl_ser_tlv_based!(LSPS1Bolt11PaymentInfo, { + (0, state, required), + (2, expires_at, required), + (4, fee_total_sat, required), + (6, order_total_sat, required), + (8, invoice, required), +}); + /// A Lightning payment using BOLT 12. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1Bolt12PaymentInfo { @@ -202,6 +251,14 @@ pub struct LSPS1Bolt12PaymentInfo { pub offer: Offer, } +impl_ser_tlv_based!(LSPS1Bolt12PaymentInfo, { + (0, state, required), + (2, expires_at, required), + (4, fee_total_sat, required), + (6, order_total_sat, required), + (8, offer, required), +}); + /// An onchain payment. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct LSPS1OnchainPaymentInfo { @@ -233,6 +290,17 @@ pub struct LSPS1OnchainPaymentInfo { pub refund_onchain_address: Option<Address>, } +impl_ser_tlv_based!(LSPS1OnchainPaymentInfo, { + (0, state, required), + (2, expires_at, required), + (4, fee_total_sat, required), + (6, order_total_sat, required), + (8, address, required), + (10, min_onchain_payment_confirmations, option), + (12, min_fee_for_0conf, required), + (14, refund_onchain_address, option), +}); + /// The state of a payment. /// /// *Note*: Previously, the spec also knew a `CANCELLED` state for BOLT11 payments, which has since @@ -242,24 +310,24 @@ pub struct LSPS1OnchainPaymentInfo { pub enum LSPS1PaymentState { /// A payment is expected. ExpectPayment, - /// A sufficient payment has been received. + /// A payment has been received but the channel has not yet been opened. + /// + /// This indicates the LSP has received the payment (e.g., Lightning HTLC held, + /// or on-chain transaction detected) but has not yet published the funding transaction. + Hold, + /// A sufficient payment has been received and the channel has been opened. Paid, /// The payment has been refunded. #[serde(alias = "CANCELLED")] Refunded, } -/// Details regarding a detected on-chain payment. -#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -pub struct LSPS1OnchainPayment { - /// The outpoint of the payment. - pub outpoint: String, - /// The amount of satoshi paid. - #[serde(with = "string_amount")] - pub sat: u64, - /// Indicates if the LSP regards the transaction as sufficiently confirmed. - pub confirmed: bool, -} +impl_ser_tlv_based_enum!(LSPS1PaymentState, + (0, ExpectPayment) => {}, + (2, Hold) => {}, + (4, Paid) => {}, + (6, Refunded) => {} +); /// Details regarding the state of an ordered channel. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] @@ -272,6 +340,12 @@ pub struct LSPS1ChannelInfo { pub expires_at: LSPSDateTime, } +impl_ser_tlv_based!(LSPS1ChannelInfo, { + (0, funded_at, required), + (2, funding_outpoint, required), + (4, expires_at, required), +}); + /// A request made to an LSP to retrieve information about an previously made order. /// /// Please refer to the [bLIP-51 / LSPS1 diff --git a/lightning-liquidity/src/lsps1/peer_state.rs b/lightning-liquidity/src/lsps1/peer_state.rs new file mode 100644 index 00000000000..26842e8e799 --- /dev/null +++ b/lightning-liquidity/src/lsps1/peer_state.rs @@ -0,0 +1,781 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains peer state objects that are used by `LSPS1ServiceHandler`. + +use super::msgs::{ + LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1OrderState, LSPS1PaymentInfo, + LSPS1PaymentState, LSPS1Request, +}; + +use crate::lsps0::ser::{LSPSDateTime, LSPSRequestId}; +use crate::prelude::HashMap; + +use lightning::util::hash_tables::new_hash_map; +use lightning::{impl_ser_tlv_based, impl_ser_tlv_based_enum}; + +use core::fmt; + +const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; + +/// Indicates which payment method was used for the order. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PaymentMethod { + /// A Lightning payment using BOLT 11. + Bolt11, + /// A Lightning payment using BOLT 12. + Bolt12, + /// An onchain payment. + Onchain, +} + +/// Error type for invalid state transitions. +#[derive(Debug, Clone)] +pub(super) enum ChannelOrderStateError { + /// Attempted an invalid state transition. + InvalidStateTransition { + /// The state from which the transition was attempted. + from: LSPS1OrderState, + /// The action that was attempted. + action: &'static str, + }, + /// The specified payment method was not configured for this order. + PaymentMethodNotConfigured, +} + +impl fmt::Display for ChannelOrderStateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidStateTransition { from, action } => { + write!(f, "invalid state transition: cannot {} from {:?}", action, from) + }, + Self::PaymentMethodNotConfigured => { + write!(f, "payment method not configured for this order") + }, + } + } +} + +/// Internal state machine for tracking channel order progress. +/// +/// This combines the wire `order_state` (CREATED/COMPLETED/FAILED) with internal +/// payment tracking to provide type-safe state transitions. +#[derive(Debug, Clone)] +pub(super) enum ChannelOrderState { + /// Initial state - awaiting payment from client. + /// Payment states within payment_details should be EXPECT_PAYMENT. + ExpectingPayment { + /// Details about how to pay for the order. + payment_details: LSPS1PaymentInfo, + }, + /// Payment received, awaiting channel open. + /// The paid method's state should be PAID. + OrderPaid { + /// Details about how to pay for the order (with paid method updated). + payment_details: LSPS1PaymentInfo, + }, + /// Channel successfully funded and opened (terminal). + /// Payment states should be PAID. + CompletedAndChannelOpened { + /// Details about how to pay for the order. + payment_details: LSPS1PaymentInfo, + /// Information about the opened channel. + channel_info: LSPS1ChannelInfo, + }, + /// Order failed, payment refunded (terminal). + /// Payment states should be REFUNDED. + FailedAndRefunded { + /// Details about how to pay for the order (with states set to REFUNDED). + payment_details: LSPS1PaymentInfo, + }, +} + +impl ChannelOrderState { + /// Creates a new state in the ExpectingPayment state. + pub(super) fn new(payment_details: LSPS1PaymentInfo) -> Self { + ChannelOrderState::ExpectingPayment { payment_details } + } + + /// Transition: ExpectingPayment -> OrderPaid + /// + /// Updates the specified payment method's state to HOLD. + pub(super) fn payment_received( + &mut self, method: PaymentMethod, + ) -> Result<(), ChannelOrderStateError> { + match self { + ChannelOrderState::ExpectingPayment { payment_details } => { + // Update the payment state for the specified method to HOLD + let method_exists = match method { + PaymentMethod::Bolt11 => { + if let Some(ref mut bolt11) = payment_details.bolt11 { + bolt11.state = LSPS1PaymentState::Hold; + true + } else { + false + } + }, + PaymentMethod::Bolt12 => { + if let Some(ref mut bolt12) = payment_details.bolt12 { + bolt12.state = LSPS1PaymentState::Hold; + true + } else { + false + } + }, + PaymentMethod::Onchain => { + if let Some(ref mut onchain) = payment_details.onchain { + onchain.state = LSPS1PaymentState::Hold; + true + } else { + false + } + }, + }; + + if !method_exists { + return Err(ChannelOrderStateError::PaymentMethodNotConfigured); + } + + // Move to OrderPaid state + *self = ChannelOrderState::OrderPaid { payment_details: payment_details.clone() }; + Ok(()) + }, + _ => Err(ChannelOrderStateError::InvalidStateTransition { + from: self.order_state(), + action: "payment_received", + }), + } + } + + /// Transition: OrderPaid -> CompletedAndChannelOpened + /// + /// Updates payment states from HOLD to PAID. + pub(super) fn channel_opened( + &mut self, channel_info: LSPS1ChannelInfo, + ) -> Result<(), ChannelOrderStateError> { + match self { + ChannelOrderState::OrderPaid { payment_details } => { + // Update payment states from HOLD to PAID + let mut paid_details = payment_details.clone(); + if let Some(ref mut bolt11) = paid_details.bolt11 { + if bolt11.state == LSPS1PaymentState::Hold { + bolt11.state = LSPS1PaymentState::Paid; + } + } + if let Some(ref mut bolt12) = paid_details.bolt12 { + if bolt12.state == LSPS1PaymentState::Hold { + bolt12.state = LSPS1PaymentState::Paid; + } + } + if let Some(ref mut onchain) = paid_details.onchain { + if onchain.state == LSPS1PaymentState::Hold { + onchain.state = LSPS1PaymentState::Paid; + } + } + + *self = ChannelOrderState::CompletedAndChannelOpened { + payment_details: paid_details, + channel_info, + }; + Ok(()) + }, + _ => Err(ChannelOrderStateError::InvalidStateTransition { + from: self.order_state(), + action: "channel_opened", + }), + } + } + + /// Transition: ExpectingPayment|OrderPaid -> FailedAndRefunded + /// + /// Updates all payment states to REFUNDED. + pub(super) fn mark_failed_and_refunded(&mut self) -> Result<(), ChannelOrderStateError> { + match self { + ChannelOrderState::ExpectingPayment { payment_details } + | ChannelOrderState::OrderPaid { payment_details } => { + // Mark all payment methods as refunded + let mut refunded_details = payment_details.clone(); + if let Some(ref mut bolt11) = refunded_details.bolt11 { + bolt11.state = LSPS1PaymentState::Refunded; + } + if let Some(ref mut bolt12) = refunded_details.bolt12 { + bolt12.state = LSPS1PaymentState::Refunded; + } + if let Some(ref mut onchain) = refunded_details.onchain { + onchain.state = LSPS1PaymentState::Refunded; + } + + *self = ChannelOrderState::FailedAndRefunded { payment_details: refunded_details }; + Ok(()) + }, + _ => Err(ChannelOrderStateError::InvalidStateTransition { + from: self.order_state(), + action: "mark_failed_and_refunded", + }), + } + } + + /// Get payment_details (available in all states). + pub(super) fn payment_details(&self) -> &LSPS1PaymentInfo { + match self { + ChannelOrderState::ExpectingPayment { payment_details } + | ChannelOrderState::OrderPaid { payment_details } + | ChannelOrderState::CompletedAndChannelOpened { payment_details, .. } + | ChannelOrderState::FailedAndRefunded { payment_details } => payment_details, + } + } + + /// Get channel_info if in CompletedAndChannelOpened state. + pub(super) fn channel_info(&self) -> Option<&LSPS1ChannelInfo> { + match self { + ChannelOrderState::CompletedAndChannelOpened { channel_info, .. } => Some(channel_info), + _ => None, + } + } + + /// Convert to wire format LSPS1OrderState. + pub(super) fn order_state(&self) -> LSPS1OrderState { + match self { + ChannelOrderState::ExpectingPayment { .. } | ChannelOrderState::OrderPaid { .. } => { + LSPS1OrderState::Created + }, + ChannelOrderState::CompletedAndChannelOpened { .. } => LSPS1OrderState::Completed, + ChannelOrderState::FailedAndRefunded { .. } => LSPS1OrderState::Failed, + } + } +} + +impl_ser_tlv_based_enum!(ChannelOrderState, + (0, ExpectingPayment) => { + (0, payment_details, required), + }, + (2, OrderPaid) => { + (0, payment_details, required), + }, + (4, CompletedAndChannelOpened) => { + (0, payment_details, required), + (2, channel_info, required), + }, + (6, FailedAndRefunded) => { + (0, payment_details, required), + } +); + +#[derive(Default)] +pub(crate) struct PeerState { + outbound_channels_by_order_id: HashMap<LSPS1OrderId, ChannelOrder>, + pending_requests: HashMap<LSPSRequestId, LSPS1Request>, + needs_persist: bool, +} + +impl PeerState { + pub(super) fn new_order( + &mut self, order_id: LSPS1OrderId, order_params: LSPS1OrderParams, + created_at: LSPSDateTime, payment_details: LSPS1PaymentInfo, + ) -> ChannelOrder { + let state = ChannelOrderState::new(payment_details); + let channel_order = ChannelOrder { order_params, state, created_at }; + self.outbound_channels_by_order_id.insert(order_id, channel_order.clone()); + self.needs_persist |= true; + channel_order + } + + pub(super) fn get_order<'a>( + &'a self, order_id: &LSPS1OrderId, + ) -> Result<&'a ChannelOrder, PeerStateError> { + let order = self + .outbound_channels_by_order_id + .get(order_id) + .ok_or(PeerStateError::UnknownOrderId)?; + Ok(order) + } + + /// Transition: ExpectingPayment -> OrderPaid + /// + /// Updates the specified payment method's state to HOLD. + pub(super) fn order_payment_received( + &mut self, order_id: &LSPS1OrderId, method: PaymentMethod, + ) -> Result<(), PeerStateError> { + let order = self + .outbound_channels_by_order_id + .get_mut(order_id) + .ok_or(PeerStateError::UnknownOrderId)?; + order.state.payment_received(method).map_err(PeerStateError::InvalidStateTransition)?; + self.needs_persist |= true; + Ok(()) + } + + /// Transition: OrderPaid -> CompletedAndChannelOpened + pub(super) fn order_channel_opened( + &mut self, order_id: &LSPS1OrderId, channel_info: LSPS1ChannelInfo, + ) -> Result<(), PeerStateError> { + let order = self + .outbound_channels_by_order_id + .get_mut(order_id) + .ok_or(PeerStateError::UnknownOrderId)?; + order.state.channel_opened(channel_info).map_err(PeerStateError::InvalidStateTransition)?; + self.needs_persist |= true; + Ok(()) + } + + /// Transition: ExpectingPayment|OrderPaid -> FailedAndRefunded + /// + /// Updates all payment states to REFUNDED. + pub(super) fn order_failed_and_refunded( + &mut self, order_id: &LSPS1OrderId, + ) -> Result<(), PeerStateError> { + let order = self + .outbound_channels_by_order_id + .get_mut(order_id) + .ok_or(PeerStateError::UnknownOrderId)?; + order.state.mark_failed_and_refunded().map_err(PeerStateError::InvalidStateTransition)?; + self.needs_persist |= true; + Ok(()) + } + + pub(super) fn register_request( + &mut self, request_id: LSPSRequestId, request: LSPS1Request, + ) -> Result<(), PeerStateError> { + if self.pending_requests_and_unpaid_orders() >= MAX_PENDING_REQUESTS_PER_PEER { + return Err(PeerStateError::TooManyPendingRequests); + } + if self.pending_requests.contains_key(&request_id) { + return Err(PeerStateError::DuplicateRequestId); + } + self.pending_requests.insert(request_id, request); + Ok(()) + } + + pub(super) fn get_request( + &self, request_id: &LSPSRequestId, + ) -> Result<&LSPS1Request, PeerStateError> { + self.pending_requests.get(request_id).ok_or(PeerStateError::UnknownRequestId) + } + + pub(super) fn remove_request( + &mut self, request_id: &LSPSRequestId, + ) -> Result<LSPS1Request, PeerStateError> { + self.pending_requests.remove(request_id).ok_or(PeerStateError::UnknownRequestId) + } + + pub(super) fn has_active_orders(&self) -> bool { + !self.outbound_channels_by_order_id.is_empty() + } + + pub(super) fn needs_persist(&self) -> bool { + self.needs_persist + } + + pub(super) fn set_needs_persist(&mut self, needs_persist: bool) { + self.needs_persist = needs_persist; + } + + pub(super) fn is_prunable(&self) -> bool { + // Return whether the entire state is empty. + self.pending_requests.is_empty() && self.outbound_channels_by_order_id.is_empty() + } + + pub(super) fn prune_pending_requests(&mut self) -> usize { + let num_pruned = self.pending_requests.len(); + self.pending_requests.clear(); + num_pruned + } + + pub(super) fn prune_expired_request_state(&mut self) { + self.outbound_channels_by_order_id.retain(|_order_id, entry| { + if entry.is_prunable() { + self.needs_persist |= true; + return false; + } + true + }); + } + + fn pending_requests_and_unpaid_orders(&self) -> usize { + let pending_requests = self.pending_requests.len(); + // We exclude paid and completed orders. + let unpaid_orders = self + .outbound_channels_by_order_id + .iter() + .filter(|(_, v)| { + !matches!( + v.state, + ChannelOrderState::OrderPaid { .. } + | ChannelOrderState::CompletedAndChannelOpened { .. } + ) + }) + .count(); + pending_requests + unpaid_orders + } +} + +impl_ser_tlv_based!(PeerState, { + (0, outbound_channels_by_order_id, required), + (_unused, pending_requests, (static_value, new_hash_map())), + (_unused, needs_persist, (static_value, false)), +}); + +#[derive(Debug, Clone)] +pub(super) enum PeerStateError { + UnknownRequestId, + DuplicateRequestId, + UnknownOrderId, + InvalidStateTransition(ChannelOrderStateError), + TooManyPendingRequests, +} + +impl fmt::Display for PeerStateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownRequestId => write!(f, "unknown request id"), + Self::DuplicateRequestId => write!(f, "duplicate request id"), + Self::UnknownOrderId => write!(f, "unknown order id"), + Self::InvalidStateTransition(e) => write!(f, "{}", e), + Self::TooManyPendingRequests => write!(f, "too many pending requests"), + } + } +} + +#[derive(Debug, Clone)] +pub(super) struct ChannelOrder { + pub(super) order_params: LSPS1OrderParams, + pub(super) state: ChannelOrderState, + pub(super) created_at: LSPSDateTime, +} + +impl ChannelOrder { + /// Returns the order state. + pub(super) fn order_state(&self) -> LSPS1OrderState { + self.state.order_state() + } + + /// Returns the payment details. + pub(super) fn payment_details(&self) -> &LSPS1PaymentInfo { + self.state.payment_details() + } + + /// Returns the channel details if the channel has been opened. + pub(super) fn channel_details(&self) -> Option<&LSPS1ChannelInfo> { + self.state.channel_info() + } + + fn is_prunable(&self) -> bool { + let all_payment_details_expired; + #[cfg(feature = "time")] + { + let details = self.state.payment_details(); + all_payment_details_expired = + details.bolt11.as_ref().map_or(true, |d| d.expires_at.is_past()) + && details.bolt12.as_ref().map_or(true, |d| d.expires_at.is_past()) + && details.onchain.as_ref().map_or(true, |d| d.expires_at.is_past()); + } + #[cfg(not(feature = "time"))] + { + // TODO: We need to find a way to check expiry times in no-std builds. + all_payment_details_expired = false; + } + + let created_or_failed = matches!( + self.state, + ChannelOrderState::ExpectingPayment { .. } + | ChannelOrderState::FailedAndRefunded { .. } + ); + + all_payment_details_expired && created_or_failed + } +} + +impl_ser_tlv_based!(ChannelOrder, { + (0, order_params, required), + (2, state, required), + (4, created_at, required), +}); + +#[cfg(test)] +mod tests { + use super::*; + use crate::lsps0::ser::LSPSDateTime; + use crate::lsps1::msgs::{LSPS1Bolt11PaymentInfo, LSPS1OnchainPaymentInfo, LSPS1PaymentState}; + + use bitcoin::{Address, FeeRate, OutPoint}; + use lightning_invoice::Bolt11Invoice; + + use core::str::FromStr; + + fn create_test_bolt11_payment_info() -> LSPS1Bolt11PaymentInfo { + let invoice_str = "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05"; + LSPS1Bolt11PaymentInfo { + state: LSPS1PaymentState::ExpectPayment, + expires_at: LSPSDateTime::from_str("2035-01-01T00:00:00Z").unwrap(), + fee_total_sat: 9999, + order_total_sat: 200999, + invoice: Bolt11Invoice::from_str(invoice_str).unwrap(), + } + } + + fn create_test_onchain_payment_info() -> LSPS1OnchainPaymentInfo { + LSPS1OnchainPaymentInfo { + state: LSPS1PaymentState::ExpectPayment, + expires_at: LSPSDateTime::from_str("2035-01-01T00:00:00Z").unwrap(), + fee_total_sat: 9999, + order_total_sat: 200999, + address: Address::from_str( + "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + ) + .unwrap() + .assume_checked(), + min_onchain_payment_confirmations: Some(1), + min_fee_for_0conf: FeeRate::from_sat_per_vb(253).unwrap(), + refund_onchain_address: None, + } + } + + fn create_test_payment_info_bolt11_only() -> LSPS1PaymentInfo { + LSPS1PaymentInfo { + bolt11: Some(create_test_bolt11_payment_info()), + bolt12: None, + onchain: None, + } + } + + fn create_test_payment_info_onchain_only() -> LSPS1PaymentInfo { + LSPS1PaymentInfo { + bolt11: None, + bolt12: None, + onchain: Some(create_test_onchain_payment_info()), + } + } + + fn create_test_channel_info() -> LSPS1ChannelInfo { + LSPS1ChannelInfo { + funded_at: LSPSDateTime::from_str("2035-01-01T00:00:00Z").unwrap(), + funding_outpoint: OutPoint::from_str( + "0301e0480b374b32851a9462db29dc19fe830a7f7d7a88b81612b9d42099c0ae:0", + ) + .unwrap(), + expires_at: LSPSDateTime::from_str("2036-01-01T00:00:00Z").unwrap(), + } + } + + // Test valid transition: ExpectingPayment -> OrderPaid via payment_received (Bolt11) + #[test] + fn test_payment_received_bolt11() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + + assert!(matches!(state, ChannelOrderState::ExpectingPayment { .. })); + assert_eq!(state.order_state(), LSPS1OrderState::Created); + + state.payment_received(PaymentMethod::Bolt11).unwrap(); + + assert!(matches!(state, ChannelOrderState::OrderPaid { .. })); + assert_eq!(state.order_state(), LSPS1OrderState::Created); + // Payment state should be HOLD (not PAID) until channel is opened + assert_eq!(state.payment_details().bolt11.as_ref().unwrap().state, LSPS1PaymentState::Hold); + } + + // Test valid transition: ExpectingPayment -> OrderPaid via payment_received (Onchain) + #[test] + fn test_payment_received_onchain() { + let payment_info = create_test_payment_info_onchain_only(); + let mut state = ChannelOrderState::new(payment_info); + + state.payment_received(PaymentMethod::Onchain).unwrap(); + + assert!(matches!(state, ChannelOrderState::OrderPaid { .. })); + // Payment state should be HOLD (not PAID) until channel is opened + assert_eq!( + state.payment_details().onchain.as_ref().unwrap().state, + LSPS1PaymentState::Hold + ); + } + + // Test valid transition: OrderPaid -> CompletedAndChannelOpened via channel_opened + #[test] + fn test_channel_opened() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.payment_received(PaymentMethod::Bolt11).unwrap(); + + // Verify payment state is HOLD before channel opens + assert_eq!(state.payment_details().bolt11.as_ref().unwrap().state, LSPS1PaymentState::Hold); + + let channel_info = create_test_channel_info(); + state.channel_opened(channel_info.clone()).unwrap(); + + assert!(matches!(state, ChannelOrderState::CompletedAndChannelOpened { .. })); + assert_eq!(state.order_state(), LSPS1OrderState::Completed); + assert_eq!(state.channel_info(), Some(&channel_info)); + // Payment state should now be PAID after channel is opened + assert_eq!(state.payment_details().bolt11.as_ref().unwrap().state, LSPS1PaymentState::Paid); + } + + // Test valid transition: ExpectingPayment -> FailedAndRefunded + #[test] + fn test_mark_failed_from_expecting_payment() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + + state.mark_failed_and_refunded().unwrap(); + + assert!(matches!(state, ChannelOrderState::FailedAndRefunded { .. })); + assert_eq!(state.order_state(), LSPS1OrderState::Failed); + assert_eq!( + state.payment_details().bolt11.as_ref().unwrap().state, + LSPS1PaymentState::Refunded + ); + } + + // Test valid transition: OrderPaid -> FailedAndRefunded + #[test] + fn test_mark_failed_from_order_paid() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.payment_received(PaymentMethod::Bolt11).unwrap(); + + // Verify payment state is HOLD before failure + assert_eq!(state.payment_details().bolt11.as_ref().unwrap().state, LSPS1PaymentState::Hold); + + state.mark_failed_and_refunded().unwrap(); + + assert!(matches!(state, ChannelOrderState::FailedAndRefunded { .. })); + assert_eq!(state.order_state(), LSPS1OrderState::Failed); + // Payment state should now be REFUNDED + assert_eq!( + state.payment_details().bolt11.as_ref().unwrap().state, + LSPS1PaymentState::Refunded + ); + } + + // Test invalid transition: payment_received from OrderPaid + #[test] + fn test_payment_received_from_order_paid_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.payment_received(PaymentMethod::Bolt11).unwrap(); + + let result = state.payment_received(PaymentMethod::Bolt11); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test invalid transition: payment_received from CompletedAndChannelOpened + #[test] + fn test_payment_received_from_completed_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.payment_received(PaymentMethod::Bolt11).unwrap(); + state.channel_opened(create_test_channel_info()).unwrap(); + + let result = state.payment_received(PaymentMethod::Bolt11); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test invalid transition: payment_received from FailedAndRefunded + #[test] + fn test_payment_received_from_failed_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.mark_failed_and_refunded().unwrap(); + + let result = state.payment_received(PaymentMethod::Bolt11); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test invalid transition: channel_opened from ExpectingPayment + #[test] + fn test_channel_opened_from_expecting_payment_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + + let result = state.channel_opened(create_test_channel_info()); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test invalid transition: channel_opened from CompletedAndChannelOpened + #[test] + fn test_channel_opened_from_completed_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.payment_received(PaymentMethod::Bolt11).unwrap(); + state.channel_opened(create_test_channel_info()).unwrap(); + + let result = state.channel_opened(create_test_channel_info()); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test invalid transition: channel_opened from FailedAndRefunded + #[test] + fn test_channel_opened_from_failed_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.mark_failed_and_refunded().unwrap(); + + let result = state.channel_opened(create_test_channel_info()); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test invalid transition: mark_failed_and_refunded from CompletedAndChannelOpened + #[test] + fn test_mark_failed_from_completed_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.payment_received(PaymentMethod::Bolt11).unwrap(); + state.channel_opened(create_test_channel_info()).unwrap(); + + let result = state.mark_failed_and_refunded(); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test invalid transition: mark_failed_and_refunded from FailedAndRefunded + #[test] + fn test_mark_failed_from_failed_fails() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + state.mark_failed_and_refunded().unwrap(); + + let result = state.mark_failed_and_refunded(); + assert!(matches!(result, Err(ChannelOrderStateError::InvalidStateTransition { .. }))); + } + + // Test error: payment_received with unconfigured payment method + #[test] + fn test_payment_received_unconfigured_method_fails() { + // Create payment info with only onchain configured + let payment_info = create_test_payment_info_onchain_only(); + let mut state = ChannelOrderState::new(payment_info); + + // Try to mark bolt11 as paid, which is not configured + let result = state.payment_received(PaymentMethod::Bolt11); + assert!(matches!(result, Err(ChannelOrderStateError::PaymentMethodNotConfigured))); + + // State should remain unchanged + assert!(matches!(state, ChannelOrderState::ExpectingPayment { .. })); + } + + // Test that channel_info is only available in CompletedAndChannelOpened state + #[test] + fn test_channel_info_availability() { + let payment_info = create_test_payment_info_bolt11_only(); + let mut state = ChannelOrderState::new(payment_info); + + // Not available in ExpectingPayment + assert!(state.channel_info().is_none()); + + state.payment_received(PaymentMethod::Bolt11).unwrap(); + + // Not available in OrderPaid + assert!(state.channel_info().is_none()); + + let channel_info = create_test_channel_info(); + state.channel_opened(channel_info.clone()).unwrap(); + + // Available in CompletedAndChannelOpened + assert_eq!(state.channel_info(), Some(&channel_info)); + } +} diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index d7010652c37..0e139907589 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -9,160 +9,104 @@ //! Contains the main bLIP-51 / LSPS1 server object, [`LSPS1ServiceHandler`]. -use alloc::string::String; +use alloc::string::ToString; +use alloc::vec::Vec; +use core::future::Future as StdFuture; use core::ops::Deref; +use core::pin::pin; +use core::sync::atomic::{AtomicUsize, Ordering}; +use core::task; use super::event::LSPS1ServiceEvent; use super::msgs::{ LSPS1ChannelInfo, LSPS1CreateOrderRequest, LSPS1CreateOrderResponse, LSPS1GetInfoResponse, LSPS1GetOrderRequest, LSPS1Message, LSPS1Options, LSPS1OrderId, LSPS1OrderParams, - LSPS1OrderState, LSPS1PaymentInfo, LSPS1Request, LSPS1Response, - LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, + LSPS1PaymentInfo, LSPS1PaymentState, LSPS1Request, LSPS1Response, + LSPS1_CREATE_ORDER_REQUEST_OPTION_MISMATCH_ERROR_CODE, + LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, + LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, }; +pub use super::peer_state::PaymentMethod; +use super::peer_state::PeerState; use crate::message_queue::MessageQueue; use crate::events::EventQueue; use crate::lsps0::ser::{ LSPSDateTime, LSPSProtocolMessageHandler, LSPSRequestId, LSPSResponseError, + LSPS0_CLIENT_REJECTED_ERROR_CODE, }; -use crate::prelude::{new_hash_map, HashMap}; +use crate::persist::{ + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use crate::prelude::hash_map::Entry; +use crate::prelude::HashMap; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils; +use crate::utils::async_poll::dummy_waker; +use crate::utils::time::TimeProvider; -use lightning::chain::Filter; use lightning::ln::channelmanager::AChannelManager; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::sign::EntropySource; use lightning::util::errors::APIError; use lightning::util::logger::Level; use lightning::util::persist::KVStore; +use lightning::util::ser::Writeable; use bitcoin::secp256k1::PublicKey; -use chrono::Utc; - /// Server-side configuration options for bLIP-51 / LSPS1 channel requests. #[derive(Clone, Debug)] pub struct LSPS1ServiceConfig { - /// A token to be send with each channel request. - pub token: Option<String>, /// The options supported by the LSP. - pub supported_options: Option<LSPS1Options>, -} - -struct ChannelStateError(String); - -impl From<ChannelStateError> for LightningError { - fn from(value: ChannelStateError) -> Self { - LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } - } -} - -#[derive(PartialEq, Debug)] -enum OutboundRequestState { - OrderCreated { order_id: LSPS1OrderId }, - WaitingPayment { order_id: LSPS1OrderId }, - Ready, -} - -impl OutboundRequestState { - fn awaiting_payment(&self) -> Result<Self, ChannelStateError> { - match self { - OutboundRequestState::OrderCreated { order_id } => { - Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() }) - }, - state => Err(ChannelStateError(format!("TODO. JIT Channel was in state: {:?}", state))), - } - } -} - -struct OutboundLSPS1Config { - order: LSPS1OrderParams, - created_at: LSPSDateTime, - payment: LSPS1PaymentInfo, -} - -struct OutboundCRChannel { - state: OutboundRequestState, - config: OutboundLSPS1Config, -} - -impl OutboundCRChannel { - fn new( - order: LSPS1OrderParams, created_at: LSPSDateTime, order_id: LSPS1OrderId, - payment: LSPS1PaymentInfo, - ) -> Self { - Self { - state: OutboundRequestState::OrderCreated { order_id }, - config: OutboundLSPS1Config { order, created_at, payment }, - } - } - fn awaiting_payment(&mut self) -> Result<(), LightningError> { - self.state = self.state.awaiting_payment()?; - Ok(()) - } - - fn check_order_validity(&self, supported_options: &LSPS1Options) -> bool { - let order = &self.config.order; - - is_valid(order, supported_options) - } + pub supported_options: LSPS1Options, } -#[derive(Default)] -struct PeerState { - outbound_channels_by_order_id: HashMap<LSPS1OrderId, OutboundCRChannel>, - request_to_cid: HashMap<LSPSRequestId, u128>, - pending_requests: HashMap<LSPSRequestId, LSPS1Request>, -} - -impl PeerState { - fn insert_outbound_channel(&mut self, order_id: LSPS1OrderId, channel: OutboundCRChannel) { - self.outbound_channels_by_order_id.insert(order_id, channel); - } - - fn insert_request(&mut self, request_id: LSPSRequestId, channel_id: u128) { - self.request_to_cid.insert(request_id, channel_id); - } - - fn remove_outbound_channel(&mut self, order_id: LSPS1OrderId) { - self.outbound_channels_by_order_id.remove(&order_id); - } -} +const MAX_TOTAL_PEERS: usize = 100000; /// The main object allowing to send and receive bLIP-51 / LSPS1 messages. -pub struct LSPS1ServiceHandler<ES: EntropySource, CM: Deref + Clone, C: Filter, K: KVStore + Clone> -where +pub struct LSPS1ServiceHandler< + ES: EntropySource, + CM: Deref + Clone, + K: KVStore + Clone, + TP: Deref + Clone, +> where CM::Target: AChannelManager, + TP::Target: TimeProvider, { entropy_source: ES, - channel_manager: CM, - chain_source: Option<C>, + _channel_manager: CM, + kv_store: K, pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue<K>>, per_peer_state: RwLock<HashMap<PublicKey, Mutex<PeerState>>>, + persistence_in_flight: AtomicUsize, + time_provider: TP, config: LSPS1ServiceConfig, } -impl<ES: EntropySource, CM: Deref + Clone, C: Filter, K: KVStore + Clone> - LSPS1ServiceHandler<ES, CM, C, K> +impl<ES: EntropySource, CM: Deref + Clone, K: KVStore + Clone, TP: Deref + Clone> + LSPS1ServiceHandler<ES, CM, K, TP> where CM::Target: AChannelManager, + TP::Target: TimeProvider, { /// Constructs a `LSPS1ServiceHandler`. pub(crate) fn new( - entropy_source: ES, pending_messages: Arc<MessageQueue>, - pending_events: Arc<EventQueue<K>>, channel_manager: CM, chain_source: Option<C>, - config: LSPS1ServiceConfig, + per_peer_state: HashMap<PublicKey, Mutex<PeerState>>, entropy_source: ES, + pending_messages: Arc<MessageQueue>, pending_events: Arc<EventQueue<K>>, + channel_manager: CM, kv_store: K, time_provider: TP, config: LSPS1ServiceConfig, ) -> Self { Self { entropy_source, - channel_manager, - chain_source, + _channel_manager: channel_manager, + kv_store, pending_messages, pending_events, - per_peer_state: RwLock::new(new_hash_map()), + per_peer_state: RwLock::new(per_peer_state), + persistence_in_flight: AtomicUsize::new(0), + time_provider, config, } } @@ -178,11 +122,160 @@ where /// `CreateOrder` request and replied with a `CreateOrder` response containing /// an `order_id`. /// Pending requests that are still awaiting our response are deliberately NOT counted. - pub(crate) fn has_active_requests(&self, counterparty_node_id: &PublicKey) -> bool { + pub(crate) fn has_active_orders(&self, counterparty_node_id: &PublicKey) -> bool { let outer_state_lock = self.per_peer_state.read().unwrap(); - outer_state_lock.get(counterparty_node_id).map_or(false, |inner| { + outer_state_lock.get(counterparty_node_id).is_some_and(|inner| { let peer_state = inner.lock().unwrap(); - !peer_state.outbound_channels_by_order_id.is_empty() + peer_state.has_active_orders() + }) + } + + pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { + let outer_state_lock = self.per_peer_state.read().unwrap(); + if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + // We clean up the peer state, but leave removing the peer entry to the prune logic in + // `persist` which removes it from the store. + peer_state_lock.prune_pending_requests(); + peer_state_lock.prune_expired_request_state(); + } + } + + pub(crate) async fn persist(&self) -> Result<bool, lightning::io::Error> { + // TODO: We should eventually persist in parallel, however, when we do, we probably want to + // introduce some batching to upper-bound the number of requests inflight at any given + // time. + + if self.persistence_in_flight.fetch_add(1, Ordering::AcqRel) > 0 { + // If we're not the first event processor to get here, just return early, the increment + // we just did will be treated as "go around again" at the end. + return Ok(false); + } + + let res = self.do_persist().await; + debug_assert!(res.is_err() || self.persistence_in_flight.load(Ordering::Acquire) == 0); + self.persistence_in_flight.store(0, Ordering::Release); + res + } + + async fn do_persist(&self) -> Result<bool, lightning::io::Error> { + let mut did_persist = false; + + loop { + let mut need_remove = Vec::new(); + let mut need_persist = Vec::new(); + + { + // First build a list of peers to persist and prune with the read lock. This allows + // us to avoid the write lock unless we actually need to remove a node. + let outer_state_lock = self.per_peer_state.read().unwrap(); + for (counterparty_node_id, inner_state_lock) in outer_state_lock.iter() { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.prune_expired_request_state(); + let is_prunable = peer_state_lock.is_prunable(); + if is_prunable { + need_remove.push(*counterparty_node_id); + } else if peer_state_lock.needs_persist() { + need_persist.push(*counterparty_node_id); + } + } + } + + for counterparty_node_id in need_persist.into_iter() { + debug_assert!(!need_remove.contains(&counterparty_node_id)); + self.persist_peer_state(counterparty_node_id).await?; + did_persist = true; + } + + for counterparty_node_id in need_remove { + let mut future_opt = None; + { + // We need to take the `per_peer_state` write lock to remove an entry, but also + // have to hold it until after the `remove` call returns (but not through + // future completion) to ensure that writes for the peer's state are + // well-ordered with other `persist_peer_state` calls even across the removal + // itself. + let mut per_peer_state = self.per_peer_state.write().unwrap(); + if let Entry::Occupied(mut entry) = per_peer_state.entry(counterparty_node_id) { + let state = entry.get_mut().get_mut().unwrap(); + if state.is_prunable() { + entry.remove(); + let key = counterparty_node_id.to_string(); + future_opt = Some(self.kv_store.remove( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &key, + true, + )); + } else { + // If the peer got new state, force a re-persist of the current state. + state.set_needs_persist(true); + } + } else { + // This should never happen, we can only have one `persist` call + // in-progress at once and map entries are only removed by it. + debug_assert!(false); + } + } + if let Some(future) = future_opt { + future.await?; + did_persist = true; + } else { + self.persist_peer_state(counterparty_node_id).await?; + } + } + + if self.persistence_in_flight.fetch_sub(1, Ordering::AcqRel) != 1 { + // If another thread incremented the state while we were running we should go + // around again, but only once. + self.persistence_in_flight.store(1, Ordering::Release); + continue; + } + break; + } + + Ok(did_persist) + } + + async fn persist_peer_state( + &self, counterparty_node_id: PublicKey, + ) -> Result<(), lightning::io::Error> { + let fut = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + None => { + // We dropped the peer state by now. + return Ok(()); + }, + Some(entry) => { + let mut peer_state_lock = entry.lock().unwrap(); + if !peer_state_lock.needs_persist() { + // We already have persisted otherwise by now. + return Ok(()); + } else { + peer_state_lock.set_needs_persist(false); + let key = counterparty_node_id.to_string(); + let encoded = peer_state_lock.encode(); + // Begin the write with the entry lock held. This avoids racing with + // potentially-in-flight `persist` calls writing state for the same peer. + self.kv_store.write( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &key, + encoded, + ) + } + }, + } + }; + + fut.await.map_err(|e| { + self.per_peer_state + .read() + .unwrap() + .get(&counterparty_node_id) + .map(|p| p.lock().unwrap().set_needs_persist(true)); + e }) } @@ -192,15 +285,7 @@ where let mut message_queue_notifier = self.pending_messages.notifier(); let response = LSPS1Response::GetInfo(LSPS1GetInfoResponse { - options: self - .config - .supported_options - .clone() - .ok_or(LightningError { - err: format!("Configuration for LSP server not set."), - action: ErrorAction::IgnoreAndLog(Level::Info), - }) - .unwrap(), + options: self.config.supported_options.clone(), }); let msg = LSPS1Message::Response(request_id, response).into(); @@ -215,14 +300,11 @@ where let mut message_queue_notifier = self.pending_messages.notifier(); let event_queue_notifier = self.pending_events.notifier(); - if !is_valid(¶ms.order, &self.config.supported_options.as_ref().unwrap()) { + if !is_valid(¶ms.order, &self.config.supported_options) { let response = LSPS1Response::CreateOrderError(LSPSResponseError { - code: LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, - message: format!("Order does not match options supported by LSP server"), - data: Some(format!( - "Supported options are {:?}", - &self.config.supported_options.as_ref().unwrap() - )), + code: LSPS1_CREATE_ORDER_REQUEST_OPTION_MISMATCH_ERROR_CODE, + message: "Order does not match options supported by LSP server".to_string(), + data: Some(format!("Supported options are {:?}", &self.config.supported_options)), }); let msg = LSPS1Message::Response(request_id, response).into(); message_queue_notifier.enqueue(counterparty_node_id, msg); @@ -237,21 +319,51 @@ where { let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let num_peers = outer_state_lock.len(); - let inner_state_lock = outer_state_lock - .entry(*counterparty_node_id) - .or_insert(Mutex::new(PeerState::default())); - let mut peer_state_lock = inner_state_lock.lock().unwrap(); + let inner_state_entry = outer_state_lock.entry(*counterparty_node_id); - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS1Request::CreateOrder(params.clone())); + if matches!(inner_state_entry, Entry::Vacant(_)) && num_peers >= MAX_TOTAL_PEERS { + let response = LSPS1Response::CreateOrderError(LSPSResponseError { + code: LSPS0_CLIENT_REJECTED_ERROR_CODE, + message: "Reached maximum number of pending requests. Please try again later." + .to_string(), + data: None, + }); + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(counterparty_node_id, msg); + return Err(LightningError { + err: format!( + "Dropping request from peer {} due to reaching maximally allowed number of total peers: {}", + counterparty_node_id, MAX_TOTAL_PEERS + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + } + + let mut peer_state_lock = + inner_state_entry.or_insert(Mutex::new(PeerState::default())).lock().unwrap(); + + let request = LSPS1Request::CreateOrder(params.clone()); + peer_state_lock.register_request(request_id.clone(), request).map_err(|e| { + let err = format!("Failed to handle request due to: {}", e); + let response = LSPS1Response::CreateOrderError(LSPSResponseError { + code: LSPS0_CLIENT_REJECTED_ERROR_CODE, + message: err.clone(), + data: None, + }); + let msg = LSPS1Message::Response(request_id.clone(), response).into(); + message_queue_notifier.enqueue(counterparty_node_id, msg); + let action = ErrorAction::IgnoreAndLog(Level::Error); + LightningError { err, action } + })?; } event_queue_notifier.enqueue(LSPS1ServiceEvent::RequestForPaymentDetails { request_id, counterparty_node_id: *counterparty_node_id, order: params.order, + refund_onchain_address: params.refund_onchain_address, }); Ok(()) @@ -261,50 +373,202 @@ where /// /// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] event. /// + /// Note that the provided `payment_details` can't include the onchain payment variant if the + /// user didn't provide a `refund_onchain_address`. If you *require* onchain payments, you need + /// to call [`Self::onchain_payments_required`] to reject the request. + /// /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails - pub fn send_payment_details( - &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, - payment: LSPS1PaymentInfo, created_at: LSPSDateTime, + pub async fn send_payment_details( + &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey, + payment_details: LSPS1PaymentInfo, ) -> Result<(), APIError> { let mut message_queue_notifier = self.pending_messages.notifier(); + let mut should_persist = false; - let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(counterparty_node_id) { + if payment_details.bolt11.is_none() + && payment_details.bolt12.is_none() + && payment_details.onchain.is_none() + { + let err = "At least one payment option must be provided".to_string(); + return Err(APIError::APIMisuseError { err }); + } + + if payment_details + .bolt11 + .as_ref() + .is_some_and(|b| b.state != LSPS1PaymentState::ExpectPayment) + || payment_details + .bolt12 + .as_ref() + .is_some_and(|b| b.state != LSPS1PaymentState::ExpectPayment) + || payment_details + .onchain + .as_ref() + .is_some_and(|o| o.state != LSPS1PaymentState::ExpectPayment) + { + return Err(APIError::APIMisuseError { + err: "All payment methods must start in ExpectPayment state".to_string(), + }); + } + + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); - match peer_state_lock.pending_requests.remove(&request_id) { - Some(LSPS1Request::CreateOrder(params)) => { + // Validate payment_details against the pending request before removing it, + // so the LSP operator can retry on failure. + if payment_details.onchain.is_some() { + let request = peer_state_lock.get_request(&request_id).map_err(|e| { + let err = format!("Failed to send response due to: {}", e); + APIError::APIMisuseError { err } + })?; + let has_refund_addr = matches!( + request, + LSPS1Request::CreateOrder(p) if p.refund_onchain_address.is_some() + ); + if !has_refund_addr { + // bLIP-51: 'LSP MUST disable on-chain payments if the client omits this field.' + let err = "Onchain payments must be disabled if no refund_onchain_address is set.".to_string(); + return Err(APIError::APIMisuseError { err }); + } + } + + let request = peer_state_lock.remove_request(&request_id).map_err(|e| { + let err = format!("Failed to send response due to: {}", e); + APIError::APIMisuseError { err } + })?; + + match request { + LSPS1Request::CreateOrder(params) => { let order_id = self.generate_order_id(); - let channel = OutboundCRChannel::new( - params.order.clone(), - created_at, - order_id.clone(), - payment.clone(), + let created_at = LSPSDateTime::new_from_duration_since_epoch( + self.time_provider.duration_since_epoch(), ); - peer_state_lock.insert_outbound_channel(order_id.clone(), channel); + let order = peer_state_lock.new_order( + order_id.clone(), + params.order, + created_at, + payment_details, + ); + should_persist |= peer_state_lock.needs_persist(); let response = LSPS1Response::CreateOrder(LSPS1CreateOrderResponse { - order: params.order, order_id, - order_state: LSPS1OrderState::Created, - created_at, - payment, - channel: None, + order_state: order.order_state(), + created_at: order.created_at.clone(), + payment: order.payment_details().clone(), + channel: order.channel_details().cloned(), + order: order.order_params, }); let msg = LSPS1Message::Response(request_id, response).into(); - message_queue_notifier.enqueue(counterparty_node_id, msg); - Ok(()) + message_queue_notifier.enqueue(&counterparty_node_id, msg); + }, + t => { + debug_assert!( + false, + "Failed to send response due to unexpected request type: {:?}", + t + ); + let err = format!( + "Failed to send response due to unexpected request type: {:?}", + t + ); + return Err(APIError::APIMisuseError { err }); }, + } + }, + None => { + return Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {}", counterparty_node_id), + }); + }, + } - _ => Err(APIError::APIMisuseError { - err: format!("No pending buy request for request_id: {:?}", request_id), - }), + if should_persist { + self.persist_peer_state(counterparty_node_id).await.map_err(|e| { + APIError::APIMisuseError { + err: format!( + "Failed to persist peer state for {}: {}", + counterparty_node_id, e + ), } + })?; + } + + Ok(()) + } + + /// Used by LSP to inform a client that an order was rejected because the used token was invalid. + /// + /// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] + /// event if the provided token is invalid. + /// + /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails + pub fn invalid_token_provided( + &self, counterparty_node_id: PublicKey, request_id: LSPSRequestId, + ) -> Result<(), APIError> { + let mut message_queue_notifier = self.pending_messages.notifier(); + + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.remove_request(&request_id).map_err(|e| { + let err = format!("Failed to send response due to: {}", e); + APIError::APIMisuseError { err } + })?; + + let response = LSPS1Response::CreateOrderError(LSPSResponseError { + code: LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, + message: "An unrecognized or stale token was provided".to_string(), + data: None, + }); + + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(&counterparty_node_id, msg); + Ok(()) }, None => Err(APIError::APIMisuseError { - err: format!("No state for the counterparty exists: {:?}", counterparty_node_id), + err: format!("No state for the counterparty exists: {}", counterparty_node_id), + }), + } + } + + /// Used by LSP to inform a client that an order was rejected because they require onchain + /// payments and the client didn't provide a `refund_onchain_address`. + /// + /// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] + /// event if the LSP requires onchain payments and `refund_onchain_address` is `None`. + /// + /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails + pub fn onchain_payments_required( + &self, counterparty_node_id: PublicKey, request_id: LSPSRequestId, + ) -> Result<(), APIError> { + let mut message_queue_notifier = self.pending_messages.notifier(); + + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.remove_request(&request_id).map_err(|e| { + debug_assert!(false, "Failed to send response due to: {}", e); + let err = format!("Failed to send response due to: {}", e); + APIError::APIMisuseError { err } + })?; + + let response = LSPS1Response::CreateOrderError(LSPSResponseError { + code: LSPS1_CREATE_ORDER_REQUEST_OPTION_MISMATCH_ERROR_CODE, + message: + "We require onchain payment but no `refund_onchain_address` was provided" + .to_string(), + data: None, + }); + + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(&counterparty_node_id, msg); + Ok(()) + }, + None => Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {}", counterparty_node_id), }), } } @@ -313,100 +577,179 @@ where &self, request_id: LSPSRequestId, counterparty_node_id: &PublicKey, params: LSPS1GetOrderRequest, ) -> Result<(), LightningError> { - let event_queue_notifier = self.pending_events.notifier(); + let mut message_queue_notifier = self.pending_messages.notifier(); let outer_state_lock = self.per_peer_state.read().unwrap(); match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { - let mut peer_state_lock = inner_state_lock.lock().unwrap(); - - let outbound_channel = peer_state_lock - .outbound_channels_by_order_id - .get_mut(¶ms.order_id) - .ok_or(LightningError { - err: format!( - "Received get order request for unknown order id {:?}", - params.order_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - })?; - - if let Err(e) = outbound_channel.awaiting_payment() { - peer_state_lock.outbound_channels_by_order_id.remove(¶ms.order_id); - event_queue_notifier.enqueue(LSPS1ServiceEvent::Refund { - request_id, - counterparty_node_id: *counterparty_node_id, - order_id: params.order_id, + let peer_state_lock = inner_state_lock.lock().unwrap(); + + let order = peer_state_lock.get_order(¶ms.order_id).map_err(|e| { + let response = LSPS1Response::GetOrderError(LSPSResponseError { + code: LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, + message: "Order with the requested order_id has not been found." + .to_string(), + data: None, }); - return Err(e); - } - - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS1Request::GetOrder(params.clone())); - - event_queue_notifier.enqueue(LSPS1ServiceEvent::CheckPaymentConfirmation { - request_id, - counterparty_node_id: *counterparty_node_id, + let msg = LSPS1Message::Response(request_id.clone(), response).into(); + message_queue_notifier.enqueue(counterparty_node_id, msg); + let err = format!("Failed to handle request due to: {}", e); + let action = ErrorAction::IgnoreAndLog(Level::Error); + LightningError { err, action } + })?; + + let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { order_id: params.order_id, + order: order.order_params.clone(), + order_state: order.order_state(), + created_at: order.created_at.clone(), + payment: order.payment_details().clone(), + channel: order.channel_details().cloned(), }); + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(&counterparty_node_id, msg); + Ok(()) }, None => { - return Err(LightningError { - err: format!("Received error response for a create order request from an unknown counterparty ({:?})", counterparty_node_id), + let response = LSPS1Response::GetOrderError(LSPSResponseError { + code: LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE, + message: "Order with the requested order_id has not been found.".to_string(), + data: None, + }); + let msg = LSPS1Message::Response(request_id, response).into(); + message_queue_notifier.enqueue(counterparty_node_id, msg); + Err(LightningError { + err: format!( + "Received get_order request from an unknown counterparty ({:?})", + counterparty_node_id + ), action: ErrorAction::IgnoreAndLog(Level::Info), + }) + }, + } + } + + /// Marks an order as paid after payment has been received. + /// + /// This should be called when the LSP detects that a Lightning payment has arrived or an + /// on-chain payment has been confirmed. + /// + /// This should be called before opening the channel and the channel should not be opened if + /// this returns an error. + /// + /// Note that in the case of a lightning payment, we expect the payment to have been received + /// (i.e. LDK's [`Event::PaymentClaimable`]) but not claimed (i.e. calling LDK's + /// [`ChannelManager::claim_funds`]), allowing the payment to be returned to the sender if + /// channel opening fails. + /// + /// [`Event::PaymentClaimable`]: lightning::events::Event::PaymentClaimable + /// [`ChannelManager::claim_funds`]: lightning::ln::channelmanager::ChannelManager::claim_funds + pub async fn order_payment_received( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, method: PaymentMethod, + ) -> Result<(), APIError> { + let mut should_persist = false; + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.order_payment_received(&order_id, method).map_err(|e| { + APIError::APIMisuseError { err: format!("Failed to update order: {}", e) } + })?; + should_persist |= peer_state_lock.needs_persist(); + }, + None => { + return Err(APIError::APIMisuseError { + err: format!("No existing state with counterparty {}", counterparty_node_id), }); }, } + if should_persist { + self.persist_peer_state(counterparty_node_id).await.map_err(|e| { + APIError::APIMisuseError { + err: format!( + "Failed to persist peer state for {}: {}", + counterparty_node_id, e + ), + } + })?; + } + Ok(()) } - /// Used by LSP to give details to client regarding the status of channel opening. - /// Called to respond to client's GetOrder request. - /// The LSP continously polls for checking payment confirmation on-chain or lighting - /// and then responds to client request. + /// Marks an order as completed after the channel has been opened. /// - /// Should be called in response to receiving a [`LSPS1ServiceEvent::CheckPaymentConfirmation`] event. - /// - /// [`LSPS1ServiceEvent::CheckPaymentConfirmation`]: crate::lsps1::event::LSPS1ServiceEvent::CheckPaymentConfirmation - pub fn update_order_status( - &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, - order_state: LSPS1OrderState, channel: Option<LSPS1ChannelInfo>, + /// This should be called when the LSP has successfully published the funding + /// transaction for the channel. + pub async fn order_channel_opened( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + channel_info: LSPS1ChannelInfo, ) -> Result<(), APIError> { - let mut message_queue_notifier = self.pending_messages.notifier(); + let mut should_persist = false; + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.order_channel_opened(&order_id, channel_info).map_err(|e| { + APIError::APIMisuseError { err: format!("Failed to update order: {}", e) } + })?; + should_persist |= peer_state_lock.needs_persist(); + }, + None => { + return Err(APIError::APIMisuseError { + err: format!("No existing state with counterparty {}", counterparty_node_id), + }); + }, + } - let outer_state_lock = self.per_peer_state.read().unwrap(); + if should_persist { + self.persist_peer_state(counterparty_node_id).await.map_err(|e| { + APIError::APIMisuseError { + err: format!( + "Failed to persist peer state for {}: {}", + counterparty_node_id, e + ), + } + })?; + } - match outer_state_lock.get(&counterparty_node_id) { + Ok(()) + } + + /// Marks an order as failed and refunded. + /// + /// This should be called when: + /// - The order expires without payment + /// - The channel open fails after payment and the LSP must refund + pub async fn order_failed_and_refunded( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + ) -> Result<(), APIError> { + let mut should_persist = false; + match self.per_peer_state.read().unwrap().get(&counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.order_failed_and_refunded(&order_id).map_err(|e| { + APIError::APIMisuseError { err: format!("Failed to update order: {}", e) } + })?; + should_persist |= peer_state_lock.needs_persist(); + }, + None => { + return Err(APIError::APIMisuseError { + err: format!("No existing state with counterparty {}", counterparty_node_id), + }); + }, + } - if let Some(outbound_channel) = - peer_state_lock.outbound_channels_by_order_id.get_mut(&order_id) - { - let config = &outbound_channel.config; - - let response = LSPS1Response::GetOrder(LSPS1CreateOrderResponse { - order_id, - order: config.order.clone(), - order_state, - created_at: config.created_at.clone(), - payment: config.payment.clone(), - channel, - }); - let msg = LSPS1Message::Response(request_id, response).into(); - message_queue_notifier.enqueue(&counterparty_node_id, msg); - Ok(()) - } else { - Err(APIError::APIMisuseError { - err: format!("Channel with order_id {} not found", order_id.0), - }) + if should_persist { + self.persist_peer_state(counterparty_node_id).await.map_err(|e| { + APIError::APIMisuseError { + err: format!( + "Failed to persist peer state for {}: {}", + counterparty_node_id, e + ), } - }, - None => Err(APIError::APIMisuseError { - err: format!("No existing state with counterparty {}", counterparty_node_id), - }), + })?; } + + Ok(()) } fn generate_order_id(&self) -> LSPS1OrderId { @@ -415,10 +758,11 @@ where } } -impl<ES: EntropySource, CM: Deref + Clone, C: Filter, K: KVStore + Clone> LSPSProtocolMessageHandler - for LSPS1ServiceHandler<ES, CM, C, K> +impl<ES: EntropySource, CM: Deref + Clone, K: KVStore + Clone, TP: Deref + Clone> + LSPSProtocolMessageHandler for LSPS1ServiceHandler<ES, CM, K, TP> where CM::Target: AChannelManager, + TP::Target: TimeProvider, { type ProtocolMessage = LSPS1Message; const PROTOCOL_NUMBER: Option<u16> = Some(1); @@ -427,16 +771,19 @@ where &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, ) -> Result<(), LightningError> { match message { - LSPS1Message::Request(request_id, request) => match request { - LSPS1Request::GetInfo(_) => { - self.handle_get_info_request(request_id, counterparty_node_id) - }, - LSPS1Request::CreateOrder(params) => { - self.handle_create_order_request(request_id, counterparty_node_id, params) - }, - LSPS1Request::GetOrder(params) => { - self.handle_get_order_request(request_id, counterparty_node_id, params) - }, + LSPS1Message::Request(request_id, request) => { + let res = match request { + LSPS1Request::GetInfo(_) => { + self.handle_get_info_request(request_id, counterparty_node_id) + }, + LSPS1Request::CreateOrder(params) => { + self.handle_create_order_request(request_id, counterparty_node_id, params) + }, + LSPS1Request::GetOrder(params) => { + self.handle_get_order_request(request_id, counterparty_node_id, params) + }, + }; + res }, _ => { debug_assert!( @@ -449,12 +796,153 @@ where } } +/// A synchroneous wrapper around [`LSPS1ServiceHandler`] to be used in contexts where async is not +/// available. +pub struct LSPS1ServiceHandlerSync< + 'a, + ES: EntropySource, + CM: Deref + Clone, + K: KVStore + Clone, + TP: Deref + Clone, +> where + CM::Target: AChannelManager, + TP::Target: TimeProvider, +{ + inner: &'a LSPS1ServiceHandler<ES, CM, K, TP>, +} + +impl<'a, ES: EntropySource, CM: Deref + Clone, K: KVStore + Clone, TP: Deref + Clone> + LSPS1ServiceHandlerSync<'a, ES, CM, K, TP> +where + CM::Target: AChannelManager, + TP::Target: TimeProvider, +{ + pub(crate) fn from_inner(inner: &'a LSPS1ServiceHandler<ES, CM, K, TP>) -> Self { + Self { inner } + } + + /// Returns a reference to the used config. + /// + /// Wraps [`LSPS1ServiceHandler::config`]. + pub fn config(&self) -> &LSPS1ServiceConfig { + &self.inner.config + } + + /// Used by LSP to send response containing details regarding the channel fees and payment information. + /// + /// Wraps [`LSPS1ServiceHandler::send_payment_details`]. + pub fn send_payment_details( + &self, request_id: LSPSRequestId, counterparty_node_id: PublicKey, + payment_details: LSPS1PaymentInfo, + ) -> Result<(), APIError> { + let mut fut = pin!(self.inner.send_payment_details( + request_id, + counterparty_node_id, + payment_details + )); + + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match fut.as_mut().poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + // In a sync context, we can't wait for the future to complete. + unreachable!("Should not be pending in a sync context"); + }, + } + } + + /// Used by LSP to inform a client that an order was rejected because the used token was invalid. + /// + /// Wraps [`LSPS1ServiceHandler::invalid_token_provided`]. + pub fn invalid_token_provided( + &self, counterparty_node_id: PublicKey, request_id: LSPSRequestId, + ) -> Result<(), APIError> { + self.inner.invalid_token_provided(counterparty_node_id, request_id) + } + + /// Used by LSP to inform a client that an order was rejected because they require onchain + /// payments and the client didn't provide a `refund_onchain_address`. + /// + /// Wraps [`LSPS1ServiceHandler::onchain_payments_required`]. + pub fn onchain_payments_required( + &self, counterparty_node_id: PublicKey, request_id: LSPSRequestId, + ) -> Result<(), APIError> { + self.inner.onchain_payments_required(counterparty_node_id, request_id) + } + + /// Marks an order as paid after payment has been received. + /// + /// Wraps [`LSPS1ServiceHandler::order_payment_received`]. + pub fn order_payment_received( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, method: PaymentMethod, + ) -> Result<(), APIError> { + let mut fut = + pin!(self.inner.order_payment_received(counterparty_node_id, order_id, method)); + + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match fut.as_mut().poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + // In a sync context, we can't wait for the future to complete. + unreachable!("Should not be pending in a sync context"); + }, + } + } + + /// Marks an order as completed after the channel has been opened. + /// + /// Wraps [`LSPS1ServiceHandler::order_channel_opened`]. + pub fn order_channel_opened( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + channel_info: LSPS1ChannelInfo, + ) -> Result<(), APIError> { + let mut fut = + pin!(self.inner.order_channel_opened(counterparty_node_id, order_id, channel_info)); + + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match fut.as_mut().poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + // In a sync context, we can't wait for the future to complete. + unreachable!("Should not be pending in a sync context"); + }, + } + } + + /// Marks an order as failed and refunded. + /// + /// Wraps [`LSPS1ServiceHandler::order_failed_and_refunded`]. + pub fn order_failed_and_refunded( + &self, counterparty_node_id: PublicKey, order_id: LSPS1OrderId, + ) -> Result<(), APIError> { + let mut fut = pin!(self.inner.order_failed_and_refunded(counterparty_node_id, order_id)); + + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match fut.as_mut().poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + // In a sync context, we can't wait for the future to complete. + unreachable!("Should not be pending in a sync context"); + }, + } + } +} + fn check_range(min: u64, max: u64, value: u64) -> bool { (value >= min) && (value <= max) } fn is_valid(order: &LSPS1OrderParams, options: &LSPS1Options) -> bool { - let bool = check_range( + let channel_balance_sat = match order.lsp_balance_sat.checked_add(order.client_balance_sat) { + Some(sum) => sum, + None => return false, + }; + + check_range( options.min_initial_client_balance_sat, options.max_initial_client_balance_sat, order.client_balance_sat, @@ -466,7 +954,10 @@ fn is_valid(order: &LSPS1OrderParams, options: &LSPS1Options) -> bool { 1, options.max_channel_expiry_blocks.into(), order.channel_expiry_blocks.into(), - ); - - bool + ) && check_range( + options.min_channel_balance_sat, + options.max_channel_balance_sat, + channel_balance_sat, + ) && order.required_channel_confirmations >= options.min_required_channel_confirmations + && order.funding_confirms_within_blocks >= options.min_funding_confirms_within_blocks } diff --git a/lightning-liquidity/src/lsps2/event.rs b/lightning-liquidity/src/lsps2/event.rs index 502429b79ec..956da403e11 100644 --- a/lightning-liquidity/src/lsps2/event.rs +++ b/lightning-liquidity/src/lsps2/event.rs @@ -16,7 +16,7 @@ use alloc::vec::Vec; use bitcoin::secp256k1::PublicKey; -use lightning::impl_writeable_tlv_based_enum; +use lightning::impl_ser_tlv_based_enum; /// An event which an LSPS2 client should take some action in response to. #[derive(Clone, Debug, PartialEq, Eq)] @@ -181,7 +181,7 @@ pub enum LSPS2ServiceEvent { }, } -impl_writeable_tlv_based_enum!(LSPS2ServiceEvent, +impl_ser_tlv_based_enum!(LSPS2ServiceEvent, (0, GetInfo) => { (0, request_id, required), (2, counterparty_node_id, required), diff --git a/lightning-liquidity/src/lsps2/msgs.rs b/lightning-liquidity/src/lsps2/msgs.rs index ba4d0fea4cd..9375069ca0a 100644 --- a/lightning-liquidity/src/lsps2/msgs.rs +++ b/lightning-liquidity/src/lsps2/msgs.rs @@ -21,7 +21,7 @@ use bitcoin::secp256k1::PublicKey; use serde::{Deserialize, Serialize}; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::util::scid_utils; use crate::lsps0::ser::{ @@ -123,7 +123,7 @@ pub struct LSPS2OpeningFeeParams { pub promise: String, } -impl_writeable_tlv_based!(LSPS2OpeningFeeParams, { +impl_ser_tlv_based!(LSPS2OpeningFeeParams, { (0, min_fee_msat, required), (2, proportional, required), (4, valid_until, required), diff --git a/lightning-liquidity/src/lsps2/payment_queue.rs b/lightning-liquidity/src/lsps2/payment_queue.rs index 003939d699d..600f588716c 100644 --- a/lightning-liquidity/src/lsps2/payment_queue.rs +++ b/lightning-liquidity/src/lsps2/payment_queue.rs @@ -9,7 +9,7 @@ use alloc::vec::Vec; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channelmanager::InterceptId; use lightning_types::payment::PaymentHash; @@ -26,21 +26,29 @@ impl PaymentQueue { PaymentQueue { payments: Vec::new() } } + fn payment_status(entry: &PaymentQueueEntry) -> (u64, usize) { + let total_expected_outbound_amount_msat = + entry.htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum(); + (total_expected_outbound_amount_msat, entry.htlcs.len()) + } + pub(crate) fn add_htlc(&mut self, new_htlc: InterceptedHTLC) -> (u64, usize) { + if let Some(entry) = self + .payments + .iter() + .find(|entry| entry.htlcs.iter().any(|htlc| htlc.intercept_id == new_htlc.intercept_id)) + { + debug_assert_eq!(entry.payment_hash, new_htlc.payment_hash); + return Self::payment_status(entry); + } + let payment = self.payments.iter_mut().find(|entry| entry.payment_hash == new_htlc.payment_hash); if let Some(entry) = payment { // HTLCs within a payment should have the same payment hash. debug_assert!(entry.htlcs.iter().all(|htlc| htlc.payment_hash == entry.payment_hash)); - // The given HTLC should not already be present. - debug_assert!(entry - .htlcs - .iter() - .all(|htlc| htlc.intercept_id != new_htlc.intercept_id)); entry.htlcs.push(new_htlc); - let total_expected_outbound_amount_msat = - entry.htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum(); - (total_expected_outbound_amount_msat, entry.htlcs.len()) + Self::payment_status(entry) } else { let expected_outbound_amount_msat = new_htlc.expected_outbound_amount_msat; let entry = @@ -63,7 +71,7 @@ impl PaymentQueue { } } -impl_writeable_tlv_based!(PaymentQueue, { +impl_ser_tlv_based!(PaymentQueue, { (0, payments, optional_vec), }); @@ -73,7 +81,7 @@ pub(crate) struct PaymentQueueEntry { pub(crate) htlcs: Vec<InterceptedHTLC>, } -impl_writeable_tlv_based!(PaymentQueueEntry, { +impl_ser_tlv_based!(PaymentQueueEntry, { (0, payment_hash, required), (2, htlcs, optional_vec), }); @@ -85,7 +93,7 @@ pub(crate) struct InterceptedHTLC { pub(crate) payment_hash: PaymentHash, } -impl_writeable_tlv_based!(InterceptedHTLC, { +impl_ser_tlv_based!(InterceptedHTLC, { (0, intercept_id, required), (2, expected_outbound_amount_msat, required), (4, payment_hash, required), @@ -127,6 +135,15 @@ mod tests { (500_000_000, 2), ); + assert_eq!( + payment_queue.add_htlc(InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([100; 32]), + }), + (500_000_000, 2), + ); + let expected_entry = PaymentQueueEntry { payment_hash: PaymentHash([100; 32]), htlcs: vec![ diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 35942dcd624..5987756be47 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -42,13 +42,13 @@ use crate::utils::async_poll::dummy_waker; use lightning::chain::chaininterface::{BroadcasterInterface, TransactionType}; use lightning::events::HTLCHandlingFailureType; -use lightning::ln::channelmanager::{AChannelManager, FailureCode, InterceptId}; +use lightning::ln::channelmanager::{AChannelManager, InterceptId}; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::ln::types::ChannelId; use lightning::util::errors::APIError; use lightning::util::logger::Level; use lightning::util::ser::Writeable; -use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; +use lightning::{impl_ser_tlv_based, impl_ser_tlv_based_enum}; use lightning_types::payment::PaymentHash; @@ -181,7 +181,7 @@ impl TrustModel { } } -impl_writeable_tlv_based_enum!(TrustModel, +impl_ser_tlv_based_enum!(TrustModel, (0, ClientTrustsLsp) => { (0, funding_tx_broadcast_safe, required), (2, funding_tx, option), @@ -468,7 +468,7 @@ impl OutboundJITChannelState { } } -impl_writeable_tlv_based_enum!(OutboundJITChannelState, +impl_ser_tlv_based_enum!(OutboundJITChannelState, (0, PendingInitialPayment) => { (0, payment_queue, required), }, @@ -499,7 +499,7 @@ struct OutboundJITChannel { trust_model: TrustModel, } -impl_writeable_tlv_based!(OutboundJITChannel, { +impl_ser_tlv_based!(OutboundJITChannel, { (0, state, required), (2, user_channel_id, required), (4, opening_fee_params, required), @@ -644,6 +644,26 @@ impl PeerState { }); } + fn remove_terminal_channel_state(&mut self, channel_id: ChannelId) -> Option<u64> { + let intercept_scid = self.intercept_scid_by_channel_id.get(&channel_id).copied()?; + let should_remove = self + .outbound_channels_by_intercept_scid + .get(&intercept_scid) + .and_then(|entry| entry.get_channel_id()) + .is_some_and(|existing_channel_id| existing_channel_id == channel_id); + + if !should_remove { + return None; + } + + self.outbound_channels_by_intercept_scid.remove(&intercept_scid); + self.intercept_scid_by_channel_id.remove(&channel_id); + self.intercept_scid_by_user_channel_id.retain(|_, iscid| *iscid != intercept_scid); + self.needs_persist = true; + + Some(intercept_scid) + } + fn pending_requests_and_channels(&self) -> usize { let pending_requests = self.pending_requests.len(); let pending_outbound_channels = self @@ -660,7 +680,7 @@ impl PeerState { } } -impl_writeable_tlv_based!(PeerState, { +impl_ser_tlv_based!(PeerState, { (0, outbound_channels_by_intercept_scid, required), (2, intercept_scid_by_user_channel_id, required), (4, intercept_scid_by_channel_id, required), @@ -1252,11 +1272,52 @@ where Ok(()) } + /// Forward [`Event::ChannelClosed`] event parameter into this function. + /// + /// Will prune terminal JIT channel state once the corresponding channel has closed. + /// + /// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed + pub async fn channel_closed(&self, channel_id: ChannelId) -> Result<(), APIError> { + let counterparty_node_id = + self.peer_by_channel_id.read().unwrap().get(&channel_id).copied(); + let Some(counterparty_node_id) = counterparty_node_id else { + return Ok(()); + }; + + let removed_intercept_scid = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + peer_state.remove_terminal_channel_state(channel_id) + }, + None => None, + } + }; + + if let Some(intercept_scid) = removed_intercept_scid { + self.peer_by_intercept_scid.write().unwrap().remove(&intercept_scid); + self.peer_by_channel_id.write().unwrap().remove(&channel_id); + self.persist_peer_state(counterparty_node_id).await.map_err(|e| { + APIError::APIMisuseError { + err: format!( + "Failed to persist peer state after channel {} closed: {}", + channel_id, e + ), + } + })?; + } + + Ok(()) + } + /// Abandons a pending JIT‐open flow for `user_channel_id`, removing all local state. /// /// This removes the intercept SCID, any outbound channel state, and associated /// channel‐ID mappings for the specified `user_channel_id`, but only while no payment /// has been forwarded yet and no channel has been opened on-chain. + /// Any held HTLCs for the pending flow are failed backwards before the local state + /// is removed. /// /// Returns an error if: /// - there is no channel matching `user_channel_id`, or @@ -1292,25 +1353,27 @@ where let jit_channel = peer_state .outbound_channels_by_intercept_scid - .get(&intercept_scid) + .get_mut(&intercept_scid) .ok_or_else(|| APIError::APIMisuseError { - err: format!( - "Failed to map intercept_scid {} for user_channel_id {} to a channel.", - intercept_scid, user_channel_id, - ), - })?; + err: format!( + "Failed to map intercept_scid {} for user_channel_id {} to a channel.", + intercept_scid, user_channel_id, + ), + })?; - let is_pending = matches!( - jit_channel.state, - OutboundJITChannelState::PendingInitialPayment { .. } - | OutboundJITChannelState::PendingChannelOpen { .. } - ); + let intercepted_htlcs = match &mut jit_channel.state { + OutboundJITChannelState::PendingInitialPayment { payment_queue } + | OutboundJITChannelState::PendingChannelOpen { payment_queue, .. } => payment_queue.clear(), + _ => { + return Err(APIError::APIMisuseError { + err: "Cannot abandon channel open after channel creation or payment forwarding" + .to_string(), + }); + }, + }; - if !is_pending { - return Err(APIError::APIMisuseError { - err: "Cannot abandon channel open after channel creation or payment forwarding" - .to_string(), - }); + for htlc in intercepted_htlcs { + let _ = self.channel_manager.get_cm().fail_intercepted_htlc(htlc.intercept_id); } peer_state.intercept_scid_by_user_channel_id.remove(&user_channel_id); @@ -1375,10 +1438,8 @@ where { let intercepted_htlcs = payment_queue.clear(); for htlc in intercepted_htlcs { - self.channel_manager.get_cm().fail_htlc_backwards_with_reason( - &htlc.payment_hash, - FailureCode::TemporaryNodeFailure, - ); + // A missing intercept has already been released; still reset this LSPS2 state. + let _ = self.channel_manager.get_cm().fail_intercepted_htlc(htlc.intercept_id); } jit_channel.state = OutboundJITChannelState::PendingInitialPayment { @@ -1786,14 +1847,22 @@ where // TODO: We should eventually persist in parallel, however, when we do, we probably want to // introduce some batching to upper-bound the number of requests inflight at any given // time. - let mut did_persist = false; if self.persistence_in_flight.fetch_add(1, Ordering::AcqRel) > 0 { // If we're not the first event processor to get here, just return early, the increment // we just did will be treated as "go around again" at the end. - return Ok(did_persist); + return Ok(false); } + let res = self.do_persist().await; + debug_assert!(res.is_err() || self.persistence_in_flight.load(Ordering::Acquire) == 0); + self.persistence_in_flight.store(0, Ordering::Release); + res + } + + async fn do_persist(&self) -> Result<bool, lightning::io::Error> { + let mut did_persist = false; + loop { let mut need_remove = Vec::new(); let mut need_persist = Vec::new(); @@ -1855,6 +1924,7 @@ where did_persist = true; } else { self.persist_peer_state(counterparty_node_id).await?; + did_persist = true; } } @@ -1871,7 +1941,7 @@ where } pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { - let outer_state_lock = self.per_peer_state.write().unwrap(); + let outer_state_lock = self.per_peer_state.read().unwrap(); if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { let mut peer_state_lock = inner_state_lock.lock().unwrap(); // We clean up the peer state, but leave removing the peer entry to the prune logic in @@ -2262,6 +2332,25 @@ where } } + /// Forward [`Event::ChannelClosed`] event parameter into this function. + /// + /// Wraps [`LSPS2ServiceHandler::channel_closed`]. + /// + /// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed + pub fn channel_closed(&self, channel_id: ChannelId) -> Result<(), APIError> { + let mut fut = pin!(self.inner.channel_closed(channel_id)); + + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match fut.as_mut().poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + // In a sync context, we can't wait for the future to complete. + unreachable!("Should not be pending in a sync context"); + }, + } + } + /// Wraps [`LSPS2ServiceHandler::channel_needs_manual_broadcast`]. pub fn channel_needs_manual_broadcast( &self, user_channel_id: u128, counterparty_node_id: &PublicKey, @@ -2353,6 +2442,8 @@ mod tests { use bitcoin::{absolute::LockTime, transaction::Version}; use core::str::FromStr; + use lightning::io::Cursor; + use lightning::util::ser::{Readable, Writeable}; const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000; @@ -2756,6 +2847,118 @@ mod tests { } } + #[test] + fn replayed_intercepted_htlc_after_persist_is_idempotent() { + let payment_size_msat = Some(500_000_000); + let opening_fee_params = LSPS2OpeningFeeParams { + min_fee_msat: 10_000_000, + proportional: 10_000, + valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(), + min_lifetime: 4032, + max_client_to_self_delay: 2016, + min_payment_size_msat: 10_000_000, + max_payment_size_msat: 1_000_000_000, + promise: "ignore".to_string(), + }; + let intercept_scid = 42; + let user_channel_id = 43; + let htlc = InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([2; 32]), + }; + + let mut jit_channel = + OutboundJITChannel::new(payment_size_msat, opening_fee_params, user_channel_id, false); + assert!(matches!( + jit_channel.htlc_intercepted(htlc).unwrap(), + Some(HTLCInterceptedAction::OpenChannel(_)) + )); + + let mut peer_state = PeerState::new(); + peer_state.intercept_scid_by_user_channel_id.insert(user_channel_id, intercept_scid); + peer_state.insert_outbound_channel(intercept_scid, jit_channel); + + let encoded_peer_state = peer_state.encode(); + let mut decoded_peer_state = PeerState::read(&mut Cursor::new(encoded_peer_state)).unwrap(); + let decoded_jit_channel = decoded_peer_state + .outbound_channels_by_intercept_scid + .get_mut(&intercept_scid) + .unwrap(); + + assert!(decoded_jit_channel.htlc_intercepted(htlc).unwrap().is_none()); + + let ForwardPaymentAction(_, fee_payment) = + decoded_jit_channel.channel_ready(ChannelId([3; 32])).unwrap(); + assert_eq!(fee_payment.htlcs, vec![htlc]); + } + + #[test] + fn removes_terminal_state_for_closed_channel() { + let opening_fee_params = LSPS2OpeningFeeParams { + min_fee_msat: 10_000_000, + proportional: 10_000, + valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(), + min_lifetime: 4032, + max_client_to_self_delay: 2016, + min_payment_size_msat: 10_000_000, + max_payment_size_msat: 1_000_000_000, + promise: "ignore".to_string(), + }; + let stale_intercept_scid = 42; + let stale_user_channel_id = 43; + let stale_channel_id = ChannelId([44; 32]); + let live_intercept_scid = 45; + let live_user_channel_id = 46; + let live_channel_id = ChannelId([47; 32]); + + let mut stale_jit_channel = + OutboundJITChannel::new(None, opening_fee_params.clone(), stale_user_channel_id, false); + stale_jit_channel.state = + OutboundJITChannelState::PaymentForwarded { channel_id: stale_channel_id }; + let mut live_jit_channel = + OutboundJITChannel::new(None, opening_fee_params, live_user_channel_id, false); + live_jit_channel.state = + OutboundJITChannelState::PaymentForwarded { channel_id: live_channel_id }; + + let mut peer_state = PeerState::new(); + peer_state.insert_outbound_channel(stale_intercept_scid, stale_jit_channel); + peer_state.insert_outbound_channel(live_intercept_scid, live_jit_channel); + peer_state + .intercept_scid_by_user_channel_id + .insert(stale_user_channel_id, stale_intercept_scid); + peer_state + .intercept_scid_by_user_channel_id + .insert(live_user_channel_id, live_intercept_scid); + peer_state.intercept_scid_by_channel_id.insert(stale_channel_id, stale_intercept_scid); + peer_state.intercept_scid_by_channel_id.insert(live_channel_id, live_intercept_scid); + peer_state.needs_persist = false; + + assert_eq!( + peer_state.remove_terminal_channel_state(stale_channel_id), + Some(stale_intercept_scid) + ); + assert!(!peer_state + .outbound_channels_by_intercept_scid + .contains_key(&stale_intercept_scid)); + assert!(peer_state.outbound_channels_by_intercept_scid.contains_key(&live_intercept_scid)); + assert!(!peer_state.intercept_scid_by_user_channel_id.contains_key(&stale_user_channel_id)); + assert_eq!( + peer_state.intercept_scid_by_user_channel_id.get(&live_user_channel_id), + Some(&live_intercept_scid) + ); + assert!(!peer_state.intercept_scid_by_channel_id.contains_key(&stale_channel_id)); + assert_eq!( + peer_state.intercept_scid_by_channel_id.get(&live_channel_id), + Some(&live_intercept_scid) + ); + assert!(peer_state.needs_persist); + + peer_state.needs_persist = false; + assert_eq!(peer_state.remove_terminal_channel_state(stale_channel_id), None); + assert!(!peer_state.needs_persist); + } + #[test] fn broadcast_not_allowed_after_non_paying_fee_payment_claimed() { let min_fee_msat: u64 = 12345; diff --git a/lightning-liquidity/src/lsps5/event.rs b/lightning-liquidity/src/lsps5/event.rs index 30e3aea5687..fbfbf153421 100644 --- a/lightning-liquidity/src/lsps5/event.rs +++ b/lightning-liquidity/src/lsps5/event.rs @@ -14,7 +14,7 @@ use alloc::string::String; use alloc::vec::Vec; use bitcoin::secp256k1::PublicKey; -use lightning::impl_writeable_tlv_based_enum; +use lightning::impl_ser_tlv_based_enum; use super::msgs::LSPS5AppName; use super::msgs::LSPS5Error; @@ -56,6 +56,9 @@ pub enum LSPS5ServiceEvent { /// /// This is the [`webhook URL`] provided by the client during registration. /// + /// Obviously as the URL provided here is untrusted you should check whether it would + /// access any internal or private resources and decline to send the request if it is. + /// /// [`webhook URL`]: super::msgs::LSPS5WebhookUrl url: LSPS5WebhookUrl, /// Notification method with its parameters. @@ -73,7 +76,7 @@ pub enum LSPS5ServiceEvent { }, } -impl_writeable_tlv_based_enum!(LSPS5ServiceEvent, +impl_ser_tlv_based_enum!(LSPS5ServiceEvent, (0, SendWebhookNotification) => { (0, counterparty_node_id, required), (2, app_name, required), diff --git a/lightning-liquidity/src/lsps5/msgs.rs b/lightning-liquidity/src/lsps5/msgs.rs index 363a3255f92..47f9d6341d8 100644 --- a/lightning-liquidity/src/lsps5/msgs.rs +++ b/lightning-liquidity/src/lsps5/msgs.rs @@ -18,7 +18,7 @@ use super::url_utils::LSPSUrl; use lightning::ln::msgs::DecodeError; use lightning::util::ser::{Readable, Writeable}; -use lightning::{impl_writeable_tlv_based, impl_writeable_tlv_based_enum}; +use lightning::{impl_ser_tlv_based, impl_ser_tlv_based_enum}; use lightning_types::string::UntrustedString; use serde::de::{self, Deserializer, MapAccess, Visitor}; @@ -457,7 +457,11 @@ impl Writeable for LSPS5WebhookUrl { impl Readable for LSPS5WebhookUrl { fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> { - Ok(Self(Readable::read(reader)?)) + let url: LSPSUrl = Readable::read(reader)?; + if url.url().len() > MAX_WEBHOOK_URL_LENGTH { + return Err(DecodeError::InvalidValue); + } + Ok(Self(url)) } } @@ -523,7 +527,7 @@ pub enum WebhookNotificationMethod { LSPS5OnionMessageIncoming, } -impl_writeable_tlv_based_enum!(WebhookNotificationMethod, +impl_ser_tlv_based_enum!(WebhookNotificationMethod, (0, LSPS5WebhookRegistered) => {}, (2, LSPS5PaymentIncoming) => {}, (4, LSPS5ExpirySoon) => { @@ -684,7 +688,7 @@ impl<'de> Deserialize<'de> for WebhookNotification { } } -impl_writeable_tlv_based!(WebhookNotification, { +impl_ser_tlv_based!(WebhookNotification, { (0, method, required), }); @@ -872,7 +876,7 @@ mod tests { } #[test] - fn test_url_security_validation() { + fn test_webhook_url_validation() { let urls_that_should_throw = [ "test-app", "http://example.com/webhook", @@ -902,6 +906,68 @@ mod tests { } } + #[test] + fn test_webhook_url_accepts_https_userinfo_and_ipv6() { + let userinfo_url = + LSPS5WebhookUrl::new("https://user:pass@example.com/webhook".to_string()).unwrap(); + assert_eq!(userinfo_url.as_str(), "https://user:pass@example.com/webhook"); + + let ipv6_url = LSPS5WebhookUrl::new("https://[::1]/webhook".to_string()).unwrap(); + assert_eq!(ipv6_url.as_str(), "https://[::1]/webhook"); + } + + #[test] + fn test_lsps_url_readable_rejects_http() { + use lightning::util::ser::Writeable; + + let raw = + lightning_types::string::UntrustedString("http://example.com/webhook".to_string()); + let encoded = raw.encode(); + let result = LSPSUrl::read(&mut lightning::io::Cursor::new(&encoded)); + assert!(result.is_err(), "LSPSUrl::Readable should reject http:// URLs"); + } + + #[test] + fn test_lsps_url_readable_accepts_https() { + use lightning::util::ser::Writeable; + + let https_url = LSPSUrl::parse("https://example.com/webhook".to_string()).unwrap(); + let encoded = https_url.encode(); + let decoded = LSPSUrl::read(&mut lightning::io::Cursor::new(&encoded)).unwrap(); + assert_eq!(decoded.url(), "https://example.com/webhook"); + } + + #[test] + fn test_webhook_url_readable_rejects_http() { + use lightning::util::ser::Writeable; + + let raw = + lightning_types::string::UntrustedString("http://example.com/webhook".to_string()); + let encoded = raw.encode(); + let result = LSPS5WebhookUrl::read(&mut lightning::io::Cursor::new(&encoded)); + assert!(result.is_err(), "Readable should reject http:// webhook URLs"); + } + + #[test] + fn test_webhook_url_readable_rejects_too_long() { + use lightning::util::ser::Writeable; + + let long_url = LSPSUrl::parse(format!("https://example.com/{}", "a".repeat(2000))).unwrap(); + let encoded = long_url.encode(); + let result = LSPS5WebhookUrl::read(&mut lightning::io::Cursor::new(&encoded)); + assert!(result.is_err(), "Readable should reject URLs exceeding MAX_WEBHOOK_URL_LENGTH"); + } + + #[test] + fn test_webhook_url_readable_accepts_valid_https() { + use lightning::util::ser::Writeable; + + let valid_url = LSPS5WebhookUrl::new("https://example.com/webhook".to_string()).unwrap(); + let encoded = valid_url.encode(); + let decoded = LSPS5WebhookUrl::read(&mut lightning::io::Cursor::new(&encoded)).unwrap(); + assert_eq!(decoded.as_str(), "https://example.com/webhook"); + } + #[test] fn test_webhook_notification_parameter_binding() { let notification = WebhookNotification::expiry_soon(144); diff --git a/lightning-liquidity/src/lsps5/service.rs b/lightning-liquidity/src/lsps5/service.rs index 4678d38dc9a..babed1c7e66 100644 --- a/lightning-liquidity/src/lsps5/service.rs +++ b/lightning-liquidity/src/lsps5/service.rs @@ -27,7 +27,7 @@ use crate::utils::time::TimeProvider; use bitcoin::secp256k1::PublicKey; -use lightning::impl_writeable_tlv_based; +use lightning::impl_ser_tlv_based; use lightning::ln::channelmanager::AChannelManager; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::sign::NodeSigner; @@ -61,12 +61,12 @@ struct Webhook { // Timestamp used for tracking when the webhook was created / updated, or when the last notification was sent. // This is used to determine if the webhook is stale and should be pruned. last_used: LSPSDateTime, - // Timestamp when we last sent a notification to the client. This is used to enforce - // notification cooldowns. + // Timestamp when we last sent a notification to the client. This enforces the notification + // cooldown that protects the client from repeated spammy wake-ups. last_notification_sent: Option<LSPSDateTime>, } -impl_writeable_tlv_based!(Webhook, { +impl_ser_tlv_based!(Webhook, { (0, _app_name, required), (2, url, required), (4, _counterparty_node_id, required), @@ -85,6 +85,12 @@ pub struct LSPS5ServiceConfig { pub const DEFAULT_MAX_WEBHOOKS_PER_CLIENT: u32 = 10; /// Default notification cooldown time in minutes. pub const NOTIFICATION_COOLDOWN_TIME: Duration = Duration::from_secs(60); // 1 minute +/// Minimum time between peer lifecycle events that are allowed to reset notification cooldowns. +/// +/// This is distinct from [`NOTIFICATION_COOLDOWN_TIME`]: that cooldown protects the client from +/// repeated spammy wake-ups, while this reset throttle protects registered notification URLs from +/// amplification via rapid peer connect/disconnect churn. +const NOTIFICATION_COOLDOWN_RESET_INTERVAL: Duration = Duration::from_millis(100); // Default configuration for LSPS5 service. impl Default for LSPS5ServiceConfig { @@ -245,84 +251,98 @@ where // introduce some batching to upper-bound the number of requests inflight at any given // time. - let mut did_persist = false; - if self.persistence_in_flight.fetch_add(1, Ordering::AcqRel) > 0 { // If we're not the first event processor to get here, just return early, the increment // we just did will be treated as "go around again" at the end. - return Ok(did_persist); + return Ok(false); } + let mut did_persist = false; + loop { - let mut need_remove = Vec::new(); - let mut need_persist = Vec::new(); + match self.do_persist().await { + Ok(pass_did_persist) => did_persist |= pass_did_persist, + Err(e) => { + self.persistence_in_flight.store(0, Ordering::Release); + return Err(e); + }, + } - self.check_prune_stale_webhooks(&mut self.per_peer_state.write().unwrap()); - { - let outer_state_lock = self.per_peer_state.read().unwrap(); - - for (client_id, peer_state) in outer_state_lock.iter() { - let is_prunable = peer_state.is_prunable(); - let has_open_channel = self.client_has_open_channel(client_id); - if is_prunable && !has_open_channel { - need_remove.push(*client_id); - } else if peer_state.needs_persist { - need_persist.push(*client_id); - } - } + if self.persistence_in_flight.fetch_sub(1, Ordering::AcqRel) != 1 { + // If another thread incremented the state while we were running we should go + // around again, but only once. + self.persistence_in_flight.store(1, Ordering::Release); + continue; } + break; + } - for client_id in need_persist.into_iter() { - debug_assert!(!need_remove.contains(&client_id)); - self.persist_peer_state(client_id).await?; - did_persist = true; + Ok(did_persist) + } + + async fn do_persist(&self) -> Result<bool, lightning::io::Error> { + let mut did_persist = false; + let mut need_remove = Vec::new(); + let mut need_persist = Vec::new(); + + self.check_prune_stale_webhooks(&mut self.per_peer_state.write().unwrap()); + { + let outer_state_lock = self.per_peer_state.read().unwrap(); + + for (client_id, peer_state) in outer_state_lock.iter() { + let is_prunable = peer_state.is_prunable(); + let has_open_channel = self.client_has_open_channel(client_id); + if is_prunable && !has_open_channel { + need_remove.push(*client_id); + } else if peer_state.needs_persist { + need_persist.push(*client_id); + } } + } + + for client_id in need_persist.into_iter() { + debug_assert!(!need_remove.contains(&client_id)); + self.persist_peer_state(client_id).await?; + did_persist = true; + } - for client_id in need_remove { - let mut future_opt = None; - { - // We need to take the `per_peer_state` write lock to remove an entry, but also - // have to hold it until after the `remove` call returns (but not through - // future completion) to ensure that writes for the peer's state are - // well-ordered with other `persist_peer_state` calls even across the removal - // itself. - let mut per_peer_state = self.per_peer_state.write().unwrap(); - if let Entry::Occupied(mut entry) = per_peer_state.entry(client_id) { - let state = entry.get_mut(); - if state.is_prunable() && !self.client_has_open_channel(&client_id) { - entry.remove(); - let key = client_id.to_string(); - future_opt = Some(self.kv_store.remove( - LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, - LSPS5_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, - &key, - true, - )); - } else { - // If the peer was re-added, force a re-persist of the current state. - state.needs_persist = true; - } + for client_id in need_remove { + let mut future_opt = None; + { + // We need to take the `per_peer_state` write lock to remove an entry, but also + // have to hold it until after the `remove` call returns (but not through + // future completion) to ensure that writes for the peer's state are + // well-ordered with other `persist_peer_state` calls even across the removal + // itself. + let mut per_peer_state = self.per_peer_state.write().unwrap(); + if let Entry::Occupied(mut entry) = per_peer_state.entry(client_id) { + let state = entry.get_mut(); + if state.is_prunable() && !self.client_has_open_channel(&client_id) { + entry.remove(); + let key = client_id.to_string(); + future_opt = Some(self.kv_store.remove( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS5_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &key, + true, + )); } else { - // This should never happen, we can only have one `persist` call - // in-progress at once and map entries are only removed by it. - debug_assert!(false); + // If the peer was re-added, force a re-persist of the current state. + state.needs_persist = true; } - } - if let Some(future) = future_opt { - future.await?; - did_persist = true; } else { - self.persist_peer_state(client_id).await?; + // This should never happen, we can only have one `persist` call + // in-progress at once and map entries are only removed by it. + debug_assert!(false); } } - - if self.persistence_in_flight.fetch_sub(1, Ordering::AcqRel) != 1 { - // If another thread incremented the state while we were running we should go - // around again, but only once. - self.persistence_in_flight.store(1, Ordering::Release); - continue; + if let Some(future) = future_opt { + future.await?; + did_persist = true; + } else { + self.persist_peer_state(client_id).await?; + did_persist = true; } - break; } Ok(did_persist) @@ -676,7 +696,10 @@ where pub(crate) fn peer_connected(&self, counterparty_node_id: &PublicKey) { let mut outer_state_lock = self.per_peer_state.write().unwrap(); if let Some(peer_state) = outer_state_lock.get_mut(counterparty_node_id) { - peer_state.reset_notification_cooldown(); + let now = LSPSDateTime::new_from_duration_since_epoch( + self.time_provider.duration_since_epoch(), + ); + peer_state.reset_notification_cooldown(now); } self.check_prune_stale_webhooks(&mut outer_state_lock); } @@ -684,7 +707,10 @@ where pub(crate) fn peer_disconnected(&self, counterparty_node_id: &PublicKey) { let mut outer_state_lock = self.per_peer_state.write().unwrap(); if let Some(peer_state) = outer_state_lock.get_mut(counterparty_node_id) { - peer_state.reset_notification_cooldown(); + let now = LSPSDateTime::new_from_duration_since_epoch( + self.time_provider.duration_since_epoch(), + ); + peer_state.reset_notification_cooldown(now); } self.check_prune_stale_webhooks(&mut outer_state_lock); } @@ -735,6 +761,11 @@ where #[derive(Debug)] pub(crate) struct PeerState { webhooks: Vec<(LSPS5AppName, Webhook)>, + // Timestamp of the last peer lifecycle event that was allowed to clear notification cooldowns. + // This is not the notification cooldown itself: `last_notification_sent` protects clients from + // repeated wake-ups, while this protects registered notification URLs from amplification via + // rapid connection churn. + last_notification_cooldown_reset: Option<LSPSDateTime>, needs_persist: bool, } @@ -790,10 +821,18 @@ impl PeerState { removed } - fn reset_notification_cooldown(&mut self) { + fn reset_notification_cooldown(&mut self, now: LSPSDateTime) { + let can_reset = self.last_notification_cooldown_reset.as_ref().map_or(true, |last_reset| { + now.duration_since(last_reset) >= NOTIFICATION_COOLDOWN_RESET_INTERVAL + }); + if !can_reset { + return; + } + for (_, h) in self.webhooks.iter_mut() { h.last_notification_sent = None; } + self.last_notification_cooldown_reset = Some(now); self.needs_persist |= true; } @@ -817,11 +856,81 @@ impl Default for PeerState { fn default() -> Self { let webhooks = Vec::new(); let needs_persist = true; - Self { webhooks, needs_persist } + let last_notification_cooldown_reset = None; + Self { webhooks, last_notification_cooldown_reset, needs_persist } } } -impl_writeable_tlv_based!(PeerState, { +impl_ser_tlv_based!(PeerState, { (0, webhooks, required_vec), + (_unused, last_notification_cooldown_reset, (static_value, None::<LSPSDateTime>)), (_unused, needs_persist, (static_value, false)), }); + +#[cfg(test)] +mod tests { + use super::*; + + use crate::alloc::string::ToString; + use crate::tests::utils::parse_pubkey; + + fn lsps_datetime(seconds: u64) -> LSPSDateTime { + LSPSDateTime::new_from_duration_since_epoch(Duration::from_secs(seconds)) + } + + fn lsps_datetime_millis(milliseconds: u64) -> LSPSDateTime { + LSPSDateTime::new_from_duration_since_epoch(Duration::from_millis(milliseconds)) + } + + fn test_webhook(last_notification_sent: Option<LSPSDateTime>) -> (LSPS5AppName, Webhook) { + let app_name = LSPS5AppName::new("test_app".to_string()).unwrap(); + let url = LSPS5WebhookUrl::new("https://example.com/webhook".to_string()).unwrap(); + let counterparty_node_id = + parse_pubkey("02c0ded160a4a70d71058509b647949a938924d3a6e109c6eb6aee8e2bb27dc79c") + .unwrap(); + let webhook = Webhook { + _app_name: app_name.clone(), + url, + _counterparty_node_id: counterparty_node_id, + last_used: lsps_datetime(1_000), + last_notification_sent, + }; + (app_name, webhook) + } + + fn test_peer_state(last_notification_sent: Option<LSPSDateTime>) -> PeerState { + PeerState { + webhooks: vec![test_webhook(last_notification_sent)], + last_notification_cooldown_reset: None, + needs_persist: false, + } + } + + #[test] + fn reset_notification_cooldown_is_throttled() { + let first_reset = lsps_datetime(2_000); + let mut peer_state = test_peer_state(Some(first_reset)); + + peer_state.reset_notification_cooldown(first_reset); + assert_eq!(peer_state.webhooks()[0].1.last_notification_sent, None); + assert_eq!(peer_state.last_notification_cooldown_reset, Some(first_reset)); + assert!(peer_state.needs_persist); + + peer_state.needs_persist = false; + let skipped_reset = lsps_datetime_millis(2_000_099); + let recent_notification = skipped_reset; + peer_state.webhooks_mut()[0].1.last_notification_sent = Some(recent_notification); + peer_state.needs_persist = false; + + peer_state.reset_notification_cooldown(skipped_reset); + assert_eq!(peer_state.webhooks()[0].1.last_notification_sent, Some(recent_notification)); + assert_eq!(peer_state.last_notification_cooldown_reset, Some(first_reset)); + assert!(!peer_state.needs_persist); + + let allowed_reset = lsps_datetime_millis(2_000_100); + peer_state.reset_notification_cooldown(allowed_reset); + assert_eq!(peer_state.webhooks()[0].1.last_notification_sent, None); + assert_eq!(peer_state.last_notification_cooldown_reset, Some(allowed_reset)); + assert!(peer_state.needs_persist); + } +} diff --git a/lightning-liquidity/src/lsps5/url_utils.rs b/lightning-liquidity/src/lsps5/url_utils.rs index 2d49c10ff08..b45152649b4 100644 --- a/lightning-liquidity/src/lsps5/url_utils.rs +++ b/lightning-liquidity/src/lsps5/url_utils.rs @@ -11,15 +11,28 @@ use super::msgs::LSPS5ProtocolError; +use bitreq::Url; use lightning::ln::msgs::DecodeError; use lightning::util::ser::{Readable, Writeable}; -use lightning_types::string::UntrustedString; use alloc::string::String; +use core::hash::{Hash, Hasher}; /// Represents a parsed URL for LSPS5 webhook notifications. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct LSPSUrl(UntrustedString); +#[derive(Debug, Clone, Eq)] +pub struct LSPSUrl(Url); + +impl PartialEq for LSPSUrl { + fn eq(&self, other: &Self) -> bool { + self.0.as_str() == other.0.as_str() + } +} + +impl Hash for LSPSUrl { + fn hash<H: Hasher>(&self, state: &mut H) { + self.0.as_str().hash(state) + } +} impl LSPSUrl { /// Parses a URL string into a URL instance. @@ -30,62 +43,23 @@ impl LSPSUrl { /// # Returns /// A Result containing either the parsed URL or an error message. pub fn parse(url_str: String) -> Result<Self, LSPS5ProtocolError> { - if url_str.chars().any(|c| !Self::is_valid_url_char(c)) { - return Err(LSPS5ProtocolError::UrlParse); - } + let url = Url::parse(&url_str).map_err(|_| LSPS5ProtocolError::UrlParse)?; - let (scheme, remainder) = - url_str.split_once("://").ok_or_else(|| LSPS5ProtocolError::UrlParse)?; - - if !scheme.eq_ignore_ascii_case("https") { + if url.scheme() != "https" { return Err(LSPS5ProtocolError::UnsupportedProtocol); } - let host_section = - remainder.split(['/', '?', '#']).next().ok_or_else(|| LSPS5ProtocolError::UrlParse)?; - - let host_without_auth = host_section - .split('@') - .next_back() - .filter(|s| !s.is_empty()) - .ok_or_else(|| LSPS5ProtocolError::UrlParse)?; - - if host_without_auth.is_empty() - || host_without_auth.chars().any(|c| !Self::is_valid_host_char(c)) - { - return Err(LSPS5ProtocolError::UrlParse); - } - - match host_without_auth.rsplit_once(':') { - Some((hostname, _)) if hostname.is_empty() => return Err(LSPS5ProtocolError::UrlParse), - Some((_, port)) => { - if !port.is_empty() && port.parse::<u16>().is_err() { - return Err(LSPS5ProtocolError::UrlParse); - } - }, - None => {}, - }; - - Ok(LSPSUrl(UntrustedString(url_str))) + Ok(LSPSUrl(url)) } - /// Returns URL length. + /// Returns URL length in bytes. pub fn url_length(&self) -> usize { - self.0 .0.chars().count() + self.0.as_str().len() } /// Returns the full URL string. pub fn url(&self) -> &str { - self.0 .0.as_str() - } - - fn is_valid_url_char(c: char) -> bool { - c.is_ascii_alphanumeric() - || matches!(c, ':' | '/' | '.' | '@' | '?' | '#' | '%' | '-' | '_' | '&' | '=') - } - - fn is_valid_host_char(c: char) -> bool { - c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | ':' | '_') + self.0.as_str() } } @@ -93,12 +67,13 @@ impl Writeable for LSPSUrl { fn write<W: lightning::util::ser::Writer>( &self, writer: &mut W, ) -> Result<(), lightning::io::Error> { - self.0.write(writer) + self.0.as_str().write(writer) } } impl Readable for LSPSUrl { fn read<R: lightning::io::Read>(reader: &mut R) -> Result<Self, DecodeError> { - Ok(Self(Readable::read(reader)?)) + let s: String = Readable::read(reader)?; + Self::parse(s).map_err(|_| DecodeError::InvalidValue) } } diff --git a/lightning-liquidity/src/lsps5/validator.rs b/lightning-liquidity/src/lsps5/validator.rs index 8063ea743b7..50a36ea1d2f 100644 --- a/lightning-liquidity/src/lsps5/validator.rs +++ b/lightning-liquidity/src/lsps5/validator.rs @@ -11,7 +11,6 @@ use super::msgs::LSPS5ClientError; -use crate::alloc::string::ToString; use crate::lsps0::ser::LSPSDateTime; use crate::lsps5::msgs::WebhookNotification; use crate::sync::Mutex; @@ -91,14 +90,17 @@ impl LSPS5Validator { } fn check_for_replay_attack(&self, signature: &str) -> Result<(), LSPS5ClientError> { + // zbase32 decoding accepts case aliases, so canonicalize the cache key + // to match verification semantics without decoding the signature again. + let signature = signature.to_ascii_lowercase(); let mut signatures = self.recent_signatures.lock().unwrap(); - if signatures.contains(&signature.to_string()) { + if signatures.contains(&signature) { return Err(LSPS5ClientError::ReplayAttack); } if signatures.len() == MAX_RECENT_SIGNATURES { signatures.pop_back(); } - signatures.push_front(signature.to_string()); + signatures.push_front(signature); Ok(()) } } diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 1f11fc8add7..b4288ad92eb 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -24,13 +24,13 @@ use crate::lsps5::msgs::LSPS5Message; use crate::lsps5::service::{LSPS5ServiceConfig, LSPS5ServiceHandler}; use crate::message_queue::MessageQueue; use crate::persist::{ - read_event_queue, read_lsps2_service_peer_states, read_lsps5_service_peer_states, + read_event_queue, read_lsps1_service_peer_states, read_lsps2_service_peer_states, + read_lsps5_service_peer_states, }; use crate::lsps1::client::{LSPS1ClientConfig, LSPS1ClientHandler}; use crate::lsps1::msgs::LSPS1Message; -#[cfg(lsps1_service)] -use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler}; +use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler, LSPS1ServiceHandlerSync}; use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler}; use crate::lsps2::msgs::LSPS2Message; @@ -43,8 +43,7 @@ use crate::utils::time::DefaultTimeProvider; use crate::utils::time::TimeProvider; use lightning::chain::chaininterface::BroadcasterInterface; -use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; -use lightning::ln::channelmanager::{AChannelManager, ChainParameters}; +use lightning::ln::channelmanager::AChannelManager; use lightning::ln::msgs::{ErrorAction, LightningError}; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; @@ -72,7 +71,6 @@ const LSPS_FEATURE_BIT: usize = 729; #[derive(Clone)] pub struct LiquidityServiceConfig { /// Optional server-side configuration for LSPS1 channel requests. - #[cfg(lsps1_service)] pub lsps1_service_config: Option<LSPS1ServiceConfig>, /// Optional server-side configuration for JIT channels /// should you want to support them. @@ -111,8 +109,6 @@ pub trait ALiquidityManager { type AChannelManager: AChannelManager + ?Sized; /// A type that may be dereferenced to [`Self::AChannelManager`]. type CM: Deref<Target = Self::AChannelManager> + Clone; - /// A type implementing [`Filter`]. - type C: Filter + Clone; /// A type implementing [`KVStore`]. type K: KVStore + Clone; /// A type implementing [`TimeProvider`]. @@ -128,7 +124,6 @@ pub trait ALiquidityManager { Self::EntropySource, Self::NodeSigner, Self::CM, - Self::C, Self::K, Self::TP, Self::BroadcasterInterface, @@ -139,11 +134,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, K: KVStore + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > ALiquidityManager for LiquidityManager<ES, NS, CM, C, K, TP, T> + > ALiquidityManager for LiquidityManager<ES, NS, CM, K, TP, T> where CM::Target: AChannelManager, TP::Target: TimeProvider, @@ -152,12 +146,11 @@ where type NodeSigner = NS; type AChannelManager = CM::Target; type CM = CM; - type C = C; type K = K; type TimeProvider = TP::Target; type TP = TP; type BroadcasterInterface = T; - fn get_lm(&self) -> &LiquidityManager<ES, NS, CM, C, K, TP, T> { + fn get_lm(&self) -> &LiquidityManager<ES, NS, CM, K, TP, T> { self } } @@ -175,8 +168,6 @@ pub trait ALiquidityManagerSync { type AChannelManager: AChannelManager + ?Sized; /// A type that may be dereferenced to [`Self::AChannelManager`]. type CM: Deref<Target = Self::AChannelManager> + Clone; - /// A type implementing [`Filter`]. - type C: Filter + Clone; /// A type implementing [`KVStoreSync`]. type KVStoreSync: KVStoreSync + ?Sized; /// A type that may be dereferenced to [`Self::KVStoreSync`]. @@ -195,7 +186,6 @@ pub trait ALiquidityManagerSync { Self::EntropySource, Self::NodeSigner, Self::CM, - Self::C, KVStoreSyncWrapper<Self::KS>, Self::TP, Self::BroadcasterInterface, @@ -207,7 +197,6 @@ pub trait ALiquidityManagerSync { Self::EntropySource, Self::NodeSigner, Self::CM, - Self::C, Self::KS, Self::TP, Self::BroadcasterInterface, @@ -218,11 +207,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, KS: Deref + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > ALiquidityManagerSync for LiquidityManagerSync<ES, NS, CM, C, KS, TP, T> + > ALiquidityManagerSync for LiquidityManagerSync<ES, NS, CM, KS, TP, T> where CM::Target: AChannelManager, KS::Target: KVStoreSync, @@ -232,7 +220,6 @@ where type NodeSigner = NS; type AChannelManager = CM::Target; type CM = CM; - type C = C; type KVStoreSync = KS::Target; type KS = KS; type TimeProvider = TP::Target; @@ -246,14 +233,13 @@ where Self::EntropySource, Self::NodeSigner, Self::CM, - Self::C, KVStoreSyncWrapper<Self::KS>, Self::TP, Self::BroadcasterInterface, > { &self.inner } - fn get_lm(&self) -> &LiquidityManagerSync<ES, NS, CM, C, KS, TP, T> { + fn get_lm(&self) -> &LiquidityManagerSync<ES, NS, CM, KS, TP, T> { self } } @@ -270,6 +256,7 @@ where /// - [`Event::ChannelReady`] to [`LSPS2ServiceHandler::channel_ready`] /// - [`Event::HTLCHandlingFailed`] to [`LSPS2ServiceHandler::htlc_handling_failed`] /// - [`Event::PaymentForwarded`] to [`LSPS2ServiceHandler::payment_forwarded`] +/// - [`Event::ChannelClosed`] to [`LSPS2ServiceHandler::channel_closed`] /// /// [`PeerManager`]: lightning::ln::peer_handler::PeerManager /// [`MessageHandler`]: lightning::ln::peer_handler::MessageHandler @@ -277,11 +264,11 @@ where /// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady /// [`Event::HTLCHandlingFailed`]: lightning::events::Event::HTLCHandlingFailed /// [`Event::PaymentForwarded`]: lightning::events::Event::PaymentForwarded +/// [`Event::ChannelClosed`]: lightning::events::Event::ChannelClosed pub struct LiquidityManager< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, K: KVStore + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, @@ -296,8 +283,7 @@ pub struct LiquidityManager< ignored_peers: RwLock<HashSet<PublicKey>>, lsps0_client_handler: LSPS0ClientHandler<ES, K>, lsps0_service_handler: Option<LSPS0ServiceHandler>, - #[cfg(lsps1_service)] - lsps1_service_handler: Option<LSPS1ServiceHandler<ES, CM, C, K>>, + lsps1_service_handler: Option<LSPS1ServiceHandler<ES, CM, K, TP>>, lsps1_client_handler: Option<LSPS1ClientHandler<ES, K>>, lsps2_service_handler: Option<LSPS2ServiceHandler<CM, K, T>>, lsps2_client_handler: Option<LSPS2ClientHandler<ES, K>>, @@ -305,8 +291,6 @@ pub struct LiquidityManager< lsps5_client_handler: Option<LSPS5ClientHandler<ES, K>>, service_config: Option<LiquidityServiceConfig>, _client_config: Option<LiquidityClientConfig>, - best_block: RwLock<Option<BestBlock>>, - _chain_source: Option<C>, pending_msgs_or_needs_persist_notifier: Arc<Notifier>, } @@ -315,10 +299,9 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, K: KVStore + Clone, T: BroadcasterInterface + Clone, - > LiquidityManager<ES, NS, CM, C, K, DefaultTimeProvider, T> + > LiquidityManager<ES, NS, CM, K, DefaultTimeProvider, T> where CM::Target: AChannelManager, { @@ -326,9 +309,8 @@ where /// /// Will read persisted service states from the given [`KVStore`]. pub async fn new( - entropy_source: ES, node_signer: NS, channel_manager: CM, chain_source: Option<C>, - chain_params: Option<ChainParameters>, kv_store: K, transaction_broadcaster: T, - service_config: Option<LiquidityServiceConfig>, + entropy_source: ES, node_signer: NS, channel_manager: CM, kv_store: K, + transaction_broadcaster: T, service_config: Option<LiquidityServiceConfig>, client_config: Option<LiquidityClientConfig>, ) -> Result<Self, lightning::io::Error> { Self::new_with_custom_time_provider( @@ -336,8 +318,6 @@ where node_signer, channel_manager, transaction_broadcaster, - chain_source, - chain_params, kv_store, service_config, client_config, @@ -351,11 +331,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, K: KVStore + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > LiquidityManager<ES, NS, CM, C, K, TP, T> + > LiquidityManager<ES, NS, CM, K, TP, T> where CM::Target: AChannelManager, TP::Target: TimeProvider, @@ -370,8 +349,7 @@ where /// [`LiquidityClientConfig`] and [`LiquidityServiceConfig`]. pub async fn new_with_custom_time_provider( entropy_source: ES, node_signer: NS, channel_manager: CM, transaction_broadcaster: T, - chain_source: Option<C>, chain_params: Option<ChainParameters>, kv_store: K, - service_config: Option<LiquidityServiceConfig>, + kv_store: K, service_config: Option<LiquidityServiceConfig>, client_config: Option<LiquidityClientConfig>, time_provider: TP, ) -> Result<Self, lightning::io::Error> { let pending_msgs_or_needs_persist_notifier = Arc::new(Notifier::new()); @@ -451,7 +429,7 @@ where kv_store.clone(), node_signer, lsps5_service_config.clone(), - time_provider, + time_provider.clone(), )) } else { None @@ -471,24 +449,32 @@ where }) }); - #[cfg(lsps1_service)] - let lsps1_service_handler = service_config.as_ref().and_then(|config| { - if let Some(number) = - <LSPS1ServiceHandler<ES, CM, C, K> as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER - { - supported_protocols.push(number); - } - config.lsps1_service_config.as_ref().map(|config| { - LSPS1ServiceHandler::new( + let lsps1_service_handler = if let Some(service_config) = service_config.as_ref() { + if let Some(lsps1_service_config) = service_config.lsps1_service_config.as_ref() { + if let Some(number) = + <LSPS1ServiceHandler<ES, CM, K, TP> as LSPSProtocolMessageHandler>::PROTOCOL_NUMBER + { + supported_protocols.push(number); + } + + let peer_states = read_lsps1_service_peer_states(kv_store.clone()).await?; + + Some(LSPS1ServiceHandler::new( + peer_states, entropy_source.clone(), Arc::clone(&pending_messages), Arc::clone(&pending_events), channel_manager.clone(), - chain_source.clone(), - config.clone(), - ) - }) - }); + kv_store.clone(), + time_provider, + lsps1_service_config.clone(), + )) + } else { + None + } + } else { + None + }; let lsps0_client_handler = LSPS0ClientHandler::new( entropy_source.clone(), @@ -510,7 +496,6 @@ where lsps0_client_handler, lsps0_service_handler, lsps1_client_handler, - #[cfg(lsps1_service)] lsps1_service_handler, lsps2_client_handler, lsps2_service_handler, @@ -518,8 +503,6 @@ where lsps5_service_handler, service_config, _client_config: client_config, - best_block: RwLock::new(chain_params.map(|chain_params| chain_params.best_block)), - _chain_source: chain_source, pending_msgs_or_needs_persist_notifier, }) } @@ -543,8 +526,7 @@ where } /// Returns a reference to the LSPS1 server-side handler. - #[cfg(lsps1_service)] - pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler<ES, CM, C, K>> { + pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler<ES, CM, K, TP>> { self.lsps1_service_handler.as_ref() } @@ -638,7 +620,7 @@ where /// Persists the state of the service handlers towards the given [`KVStore`] implementation if /// needed. /// - /// Returns `true` if it persisted sevice handler data. + /// Returns `true` if it persisted service handler data. /// /// This will be regularly called by LDK's background processor if necessary and only needs to /// be called manually if it's not utilized. @@ -647,6 +629,10 @@ where let mut did_persist = false; did_persist |= self.pending_events.persist().await?; + if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() { + did_persist |= lsps1_service_handler.persist().await?; + } + if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() { did_persist |= lsps2_service_handler.persist().await?; } @@ -688,18 +674,15 @@ where }, } }, - LSPSMessage::LSPS1(_msg @ LSPS1Message::Request(..)) => { - #[cfg(lsps1_service)] + LSPSMessage::LSPS1(msg @ LSPS1Message::Request(..)) => { match &self.lsps1_service_handler { Some(lsps1_service_handler) => { - lsps1_service_handler.handle_message(_msg, sender_node_id)?; + lsps1_service_handler.handle_message(msg, sender_node_id)?; }, None => { return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node {}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Debug)}); }, } - #[cfg(not(lsps1_service))] - return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node {}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Debug)}); }, LSPSMessage::LSPS2(msg @ LSPS2Message::Response(..)) => { match &self.lsps2_client_handler { @@ -740,18 +723,14 @@ where .lsps2_service_handler .as_ref() .is_some_and(|h| h.has_active_requests(sender_node_id)); - #[cfg(lsps1_service)] - let lsps1_has_active_requests = self + let lsps1_has_active_orders = self .lsps1_service_handler .as_ref() - .is_some_and(|h| h.has_active_requests(sender_node_id)); - #[cfg(not(lsps1_service))] - let lsps1_has_active_requests = false; - + .is_some_and(|h| h.has_active_orders(sender_node_id)); lsps5_service_handler.enforce_prior_activity_or_reject( sender_node_id, lsps2_has_active_requests, - lsps1_has_active_requests, + lsps1_has_active_orders, req_id.clone(), )? } @@ -773,11 +752,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, K: KVStore + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > CustomMessageReader for LiquidityManager<ES, NS, CM, C, K, TP, T> + > CustomMessageReader for LiquidityManager<ES, NS, CM, K, TP, T> where CM::Target: AChannelManager, TP::Target: TimeProvider, @@ -800,11 +778,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, K: KVStore + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > CustomMessageHandler for LiquidityManager<ES, NS, CM, C, K, TP, T> + > CustomMessageHandler for LiquidityManager<ES, NS, CM, K, TP, T> where CM::Target: AChannelManager, TP::Target: TimeProvider, @@ -905,6 +882,10 @@ where // If the peer was misbehaving, drop it from the ignored list to cleanup the kept state. self.ignored_peers.write().unwrap().remove(&counterparty_node_id); + if let Some(lsps1_service_handler) = self.lsps1_service_handler.as_ref() { + lsps1_service_handler.peer_disconnected(counterparty_node_id); + } + if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() { lsps2_service_handler.peer_disconnected(counterparty_node_id); } @@ -925,93 +906,12 @@ where } } -impl< - ES: EntropySource + Clone, - NS: NodeSigner + Clone, - CM: Deref + Clone, - C: Filter + Clone, - K: KVStore + Clone, - TP: Deref + Clone, - T: BroadcasterInterface + Clone, - > Listen for LiquidityManager<ES, NS, CM, C, K, TP, T> -where - CM::Target: AChannelManager, - TP::Target: TimeProvider, -{ - fn filtered_block_connected( - &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - if let Some(best_block) = self.best_block.read().unwrap().as_ref() { - assert_eq!(best_block.block_hash, header.prev_blockhash, - "Blocks must be connected in chain-order - the connected header must build on the last connected header"); - assert_eq!(best_block.height, height - 1, - "Blocks must be connected in chain-order - the connected block height must be one greater than the previous height"); - } - - self.transactions_confirmed(header, txdata, height); - self.best_block_updated(header, height); - } - - fn blocks_disconnected(&self, fork_point: BestBlock) { - if let Some(best_block) = self.best_block.write().unwrap().as_mut() { - assert!(best_block.height > fork_point.height, - "Blocks disconnected must indicate disconnection from the current best height, i.e. the new chain tip must be lower than the previous best height"); - *best_block = fork_point; - } - - // TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler. - // Internally this should call transaction_unconfirmed for all transactions that were - // confirmed at a height <= the one we now disconnected. - } -} - -impl< - ES: EntropySource + Clone, - NS: NodeSigner + Clone, - CM: Deref + Clone, - C: Filter + Clone, - K: KVStore + Clone, - TP: Deref + Clone, - T: BroadcasterInterface + Clone, - > Confirm for LiquidityManager<ES, NS, CM, C, K, TP, T> -where - CM::Target: AChannelManager, - TP::Target: TimeProvider, -{ - fn transactions_confirmed( - &self, _header: &bitcoin::block::Header, _txdata: &chain::transaction::TransactionData, - _height: u32, - ) { - // TODO: Call transactions_confirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. - } - - fn transaction_unconfirmed(&self, _txid: &bitcoin::Txid) { - // TODO: Call transaction_unconfirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. - // Internally this should call transaction_unconfirmed for all transactions that were - // confirmed at a height <= the one we now unconfirmed. - } - - fn best_block_updated(&self, header: &bitcoin::block::Header, height: u32) { - let new_best_block = BestBlock::new(header.block_hash(), height); - *self.best_block.write().unwrap() = Some(new_best_block); - - // TODO: Call best_block_updated on all sub-modules that require it, e.g., LSPS1MessageHandler. - } - - fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, u32, Option<bitcoin::BlockHash>)> { - // TODO: Collect relevant txids from all sub-modules that, e.g., LSPS1MessageHandler. - Vec::new() - } -} - /// A synchroneous wrapper around [`LiquidityManager`] to be used in contexts where async is not /// available. pub struct LiquidityManagerSync< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, KS: Deref + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, @@ -1020,7 +920,7 @@ pub struct LiquidityManagerSync< KS::Target: KVStoreSync, TP::Target: TimeProvider, { - inner: LiquidityManager<ES, NS, CM, C, KVStoreSyncWrapper<KS>, TP, T>, + inner: LiquidityManager<ES, NS, CM, KVStoreSyncWrapper<KS>, TP, T>, } #[cfg(feature = "time")] @@ -1028,10 +928,9 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, KS: Deref + Clone, T: BroadcasterInterface + Clone, - > LiquidityManagerSync<ES, NS, CM, C, KS, DefaultTimeProvider, T> + > LiquidityManagerSync<ES, NS, CM, KS, DefaultTimeProvider, T> where CM::Target: AChannelManager, KS::Target: KVStoreSync, @@ -1040,9 +939,8 @@ where /// /// Wraps [`LiquidityManager::new`]. pub fn new( - entropy_source: ES, node_signer: NS, channel_manager: CM, chain_source: Option<C>, - chain_params: Option<ChainParameters>, kv_store_sync: KS, transaction_broadcaster: T, - service_config: Option<LiquidityServiceConfig>, + entropy_source: ES, node_signer: NS, channel_manager: CM, kv_store_sync: KS, + transaction_broadcaster: T, service_config: Option<LiquidityServiceConfig>, client_config: Option<LiquidityClientConfig>, ) -> Result<Self, lightning::io::Error> { let kv_store = KVStoreSyncWrapper(kv_store_sync); @@ -1051,8 +949,6 @@ where entropy_source, node_signer, channel_manager, - chain_source, - chain_params, kv_store, transaction_broadcaster, service_config, @@ -1076,11 +972,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, KS: Deref + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > LiquidityManagerSync<ES, NS, CM, C, KS, TP, T> + > LiquidityManagerSync<ES, NS, CM, KS, TP, T> where CM::Target: AChannelManager, KS::Target: KVStoreSync, @@ -1090,9 +985,8 @@ where /// /// Wraps [`LiquidityManager::new_with_custom_time_provider`]. pub fn new_with_custom_time_provider( - entropy_source: ES, node_signer: NS, channel_manager: CM, chain_source: Option<C>, - chain_params: Option<ChainParameters>, kv_store_sync: KS, transaction_broadcaster: T, - service_config: Option<LiquidityServiceConfig>, + entropy_source: ES, node_signer: NS, channel_manager: CM, kv_store_sync: KS, + transaction_broadcaster: T, service_config: Option<LiquidityServiceConfig>, client_config: Option<LiquidityClientConfig>, time_provider: TP, ) -> Result<Self, lightning::io::Error> { let kv_store = KVStoreSyncWrapper(kv_store_sync); @@ -1101,8 +995,6 @@ where node_signer, channel_manager, transaction_broadcaster, - chain_source, - chain_params, kv_store, service_config, client_config, @@ -1145,11 +1037,10 @@ where /// Returns a reference to the LSPS1 server-side handler. /// /// Wraps [`LiquidityManager::lsps1_service_handler`]. - #[cfg(lsps1_service)] - pub fn lsps1_service_handler( - &self, - ) -> Option<&LSPS1ServiceHandler<ES, CM, C, KVStoreSyncWrapper<KS>>> { - self.inner.lsps1_service_handler() + pub fn lsps1_service_handler<'a>( + &'a self, + ) -> Option<LSPS1ServiceHandlerSync<'a, ES, CM, KVStoreSyncWrapper<KS>, TP>> { + self.inner.lsps1_service_handler.as_ref().map(|r| LSPS1ServiceHandlerSync::from_inner(r)) } /// Returns a reference to the LSPS2 client-side handler. @@ -1222,7 +1113,7 @@ where /// Persists the state of the service handlers towards the given [`KVStoreSync`] implementation. /// - /// Returns `true` if it persisted sevice handler data. + /// Returns `true` if it persisted service handler data. /// /// Wraps [`LiquidityManager::persist`]. pub fn persist(&self) -> Result<bool, lightning::io::Error> { @@ -1242,11 +1133,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, KS: Deref + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > CustomMessageReader for LiquidityManagerSync<ES, NS, CM, C, KS, TP, T> + > CustomMessageReader for LiquidityManagerSync<ES, NS, CM, KS, TP, T> where CM::Target: AChannelManager, KS::Target: KVStoreSync, @@ -1265,11 +1155,10 @@ impl< ES: EntropySource + Clone, NS: NodeSigner + Clone, CM: Deref + Clone, - C: Filter + Clone, KS: Deref + Clone, TP: Deref + Clone, T: BroadcasterInterface + Clone, - > CustomMessageHandler for LiquidityManagerSync<ES, NS, CM, C, KS, TP, T> + > CustomMessageHandler for LiquidityManagerSync<ES, NS, CM, KS, TP, T> where CM::Target: AChannelManager, KS::Target: KVStoreSync, @@ -1303,63 +1192,3 @@ where self.inner.peer_connected(counterparty_node_id, init_msg, inbound) } } - -impl< - ES: EntropySource + Clone, - NS: NodeSigner + Clone, - CM: Deref + Clone, - C: Filter + Clone, - KS: Deref + Clone, - TP: Deref + Clone, - T: BroadcasterInterface + Clone, - > Listen for LiquidityManagerSync<ES, NS, CM, C, KS, TP, T> -where - CM::Target: AChannelManager, - KS::Target: KVStoreSync, - TP::Target: TimeProvider, -{ - fn filtered_block_connected( - &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - self.inner.filtered_block_connected(header, txdata, height) - } - - fn blocks_disconnected(&self, fork_point: BestBlock) { - self.inner.blocks_disconnected(fork_point); - } -} - -impl< - ES: EntropySource + Clone, - NS: NodeSigner + Clone, - CM: Deref + Clone, - C: Filter + Clone, - KS: Deref + Clone, - TP: Deref + Clone, - T: BroadcasterInterface + Clone, - > Confirm for LiquidityManagerSync<ES, NS, CM, C, KS, TP, T> -where - CM::Target: AChannelManager, - KS::Target: KVStoreSync, - TP::Target: TimeProvider, -{ - fn transactions_confirmed( - &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, - height: u32, - ) { - self.inner.transactions_confirmed(header, txdata, height) - } - - fn transaction_unconfirmed(&self, txid: &bitcoin::Txid) { - self.inner.transaction_unconfirmed(txid) - } - - fn best_block_updated(&self, header: &bitcoin::block::Header, height: u32) { - self.inner.best_block_updated(header, height) - } - - fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, u32, Option<bitcoin::BlockHash>)> { - self.inner.get_relevant_txids() - } -} diff --git a/lightning-liquidity/src/persist.rs b/lightning-liquidity/src/persist.rs index d0199440514..30d78249796 100644 --- a/lightning-liquidity/src/persist.rs +++ b/lightning-liquidity/src/persist.rs @@ -10,6 +10,7 @@ //! Types and utils for persistence. use crate::events::{EventQueueDeserWrapper, LiquidityEvent}; +use crate::lsps1::peer_state::PeerState as LSPS1ServicePeerState; use crate::lsps2::service::PeerState as LSPS2ServicePeerState; use crate::lsps5::service::PeerState as LSPS5ServicePeerState; use crate::prelude::{new_hash_map, HashMap}; @@ -39,6 +40,11 @@ pub const LIQUIDITY_MANAGER_EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE: &str = /// [`LiquidityManager`]: crate::LiquidityManager pub const LIQUIDITY_MANAGER_EVENT_QUEUE_PERSISTENCE_KEY: &str = "event_queue"; +/// The secondary namespace under which the [`LSPS1ServiceHandler`] data will be persisted. +/// +/// [`LSPS1ServiceHandler`]: crate::lsps1::service::LSPS1ServiceHandler +pub const LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE: &str = "lsps1_service"; + /// The secondary namespace under which the [`LSPS2ServiceHandler`] data will be persisted. /// /// [`LSPS2ServiceHandler`]: crate::lsps2::service::LSPS2ServiceHandler @@ -80,6 +86,47 @@ pub(crate) async fn read_event_queue<K: KVStore>( Ok(Some(queue.0)) } +pub(crate) async fn read_lsps1_service_peer_states<K: KVStore>( + kv_store: K, +) -> Result<HashMap<PublicKey, Mutex<LSPS1ServicePeerState>>, lightning::io::Error> { + let mut res = new_hash_map(); + + for stored_key in kv_store + .list( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + ) + .await? + { + let mut reader = Cursor::new( + kv_store + .read( + LIQUIDITY_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + LSPS1_SERVICE_PERSISTENCE_SECONDARY_NAMESPACE, + &stored_key, + ) + .await?, + ); + + let peer_state = LSPS1ServicePeerState::read(&mut reader).map_err(|_| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + "Failed to deserialize LSPS1 peer state", + ) + })?; + + let key = PublicKey::from_str(&stored_key).map_err(|_| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + "Failed to deserialize stored key entry", + ) + })?; + + res.insert(key, Mutex::new(peer_state)); + } + Ok(res) +} + pub(crate) async fn read_lsps2_service_peer_states<K: KVStore>( kv_store: K, ) -> Result<HashMap<PublicKey, Mutex<LSPS2ServicePeerState>>, lightning::io::Error> { diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs index dea987527ad..2716df7c0a3 100644 --- a/lightning-liquidity/tests/common/mod.rs +++ b/lightning-liquidity/tests/common/mod.rs @@ -3,13 +3,9 @@ use lightning_liquidity::utils::time::TimeProvider; use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig}; -use lightning::chain::{BestBlock, Filter}; -use lightning::ln::channelmanager::ChainParameters; use lightning::ln::functional_test_utils::{Node, TestChannelManager}; use lightning::util::test_utils::{TestBroadcaster, TestKeysInterface, TestStore}; -use bitcoin::Network; - use core::ops::Deref; use std::sync::Arc; @@ -26,11 +22,6 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( ) -> (LiquidityNode<'a, 'b, 'c>, LiquidityNode<'a, 'b, 'c>, Option<Node<'a, 'b, 'c>>) { assert!(nodes.len() >= 2, "Need at least two nodes (service and client)"); - let chain_params = ChainParameters { - network: Network::Testnet, - best_block: BestBlock::from_network(Network::Testnet), - }; - let mut nodes_iter = nodes.into_iter(); let service_inner = nodes_iter.next().expect("missing service node"); let client_inner = nodes_iter.next().expect("missing client node"); @@ -40,8 +31,6 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( service_inner.keys_manager, service_inner.keys_manager, service_inner.node, - None::<Arc<dyn Filter + Send + Sync>>, - Some(chain_params.clone()), service_kv_store, service_inner.tx_broadcaster, Some(service_config), @@ -54,8 +43,6 @@ fn build_service_and_client_nodes<'a, 'b, 'c>( client_inner.keys_manager, client_inner.keys_manager, client_inner.node, - None::<Arc<dyn Filter + Send + Sync>>, - Some(chain_params), client_kv_store, client_inner.tx_broadcaster, None, @@ -137,7 +124,6 @@ pub(crate) struct LiquidityNode<'a, 'b, 'c> { &'c TestKeysInterface, &'c TestKeysInterface, &'a TestChannelManager<'b, 'c>, - Arc<dyn Filter + Send + Sync>, Arc<TestStore>, Arc<dyn TimeProvider + Send + Sync>, &'c TestBroadcaster, @@ -151,7 +137,6 @@ impl<'a, 'b, 'c> LiquidityNode<'a, 'b, 'c> { &'c TestKeysInterface, &'c TestKeysInterface, &'a TestChannelManager<'b, 'c>, - Arc<dyn Filter + Send + Sync>, Arc<TestStore>, Arc<dyn TimeProvider + Send + Sync>, &'c TestBroadcaster, diff --git a/lightning-liquidity/tests/lsps0_integration_tests.rs b/lightning-liquidity/tests/lsps0_integration_tests.rs index 423d49785f2..c2e94e30661 100644 --- a/lightning-liquidity/tests/lsps0_integration_tests.rs +++ b/lightning-liquidity/tests/lsps0_integration_tests.rs @@ -6,9 +6,8 @@ use common::{create_service_and_client_nodes, get_lsps_message, LSPSNodes}; use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::event::LSPS0ClientEvent; -#[cfg(lsps1_service)] use lightning_liquidity::lsps1::client::LSPS1ClientConfig; -#[cfg(lsps1_service)] +use lightning_liquidity::lsps1::msgs::LSPS1Options; use lightning_liquidity::lsps1::service::LSPS1ServiceConfig; use lightning_liquidity::lsps2::client::LSPS2ClientConfig; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; @@ -33,11 +32,23 @@ fn list_protocols_integration_test() { let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let promise_secret = [42; 32]; let lsps2_service_config = LSPS2ServiceConfig { promise_secret }; - #[cfg(lsps1_service)] - let lsps1_service_config = LSPS1ServiceConfig { supported_options: None, token: None }; + let lsps1_service_config = { + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + LSPS1ServiceConfig { supported_options } + }; let lsps5_service_config = LSPS5ServiceConfig::default(); let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: Some(lsps1_service_config), lsps2_service_config: Some(lsps2_service_config), lsps5_service_config: Some(lsps5_service_config), @@ -45,14 +56,10 @@ fn list_protocols_integration_test() { }; let lsps2_client_config = LSPS2ClientConfig::default(); - #[cfg(lsps1_service)] let lsps1_client_config: LSPS1ClientConfig = LSPS1ClientConfig { max_channel_fees_msat: None }; let lsps5_client_config = LSPS5ClientConfig::default(); let client_config = LiquidityClientConfig { - #[cfg(lsps1_service)] lsps1_client_config: Some(lsps1_client_config), - #[cfg(not(lsps1_service))] - lsps1_client_config: None, lsps2_client_config: Some(lsps2_client_config), lsps5_client_config: Some(lsps5_client_config), }; @@ -91,16 +98,12 @@ fn list_protocols_integration_test() { protocols, }) => { assert_eq!(counterparty_node_id, client_node_id); - #[cfg(lsps1_service)] { assert!(protocols.contains(&1)); assert!(protocols.contains(&2)); assert!(protocols.contains(&5)); assert_eq!(protocols.len(), 3); } - - #[cfg(not(lsps1_service))] - assert_eq!(protocols, vec![2, 5]); }, _ => panic!("Unexpected event"), } diff --git a/lightning-liquidity/tests/lsps1_integration_tests.rs b/lightning-liquidity/tests/lsps1_integration_tests.rs new file mode 100644 index 00000000000..a177b338ad7 --- /dev/null +++ b/lightning-liquidity/tests/lsps1_integration_tests.rs @@ -0,0 +1,1232 @@ +#![cfg(all(test, feature = "time"))] + +mod common; + +use common::create_service_and_client_nodes_with_kv_stores; +use common::{get_lsps_message, LSPSNodes}; + +use lightning::ln::peer_handler::CustomMessageHandler; +use lightning_liquidity::events::LiquidityEvent; +use lightning_liquidity::lsps0::ser::LSPSDateTime; +use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps1::event::LSPS1ClientEvent; +use lightning_liquidity::lsps1::event::LSPS1ServiceEvent; +use lightning_liquidity::lsps1::msgs::{ + LSPS1ChannelInfo, LSPS1OnchainPaymentInfo, LSPS1Options, LSPS1OrderParams, LSPS1PaymentInfo, + LSPS1PaymentState, +}; +use lightning_liquidity::lsps1::service::{LSPS1ServiceConfig, PaymentMethod}; +use lightning_liquidity::utils::time::DefaultTimeProvider; +use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig}; + +use lightning::ln::functional_test_utils::{ + create_chanmon_cfgs, create_node_cfgs, create_node_chanmgrs, +}; +use lightning::util::test_utils::{TestBroadcaster, TestStore}; + +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Address, Network, OutPoint}; + +use std::str::FromStr; +use std::sync::Arc; + +use lightning::ln::functional_test_utils::{create_network, Node}; +use lightning_liquidity::lsps1::msgs::LSPS1OrderId; +use lightning_liquidity::utils::time::TimeProvider; + +const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; + +fn build_lsps1_configs( + supported_options: LSPS1Options, +) -> (LiquidityServiceConfig, LiquidityClientConfig) { + let lsps1_service_config = LSPS1ServiceConfig { supported_options }; + let service_config = LiquidityServiceConfig { + lsps1_service_config: Some(lsps1_service_config), + lsps2_service_config: None, + lsps5_service_config: None, + advertise_service: true, + }; + + let lsps1_client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let client_config = LiquidityClientConfig { + lsps1_client_config: Some(lsps1_client_config), + lsps2_client_config: None, + lsps5_client_config: None, + }; + + (service_config, client_config) +} + +fn setup_test_lsps1_nodes_with_kv_stores<'a, 'b, 'c>( + nodes: Vec<Node<'a, 'b, 'c>>, service_kv_store: Arc<TestStore>, + client_kv_store: Arc<TestStore>, supported_options: LSPS1Options, +) -> LSPSNodes<'a, 'b, 'c> { + let (service_config, client_config) = build_lsps1_configs(supported_options); + let lsps_nodes = create_service_and_client_nodes_with_kv_stores( + nodes, + service_config, + client_config, + Arc::new(DefaultTimeProvider), + service_kv_store, + client_kv_store, + ); + lsps_nodes +} + +fn setup_test_lsps1_nodes<'a, 'b, 'c>( + nodes: Vec<Node<'a, 'b, 'c>>, supported_options: LSPS1Options, +) -> LSPSNodes<'a, 'b, 'c> { + let service_kv_store = Arc::new(TestStore::new(false)); + let client_kv_store = Arc::new(TestStore::new(false)); + setup_test_lsps1_nodes_with_kv_stores( + nodes, + service_kv_store, + client_kv_store, + supported_options, + ) +} + +#[test] +fn lsps1_happy_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let expected_options_supported = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let LSPSNodes { service_node, client_node } = + setup_test_lsps1_nodes(nodes, expected_options_supported.clone()); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + let request_supported_options_id = client_handler.request_supported_options(service_node_id); + let request_supported_options = get_lsps_message!(client_node, service_node_id); + + service_node + .liquidity_manager + .handle_custom_message(request_supported_options, client_node_id) + .unwrap(); + + let get_info_message = get_lsps_message!(service_node, client_node_id); + + client_node.liquidity_manager.handle_custom_message(get_info_message, service_node_id).unwrap(); + + let get_info_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + request_id, + counterparty_node_id, + supported_options, + }) = get_info_event + { + assert_eq!(request_id, request_supported_options_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(expected_options_supported, supported_options); + } else { + panic!("Unexpected event"); + } + + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let _create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address.clone()), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + let _request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + counterparty_node_id, + order, + refund_onchain_address: refund_addr, + .. + }) = _request_for_payment_event + { + assert_eq!(request_id, _create_order_id.clone()); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(order, order_params); + assert_eq!(refund_addr, Some(refund_onchain_address)); + } else { + panic!("Unexpected event"); + } + + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2025-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + + let onchain: LSPS1OnchainPaymentInfo = + serde_json::from_str(json_str).expect("Failed to parse JSON"); + let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; + service_handler + .send_payment_details(_create_order_id.clone(), client_node_id, payment_info.clone()) + .unwrap(); + + let create_order_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(create_order_response, service_node_id) + .unwrap(); + + let order_created_event = client_node.liquidity_manager.next_event().unwrap(); + let expected_order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) = order_created_event + { + assert_eq!(request_id, _create_order_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(order, order_params); + assert_eq!(payment, payment_info); + assert!(channel.is_none()); + order_id + } else { + panic!("Unexpected event"); + }; + + let check_order_status_id = + client_handler.check_order_status(&service_node_id, expected_order_id.clone()); + let check_order_status = get_lsps_message!(client_node, service_node_id); + + service_node + .liquidity_manager + .handle_custom_message(check_order_status, client_node_id) + .unwrap(); + + let order_status_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(order_status_response, service_node_id) + .unwrap(); + + let order_status_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) = order_status_event + { + assert_eq!(request_id, check_order_status_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(order, order_params); + assert_eq!(payment, payment_info); + assert!(channel.is_none()); + assert_eq!(order_id, expected_order_id); + } else { + panic!("Unexpected event"); + } +} + +#[test] +fn lsps1_service_handler_persistence_across_restarts() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Create shared KV store for service node that will persist across restarts + let service_kv_store = Arc::new(TestStore::new(false)); + let client_kv_store = Arc::new(TestStore::new(false)); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let service_config = LiquidityServiceConfig { + lsps1_service_config: Some(LSPS1ServiceConfig { + supported_options: supported_options.clone(), + }), + lsps2_service_config: None, + lsps5_service_config: None, + advertise_service: true, + }; + let time_provider: Arc<dyn TimeProvider + Send + Sync> = Arc::new(DefaultTimeProvider); + + // Variables to carry state between scopes + let client_node_id: PublicKey; + let expected_order_id: LSPS1OrderId; + let order_params: LSPS1OrderParams; + let payment_info: LSPS1PaymentInfo; + + // First scope: Setup, persistence, and dropping of all node objects + { + let LSPSNodes { service_node, client_node } = setup_test_lsps1_nodes_with_kv_stores( + nodes, + Arc::clone(&service_kv_store), + client_kv_store, + supported_options.clone(), + ); + + let service_node_id = service_node.inner.node.get_our_node_id(); + client_node_id = client_node.inner.node.get_our_node_id(); + + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + // Request supported options + let _request_supported_options_id = + client_handler.request_supported_options(service_node_id); + let request_supported_options = get_lsps_message!(client_node, service_node_id); + + service_node + .liquidity_manager + .handle_custom_message(request_supported_options, client_node_id) + .unwrap(); + + let get_info_message = get_lsps_message!(service_node, client_node_id); + client_node + .liquidity_manager + .handle_custom_message(get_info_message, service_node_id) + .unwrap(); + + let _get_info_event = client_node.liquidity_manager.next_event().unwrap(); + + // Create an order to establish persistent state + order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address.clone()), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + let request_id = + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + .. + }) = request_for_payment_event + { + request_id + } else { + panic!("Unexpected event"); + }; + + // Service sends payment details, creating persistent order state + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2035-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + + let onchain: LSPS1OnchainPaymentInfo = + serde_json::from_str(json_str).expect("Failed to parse JSON"); + payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; + service_handler + .send_payment_details(request_id.clone(), client_node_id, payment_info.clone()) + .unwrap(); + + let create_order_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(create_order_response, service_node_id) + .unwrap(); + + let order_created_event = client_node.liquidity_manager.next_event().unwrap(); + expected_order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + order_id, + .. + }) = order_created_event + { + assert_eq!(request_id, create_order_id); + order_id + } else { + panic!("Unexpected event"); + }; + + // Trigger persistence by calling persist + service_node.liquidity_manager.persist().unwrap(); + + // All node objects are dropped at the end of this scope + } + + // Second scope: Recovery from persisted store and verification + { + // Create fresh node configurations for restart + let node_chanmgrs_restart = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart); + + // Create a new LiquidityManager with the same configuration and KV store to simulate restart + let service_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let client_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let client_kv_store_restart = Arc::new(TestStore::new(false)); + + let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes_restart[0].keys_manager, + nodes_restart[0].keys_manager, + nodes_restart[0].node, + service_kv_store, + service_transaction_broadcaster, + Some(service_config), + None, + Arc::clone(&time_provider), + ) + .unwrap(); + + // Create a fresh client to query the restarted service + let lsps1_client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let client_config = LiquidityClientConfig { + lsps1_client_config: Some(lsps1_client_config), + lsps2_client_config: None, + lsps5_client_config: None, + }; + + let client_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes_restart[1].keys_manager, + nodes_restart[1].keys_manager, + nodes_restart[1].node, + client_kv_store_restart, + client_transaction_broadcaster, + None, + Some(client_config), + time_provider, + ) + .unwrap(); + + let service_node_id = nodes_restart[0].node.get_our_node_id(); + let client_node_id_restart = nodes_restart[1].node.get_our_node_id(); + + // Verify node IDs match (since we use same node_cfgs) + assert_eq!(client_node_id_restart, client_node_id); + + // Use the client to send a GetOrder request + let client_handler = client_lm.lsps1_client_handler().unwrap(); + let check_order_status_id = + client_handler.check_order_status(&service_node_id, expected_order_id.clone()); + + // Get the request message from client + let pending_client_msgs = client_lm.get_and_clear_pending_msg(); + assert_eq!(pending_client_msgs.len(), 1); + let (target_node_id, request_msg) = pending_client_msgs.into_iter().next().unwrap(); + assert_eq!(target_node_id, service_node_id); + + // Pass the request to the restarted service + restarted_service_lm.handle_custom_message(request_msg, client_node_id).unwrap(); + + // Get the response from the service + let pending_service_msgs = restarted_service_lm.get_and_clear_pending_msg(); + assert_eq!(pending_service_msgs.len(), 1); + let (target_node_id, response_msg) = pending_service_msgs.into_iter().next().unwrap(); + assert_eq!(target_node_id, client_node_id); + + // Pass the response to the client + client_lm.handle_custom_message(response_msg, service_node_id).unwrap(); + + // Verify the client receives the order status event with correct data + let order_status_event = client_lm.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { + request_id, + counterparty_node_id, + order_id, + order, + payment, + channel, + }) = order_status_event + { + assert_eq!(request_id, check_order_status_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(order_id, expected_order_id); + assert_eq!(order, order_params); + assert_eq!(payment, payment_info); + assert!(channel.is_none()); + } else { + panic!("Expected OrderStatus event after restart, got: {:?}", order_status_event); + } + } +} + +#[test] +fn lsps1_invalid_token_error() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let LSPSNodes { service_node, client_node } = + setup_test_lsps1_nodes(nodes, supported_options.clone()); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + // Create an order with an invalid token + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: Some("invalid_token".to_string()), + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address.clone()), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + // Service receives the create_order request + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + // Service emits RequestForPaymentDetails event + let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + let request_id = + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + counterparty_node_id, + order, + refund_onchain_address: refund_addr, + .. + }) = request_for_payment_event + { + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(order, order_params); + assert_eq!(refund_addr, Some(refund_onchain_address)); + request_id + } else { + panic!("Unexpected event: expected RequestForPaymentDetails"); + }; + + // Service rejects the order due to invalid token + service_handler.invalid_token_provided(client_node_id, request_id).unwrap(); + + // Get the error response message + let error_response = get_lsps_message!(service_node, client_node_id); + + // Client receives the error response + client_node + .liquidity_manager + .handle_custom_message(error_response, service_node_id) + .unwrap_err(); + + // Client receives OrderRequestFailed event with error code 102 + let error_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderRequestFailed { + request_id, + counterparty_node_id, + error, + }) = error_event + { + assert_eq!(request_id, create_order_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(error.code, 102); // LSPS1_CREATE_ORDER_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE + } else { + panic!("Unexpected event: expected OrderRequestFailed"); + } +} + +#[test] +fn lsps1_order_state_transitions() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let LSPSNodes { service_node, client_node } = + setup_test_lsps1_nodes(nodes, supported_options.clone()); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + // Create an order + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + let request_id = + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + .. + }) = request_for_payment_event + { + request_id + } else { + panic!("Unexpected event"); + }; + + // Send payment details with onchain payment option + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2035-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + + let onchain: LSPS1OnchainPaymentInfo = + serde_json::from_str(json_str).expect("Failed to parse JSON"); + let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; + service_handler + .send_payment_details(request_id.clone(), client_node_id, payment_info.clone()) + .unwrap(); + + let create_order_response = get_lsps_message!(service_node, client_node_id); + client_node + .liquidity_manager + .handle_custom_message(create_order_response, service_node_id) + .unwrap(); + + let order_created_event = client_node.liquidity_manager.next_event().unwrap(); + let order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + order_id, + payment, + .. + }) = order_created_event + { + assert_eq!(request_id, create_order_id); + // Initially, payment state should be ExpectPayment + assert_eq!(payment.onchain.as_ref().unwrap().state, LSPS1PaymentState::ExpectPayment); + order_id + } else { + panic!("Unexpected event"); + }; + + // Test order_payment_received: mark the order as paid + service_handler + .order_payment_received(client_node_id, order_id.clone(), PaymentMethod::Onchain) + .unwrap(); + + // Client checks order status - should see payment state as Paid + let _check_order_id = client_handler.check_order_status(&service_node_id, order_id.clone()); + let check_order = get_lsps_message!(client_node, service_node_id); + service_node.liquidity_manager.handle_custom_message(check_order, client_node_id).unwrap(); + let order_response = get_lsps_message!(service_node, client_node_id); + client_node.liquidity_manager.handle_custom_message(order_response, service_node_id).unwrap(); + + let order_status_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { payment, channel, .. }) = + order_status_event + { + // Payment state should be Hold (payment received but channel not yet opened) + assert_eq!(payment.onchain.as_ref().unwrap().state, LSPS1PaymentState::Hold); + // No channel info yet (order state is still Created internally) + assert!(channel.is_none()); + } else { + panic!("Unexpected event"); + } + + // Test order_channel_opened: mark the channel as opened + let channel_info = LSPS1ChannelInfo { + funded_at: LSPSDateTime::from_str("2035-01-01T00:00:00Z").unwrap(), + funding_outpoint: OutPoint::from_str( + "0301e0480b374b32851a9462db29dc19fe830a7f7d7a88b81612b9d42099c0ae:0", + ) + .unwrap(), + expires_at: LSPSDateTime::from_str("2036-01-01T00:00:00Z").unwrap(), + }; + service_handler + .order_channel_opened(client_node_id, order_id.clone(), channel_info.clone()) + .unwrap(); + + // Client checks order status - should see Completed state with channel info + let _check_order_id = client_handler.check_order_status(&service_node_id, order_id.clone()); + let check_order = get_lsps_message!(client_node, service_node_id); + service_node.liquidity_manager.handle_custom_message(check_order, client_node_id).unwrap(); + let order_response = get_lsps_message!(service_node, client_node_id); + client_node.liquidity_manager.handle_custom_message(order_response, service_node_id).unwrap(); + + let order_status_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { payment, channel, .. }) = + order_status_event + { + // Payment state should now be Paid (channel has been opened) + assert_eq!(payment.onchain.as_ref().unwrap().state, LSPS1PaymentState::Paid); + // Channel info should be present (indicates Completed state) + assert_eq!(channel, Some(channel_info)); + } else { + panic!("Unexpected event"); + } +} + +#[test] +fn lsps1_order_failed_and_refunded() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let LSPSNodes { service_node, client_node } = + setup_test_lsps1_nodes(nodes, supported_options.clone()); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + // Create an order + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + let request_id = + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + .. + }) = request_for_payment_event + { + request_id + } else { + panic!("Unexpected event"); + }; + + // Send payment details + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2035-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + + let onchain: LSPS1OnchainPaymentInfo = + serde_json::from_str(json_str).expect("Failed to parse JSON"); + let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; + service_handler + .send_payment_details(request_id.clone(), client_node_id, payment_info.clone()) + .unwrap(); + + let create_order_response = get_lsps_message!(service_node, client_node_id); + client_node + .liquidity_manager + .handle_custom_message(create_order_response, service_node_id) + .unwrap(); + + let order_created_event = client_node.liquidity_manager.next_event().unwrap(); + let order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + order_id, + .. + }) = order_created_event + { + assert_eq!(request_id, create_order_id); + order_id + } else { + panic!("Unexpected event"); + }; + + // Test order_failed_and_refunded: mark the order as failed + service_handler.order_failed_and_refunded(client_node_id, order_id.clone()).unwrap(); + + // Client checks order status - should see Failed state with Refunded payment + let _check_order_id = client_handler.check_order_status(&service_node_id, order_id.clone()); + let check_order = get_lsps_message!(client_node, service_node_id); + service_node.liquidity_manager.handle_custom_message(check_order, client_node_id).unwrap(); + let order_response = get_lsps_message!(service_node, client_node_id); + client_node.liquidity_manager.handle_custom_message(order_response, service_node_id).unwrap(); + + let order_status_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { payment, channel, .. }) = + order_status_event + { + // Payment state should be Refunded (indicates Failed state) + assert_eq!(payment.onchain.as_ref().unwrap().state, LSPS1PaymentState::Refunded); + // No channel info + assert!(channel.is_none()); + } else { + panic!("Unexpected event"); + } +} + +#[test] +fn lsps1_expired_orders_are_pruned_and_not_persisted() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Create shared KV store for service node that will persist across restarts + let service_kv_store = Arc::new(TestStore::new(false)); + let client_kv_store = Arc::new(TestStore::new(false)); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let service_config = LiquidityServiceConfig { + lsps1_service_config: Some(LSPS1ServiceConfig { + supported_options: supported_options.clone(), + }), + lsps2_service_config: None, + lsps5_service_config: None, + advertise_service: true, + }; + let time_provider: Arc<dyn TimeProvider + Send + Sync> = Arc::new(DefaultTimeProvider); + + // Variables to carry state between scopes + let client_node_id: PublicKey; + let expected_order_id: LSPS1OrderId; + + // First scope: Create an order with EXPIRED payment details + { + let LSPSNodes { service_node, client_node } = setup_test_lsps1_nodes_with_kv_stores( + nodes, + Arc::clone(&service_kv_store), + Arc::clone(&client_kv_store), + supported_options.clone(), + ); + + let service_node_id = service_node.inner.node.get_our_node_id(); + client_node_id = client_node.inner.node.get_our_node_id(); + + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + let service_handler = service_node.liquidity_manager.lsps1_service_handler().unwrap(); + + // Create an order + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + let create_order_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address), + ); + let create_order = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(create_order, client_node_id).unwrap(); + + let request_for_payment_event = service_node.liquidity_manager.next_event().unwrap(); + let request_id = + if let LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + .. + }) = request_for_payment_event + { + request_id + } else { + panic!("Unexpected event"); + }; + + // Send payment details with EXPIRED expiry time (in the past) + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2020-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + + let onchain: LSPS1OnchainPaymentInfo = + serde_json::from_str(json_str).expect("Failed to parse JSON"); + let payment_info = LSPS1PaymentInfo { bolt11: None, bolt12: None, onchain: Some(onchain) }; + service_handler + .send_payment_details(request_id.clone(), client_node_id, payment_info.clone()) + .unwrap(); + + let create_order_response = get_lsps_message!(service_node, client_node_id); + client_node + .liquidity_manager + .handle_custom_message(create_order_response, service_node_id) + .unwrap(); + + let order_created_event = client_node.liquidity_manager.next_event().unwrap(); + expected_order_id = if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + order_id, + .. + }) = order_created_event + { + assert_eq!(request_id, create_order_id); + order_id + } else { + panic!("Unexpected event"); + }; + + // Verify the order exists by querying it (before persist is called) + let _check_order_id = + client_handler.check_order_status(&service_node_id, expected_order_id.clone()); + let check_order = get_lsps_message!(client_node, service_node_id); + service_node.liquidity_manager.handle_custom_message(check_order, client_node_id).unwrap(); + let order_response = get_lsps_message!(service_node, client_node_id); + client_node + .liquidity_manager + .handle_custom_message(order_response, service_node_id) + .unwrap(); + + // Should get the order status (order exists before pruning) + let order_status_event = client_node.liquidity_manager.next_event().unwrap(); + assert!(matches!( + order_status_event, + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { .. }) + )); + + // Now call persist - this should prune the expired order since expires_at is in the past + // (prune_expired_request_state is called during persist) + service_node.liquidity_manager.persist().unwrap(); + + // Try to query the order again - it should fail (order not found) + let _check_order_id = + client_handler.check_order_status(&service_node_id, expected_order_id.clone()); + let check_order = get_lsps_message!(client_node, service_node_id); + + // This should return an error response since the order was pruned + service_node + .liquidity_manager + .handle_custom_message(check_order, client_node_id) + .unwrap_err(); + + let error_response = get_lsps_message!(service_node, client_node_id); + client_node + .liquidity_manager + .handle_custom_message(error_response, service_node_id) + .unwrap_err(); + + // Should get an error event (order not found) + let error_event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderRequestFailed { error, .. }) = + error_event + { + // Error code 101 is LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE + assert_eq!(error.code, 101); + } else { + panic!("Expected OrderRequestFailed event"); + } + + // All node objects are dropped at the end of this scope + } + + // Second scope: Restart and verify pruned order is NOT recovered + { + let node_chanmgrs_restart = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart); + + let service_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + let client_transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); + + let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes_restart[0].keys_manager, + nodes_restart[0].keys_manager, + nodes_restart[0].node, + Arc::clone(&service_kv_store), + service_transaction_broadcaster, + Some(service_config), + None, + Arc::clone(&time_provider), + ) + .unwrap(); + + let lsps1_client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let client_config = LiquidityClientConfig { + lsps1_client_config: Some(lsps1_client_config), + lsps2_client_config: None, + lsps5_client_config: None, + }; + + let client_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes_restart[1].keys_manager, + nodes_restart[1].keys_manager, + nodes_restart[1].node, + Arc::clone(&client_kv_store), + client_transaction_broadcaster, + None, + Some(client_config), + time_provider, + ) + .unwrap(); + + let service_node_id = nodes_restart[0].node.get_our_node_id(); + + // Try to query the previously pruned order - it should NOT be recovered + let client_handler = client_lm.lsps1_client_handler().unwrap(); + let _check_order_id = + client_handler.check_order_status(&service_node_id, expected_order_id.clone()); + + let pending_client_msgs = client_lm.get_and_clear_pending_msg(); + assert_eq!(pending_client_msgs.len(), 1); + let (_, request_msg) = pending_client_msgs.into_iter().next().unwrap(); + + // This should return an error since the order was pruned and not persisted + restarted_service_lm.handle_custom_message(request_msg, client_node_id).unwrap_err(); + + let pending_service_msgs = restarted_service_lm.get_and_clear_pending_msg(); + assert_eq!(pending_service_msgs.len(), 1); + let (_, response_msg) = pending_service_msgs.into_iter().next().unwrap(); + + client_lm.handle_custom_message(response_msg, service_node_id).unwrap_err(); + + // Should get an error event (order not found after restart) + let error_event = client_lm.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderRequestFailed { error, .. }) = + error_event + { + // Error code 101 is LSPS1_GET_ORDER_REQUEST_ORDER_NOT_FOUND_ERROR_CODE + assert_eq!(error.code, 101); + } else { + panic!("Expected OrderRequestFailed event after restart, got: {:?}", error_event); + } + } +} + +#[test] +fn max_pending_requests_per_peer_rejected() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let supported_options = LSPS1Options { + min_required_channel_confirmations: 0, + min_funding_confirms_within_blocks: 6, + supports_zero_channel_reserve: true, + max_channel_expiry_blocks: 144, + min_initial_client_balance_sat: 10_000_000, + max_initial_client_balance_sat: 100_000_000, + min_initial_lsp_balance_sat: 100_000, + max_initial_lsp_balance_sat: 100_000_000, + min_channel_balance_sat: 100_000, + max_channel_balance_sat: 100_000_000, + }; + + let LSPSNodes { service_node, client_node } = + setup_test_lsps1_nodes(nodes, supported_options.clone()); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + let client_handler = client_node.liquidity_manager.lsps1_client_handler().unwrap(); + + let order_params = LSPS1OrderParams { + lsp_balance_sat: 100_000, + client_balance_sat: 10_000_000, + required_channel_confirmations: 0, + funding_confirms_within_blocks: 6, + channel_expiry_blocks: 144, + token: None, + announce_channel: true, + }; + + let refund_onchain_address = + Address::from_str("bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr") + .unwrap() + .assume_checked(); + + // Send MAX_PENDING_REQUESTS_PER_PEER create_order requests, all should succeed. + for _ in 0..MAX_PENDING_REQUESTS_PER_PEER { + let _ = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address.clone()), + ); + let req_msg = get_lsps_message!(client_node, service_node_id); + let result = service_node.liquidity_manager.handle_custom_message(req_msg, client_node_id); + assert!(result.is_ok()); + let event = service_node.liquidity_manager.next_event().unwrap(); + assert!(matches!( + event, + LiquidityEvent::LSPS1Service(LSPS1ServiceEvent::RequestForPaymentDetails { .. }) + )); + } + + // The next request should be rejected due to per-peer limit. + let rejected_req_id = client_handler.create_order( + &service_node_id, + order_params.clone(), + Some(refund_onchain_address), + ); + let rejected_req_msg = get_lsps_message!(client_node, service_node_id); + let result = + service_node.liquidity_manager.handle_custom_message(rejected_req_msg, client_node_id); + assert!(result.is_err(), "We should have hit the per-peer limit"); + + let error_response = get_lsps_message!(service_node, client_node_id); + let result = + client_node.liquidity_manager.handle_custom_message(error_response, service_node_id); + assert!(result.is_err()); + + let event = client_node.liquidity_manager.next_event().unwrap(); + if let LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderRequestFailed { + request_id, + counterparty_node_id, + error, + }) = event + { + assert_eq!(request_id, rejected_req_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(error.code, 1); // LSPS0_CLIENT_REJECTED_ERROR_CODE + } else { + panic!("Expected LSPS1ClientEvent::OrderRequestFailed event"); + } +} diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs index 33a6dd697cf..241fabe5a72 100644 --- a/lightning-liquidity/tests/lsps2_integration_tests.rs +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -7,9 +7,11 @@ use common::{ get_lsps_message, LSPSNodes, LSPSNodesWithPayer, LiquidityNode, }; -use lightning::events::{ClosureReason, Event}; +use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType}; use lightning::get_event_msg; -use lightning::ln::channelmanager::{OptionalBolt11PaymentParams, PaymentId}; +use lightning::ln::channelmanager::{ + OptionalBolt11PaymentParams, PaymentId, TrustedChannelFeatures, +}; use lightning::ln::functional_test_utils::*; use lightning::ln::msgs::BaseMessageHandler; use lightning::ln::msgs::ChannelMessageHandler; @@ -27,8 +29,7 @@ use lightning_liquidity::lsps2::utils::is_valid_opening_fee_params; use lightning_liquidity::utils::time::{DefaultTimeProvider, TimeProvider}; use lightning_liquidity::{LiquidityClientConfig, LiquidityManagerSync, LiquidityServiceConfig}; -use lightning::chain::{BestBlock, Filter}; -use lightning::ln::channelmanager::{ChainParameters, InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; +use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::functional_test_utils::{ create_chanmon_cfgs, create_node_cfgs, create_node_chanmgrs, }; @@ -61,7 +62,6 @@ fn build_lsps2_configs() -> ([u8; 32], LiquidityServiceConfig, LiquidityClientCo let promise_secret = [42; 32]; let lsps2_service_config = LSPS2ServiceConfig { promise_secret }; let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(lsps2_service_config), lsps5_service_config: None, @@ -120,9 +120,9 @@ fn create_jit_invoice( ) -> Result<Bolt11Invoice, ()> { // 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) = node + let (payment_hash, payment_secret, _) = node .node - .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) + .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta), None) .map_err(|e| { log_error!(node.logger, "Failed to register inbound payment: {:?}", e); })?; @@ -453,6 +453,125 @@ fn channel_open_failed() { }; } +#[test] +fn channel_open_failed_releases_intercepted_htlcs() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut service_node_config = test_default_channel_config(); + service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8; + + let mut client_node_config = test_default_channel_config(); + client_node_config.channel_config.accept_underpaying_htlcs = true; + + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(service_node_config), Some(client_node_config), None], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let (lsps_nodes, promise_secret) = setup_test_lsps2_nodes_with_payer(nodes); + let LSPSNodesWithPayer { ref service_node, ref client_node, ref payer_node } = lsps_nodes; + + let payer_node_id = payer_node.node.get_our_node_id(); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + create_chan_between_nodes_with_value(&payer_node, &service_node.inner, 2_000_000, 100_000); + + let intercept_scid = service_node.node.get_intercept_scid(); + let user_channel_id = 42u128; + let cltv_expiry_delta: u32 = 144; + let payment_size_msat = Some(1_000_000); + let fee_base_msat: u64 = 1_000; + + execute_lsps2_dance( + &lsps_nodes, + intercept_scid, + user_channel_id, + cltv_expiry_delta, + promise_secret, + payment_size_msat, + fee_base_msat, + ); + + let invoice = create_jit_invoice( + &client_node, + service_node_id, + intercept_scid, + cltv_expiry_delta, + payment_size_msat, + "channel-open-failed-cleanup", + 3600, + ) + .unwrap(); + + payer_node + .node + .pay_for_bolt11_invoice( + &invoice, + PaymentId(invoice.payment_hash().0), + None, + OptionalBolt11PaymentParams::default(), + ) + .unwrap(); + + check_added_monitors(&payer_node, 1); + let events = payer_node.node.get_and_clear_pending_msg_events(); + let ev = SendEvent::from_event(events[0].clone()); + service_node.inner.node.handle_update_add_htlc(payer_node_id, &ev.msgs[0]); + do_commitment_signed_dance(&service_node.inner, &payer_node, &ev.commitment_msg, false, true); + service_node.inner.node.process_pending_htlc_forwards(); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let intercept_id = match &events[0] { + Event::HTLCIntercepted { + intercept_id, + requested_next_hop_scid, + payment_hash, + expected_outbound_amount_msat, + .. + } => { + assert_eq!(*requested_next_hop_scid, intercept_scid); + service_handler + .htlc_intercepted( + *requested_next_hop_scid, + *intercept_id, + *expected_outbound_amount_msat, + *payment_hash, + ) + .unwrap(); + *intercept_id + }, + other => panic!("Expected HTLCIntercepted, got {:?}", other), + }; + + match service_node.liquidity_manager.next_event().unwrap() { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { .. }) => {}, + other => panic!("Unexpected event: {:?}", other), + }; + + service_handler.channel_open_failed(&client_node_id, user_channel_id).unwrap(); + + let res = service_node.inner.node.fail_intercepted_htlc(intercept_id); + assert!( + res.is_err(), + "channel_open_failed must release the intercepted HTLC via fail_intercepted_htlc, but the entry is still pending: {:?}", + res, + ); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::HTLCHandlingFailed { + failure_type: HTLCHandlingFailureType::InvalidForward { requested_forward_scid }, + .. + } => assert_eq!(*requested_forward_scid, intercept_scid), + other => panic!("Expected HTLCHandlingFailed, got {:?}", other), + } +} + #[test] fn channel_open_failed_nonexistent_channel() { let chanmon_cfgs = create_chanmon_cfgs(2); @@ -563,6 +682,125 @@ fn channel_open_abandoned() { assert!(result.is_err()); } +#[test] +fn channel_open_abandoned_releases_intercepted_htlcs() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut service_node_config = test_default_channel_config(); + service_node_config.htlc_interception_flags = HTLCInterceptionFlags::ToInterceptSCIDs as u8; + + let mut client_node_config = test_default_channel_config(); + client_node_config.channel_config.accept_underpaying_htlcs = true; + + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(service_node_config), Some(client_node_config), None], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let (lsps_nodes, promise_secret) = setup_test_lsps2_nodes_with_payer(nodes); + let LSPSNodesWithPayer { ref service_node, ref client_node, ref payer_node } = lsps_nodes; + + let payer_node_id = payer_node.node.get_our_node_id(); + let service_node_id = service_node.inner.node.get_our_node_id(); + let client_node_id = client_node.inner.node.get_our_node_id(); + + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + create_chan_between_nodes_with_value(&payer_node, &service_node.inner, 2_000_000, 100_000); + + let intercept_scid = service_node.node.get_intercept_scid(); + let user_channel_id = 42u128; + let cltv_expiry_delta: u32 = 144; + let payment_size_msat = Some(1_000_000); + let fee_base_msat: u64 = 1_000; + + execute_lsps2_dance( + &lsps_nodes, + intercept_scid, + user_channel_id, + cltv_expiry_delta, + promise_secret, + payment_size_msat, + fee_base_msat, + ); + + let invoice = create_jit_invoice( + &client_node, + service_node_id, + intercept_scid, + cltv_expiry_delta, + payment_size_msat, + "channel-open-abandoned-cleanup", + 3600, + ) + .unwrap(); + + payer_node + .node + .pay_for_bolt11_invoice( + &invoice, + PaymentId(invoice.payment_hash().0), + None, + OptionalBolt11PaymentParams::default(), + ) + .unwrap(); + + check_added_monitors(&payer_node, 1); + let events = payer_node.node.get_and_clear_pending_msg_events(); + let ev = SendEvent::from_event(events[0].clone()); + service_node.inner.node.handle_update_add_htlc(payer_node_id, &ev.msgs[0]); + do_commitment_signed_dance(&service_node.inner, &payer_node, &ev.commitment_msg, false, true); + service_node.inner.node.process_pending_htlc_forwards(); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let intercept_id = match &events[0] { + Event::HTLCIntercepted { + intercept_id, + requested_next_hop_scid, + payment_hash, + expected_outbound_amount_msat, + .. + } => { + assert_eq!(*requested_next_hop_scid, intercept_scid); + service_handler + .htlc_intercepted( + *requested_next_hop_scid, + *intercept_id, + *expected_outbound_amount_msat, + *payment_hash, + ) + .unwrap(); + *intercept_id + }, + other => panic!("Expected HTLCIntercepted, got {:?}", other), + }; + + match service_node.liquidity_manager.next_event().unwrap() { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { .. }) => {}, + other => panic!("Unexpected event: {:?}", other), + }; + + service_handler.channel_open_abandoned(&client_node_id, user_channel_id).unwrap(); + + let res = service_node.inner.node.fail_intercepted_htlc(intercept_id); + assert!( + res.is_err(), + "channel_open_abandoned must release the intercepted HTLC via fail_intercepted_htlc, but the entry is still pending: {:?}", + res, + ); + + let events = service_node.inner.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::HTLCHandlingFailed { + failure_type: HTLCHandlingFailureType::InvalidForward { requested_forward_scid }, + .. + } => assert_eq!(*requested_forward_scid, intercept_scid), + other => panic!("Expected HTLCHandlingFailed, got {:?}", other), + } +} + #[test] fn channel_open_abandoned_nonexistent_channel() { let chanmon_cfgs = create_chanmon_cfgs(2); @@ -942,7 +1180,6 @@ fn lsps2_service_handler_persistence_across_restarts() { let promise_secret = [42; 32]; let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(LSPS2ServiceConfig { promise_secret }), lsps5_service_config: None, @@ -1071,19 +1308,12 @@ fn lsps2_service_handler_persistence_across_restarts() { let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart); // Create a new LiquidityManager with the same configuration and KV store to simulate restart - let chain_params = ChainParameters { - network: Network::Testnet, - best_block: BestBlock::from_network(Network::Testnet), - }; - let transaction_broadcaster = Arc::new(TestBroadcaster::new(Network::Testnet)); let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider( nodes_restart[0].keys_manager, nodes_restart[0].keys_manager, nodes_restart[0].node, - None::<Arc<dyn Filter + Send + Sync>>, - Some(chain_params), service_kv_store, transaction_broadcaster, Some(service_config), @@ -1331,14 +1561,14 @@ fn client_trusts_lsp_end_to_end_test() { let total_fee_msat = match service_events[0].clone() { Event::PaymentForwarded { - prev_node_id, - next_node_id, + ref prev_htlcs, + ref next_htlcs, skimmed_fee_msat, total_fee_earned_msat, .. } => { - assert_eq!(prev_node_id, Some(payer_node_id)); - assert_eq!(next_node_id, Some(client_node_id)); + assert_eq!(prev_htlcs[0].node_id, Some(payer_node_id)); + assert_eq!(next_htlcs[0].node_id, Some(client_node_id)); service_handler.payment_forwarded(channel_id, skimmed_fee_msat.unwrap_or(0)).unwrap(); Some(total_fee_earned_msat.unwrap() - skimmed_fee_msat.unwrap()) }, @@ -1513,10 +1743,11 @@ fn create_channel_with_manual_broadcast( Event::OpenChannelRequest { temporary_channel_id, .. } => { client_node .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( &temporary_channel_id, &service_node_id, user_channel_id, + TrustedChannelFeatures::ZeroConf, None, ) .unwrap(); diff --git a/lightning-liquidity/tests/lsps5_integration_tests.rs b/lightning-liquidity/tests/lsps5_integration_tests.rs index 16f20fd095f..07e0351ac09 100644 --- a/lightning-liquidity/tests/lsps5_integration_tests.rs +++ b/lightning-liquidity/tests/lsps5_integration_tests.rs @@ -7,9 +7,8 @@ use common::{ get_lsps_message, LSPSNodes, LiquidityNode, }; -use lightning::chain::{BestBlock, Filter}; use lightning::events::ClosureReason; -use lightning::ln::channelmanager::{ChainParameters, InterceptId}; +use lightning::ln::channelmanager::InterceptId; use lightning::ln::functional_test_utils::{ check_closed_event, close_channel, create_chan_between_nodes, create_chanmon_cfgs, create_network, create_node_cfgs, create_node_chanmgrs, Node, @@ -43,8 +42,6 @@ use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; use lightning_types::payment::PaymentHash; -use bitcoin::Network; - use std::str::FromStr; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -55,7 +52,6 @@ pub(crate) fn lsps5_test_setup_with_kv_stores<'a, 'b, 'c>( ) -> (LSPSNodes<'a, 'b, 'c>, LSPS5Validator) { let lsps5_service_config = LSPS5ServiceConfig::default(); let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: None, lsps5_service_config: Some(lsps5_service_config), @@ -239,7 +235,6 @@ pub(crate) fn lsps5_lsps2_test_setup<'a, 'b, 'c>( let lsps5_service_config = LSPS5ServiceConfig::default(); let lsps2_service_config = LSPS2ServiceConfig { promise_secret: [42; 32] }; let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: Some(lsps2_service_config), lsps5_service_config: Some(lsps5_service_config), @@ -279,6 +274,11 @@ impl MockTimeProvider { let mut time = self.current_time.write().unwrap(); *time += Duration::from_secs(seconds); } + + fn advance_time_millis(&self, milliseconds: u64) { + let mut time = self.current_time.write().unwrap(); + *time += Duration::from_millis(milliseconds); + } } impl TimeProvider for MockTimeProvider { @@ -993,6 +993,16 @@ fn replay_prevention_test() { assert!(replay_result.is_err(), "Immediate replay attack should be detected"); assert_eq!(replay_result.unwrap_err(), LSPS5ClientError::ReplayAttack); + let case_modified_signature = signature.to_ascii_uppercase(); + assert_ne!(case_modified_signature, signature); + let case_modified_replay_result = + validator.validate(service_node_id, ×tamp, &case_modified_signature, &body); + assert!( + case_modified_replay_result.is_err(), + "Immediate replay attack should be detected when the signature case changes" + ); + assert_eq!(case_modified_replay_result.unwrap_err(), LSPS5ClientError::ReplayAttack); + // Fill up the validator's signature cache to push out the original signature. for i in 0..MAX_RECENT_SIGNATURES { // Advance time, allowing for another notification @@ -1296,7 +1306,7 @@ fn test_notify_without_webhooks_does_nothing() { } #[test] -fn test_notifications_and_peer_connected_resets_cooldown() { +fn test_notifications_and_peer_connected_reset_is_throttled() { let mock_time_provider = Arc::new(MockTimeProvider::new(1000)); let time_provider = Arc::<MockTimeProvider>::clone(&mock_time_provider); let chanmon_cfgs = create_chanmon_cfgs(2); @@ -1374,7 +1384,7 @@ fn test_notifications_and_peer_connected_resets_cooldown() { "Should not emit event due to cooldown" ); - // 5. After peer_connected, notification should be sent again immediately + // 5. The first peer_connected reset should allow another notification immediately. let init_msg = Init { features: lightning_types::features::InitFeatures::empty(), remote_network_address: None, @@ -1392,6 +1402,31 @@ fn test_notifications_and_peer_connected_resets_cooldown() { }, _ => panic!("Expected SendWebhookNotification event after peer_connected"), } + + // 6. A rapid peer lifecycle update should not clear the cooldown again. + service_node.liquidity_manager.peer_disconnected(client_node_id); + let result = service_handler.notify_payment_incoming(client_node_id); + let error = result.unwrap_err(); + assert_eq!(error, LSPS5ProtocolError::SlowDownError); + assert!( + service_node.liquidity_manager.next_event().is_none(), + "Should not emit event after a rapid lifecycle reset" + ); + + // 7. Once the reset throttle has elapsed, peer_connected can reset the cooldown again. + mock_time_provider.advance_time_millis(100); + service_node.liquidity_manager.peer_connected(client_node_id, &init_msg, false).unwrap(); + let _ = service_handler.notify_payment_incoming(client_node_id); + let event = service_node.liquidity_manager.next_event().unwrap(); + match event { + LiquidityEvent::LSPS5Service(LSPS5ServiceEvent::SendWebhookNotification { + notification, + .. + }) => { + assert_eq!(notification.method, WebhookNotificationMethod::LSPS5PaymentIncoming); + }, + _ => panic!("Expected SendWebhookNotification event after reset throttle elapsed"), + } } #[test] @@ -1515,7 +1550,6 @@ fn lsps5_service_handler_persistence_across_restarts() { let client_kv_store = Arc::new(TestStore::new(false)); let service_config = LiquidityServiceConfig { - #[cfg(lsps1_service)] lsps1_service_config: None, lsps2_service_config: None, lsps5_service_config: Some(LSPS5ServiceConfig::default()), @@ -1601,18 +1635,10 @@ fn lsps5_service_handler_persistence_across_restarts() { let node_chanmgrs_restart = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes_restart = create_network(2, &node_cfgs, &node_chanmgrs_restart); - // Create a new LiquidityManager with the same configuration and KV store to simulate restart - let chain_params = ChainParameters { - network: Network::Testnet, - best_block: BestBlock::from_network(Network::Testnet), - }; - let restarted_service_lm = LiquidityManagerSync::new_with_custom_time_provider( nodes_restart[0].keys_manager, nodes_restart[0].keys_manager, nodes_restart[0].node, - None::<Arc<dyn Filter + Send + Sync>>, - Some(chain_params), service_kv_store, nodes_restart[0].tx_broadcaster, Some(service_config), @@ -1647,3 +1673,173 @@ fn lsps5_service_handler_persistence_across_restarts() { } } } + +struct FailableKVStore { + inner: TestStore, + fail_lsps5: std::sync::atomic::AtomicBool, +} + +impl FailableKVStore { + fn new() -> Self { + Self { inner: TestStore::new(false), fail_lsps5: std::sync::atomic::AtomicBool::new(false) } + } + + fn set_fail_lsps5(&self, fail: bool) { + self.fail_lsps5.store(fail, std::sync::atomic::Ordering::SeqCst); + } +} + +impl lightning::util::persist::KVStoreSync for FailableKVStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> lightning::io::Result<Vec<u8>> { + <TestStore as lightning::util::persist::KVStoreSync>::read( + &self.inner, + primary_namespace, + secondary_namespace, + key, + ) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> lightning::io::Result<()> { + if secondary_namespace == "lsps5_service" + && self.fail_lsps5.load(std::sync::atomic::Ordering::SeqCst) + { + return Err(lightning::io::Error::new( + lightning::io::ErrorKind::Other, + "intentional failure for lsps5 namespace", + )); + } + <TestStore as lightning::util::persist::KVStoreSync>::write( + &self.inner, + primary_namespace, + secondary_namespace, + key, + buf, + ) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> lightning::io::Result<()> { + if secondary_namespace == "lsps5_service" + && self.fail_lsps5.load(std::sync::atomic::Ordering::SeqCst) + { + return Err(lightning::io::Error::new( + lightning::io::ErrorKind::Other, + "intentional failure for lsps5 namespace", + )); + } + <TestStore as lightning::util::persist::KVStoreSync>::remove( + &self.inner, + primary_namespace, + secondary_namespace, + key, + lazy, + ) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> lightning::io::Result<Vec<String>> { + <TestStore as lightning::util::persist::KVStoreSync>::list( + &self.inner, + primary_namespace, + secondary_namespace, + ) + } +} + +#[test] +fn lsps5_service_persist_resets_in_flight_counter_on_io_error() { + use lightning::ln::peer_handler::CustomMessageHandler; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let service_kv_store = Arc::new(FailableKVStore::new()); + let client_kv_store = Arc::new(TestStore::new(false)); + + let service_config = LiquidityServiceConfig { + lsps1_service_config: None, + lsps2_service_config: None, + lsps5_service_config: Some(LSPS5ServiceConfig::default()), + advertise_service: true, + }; + let client_config = LiquidityClientConfig { + lsps1_client_config: None, + lsps2_client_config: None, + lsps5_client_config: Some(LSPS5ClientConfig::default()), + }; + let time_provider: Arc<dyn TimeProvider + Send + Sync> = Arc::new(DefaultTimeProvider); + + let service_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes[0].keys_manager, + nodes[0].keys_manager, + nodes[0].node, + Arc::clone(&service_kv_store), + nodes[0].tx_broadcaster, + Some(service_config), + None, + Arc::clone(&time_provider), + ) + .unwrap(); + + let client_lm = LiquidityManagerSync::new_with_custom_time_provider( + nodes[1].keys_manager, + nodes[1].keys_manager, + nodes[1].node, + client_kv_store, + nodes[1].tx_broadcaster, + None, + Some(client_config), + Arc::clone(&time_provider), + ) + .unwrap(); + + let service_node_id = nodes[0].node.get_our_node_id(); + let client_node_id = nodes[1].node.get_our_node_id(); + + create_chan_between_nodes(&nodes[0], &nodes[1]); + + let client_handler = client_lm.lsps5_client_handler().unwrap(); + client_handler + .set_webhook(service_node_id, "App".to_string(), "https://example.org/hook".to_string()) + .unwrap(); + + let req_msgs = client_lm.get_and_clear_pending_msg(); + assert_eq!(req_msgs.len(), 1); + let (_, request) = req_msgs.into_iter().next().unwrap(); + service_lm.handle_custom_message(request, client_node_id).unwrap(); + + // Consume the SendWebhookNotification event so pending events queue is drained. + let _ = service_lm.next_event(); + let _ = service_lm.get_and_clear_pending_msg(); + + // Initial persist should succeed and clear all needs_persist flags. + service_lm.persist().expect("initial persist should succeed"); + + // Now arrange for lsps5 writes to fail and dirty lsps5 state without dirtying + // pending_events (which lives in a different namespace). + service_kv_store.set_fail_lsps5(true); + service_lm.peer_disconnected(client_node_id); + + // First persist attempt should error out due to the failing kv_store. + let res1 = service_lm.persist(); + assert!(res1.is_err(), "persist should fail when lsps5 kv_store write fails"); + + // Second persist attempt must still attempt the write (and fail again). With the + // bug, the LSPS5 service handler's `persistence_in_flight` counter is left above + // zero on error so this returns Ok(false) immediately, silently dropping the + // pending state and breaking persistence forever. + let res2 = service_lm.persist(); + assert!( + res2.is_err(), + "after a failed persist, subsequent persist calls must still attempt to persist; got {:?}", + res2, + ); +} diff --git a/lightning-net-tokio/src/lib.rs b/lightning-net-tokio/src/lib.rs index ee129669410..1d853ceb2d1 100644 --- a/lightning-net-tokio/src/lib.rs +++ b/lightning-net-tokio/src/lib.rs @@ -663,8 +663,7 @@ fn clone_socket_waker(orig_ptr: *const ()) -> task::RawWaker { // sending thread may have already gone away due to a socket close, in which case there's nothing // to wake up anyway. fn wake_socket_waker(orig_ptr: *const ()) { - let sender = unsafe { &mut *(orig_ptr as *mut mpsc::Sender<()>) }; - let _ = sender.try_send(()); + wake_socket_waker_by_ref(orig_ptr); drop_socket_waker(orig_ptr); } fn wake_socket_waker_by_ref(orig_ptr: *const ()) { @@ -1120,6 +1119,19 @@ mod tests { // Set TOR_PROXY=127.0.0.1:9050 let tor_proxy_addr: SocketAddr = std::env!("TOR_PROXY").parse().unwrap(); + let mut google_addresses: Vec<_> = + tokio::net::lookup_host("google.com:80").await.unwrap().collect(); + let ipv6_pos = google_addresses + .iter() + .position(|a| a.is_ipv6()) + .expect("must resolve at least one ipv6 address"); + let mut google_ipv6 = google_addresses.remove(ipv6_pos); + let ipv4_pos = google_addresses + .iter() + .position(|a| a.is_ipv4()) + .expect("must resolve at least one ipv4 address"); + let mut google_ipv4 = google_addresses.remove(ipv4_pos); + struct TestEntropySource; impl EntropySource for TestEntropySource { @@ -1132,17 +1144,16 @@ mod tests { // Success cases - for addr_str in [ + for addr in [ // google.com - "142.250.189.196:80", + google_ipv4.into(), // google.com - "[2607:f8b0:4005:813::2004]:80", + google_ipv6.into(), // torproject.org - "torproject.org:80", + "torproject.org:80".parse().unwrap(), // torproject.org - "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80", + "2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80".parse().unwrap(), ] { - let addr: SocketAddress = addr_str.parse().unwrap(); let tcp_stream = tor_connect(addr, tor_proxy_addr, &entropy_source).await.unwrap(); assert_eq!( tcp_stream.try_read(&mut [0u8; 1]).unwrap_err().kind(), @@ -1151,18 +1162,19 @@ mod tests { } // Failure cases + google_ipv4.set_port(1234); + google_ipv6.set_port(1234); - for addr_str in [ + for addr in [ // google.com, with some invalid port - "142.250.189.196:1234", + google_ipv4.into(), // google.com, with some invalid port - "[2607:f8b0:4005:813::2004]:1234", + google_ipv6.into(), // torproject.org, with some invalid port - "torproject.org:1234", + "torproject.org:1234".parse().unwrap(), // torproject.org, with a typo - "3gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80", + "3gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion:80".parse().unwrap(), ] { - let addr: SocketAddress = addr_str.parse().unwrap(); assert!(tor_connect(addr, tor_proxy_addr, &entropy_source).await.is_err()); } } diff --git a/lightning-persister/Cargo.toml b/lightning-persister/Cargo.toml index 19c5ac2545e..cb2aae556b6 100644 --- a/lightning-persister/Cargo.toml +++ b/lightning-persister/Cargo.toml @@ -20,7 +20,7 @@ tokio = ["dep:tokio"] [dependencies] bitcoin = "0.32.2" -lightning = { version = "0.3.0", path = "../lightning" } +lightning = { version = "0.3.0", path = "../lightning", default-features = false, features = ["std"] } tokio = { version = "1.35", optional = true, default-features = false, features = ["rt-multi-thread"] } [target.'cfg(windows)'.dependencies] diff --git a/lightning-persister/src/fs_store.rs b/lightning-persister/src/fs_store/common.rs similarity index 58% rename from lightning-persister/src/fs_store.rs rename to lightning-persister/src/fs_store/common.rs index 3129748afda..b591e43bda0 100644 --- a/lightning-persister/src/fs_store.rs +++ b/lightning-persister/src/fs_store/common.rs @@ -1,24 +1,27 @@ -//! Objects related to [`FilesystemStore`] live here. +//! Common utilities shared between [`FilesystemStore`] and [`FilesystemStoreV2`] implementations. +//! +//! [`FilesystemStore`]: crate::fs_store::v1::FilesystemStore +//! [`FilesystemStoreV2`]: crate::fs_store::v2::FilesystemStoreV2 + use crate::utils::{check_namespace_key_validity, is_valid_kvstore_str}; use lightning::types::string::PrintableString; -use lightning::util::persist::{KVStoreSync, MigratableKVStore}; use std::collections::HashMap; use std::fs; -use std::io::{Read, Write}; +use std::io::{ErrorKind, Read, Write}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; +#[cfg(target_os = "windows")] +use std::ffi::OsStr; #[cfg(feature = "tokio")] -use core::future::Future; -#[cfg(feature = "tokio")] -use lightning::util::persist::KVStore; - +use std::future::Future; #[cfg(target_os = "windows")] -use {std::ffi::OsStr, std::os::windows::ffi::OsStrExt}; +use std::os::windows::ffi::OsStrExt; +/// Calls a Windows API function and returns Ok(()) on success or the last OS error on failure. #[cfg(target_os = "windows")] macro_rules! call { ($e: expr) => { @@ -30,6 +33,10 @@ macro_rules! call { }; } +#[cfg(target_os = "windows")] +use call; + +/// Converts a path to a null-terminated wide string for Windows API calls. #[cfg(target_os = "windows")] fn path_to_windows_str<T: AsRef<OsStr>>(path: &T) -> Vec<u16> { path.as_ref().encode_wide().chain(Some(0)).collect() @@ -39,6 +46,15 @@ fn path_to_windows_str<T: AsRef<OsStr>>(path: &T) -> Vec<u16> { // a consistent view and error out. const LIST_DIR_CONSISTENCY_RETRIES: usize = 10; +// The directory name used for empty namespaces in v2. +// Uses brackets which are not in KVSTORE_NAMESPACE_KEY_ALPHABET, preventing collisions +// with valid namespace names. +pub(crate) const EMPTY_NAMESPACE_DIR: &str = "[empty]"; + +/// Inner state shared between sync and async operations for filesystem stores. +/// +/// This struct manages the data directory, temporary file counter, and per-path locks +/// that ensure we don't have concurrent writes to the same file. struct FilesystemStoreInner { data_dir: PathBuf, tmp_file_counter: AtomicUsize, @@ -48,10 +64,7 @@ struct FilesystemStoreInner { locks: Mutex<HashMap<PathBuf, Arc<RwLock<u64>>>>, } -/// A [`KVStore`] and [`KVStoreSync`] implementation that writes to and reads from the file system. -/// -/// [`KVStore`]: lightning::util::persist::KVStore -pub struct FilesystemStore { +pub(crate) struct FilesystemStoreState { inner: Arc<FilesystemStoreInner>, // Version counter to ensure that writes are applied in the correct order. It is assumed that read and list @@ -59,13 +72,15 @@ pub struct FilesystemStore { next_version: AtomicU64, } -impl FilesystemStore { - /// Constructs a new [`FilesystemStore`]. - pub fn new(data_dir: PathBuf) -> Self { - let locks = Mutex::new(HashMap::new()); - let tmp_file_counter = AtomicUsize::new(0); +impl FilesystemStoreState { + /// Creates a new [`FilesystemStoreInner`] with the given data directory. + pub(crate) fn new(data_dir: PathBuf) -> Self { Self { - inner: Arc::new(FilesystemStoreInner { data_dir, tmp_file_counter, locks }), + inner: Arc::new(FilesystemStoreInner { + data_dir, + tmp_file_counter: AtomicUsize::new(0), + locks: Mutex::new(HashMap::new()), + }), next_version: AtomicU64::new(1), } } @@ -94,57 +109,18 @@ impl FilesystemStore { let outer_lock = self.inner.locks.lock().unwrap(); outer_lock.len() } -} - -impl KVStoreSync for FilesystemStore { - fn read( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> Result<Vec<u8>, lightning::io::Error> { - let path = self.inner.get_checked_dest_file_path( - primary_namespace, - secondary_namespace, - Some(key), - "read", - )?; - self.inner.read(path) - } - - fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, - ) -> Result<(), lightning::io::Error> { - let path = self.inner.get_checked_dest_file_path( - primary_namespace, - secondary_namespace, - Some(key), - "write", - )?; - let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(path.clone()); - self.inner.write_version(inner_lock_ref, path, buf, version) - } - - fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, - ) -> Result<(), lightning::io::Error> { - let path = self.inner.get_checked_dest_file_path( - primary_namespace, - secondary_namespace, - Some(key), - "remove", - )?; - let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(path.clone()); - self.inner.remove_version(inner_lock_ref, path, lazy, version) - } - fn list( - &self, primary_namespace: &str, secondary_namespace: &str, - ) -> Result<Vec<String>, lightning::io::Error> { - let path = self.inner.get_checked_dest_file_path( + pub(crate) fn get_checked_dest_file_path( + &self, primary_namespace: &str, secondary_namespace: &str, key: Option<&str>, + operation: &str, use_empty_ns_dir: bool, + ) -> lightning::io::Result<PathBuf> { + self.inner.get_checked_dest_file_path( primary_namespace, secondary_namespace, - None, - "list", - )?; - self.inner.list(path) + key, + operation, + use_empty_ns_dir, + ) } } @@ -155,7 +131,7 @@ impl FilesystemStoreInner { } fn get_dest_dir_path( - &self, primary_namespace: &str, secondary_namespace: &str, + &self, primary_namespace: &str, secondary_namespace: &str, use_empty_ns_dir: bool, ) -> std::io::Result<PathBuf> { let mut dest_dir_path = { #[cfg(target_os = "windows")] @@ -170,9 +146,22 @@ impl FilesystemStoreInner { } }; - dest_dir_path.push(primary_namespace); - if !secondary_namespace.is_empty() { - dest_dir_path.push(secondary_namespace); + if use_empty_ns_dir { + dest_dir_path.push(if primary_namespace.is_empty() { + EMPTY_NAMESPACE_DIR + } else { + primary_namespace + }); + dest_dir_path.push(if secondary_namespace.is_empty() { + EMPTY_NAMESPACE_DIR + } else { + secondary_namespace + }); + } else { + dest_dir_path.push(primary_namespace); + if !secondary_namespace.is_empty() { + dest_dir_path.push(secondary_namespace); + } } Ok(dest_dir_path) @@ -180,11 +169,12 @@ impl FilesystemStoreInner { fn get_checked_dest_file_path( &self, primary_namespace: &str, secondary_namespace: &str, key: Option<&str>, - operation: &str, + operation: &str, use_empty_ns_dir: bool, ) -> lightning::io::Result<PathBuf> { check_namespace_key_validity(primary_namespace, secondary_namespace, key, operation)?; - let mut dest_file_path = self.get_dest_dir_path(primary_namespace, secondary_namespace)?; + let mut dest_file_path = + self.get_dest_dir_path(primary_namespace, secondary_namespace, use_empty_ns_dir)?; if let Some(key) = key { dest_file_path.push(key); } @@ -260,8 +250,17 @@ impl FilesystemStoreInner { /// returns early without writing. fn write_version( &self, inner_lock_ref: Arc<RwLock<u64>>, dest_file_path: PathBuf, buf: Vec<u8>, - version: u64, + version: u64, preserve_mtime: bool, ) -> lightning::io::Result<()> { + let mtime = if preserve_mtime { + match fs::metadata(&dest_file_path) { + Err(e) if e.kind() == ErrorKind::NotFound => None, + Err(e) => return Err(e.into()), + Ok(m) => Some(m.modified()?), + } + } else { + None + }; let parent_directory = dest_file_path.parent().ok_or_else(|| { let msg = format!("Could not retrieve parent directory of {}.", dest_file_path.display()); @@ -278,26 +277,43 @@ impl FilesystemStoreInner { let tmp_file_ext = format!("{}.tmp", self.tmp_file_counter.fetch_add(1, Ordering::AcqRel)); tmp_file_path.set_extension(tmp_file_ext); - { - let mut tmp_file = fs::File::create(&tmp_file_path)?; - tmp_file.write_all(&buf)?; - tmp_file.sync_all()?; - } + let tmp_file_res = match fs::File::create(&tmp_file_path) { + Ok(mut tmp_file) => (|| -> lightning::io::Result<()> { + tmp_file.write_all(&buf)?; - self.execute_locked_write(inner_lock_ref, dest_file_path.clone(), version, || { - #[cfg(not(target_os = "windows"))] - { - fs::rename(&tmp_file_path, &dest_file_path)?; - let dir_file = fs::OpenOptions::new().read(true).open(&parent_directory)?; - dir_file.sync_all()?; + // If we need to preserve the original mtime (for updates), set it before fsync. + if let Some(mtime) = mtime { + let times = fs::FileTimes::new().set_modified(mtime); + tmp_file.set_times(times)?; + } + + tmp_file.sync_all()?; Ok(()) - } + })(), + Err(e) => return Err(e.into()), + }; + if let Err(e) = tmp_file_res { + let _ = fs::remove_file(&tmp_file_path); + return Err(e); + } - #[cfg(target_os = "windows")] - { - let res = if dest_file_path.exists() { - call!(unsafe { - windows_sys::Win32::Storage::FileSystem::ReplaceFileW( + let mut tmp_file_needs_cleanup = true; + let write_res = + self.execute_locked_write(inner_lock_ref, dest_file_path.clone(), version, || { + #[cfg(not(target_os = "windows"))] + { + fs::rename(&tmp_file_path, &dest_file_path)?; + tmp_file_needs_cleanup = false; + let dir_file = fs::OpenOptions::new().read(true).open(&parent_directory)?; + dir_file.sync_all()?; + Ok(()) + } + + #[cfg(target_os = "windows")] + { + let res = if dest_file_path.exists() { + call!(unsafe { + windows_sys::Win32::Storage::FileSystem::ReplaceFileW( path_to_windows_str(&dest_file_path).as_ptr(), path_to_windows_str(&tmp_file_path).as_ptr(), std::ptr::null(), @@ -305,30 +321,37 @@ impl FilesystemStoreInner { std::ptr::null_mut() as *const core::ffi::c_void, std::ptr::null_mut() as *const core::ffi::c_void, ) - }) - } else { - call!(unsafe { - windows_sys::Win32::Storage::FileSystem::MoveFileExW( + }) + } else { + call!(unsafe { + windows_sys::Win32::Storage::FileSystem::MoveFileExW( path_to_windows_str(&tmp_file_path).as_ptr(), path_to_windows_str(&dest_file_path).as_ptr(), windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH | windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING, ) - }) - }; - - match res { - Ok(()) => { - // We fsync the dest file in hopes this will also flush the metadata to disk. - let dest_file = - fs::OpenOptions::new().read(true).write(true).open(&dest_file_path)?; - dest_file.sync_all()?; - Ok(()) - }, - Err(e) => Err(e.into()), + }) + }; + + match res { + Ok(()) => { + tmp_file_needs_cleanup = false; + // We fsync the dest file in hopes this will also flush the metadata to disk. + let dest_file = fs::OpenOptions::new() + .read(true) + .write(true) + .open(&dest_file_path)?; + dest_file.sync_all()?; + Ok(()) + }, + Err(e) => Err(e.into()), + } } - } - }) + }); + if tmp_file_needs_cleanup { + let _ = fs::remove_file(&tmp_file_path); + } + write_res } fn remove_version( @@ -413,13 +436,13 @@ impl FilesystemStoreInner { }) } - fn list(&self, prefixed_dest: PathBuf) -> lightning::io::Result<Vec<String>> { + fn list(&self, prefixed_dest: PathBuf, is_v2: bool) -> lightning::io::Result<Vec<String>> { if !Path::new(&prefixed_dest).exists() { return Ok(Vec::new()); } let mut keys; - let mut retries = LIST_DIR_CONSISTENCY_RETRIES; + let mut retries = if is_v2 { 0 } else { LIST_DIR_CONSISTENCY_RETRIES }; 'retry_list: loop { keys = Vec::new(); @@ -430,7 +453,7 @@ impl FilesystemStoreInner { let res = dir_entry_is_key(&entry); match res { Ok(true) => { - let key = get_key_from_dir_entry_path(&p, &prefixed_dest)?; + let key = get_key_from_dir_entry_path(&p, &prefixed_dest, false)?; keys.push(key); }, Ok(false) => { @@ -439,6 +462,14 @@ impl FilesystemStoreInner { continue 'skip_entry; }, Err(e) => { + // In version 2 if a file has been deleted between the `read_dir` and our attempt + // to access it, we should just add it to the list to give a more consistent view. + if is_v2 { + let key = get_key_from_dir_entry_path(&p, &prefixed_dest, false)?; + keys.push(key); + continue 'skip_entry; + } + if e.kind() == lightning::io::ErrorKind::NotFound && retries > 0 { // We had found the entry in `read_dir` above, so some race happend. // Retry the `read_dir` to get a consistent view. @@ -456,12 +487,158 @@ impl FilesystemStoreInner { Ok(keys) } + + fn list_all_keys( + &self, use_empty_ns_dir: bool, + ) -> Result<Vec<(String, String, String)>, lightning::io::Error> { + let prefixed_dest = &self.data_dir; + if !prefixed_dest.exists() { + return Ok(Vec::new()); + } + + let mut keys = Vec::new(); + + 'primary_loop: for primary_entry in fs::read_dir(prefixed_dest)? { + let primary_entry = primary_entry?; + let primary_path = primary_entry.path(); + if dir_entry_is_store_artifact(&primary_path) { + continue 'primary_loop; + } + + if dir_entry_is_key(&primary_entry)? { + let primary_namespace = String::new(); + let secondary_namespace = String::new(); + let key = get_key_from_dir_entry_path(&primary_path, prefixed_dest, false)?; + keys.push((primary_namespace, secondary_namespace, key)); + continue 'primary_loop; + } + + // The primary_entry is actually also a directory. + 'secondary_loop: for secondary_entry in fs::read_dir(&primary_path)? { + let secondary_entry = secondary_entry?; + let secondary_path = secondary_entry.path(); + if dir_entry_is_store_artifact(&secondary_path) { + continue 'secondary_loop; + } + + if dir_entry_is_key(&secondary_entry)? { + let primary_namespace = get_key_from_dir_entry_path( + &primary_path, + prefixed_dest, + use_empty_ns_dir, + )?; + let secondary_namespace = String::new(); + let key = get_key_from_dir_entry_path(&secondary_path, &primary_path, false)?; + keys.push((primary_namespace, secondary_namespace, key)); + continue 'secondary_loop; + } + + // The secondary_entry is actually also a directory. + for tertiary_entry in fs::read_dir(&secondary_path)? { + let tertiary_entry = tertiary_entry?; + let tertiary_path = tertiary_entry.path(); + if dir_entry_is_store_artifact(&tertiary_path) { + continue; + } + + if dir_entry_is_key(&tertiary_entry)? { + let primary_namespace = get_key_from_dir_entry_path( + &primary_path, + prefixed_dest, + use_empty_ns_dir, + )?; + let secondary_namespace = get_key_from_dir_entry_path( + &secondary_path, + &primary_path, + use_empty_ns_dir, + )?; + let key = + get_key_from_dir_entry_path(&tertiary_path, &secondary_path, false)?; + keys.push((primary_namespace, secondary_namespace, key)); + } else { + debug_assert!( + false, + "Failed to list keys of path {}: only two levels of namespaces are supported", + PrintableString(tertiary_path.to_str().unwrap_or_default()) + ); + let msg = format!( + "Failed to list keys of path {}: only two levels of namespaces are supported", + PrintableString(tertiary_path.to_str().unwrap_or_default()) + ); + return Err(lightning::io::Error::new( + lightning::io::ErrorKind::Other, + msg, + )); + } + } + } + } + Ok(keys) + } } -#[cfg(feature = "tokio")] -impl KVStore for FilesystemStore { - fn read( +impl FilesystemStoreState { + pub(crate) fn read_impl( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + use_empty_ns_dir: bool, + ) -> Result<Vec<u8>, lightning::io::Error> { + let path = self.inner.get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + Some(key), + "read", + use_empty_ns_dir, + )?; + self.inner.read(path) + } + + pub(crate) fn write_impl( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + use_empty_ns_dir: bool, + ) -> Result<(), lightning::io::Error> { + let path = self.inner.get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + Some(key), + "write", + use_empty_ns_dir, + )?; + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(path.clone()); + self.inner.write_version(inner_lock_ref, path, buf, version, use_empty_ns_dir) + } + + pub(crate) fn remove_impl( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + use_empty_ns_dir: bool, + ) -> Result<(), lightning::io::Error> { + let path = self.inner.get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + Some(key), + "remove", + use_empty_ns_dir, + )?; + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(path.clone()); + self.inner.remove_version(inner_lock_ref, path, lazy, version) + } + + pub(crate) fn list_impl( + &self, primary_namespace: &str, secondary_namespace: &str, use_empty_ns_dir: bool, + ) -> Result<Vec<String>, lightning::io::Error> { + let path = self.inner.get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + None, + "list", + use_empty_ns_dir, + )?; + self.inner.list(path, use_empty_ns_dir) + } + + #[cfg(feature = "tokio")] + pub(crate) fn read_async( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + use_empty_ns_dir: bool, ) -> impl Future<Output = Result<Vec<u8>, lightning::io::Error>> + 'static + Send { let this = Arc::clone(&self.inner); let path = this.get_checked_dest_file_path( @@ -469,6 +646,7 @@ impl KVStore for FilesystemStore { secondary_namespace, Some(key), "read", + use_empty_ns_dir, ); async move { @@ -482,12 +660,20 @@ impl KVStore for FilesystemStore { } } - fn write( + #[cfg(feature = "tokio")] + pub(crate) fn write_async( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + use_empty_ns_dir: bool, ) -> impl Future<Output = Result<(), lightning::io::Error>> + 'static + Send { let this = Arc::clone(&self.inner); let path = this - .get_checked_dest_file_path(primary_namespace, secondary_namespace, Some(key), "write") + .get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + Some(key), + "write", + use_empty_ns_dir, + ) .map(|path| (self.get_new_version_and_lock_ref(path.clone()), path)); async move { @@ -496,19 +682,27 @@ impl KVStore for FilesystemStore { Err(e) => return Err(e), }; tokio::task::spawn_blocking(move || { - this.write_version(inner_lock_ref, path, buf, version) + this.write_version(inner_lock_ref, path, buf, version, use_empty_ns_dir) }) .await .unwrap_or_else(|e| Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e))) } } - fn remove( + #[cfg(feature = "tokio")] + pub(crate) fn remove_async( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + use_empty_ns_dir: bool, ) -> impl Future<Output = Result<(), lightning::io::Error>> + 'static + Send { let this = Arc::clone(&self.inner); let path = this - .get_checked_dest_file_path(primary_namespace, secondary_namespace, Some(key), "remove") + .get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + Some(key), + "remove", + use_empty_ns_dir, + ) .map(|path| (self.get_new_version_and_lock_ref(path.clone()), path)); async move { @@ -524,40 +718,75 @@ impl KVStore for FilesystemStore { } } - fn list( - &self, primary_namespace: &str, secondary_namespace: &str, + #[cfg(feature = "tokio")] + pub(crate) fn list_async( + &self, primary_namespace: &str, secondary_namespace: &str, use_empty_ns_dir: bool, ) -> impl Future<Output = Result<Vec<String>, lightning::io::Error>> + 'static + Send { let this = Arc::clone(&self.inner); - let path = - this.get_checked_dest_file_path(primary_namespace, secondary_namespace, None, "list"); + let path = this.get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + None, + "list", + use_empty_ns_dir, + ); async move { let path = match path { Ok(path) => path, Err(e) => return Err(e), }; - tokio::task::spawn_blocking(move || this.list(path)).await.unwrap_or_else(|e| { - Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e)) - }) + tokio::task::spawn_blocking(move || this.list(path, use_empty_ns_dir)) + .await + .unwrap_or_else(|e| { + Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e)) + }) + } + } + + #[cfg(feature = "tokio")] + pub(crate) fn list_all_keys_async( + &self, use_empty_ns_dir: bool, + ) -> impl Future<Output = Result<Vec<(String, String, String)>, lightning::io::Error>> + 'static + Send + { + let this = Arc::clone(&self.inner); + + async move { + tokio::task::spawn_blocking(move || this.list_all_keys(use_empty_ns_dir)) + .await + .unwrap_or_else(|e| { + Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e)) + }) } } + + pub(crate) fn list_all_keys_impl( + &self, use_empty_ns_dir: bool, + ) -> Result<Vec<(String, String, String)>, lightning::io::Error> { + self.inner.list_all_keys(use_empty_ns_dir) + } } -fn dir_entry_is_key(dir_entry: &fs::DirEntry) -> Result<bool, lightning::io::Error> { - let p = dir_entry.path(); - if let Some(ext) = p.extension() { - #[cfg(target_os = "windows")] - { - // Clean up any trash files lying around. - if ext == "trash" { - fs::remove_file(p).ok(); - return Ok(false); +fn dir_entry_is_store_artifact(path: &Path) -> bool { + match path.extension().and_then(|ext| ext.to_str()) { + Some("tmp") => true, + Some("trash") => { + #[cfg(target_os = "windows")] + { + // Clean up any trash files lying around. + fs::remove_file(path).ok(); } - } - if ext == "tmp" { - return Ok(false); - } + true + }, + _ => false, + } +} + +pub(crate) fn dir_entry_is_key(dir_entry: &fs::DirEntry) -> Result<bool, lightning::io::Error> { + let p = dir_entry.path(); + if dir_entry_is_store_artifact(&p) { + return Ok(false); } let file_type = dir_entry.file_type()?; @@ -584,10 +813,18 @@ fn dir_entry_is_key(dir_entry: &fs::DirEntry) -> Result<bool, lightning::io::Err Ok(true) } -fn get_key_from_dir_entry_path(p: &Path, base_path: &Path) -> Result<String, lightning::io::Error> { +/// Gets the key from a directory entry path by stripping the base path and validating the result. +/// If `map_empty_ns_dir` is true, treats entries with the name of `EMPTY_NAMESPACE_DIR` as an empty string. +/// `map_empty_ns_dir` should always be false when reading keys and only be true when listing namespaces. +pub(crate) fn get_key_from_dir_entry_path( + p: &Path, base_path: &Path, map_empty_ns_dir: bool, +) -> Result<String, lightning::io::Error> { match p.strip_prefix(&base_path) { Ok(stripped_path) => { if let Some(relative_path) = stripped_path.to_str() { + if map_empty_ns_dir && relative_path == EMPTY_NAMESPACE_DIR { + return Ok(String::new()); + } if is_valid_kvstore_str(relative_path) { return Ok(relative_path.to_string()); } else { @@ -631,325 +868,3 @@ fn get_key_from_dir_entry_path(p: &Path, base_path: &Path) -> Result<String, lig }, } } - -impl MigratableKVStore for FilesystemStore { - fn list_all_keys(&self) -> Result<Vec<(String, String, String)>, lightning::io::Error> { - let prefixed_dest = &self.inner.data_dir; - if !prefixed_dest.exists() { - return Ok(Vec::new()); - } - - let mut keys = Vec::new(); - - 'primary_loop: for primary_entry in fs::read_dir(prefixed_dest)? { - let primary_entry = primary_entry?; - let primary_path = primary_entry.path(); - - if dir_entry_is_key(&primary_entry)? { - let primary_namespace = String::new(); - let secondary_namespace = String::new(); - let key = get_key_from_dir_entry_path(&primary_path, prefixed_dest)?; - keys.push((primary_namespace, secondary_namespace, key)); - continue 'primary_loop; - } - - // The primary_entry is actually also a directory. - 'secondary_loop: for secondary_entry in fs::read_dir(&primary_path)? { - let secondary_entry = secondary_entry?; - let secondary_path = secondary_entry.path(); - - if dir_entry_is_key(&secondary_entry)? { - let primary_namespace = - get_key_from_dir_entry_path(&primary_path, prefixed_dest)?; - let secondary_namespace = String::new(); - let key = get_key_from_dir_entry_path(&secondary_path, &primary_path)?; - keys.push((primary_namespace, secondary_namespace, key)); - continue 'secondary_loop; - } - - // The secondary_entry is actually also a directory. - for tertiary_entry in fs::read_dir(&secondary_path)? { - let tertiary_entry = tertiary_entry?; - let tertiary_path = tertiary_entry.path(); - - if dir_entry_is_key(&tertiary_entry)? { - let primary_namespace = - get_key_from_dir_entry_path(&primary_path, prefixed_dest)?; - let secondary_namespace = - get_key_from_dir_entry_path(&secondary_path, &primary_path)?; - let key = get_key_from_dir_entry_path(&tertiary_path, &secondary_path)?; - keys.push((primary_namespace, secondary_namespace, key)); - } else { - debug_assert!( - false, - "Failed to list keys of path {}: only two levels of namespaces are supported", - PrintableString(tertiary_path.to_str().unwrap_or_default()) - ); - let msg = format!( - "Failed to list keys of path {}: only two levels of namespaces are supported", - PrintableString(tertiary_path.to_str().unwrap_or_default()) - ); - return Err(lightning::io::Error::new( - lightning::io::ErrorKind::Other, - msg, - )); - } - } - } - } - Ok(keys) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::{ - do_read_write_remove_list_persist, do_test_data_migration, do_test_store, - }; - - use lightning::chain::chainmonitor::Persist; - use lightning::chain::ChannelMonitorUpdateStatus; - use lightning::events::ClosureReason; - use lightning::ln::functional_test_utils::*; - use lightning::ln::msgs::BaseMessageHandler; - use lightning::util::persist::read_channel_monitors; - use lightning::util::test_utils; - - impl Drop for FilesystemStore { - fn drop(&mut self) { - // We test for invalid directory names, so it's OK if directory removal - // fails. - match fs::remove_dir_all(&self.inner.data_dir) { - Err(e) => println!("Failed to remove test persister directory: {}", e), - _ => {}, - } - } - } - - #[test] - fn read_write_remove_list_persist() { - let mut temp_path = std::env::temp_dir(); - temp_path.push("test_read_write_remove_list_persist"); - let fs_store = FilesystemStore::new(temp_path); - do_read_write_remove_list_persist(&fs_store); - } - - #[cfg(feature = "tokio")] - #[tokio::test] - async fn read_write_remove_list_persist_async() { - use crate::fs_store::FilesystemStore; - use lightning::util::persist::KVStore; - use std::sync::Arc; - - let mut temp_path = std::env::temp_dir(); - temp_path.push("test_read_write_remove_list_persist_async"); - let fs_store = Arc::new(FilesystemStore::new(temp_path)); - assert_eq!(fs_store.state_size(), 0); - - let async_fs_store = Arc::clone(&fs_store); - - let data1 = vec![42u8; 32]; - let data2 = vec![43u8; 32]; - - let primary = "testspace"; - let secondary = "testsubspace"; - let key = "testkey"; - - // Test writing the same key twice with different data. Execute the asynchronous part out of order to ensure - // that eventual consistency works. - let fut1 = KVStore::write(&*async_fs_store, primary, secondary, key, data1); - assert_eq!(fs_store.state_size(), 1); - - let fut2 = KVStore::remove(&*async_fs_store, primary, secondary, key, false); - assert_eq!(fs_store.state_size(), 1); - - let fut3 = KVStore::write(&*async_fs_store, primary, secondary, key, data2.clone()); - assert_eq!(fs_store.state_size(), 1); - - fut3.await.unwrap(); - assert_eq!(fs_store.state_size(), 1); - - fut2.await.unwrap(); - assert_eq!(fs_store.state_size(), 1); - - fut1.await.unwrap(); - assert_eq!(fs_store.state_size(), 0); - - // Test list. - let listed_keys = KVStore::list(&*async_fs_store, primary, secondary).await.unwrap(); - assert_eq!(listed_keys.len(), 1); - assert_eq!(listed_keys[0], key); - - // Test read. We expect to read data2, as the write call was initiated later. - let read_data = KVStore::read(&*async_fs_store, primary, secondary, key).await.unwrap(); - assert_eq!(data2, &*read_data); - - // Test remove. - KVStore::remove(&*async_fs_store, primary, secondary, key, false).await.unwrap(); - - let listed_keys = KVStore::list(&*async_fs_store, primary, secondary).await.unwrap(); - assert_eq!(listed_keys.len(), 0); - } - - #[test] - fn test_data_migration() { - let mut source_temp_path = std::env::temp_dir(); - source_temp_path.push("test_data_migration_source"); - let mut source_store = FilesystemStore::new(source_temp_path); - - let mut target_temp_path = std::env::temp_dir(); - target_temp_path.push("test_data_migration_target"); - let mut target_store = FilesystemStore::new(target_temp_path); - - do_test_data_migration(&mut source_store, &mut target_store); - } - - #[test] - fn test_if_monitors_is_not_dir() { - let store = FilesystemStore::new("test_monitors_is_not_dir".into()); - - fs::create_dir_all(&store.get_data_dir()).unwrap(); - let mut path = std::path::PathBuf::from(&store.get_data_dir()); - path.push("monitors"); - fs::File::create(path).unwrap(); - - let chanmon_cfgs = create_chanmon_cfgs(1); - let mut node_cfgs = create_node_cfgs(1, &chanmon_cfgs); - let chain_mon_0 = test_utils::TestChainMonitor::new( - Some(&chanmon_cfgs[0].chain_source), - &chanmon_cfgs[0].tx_broadcaster, - &chanmon_cfgs[0].logger, - &chanmon_cfgs[0].fee_estimator, - &store, - node_cfgs[0].keys_manager, - ); - node_cfgs[0].chain_monitor = chain_mon_0; - let node_chanmgrs = create_node_chanmgrs(1, &node_cfgs, &[None]); - let nodes = create_network(1, &node_cfgs, &node_chanmgrs); - - // Check that read_channel_monitors() returns error if monitors/ is not a - // directory. - assert!( - read_channel_monitors(&store, nodes[0].keys_manager, nodes[0].keys_manager).is_err() - ); - } - - #[test] - fn test_filesystem_store() { - // Create the nodes, giving them FilesystemStores for data stores. - let store_0 = FilesystemStore::new("test_filesystem_store_0".into()); - let store_1 = FilesystemStore::new("test_filesystem_store_1".into()); - do_test_store(&store_0, &store_1) - } - - // Test that if the store's path to channel data is read-only, writing a - // monitor to it results in the store returning an UnrecoverableError. - // Windows ignores the read-only flag for folders, so this test is Unix-only. - #[cfg(not(target_os = "windows"))] - #[test] - fn test_readonly_dir_perm_failure() { - let store = FilesystemStore::new("test_readonly_dir_perm_failure".into()); - fs::create_dir_all(&store.get_data_dir()).unwrap(); - - // Set up a dummy channel and force close. This will produce a monitor - // that we can then use to test persistence. - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - - let node_a_id = nodes[0].node.get_our_node_id(); - - let chan = create_announced_chan_between_nodes(&nodes, 0, 1); - - let message = "Channel force-closed".to_owned(); - nodes[1] - .node - .force_close_broadcasting_latest_txn(&chan.2, &node_a_id, message.clone()) - .unwrap(); - let reason = - ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; - check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); - let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap(); - - // Set the store's directory to read-only, which should result in - // returning an unrecoverable failure when we then attempt to persist a - // channel update. - let path = &store.get_data_dir(); - let mut perms = fs::metadata(path).unwrap().permissions(); - perms.set_readonly(true); - fs::set_permissions(path, perms).unwrap(); - - let monitor_name = added_monitors[0].1.persistence_key(); - match store.persist_new_channel(monitor_name, &added_monitors[0].1) { - ChannelMonitorUpdateStatus::UnrecoverableError => {}, - _ => panic!("unexpected result from persisting new channel"), - } - - nodes[1].node.get_and_clear_pending_msg_events(); - added_monitors.clear(); - } - - // Test that if a store's directory name is invalid, monitor persistence - // will fail. - #[cfg(target_os = "windows")] - #[test] - fn test_fail_on_open() { - // Set up a dummy channel and force close. This will produce a monitor - // that we can then use to test persistence. - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - - let node_a_id = nodes[0].node.get_our_node_id(); - - let chan = create_announced_chan_between_nodes(&nodes, 0, 1); - - let message = "Channel force-closed".to_owned(); - nodes[1] - .node - .force_close_broadcasting_latest_txn(&chan.2, &node_a_id, message.clone()) - .unwrap(); - let reason = - ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; - check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); - let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap(); - let update_map = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap(); - let update_id = update_map.get(&added_monitors[0].1.channel_id()).unwrap(); - - // Create the store with an invalid directory name and test that the - // channel fails to open because the directories fail to be created. There - // don't seem to be invalid filename characters on Unix that Rust doesn't - // handle, hence why the test is Windows-only. - let store = FilesystemStore::new(":<>/".into()); - - let monitor_name = added_monitors[0].1.persistence_key(); - match store.persist_new_channel(monitor_name, &added_monitors[0].1) { - ChannelMonitorUpdateStatus::UnrecoverableError => {}, - _ => panic!("unexpected result from persisting new channel"), - } - - nodes[1].node.get_and_clear_pending_msg_events(); - added_monitors.clear(); - } -} - -#[cfg(ldk_bench)] -/// Benches -pub mod bench { - use criterion::Criterion; - - /// Bench! - pub fn bench_sends(bench: &mut Criterion) { - let store_a = super::FilesystemStore::new("bench_filesystem_store_a".into()); - let store_b = super::FilesystemStore::new("bench_filesystem_store_b".into()); - lightning::ln::channelmanager::bench::bench_two_sends( - bench, - "bench_filesystem_persisted_sends", - store_a, - store_b, - ); - } -} diff --git a/lightning-persister/src/fs_store/mod.rs b/lightning-persister/src/fs_store/mod.rs new file mode 100644 index 00000000000..5fe7f6542ce --- /dev/null +++ b/lightning-persister/src/fs_store/mod.rs @@ -0,0 +1,6 @@ +//! Implementations of filesystem-backed key-value stores. + +pub mod v1; +pub mod v2; + +pub(crate) mod common; diff --git a/lightning-persister/src/fs_store/v1.rs b/lightning-persister/src/fs_store/v1.rs new file mode 100644 index 00000000000..4f24d8d961f --- /dev/null +++ b/lightning-persister/src/fs_store/v1.rs @@ -0,0 +1,397 @@ +//! Objects related to [`FilesystemStore`] live here. +use crate::fs_store::common::FilesystemStoreState; + +use lightning::util::persist::{KVStoreSync, MigratableKVStoreSync}; + +use std::path::PathBuf; + +#[cfg(feature = "tokio")] +use core::future::Future; +#[cfg(feature = "tokio")] +use lightning::util::persist::KVStore; + +/// A [`KVStore`] and [`KVStoreSync`] implementation that writes to and reads from the file system. +/// +/// [`KVStore`]: lightning::util::persist::KVStore +pub struct FilesystemStore { + state: FilesystemStoreState, +} + +impl FilesystemStore { + /// Constructs a new [`FilesystemStore`]. + pub fn new(data_dir: PathBuf) -> Self { + Self { state: FilesystemStoreState::new(data_dir) } + } + + /// Returns the data directory. + pub fn get_data_dir(&self) -> PathBuf { + self.state.get_data_dir() + } + + #[cfg(any(all(feature = "tokio", test), fuzzing))] + /// Returns the size of the async state. + pub fn state_size(&self) -> usize { + self.state.state_size() + } +} + +impl KVStoreSync for FilesystemStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Result<Vec<u8>, lightning::io::Error> { + self.state.read_impl(primary_namespace, secondary_namespace, key, false) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> Result<(), lightning::io::Error> { + self.state.write_impl(primary_namespace, secondary_namespace, key, buf, false) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Result<(), lightning::io::Error> { + self.state.remove_impl(primary_namespace, secondary_namespace, key, lazy, false) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Result<Vec<String>, lightning::io::Error> { + self.state.list_impl(primary_namespace, secondary_namespace, false) + } +} + +#[cfg(feature = "tokio")] +impl KVStore for FilesystemStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> impl Future<Output = Result<Vec<u8>, lightning::io::Error>> + 'static + Send { + self.state.read_async(primary_namespace, secondary_namespace, key, false) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> impl Future<Output = Result<(), lightning::io::Error>> + 'static + Send { + self.state.write_async(primary_namespace, secondary_namespace, key, buf, false) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> impl Future<Output = Result<(), lightning::io::Error>> + 'static + Send { + self.state.remove_async(primary_namespace, secondary_namespace, key, lazy, false) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> impl Future<Output = Result<Vec<String>, lightning::io::Error>> + 'static + Send { + self.state.list_async(primary_namespace, secondary_namespace, false) + } +} + +impl MigratableKVStoreSync for FilesystemStore { + fn list_all_keys(&self) -> Result<Vec<(String, String, String)>, lightning::io::Error> { + self.state.list_all_keys_impl(false) + } +} + +#[cfg(feature = "tokio")] +impl lightning::util::persist::MigratableKVStore for FilesystemStore { + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<(String, String, String)>, lightning::io::Error>> + 'static + Send + { + self.state.list_all_keys_async(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(feature = "tokio")] + use crate::test_utils::do_test_data_migration_async; + use crate::test_utils::{ + do_read_write_remove_list_persist, do_test_data_migration, do_test_store, + }; + + use lightning::chain::chainmonitor::Persist; + use lightning::chain::ChannelMonitorUpdateStatus; + use lightning::events::ClosureReason; + use lightning::ln::functional_test_utils::*; + use lightning::ln::msgs::BaseMessageHandler; + use lightning::util::persist::read_channel_monitors; + use lightning::util::test_utils; + + use std::fs; + + impl Drop for FilesystemStore { + fn drop(&mut self) { + // We test for invalid directory names, so it's OK if directory removal + // fails. + match fs::remove_dir_all(&self.get_data_dir()) { + Err(e) => println!("Failed to remove test persister directory: {}", e), + _ => {}, + } + } + } + + #[test] + fn read_write_remove_list_persist() { + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_read_write_remove_list_persist"); + let fs_store = FilesystemStore::new(temp_path); + do_read_write_remove_list_persist(&fs_store); + } + + #[cfg(feature = "tokio")] + #[tokio::test] + async fn read_write_remove_list_persist_async() { + use lightning::util::persist::KVStore; + use std::sync::Arc; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_read_write_remove_list_persist_async"); + let fs_store = Arc::new(FilesystemStore::new(temp_path)); + assert_eq!(fs_store.state_size(), 0); + + let async_fs_store = Arc::clone(&fs_store); + + let data1 = vec![42u8; 32]; + let data2 = vec![43u8; 32]; + + let primary = "testspace"; + let secondary = "testsubspace"; + let key = "testkey"; + + // Test writing the same key twice with different data. Execute the asynchronous part out of order to ensure + // that eventual consistency works. + let fut1 = KVStore::write(&*async_fs_store, primary, secondary, key, data1); + assert_eq!(fs_store.state_size(), 1); + + let fut2 = KVStore::remove(&*async_fs_store, primary, secondary, key, false); + assert_eq!(fs_store.state_size(), 1); + + let fut3 = KVStore::write(&*async_fs_store, primary, secondary, key, data2.clone()); + assert_eq!(fs_store.state_size(), 1); + + fut3.await.unwrap(); + assert_eq!(fs_store.state_size(), 1); + + fut2.await.unwrap(); + assert_eq!(fs_store.state_size(), 1); + + fut1.await.unwrap(); + assert_eq!(fs_store.state_size(), 0); + + // Test list. + let listed_keys = KVStore::list(&*async_fs_store, primary, secondary).await.unwrap(); + assert_eq!(listed_keys.len(), 1); + assert_eq!(listed_keys[0], key); + + // Test read. We expect to read data2, as the write call was initiated later. + let read_data = KVStore::read(&*async_fs_store, primary, secondary, key).await.unwrap(); + assert_eq!(data2, &*read_data); + + // Test remove. + KVStore::remove(&*async_fs_store, primary, secondary, key, false).await.unwrap(); + + let listed_keys = KVStore::list(&*async_fs_store, primary, secondary).await.unwrap(); + assert_eq!(listed_keys.len(), 0); + } + + #[test] + fn list_all_keys_skips_leftover_store_artifacts() { + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_list_all_keys_skips_leftover_store_artifacts"); + let fs_store = FilesystemStore::new(temp_path.clone()); + KVStoreSync::write(&fs_store, "primary", "secondary", "key", vec![1]).unwrap(); + + fs::write(temp_path.join("top_level.0.tmp"), b"stale").unwrap(); + fs::write(temp_path.join("top_level.0.trash"), b"stale").unwrap(); + + let primary_path = temp_path.join("primary"); + fs::write(primary_path.join("primary_level.0.tmp"), b"stale").unwrap(); + fs::write(primary_path.join("primary_level.0.trash"), b"stale").unwrap(); + + let secondary_path = primary_path.join("secondary"); + fs::write(secondary_path.join("secondary_level.0.tmp"), b"stale").unwrap(); + fs::write(secondary_path.join("secondary_level.0.trash"), b"stale").unwrap(); + + let keys = fs_store.list_all_keys().unwrap(); + assert_eq!(keys, vec![("primary".to_string(), "secondary".to_string(), "key".to_string())]); + } + + #[test] + fn test_data_migration() { + let mut source_temp_path = std::env::temp_dir(); + source_temp_path.push("test_data_migration_source"); + let mut source_store = FilesystemStore::new(source_temp_path); + + let mut target_temp_path = std::env::temp_dir(); + target_temp_path.push("test_data_migration_target"); + let mut target_store = FilesystemStore::new(target_temp_path); + + do_test_data_migration(&mut source_store, &mut target_store); + } + + #[cfg(feature = "tokio")] + #[tokio::test] + async fn test_data_migration_async() { + let mut source_temp_path = std::env::temp_dir(); + source_temp_path.push("test_data_migration_source_async"); + let source_store = FilesystemStore::new(source_temp_path); + + let mut target_temp_path = std::env::temp_dir(); + target_temp_path.push("test_data_migration_target_async"); + let target_store = FilesystemStore::new(target_temp_path); + + do_test_data_migration_async(&source_store, &target_store).await; + } + + #[test] + fn test_if_monitors_is_not_dir() { + let store = FilesystemStore::new("test_monitors_is_not_dir".into()); + + fs::create_dir_all(&store.get_data_dir()).unwrap(); + let mut path = std::path::PathBuf::from(&store.get_data_dir()); + path.push("monitors"); + fs::File::create(path).unwrap(); + + let chanmon_cfgs = create_chanmon_cfgs(1); + let mut node_cfgs = create_node_cfgs(1, &chanmon_cfgs); + let chain_mon_0 = test_utils::TestChainMonitor::new( + Some(&chanmon_cfgs[0].chain_source), + &chanmon_cfgs[0].tx_broadcaster, + &chanmon_cfgs[0].logger, + &chanmon_cfgs[0].fee_estimator, + &store, + node_cfgs[0].keys_manager, + ); + node_cfgs[0].chain_monitor = chain_mon_0; + let node_chanmgrs = create_node_chanmgrs(1, &node_cfgs, &[None]); + let nodes = create_network(1, &node_cfgs, &node_chanmgrs); + + // Check that read_channel_monitors() returns error if monitors/ is not a + // directory. + assert!( + read_channel_monitors(&store, nodes[0].keys_manager, nodes[0].keys_manager).is_err() + ); + } + + #[test] + fn test_filesystem_store() { + // Create the nodes, giving them FilesystemStores for data stores. + let store_0 = FilesystemStore::new("test_filesystem_store_0".into()); + let store_1 = FilesystemStore::new("test_filesystem_store_1".into()); + do_test_store(&store_0, &store_1) + } + + // Test that if the store's path to channel data is read-only, writing a + // monitor to it results in the store returning an UnrecoverableError. + // Windows ignores the read-only flag for folders, so this test is Unix-only. + #[cfg(not(target_os = "windows"))] + #[test] + fn test_readonly_dir_perm_failure() { + let store = FilesystemStore::new("test_readonly_dir_perm_failure".into()); + fs::create_dir_all(&store.get_data_dir()).unwrap(); + + // Set up a dummy channel and force close. This will produce a monitor + // that we can then use to test persistence. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + + let chan = create_announced_chan_between_nodes(&nodes, 0, 1); + + let message = "Channel force-closed".to_owned(); + nodes[1] + .node + .force_close_broadcasting_latest_txn(&chan.2, &node_a_id, message.clone()) + .unwrap(); + let reason = + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; + check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); + let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap(); + + // Set the store's directory to read-only, which should result in + // returning an unrecoverable failure when we then attempt to persist a + // channel update. + let path = &store.get_data_dir(); + let mut perms = fs::metadata(path).unwrap().permissions(); + perms.set_readonly(true); + fs::set_permissions(path, perms).unwrap(); + + let monitor_name = added_monitors[0].1.persistence_key(); + match store.persist_new_channel(monitor_name, &added_monitors[0].1) { + ChannelMonitorUpdateStatus::UnrecoverableError => {}, + _ => panic!("unexpected result from persisting new channel"), + } + + nodes[1].node.get_and_clear_pending_msg_events(); + added_monitors.clear(); + } + + // Test that if a store's directory name is invalid, monitor persistence + // will fail. + #[cfg(target_os = "windows")] + #[test] + fn test_fail_on_open() { + // Set up a dummy channel and force close. This will produce a monitor + // that we can then use to test persistence. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + + let chan = create_announced_chan_between_nodes(&nodes, 0, 1); + + let message = "Channel force-closed".to_owned(); + nodes[1] + .node + .force_close_broadcasting_latest_txn(&chan.2, &node_a_id, message.clone()) + .unwrap(); + let reason = + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; + check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); + let mut added_monitors = nodes[1].chain_monitor.added_monitors.lock().unwrap(); + let update_map = nodes[1].chain_monitor.latest_monitor_update_id.lock().unwrap(); + let update_id = update_map.get(&added_monitors[0].1.channel_id()).unwrap(); + + // Create the store with an invalid directory name and test that the + // channel fails to open because the directories fail to be created. There + // don't seem to be invalid filename characters on Unix that Rust doesn't + // handle, hence why the test is Windows-only. + let store = FilesystemStore::new(":<>/".into()); + + let monitor_name = added_monitors[0].1.persistence_key(); + match store.persist_new_channel(monitor_name, &added_monitors[0].1) { + ChannelMonitorUpdateStatus::UnrecoverableError => {}, + _ => panic!("unexpected result from persisting new channel"), + } + + nodes[1].node.get_and_clear_pending_msg_events(); + added_monitors.clear(); + } +} + +#[cfg(ldk_bench)] +/// Benches +pub mod bench { + use criterion::Criterion; + + /// Bench! + pub fn bench_sends(bench: &mut Criterion) { + let store_a = super::FilesystemStore::new("bench_filesystem_store_a".into()); + let store_b = super::FilesystemStore::new("bench_filesystem_store_b".into()); + lightning::ln::channelmanager::bench::bench_two_sends( + bench, + "bench_filesystem_persisted_sends", + store_a, + store_b, + ); + } +} diff --git a/lightning-persister/src/fs_store/v2.rs b/lightning-persister/src/fs_store/v2.rs new file mode 100644 index 00000000000..af0ad4f155c --- /dev/null +++ b/lightning-persister/src/fs_store/v2.rs @@ -0,0 +1,842 @@ +//! Objects related to [`FilesystemStoreV2`] live here. +use crate::fs_store::common::{ + dir_entry_is_key, get_key_from_dir_entry_path, FilesystemStoreState, +}; + +use lightning::util::persist::{ + KVStoreSync, MigratableKVStoreSync, PageToken, PaginatedKVStoreSync, PaginatedListResponse, +}; + +use std::fs; +use std::path::PathBuf; +use std::time::UNIX_EPOCH; +use std::{error, fmt, io}; + +#[cfg(feature = "tokio")] +use core::future::Future; +#[cfg(feature = "tokio")] +use lightning::util::persist::{KVStore, PaginatedKVStore}; +use std::sync::Arc; + +/// An error returned when constructing a [`FilesystemStoreV2`]. +#[derive(Debug)] +pub enum FilesystemStoreV2Error { + /// The data directory contains a file where v2 expects a namespace directory, indicating it + /// was previously used by [`FilesystemStore`] (v1). Contains the path of the offending file. + /// + /// [`FilesystemStore`]: crate::fs_store::v1::FilesystemStore + V1DataDetected(PathBuf), + /// An I/O error occurred while inspecting the data directory. + Io(io::Error), +} + +impl fmt::Display for FilesystemStoreV2Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::V1DataDetected(path) => write!( + f, + "Found file `{}` where FilesystemStoreV2 expects a namespace directory. \ + This indicates the directory was previously used by FilesystemStore (v1). \ + Please migrate your data or use a different directory.", + path.display() + ), + Self::Io(err) => write!(f, "{}", err), + } + } +} + +impl error::Error for FilesystemStoreV2Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + match self { + Self::V1DataDetected(_) => None, + Self::Io(err) => Some(err), + } + } +} + +impl From<io::Error> for FilesystemStoreV2Error { + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} + +/// A [`KVStore`] and [`KVStoreSync`] implementation that writes to and reads from the file system. +/// +/// This is version 2 of the filesystem store which provides: +/// - Consistent directory structure using `[empty]` for empty namespaces +/// - File modification times for creation-order pagination +/// - Support for [`PaginatedKVStoreSync`] with newest-first ordering +/// +/// ## Directory Structure +/// +/// Files are stored with a consistent two-level namespace hierarchy: +/// ```text +/// data_dir/ +/// [empty]/ # empty primary namespace +/// [empty]/ # empty secondary namespace +/// {key} +/// primary_ns/ +/// [empty]/ # empty secondary namespace +/// {key} +/// secondary_ns/ +/// {key} +/// ``` +/// +/// ## File Ordering +/// +/// Files are ordered by their modification time (mtime). When a file is created, it gets +/// the current time. When updated, the original creation time is preserved by setting +/// the mtime of the new file to match the original before the atomic rename. +/// +/// [`KVStore`]: lightning::util::persist::KVStore +pub struct FilesystemStoreV2 { + inner: Arc<FilesystemStoreState>, +} + +impl FilesystemStoreV2 { + /// Constructs a new [`FilesystemStoreV2`]. + /// + /// Returns [`FilesystemStoreV2Error::V1DataDetected`] if the data directory already exists + /// and contains files where v2 expects namespace directories, which would indicate it was + /// previously used by a [`FilesystemStore`] (v1). The v2 store expects only directories at + /// the top level and one level down. + /// + /// [`FilesystemStore`]: crate::fs_store::v1::FilesystemStore + pub fn new(data_dir: PathBuf) -> Result<Self, FilesystemStoreV2Error> { + if data_dir.exists() { + for entry in fs::read_dir(&data_dir)? { + let entry = entry?; + let file_type = entry.file_type()?; + if file_type.is_file() { + return Err(FilesystemStoreV2Error::V1DataDetected(entry.path())); + } + + if file_type.is_dir() { + for child_entry in fs::read_dir(entry.path())? { + let child_entry = child_entry?; + if child_entry.file_type()?.is_file() { + return Err(FilesystemStoreV2Error::V1DataDetected(child_entry.path())); + } + } + } + } + } + + Ok(Self { inner: Arc::new(FilesystemStoreState::new(data_dir)) }) + } + + /// Returns the data directory. + pub fn get_data_dir(&self) -> PathBuf { + self.inner.get_data_dir() + } + + #[cfg(any(all(feature = "tokio", test), fuzzing))] + /// Returns the size of the async state. + pub fn state_size(&self) -> usize { + self.inner.state_size() + } +} + +/// The fixed page size for paginated listing operations. +pub(crate) const PAGE_SIZE: usize = 50; + +/// The length of the timestamp in a page token (milliseconds since epoch as 16-digit decimal). +const PAGE_TOKEN_TIMESTAMP_LEN: usize = 16; + +impl FilesystemStoreState { + fn list_paginated_impl( + &self, prefixed_dest: PathBuf, page_token: Option<PageToken>, + ) -> Result<PaginatedListResponse, lightning::io::Error> { + if !prefixed_dest.exists() { + return Ok(PaginatedListResponse { keys: Vec::new(), next_page_token: None }); + } + + // Collect all entries with their modification times + let mut entries: Vec<(u64, String)> = Vec::new(); + for dir_entry in fs::read_dir(&prefixed_dest)? { + let dir_entry = dir_entry?; + + match dir_entry_is_key(&dir_entry) { + // Entry is not a key (e.g., .tmp file, directory), skip it. + Ok(false) => continue, + // Entry is a valid key file, proceed to collect it. + Ok(true) => {}, + // Entry may have been deleted between read_dir and our check. Include + // it anyway to give a more consistent view, matching list's behavior. + Err(_) => {}, + } + + let key = + get_key_from_dir_entry_path(&dir_entry.path(), prefixed_dest.as_path(), false)?; + // Get modification time as millis since epoch + let mtime_millis = dir_entry + .metadata() + .ok() + .and_then(|m| m.modified().ok()) + .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + + entries.push((mtime_millis, key)); + } + + // Sort by mtime descending (newest first), then by key descending for same mtime + entries.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| b.1.cmp(&a.1))); + + // Find starting position based on page token + let start_idx = if let Some(token) = page_token { + let (token_mtime, token_key) = parse_page_token(token.as_str())?; + + // Find entries that come after the token (older entries = lower mtime) + // or same mtime but lexicographically smaller key (since we sort descending) + entries + .iter() + .position(|(mtime, key)| { + *mtime < token_mtime + || (*mtime == token_mtime && key.as_str() < token_key.as_str()) + }) + .unwrap_or(entries.len()) + } else { + 0 + }; + + // Take PAGE_SIZE entries starting from start_idx + let page_entries: Vec<_> = + entries.iter().skip(start_idx).take(PAGE_SIZE).cloned().collect(); + + // Determine next page token + let next_page_token = if start_idx + PAGE_SIZE < entries.len() { + page_entries.last().map(|(mtime, key)| PageToken::new(format_page_token(*mtime, key))) + } else { + None + }; + + let keys: Vec<String> = page_entries.into_iter().map(|(_, key)| key).collect(); + + Ok(PaginatedListResponse { keys, next_page_token }) + } +} + +impl KVStoreSync for FilesystemStoreV2 { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Result<Vec<u8>, lightning::io::Error> { + self.inner.read_impl(primary_namespace, secondary_namespace, key, true) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> Result<(), lightning::io::Error> { + self.inner.write_impl(primary_namespace, secondary_namespace, key, buf, true) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Result<(), lightning::io::Error> { + self.inner.remove_impl(primary_namespace, secondary_namespace, key, lazy, true) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Result<Vec<String>, lightning::io::Error> { + self.inner.list_impl(primary_namespace, secondary_namespace, true) + } +} + +impl PaginatedKVStoreSync for FilesystemStoreV2 { + fn list_paginated( + &self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>, + ) -> Result<PaginatedListResponse, lightning::io::Error> { + let prefixed_dest = self.inner.get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + None, + "list_paginated", + true, + )?; + self.inner.list_paginated_impl(prefixed_dest, page_token) + } +} + +#[cfg(feature = "tokio")] +impl KVStore for FilesystemStoreV2 { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> impl Future<Output = Result<Vec<u8>, lightning::io::Error>> + 'static + Send { + self.inner.read_async(primary_namespace, secondary_namespace, key, true) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> impl Future<Output = Result<(), lightning::io::Error>> + 'static + Send { + self.inner.write_async(primary_namespace, secondary_namespace, key, buf, true) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> impl Future<Output = Result<(), lightning::io::Error>> + 'static + Send { + self.inner.remove_async(primary_namespace, secondary_namespace, key, lazy, true) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> impl Future<Output = Result<Vec<String>, lightning::io::Error>> + 'static + Send { + self.inner.list_async(primary_namespace, secondary_namespace, true) + } +} + +#[cfg(feature = "tokio")] +impl PaginatedKVStore for FilesystemStoreV2 { + fn list_paginated( + &self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>, + ) -> impl Future<Output = Result<PaginatedListResponse, lightning::io::Error>> + 'static + Send + { + let this = Arc::clone(&self.inner); + + let path = this.get_checked_dest_file_path( + primary_namespace, + secondary_namespace, + None, + "list_paginated", + true, + ); + + async move { + let path = match path { + Ok(path) => path, + Err(e) => return Err(e), + }; + tokio::task::spawn_blocking(move || this.list_paginated_impl(path, page_token)) + .await + .unwrap_or_else(|e| { + Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, e)) + }) + } + } +} + +impl MigratableKVStoreSync for FilesystemStoreV2 { + fn list_all_keys(&self) -> Result<Vec<(String, String, String)>, lightning::io::Error> { + self.inner.list_all_keys_impl(true) + } +} + +#[cfg(feature = "tokio")] +impl lightning::util::persist::MigratableKVStore for FilesystemStoreV2 { + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<(String, String, String)>, lightning::io::Error>> + 'static + Send + { + self.inner.list_all_keys_async(true) + } +} + +/// Formats a page token from mtime (millis since epoch) and key. +pub(crate) fn format_page_token(mtime_millis: u64, key: &str) -> String { + format!("{mtime_millis:016}:{key}") +} + +/// Parses a page token into mtime (millis since epoch) and key. +pub(crate) fn parse_page_token(token: &str) -> lightning::io::Result<(u64, String)> { + if token.as_bytes().get(PAGE_TOKEN_TIMESTAMP_LEN) != Some(&b':') { + return Err(lightning::io::Error::new( + lightning::io::ErrorKind::InvalidInput, + "Invalid page token format", + )); + } + + let mtime = token[..PAGE_TOKEN_TIMESTAMP_LEN].parse::<u64>().map_err(|_| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidInput, + "Invalid page token timestamp", + ) + })?; + + let key = token[PAGE_TOKEN_TIMESTAMP_LEN + 1..].to_string(); + + Ok((mtime, key)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fs_store::common::EMPTY_NAMESPACE_DIR; + #[cfg(feature = "tokio")] + use crate::test_utils::do_test_data_migration_async; + use crate::test_utils::{ + do_read_write_remove_list_persist, do_test_data_migration, do_test_store, + }; + use std::fs::FileTimes; + use std::time::UNIX_EPOCH; + + impl Drop for FilesystemStoreV2 { + fn drop(&mut self) { + // We test for invalid directory names, so it's OK if directory removal + // fails. + match fs::remove_dir_all(&self.inner.get_data_dir()) { + Err(e) => println!("Failed to remove test persister directory: {}", e), + _ => {}, + } + } + } + + #[test] + fn read_write_remove_list_persist() { + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_read_write_remove_list_persist_v2"); + let fs_store = FilesystemStoreV2::new(temp_path).unwrap(); + do_read_write_remove_list_persist(&fs_store); + } + + #[cfg(feature = "tokio")] + #[tokio::test] + async fn read_write_remove_list_persist_async() { + use lightning::util::persist::KVStore; + use std::sync::Arc; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_read_write_remove_list_persist_async_v2"); + let fs_store = Arc::new(FilesystemStoreV2::new(temp_path).unwrap()); + assert_eq!(fs_store.state_size(), 0); + + let async_fs_store = Arc::clone(&fs_store); + + let data1 = vec![42u8; 32]; + let data2 = vec![43u8; 32]; + + let primary = "testspace"; + let secondary = "testsubspace"; + let key = "testkey"; + + // Test writing the same key twice with different data. Execute the asynchronous part out of order to ensure + // that eventual consistency works. + let fut1 = KVStore::write(&*async_fs_store, primary, secondary, key, data1); + assert_eq!(fs_store.state_size(), 1); + + let fut2 = KVStore::remove(&*async_fs_store, primary, secondary, key, false); + assert_eq!(fs_store.state_size(), 1); + + let fut3 = KVStore::write(&*async_fs_store, primary, secondary, key, data2.clone()); + assert_eq!(fs_store.state_size(), 1); + + fut3.await.unwrap(); + assert_eq!(fs_store.state_size(), 1); + + fut2.await.unwrap(); + assert_eq!(fs_store.state_size(), 1); + + fut1.await.unwrap(); + assert_eq!(fs_store.state_size(), 0); + + // Test list. + let listed_keys = KVStore::list(&*async_fs_store, primary, secondary).await.unwrap(); + assert_eq!(listed_keys.len(), 1); + assert_eq!(listed_keys[0], key); + + // Test read. We expect to read data2, as the write call was initiated later. + let read_data = KVStore::read(&*async_fs_store, primary, secondary, key).await.unwrap(); + assert_eq!(data2, &*read_data); + + // Test remove. + KVStore::remove(&*async_fs_store, primary, secondary, key, false).await.unwrap(); + + let listed_keys = KVStore::list(&*async_fs_store, primary, secondary).await.unwrap(); + assert_eq!(listed_keys.len(), 0); + } + + #[cfg(feature = "tokio")] + #[tokio::test] + async fn stale_write_does_not_leak_tmp_file() { + use lightning::util::persist::KVStore; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_stale_write_does_not_leak_tmp_file_v2"); + let _ = fs::remove_dir_all(&temp_path); + let fs_store = FilesystemStoreV2::new(temp_path.clone()).unwrap(); + + let data1 = vec![1u8; 32]; + let data2 = vec![2u8; 32]; + + let primary = "testspace"; + let secondary = "testsubspace"; + let key = "testkey"; + + let fut1 = KVStore::write(&fs_store, primary, secondary, key, data1); + let fut2 = KVStore::write(&fs_store, primary, secondary, key, data2); + + fut2.await.unwrap(); + fut1.await.unwrap(); + + let dir = temp_path.join(primary).join(secondary); + let tmp_files: Vec<_> = fs::read_dir(&dir) + .unwrap() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| p.extension().map_or(false, |ext| ext == "tmp")) + .collect(); + assert!(tmp_files.is_empty(), "Found leaked tmp files: {:?}", tmp_files); + } + + #[test] + fn test_data_migration() { + let mut source_temp_path = std::env::temp_dir(); + source_temp_path.push("test_data_migration_source_v2"); + let mut source_store = FilesystemStoreV2::new(source_temp_path).unwrap(); + + let mut target_temp_path = std::env::temp_dir(); + target_temp_path.push("test_data_migration_target_v2"); + let mut target_store = FilesystemStoreV2::new(target_temp_path).unwrap(); + + do_test_data_migration(&mut source_store, &mut target_store); + } + + #[cfg(feature = "tokio")] + #[tokio::test] + async fn test_data_migration_async() { + let mut source_temp_path = std::env::temp_dir(); + source_temp_path.push("test_data_migration_source_async_v2"); + let source_store = FilesystemStoreV2::new(source_temp_path).unwrap(); + + let mut target_temp_path = std::env::temp_dir(); + target_temp_path.push("test_data_migration_target_async_v2"); + let target_store = FilesystemStoreV2::new(target_temp_path).unwrap(); + + do_test_data_migration_async(&source_store, &target_store).await; + } + + #[test] + fn test_filesystem_store_v2() { + // Create the nodes, giving them FilesystemStoreV2s for data stores. + let store_0 = FilesystemStoreV2::new("test_filesystem_store_v2_0".into()).unwrap(); + let store_1 = FilesystemStoreV2::new("test_filesystem_store_v2_1".into()).unwrap(); + do_test_store(&store_0, &store_1) + } + + #[test] + fn test_page_token_format() { + let mtime: u64 = 1706500000000; + let key = "test_key"; + let token = format_page_token(mtime, key); + assert_eq!(token, "0001706500000000:test_key"); + + let parsed = parse_page_token(&token).unwrap(); + assert_eq!(parsed, (mtime, key.to_string())); + + // Test invalid tokens + assert!(parse_page_token("invalid").is_err()); + assert!(parse_page_token("0001706500000000_key").is_err()); // wrong separator + assert!(parse_page_token("0001706500000000").is_err()); // no separator and key + assert!(parse_page_token("1706500000000:key").is_err()); // too short timestamp + } + + #[test] + fn test_directory_structure() { + use lightning::util::persist::KVStoreSync; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_directory_structure_v2"); + let fs_store = FilesystemStoreV2::new(temp_path.clone()).unwrap(); + + let data = vec![42u8; 32]; + + // Write with empty namespaces + KVStoreSync::write(&fs_store, "", "", "key1", data.clone()).unwrap(); + assert!(temp_path.join(EMPTY_NAMESPACE_DIR).join(EMPTY_NAMESPACE_DIR).exists()); + + // Write with non-empty primary, empty secondary + KVStoreSync::write(&fs_store, "primary", "", "key2", data.clone()).unwrap(); + assert!(temp_path.join("primary").join(EMPTY_NAMESPACE_DIR).exists()); + + // Write with both non-empty + KVStoreSync::write(&fs_store, "primary", "secondary", "key3", data.clone()).unwrap(); + assert!(temp_path.join("primary").join("secondary").exists()); + + // Verify we can read them back + assert_eq!(KVStoreSync::read(&fs_store, "", "", "key1").unwrap(), data); + assert_eq!(KVStoreSync::read(&fs_store, "primary", "", "key2").unwrap(), data); + assert_eq!(KVStoreSync::read(&fs_store, "primary", "secondary", "key3").unwrap(), data); + + // Verify files are named just by key (no timestamp prefix) + assert!(temp_path + .join(EMPTY_NAMESPACE_DIR) + .join(EMPTY_NAMESPACE_DIR) + .join("key1") + .exists()); + assert!(temp_path.join("primary").join(EMPTY_NAMESPACE_DIR).join("key2").exists()); + assert!(temp_path.join("primary").join("secondary").join("key3").exists()); + } + + #[test] + fn test_update_preserves_mtime() { + use lightning::util::persist::KVStoreSync; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_update_preserves_mtime_v2"); + let fs_store = FilesystemStoreV2::new(temp_path.clone()).unwrap(); + + let data1 = vec![42u8; 32]; + let data2 = vec![43u8; 32]; + + // Write initial data + KVStoreSync::write(&fs_store, "ns", "sub", "key", data1).unwrap(); + + // Get the original mtime + let file_path = temp_path.join("ns").join("sub").join("key"); + let original_mtime = fs::metadata(&file_path).unwrap().modified().unwrap(); + + // Sleep briefly to ensure different timestamp if not preserved + std::thread::sleep(std::time::Duration::from_millis(50)); + + // Update with new data + KVStoreSync::write(&fs_store, "ns", "sub", "key", data2.clone()).unwrap(); + + // Verify mtime is preserved + let updated_mtime = fs::metadata(&file_path).unwrap().modified().unwrap(); + assert_eq!(original_mtime, updated_mtime); + + // Verify data was updated + assert_eq!(KVStoreSync::read(&fs_store, "ns", "sub", "key").unwrap(), data2); + } + + #[test] + fn test_paginated_listing() { + use lightning::util::persist::{KVStoreSync, PaginatedKVStoreSync}; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_paginated_listing_v2"); + let fs_store = FilesystemStoreV2::new(temp_path).unwrap(); + + let data = vec![42u8; 32]; + + // Write several keys with small delays to ensure different mtimes + let keys: Vec<String> = (0..5).map(|i| format!("key{}", i)).collect(); + for key in &keys { + KVStoreSync::write(&fs_store, "ns", "sub", key, data.clone()).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // List paginated - should return newest first + let response = PaginatedKVStoreSync::list_paginated(&fs_store, "ns", "sub", None).unwrap(); + assert_eq!(response.keys.len(), 5); + // Newest key (key4) should be first + assert_eq!(response.keys[0], "key4"); + assert_eq!(response.keys[4], "key0"); + assert!(response.next_page_token.is_none()); // Less than PAGE_SIZE items + } + + #[test] + fn test_paginated_listing_with_pagination() { + use lightning::util::persist::{KVStoreSync, PaginatedKVStoreSync}; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_paginated_listing_with_pagination_v2"); + let fs_store = FilesystemStoreV2::new(temp_path).unwrap(); + + let data = vec![42u8; 32]; + + // Write more than PAGE_SIZE keys + let num_keys = PAGE_SIZE + 50; + for i in 0..num_keys { + let key = format!("key{:04}", i); + KVStoreSync::write(&fs_store, "ns", "sub", &key, data.clone()).unwrap(); + // Small delay to ensure ordering + if i % 10 == 0 { + std::thread::sleep(std::time::Duration::from_millis(1)); + } + } + + // First page + let response1 = PaginatedKVStoreSync::list_paginated(&fs_store, "ns", "sub", None).unwrap(); + assert_eq!(response1.keys.len(), PAGE_SIZE); + assert!(response1.next_page_token.is_some()); + + // Second page + let response2 = + PaginatedKVStoreSync::list_paginated(&fs_store, "ns", "sub", response1.next_page_token) + .unwrap(); + assert_eq!(response2.keys.len(), 50); + assert!(response2.next_page_token.is_none()); + + // Verify no duplicates between pages + let all_keys: std::collections::HashSet<_> = + response1.keys.iter().chain(response2.keys.iter()).collect(); + assert_eq!(all_keys.len(), num_keys); + } + + #[test] + fn test_page_token_after_deletion() { + use lightning::util::persist::{KVStoreSync, PaginatedKVStoreSync}; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_page_token_after_deletion_v2"); + let fs_store = FilesystemStoreV2::new(temp_path).unwrap(); + + let data = vec![42u8; 32]; + + // Write keys + for i in 0..10 { + let key = format!("key{}", i); + KVStoreSync::write(&fs_store, "ns", "sub", &key, data.clone()).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // Verify initial listing + let response1 = PaginatedKVStoreSync::list_paginated(&fs_store, "ns", "sub", None).unwrap(); + assert_eq!(response1.keys.len(), 10); + + // Delete some keys + KVStoreSync::remove(&fs_store, "ns", "sub", "key5", false).unwrap(); + KVStoreSync::remove(&fs_store, "ns", "sub", "key3", false).unwrap(); + + // List again - should work fine with deleted keys + let response2 = PaginatedKVStoreSync::list_paginated(&fs_store, "ns", "sub", None).unwrap(); + assert_eq!(response2.keys.len(), 8); // 10 - 2 deleted + } + + #[test] + fn test_same_mtime_sorted_by_key() { + use lightning::util::persist::PaginatedKVStoreSync; + use std::time::Duration; + + // Create files directly on disk first with the same mtime + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_same_mtime_sorted_by_key_v2"); + let _ = fs::remove_dir_all(&temp_path); + + let data = vec![42u8; 32]; + let dir = temp_path.join("ns").join("sub"); + fs::create_dir_all(&dir).unwrap(); + + // Write files with the same mtime but different keys + let keys = vec!["zebra", "apple", "mango", "banana"]; + let fixed_time = UNIX_EPOCH + Duration::from_secs(1706500000); + + for key in &keys { + let file_path = dir.join(key); + let file = fs::File::create(&file_path).unwrap(); + std::io::Write::write_all(&mut &file, &data).unwrap(); + file.set_times(FileTimes::new().set_modified(fixed_time)).unwrap(); + } + + // Open the store + let fs_store = FilesystemStoreV2::new(temp_path.clone()).unwrap(); + + // List paginated - should return keys sorted by key in reverse order + // (for same mtime, keys are sorted reverse alphabetically) + let response = PaginatedKVStoreSync::list_paginated(&fs_store, "ns", "sub", None).unwrap(); + assert_eq!(response.keys.len(), 4); + + // Same mtime means sorted by key in reverse order (z > m > b > a) + assert_eq!(response.keys[0], "zebra"); + assert_eq!(response.keys[1], "mango"); + assert_eq!(response.keys[2], "banana"); + assert_eq!(response.keys[3], "apple"); + } + + #[test] + fn test_paginated_listing_skips_tmp_files() { + use lightning::util::persist::{KVStoreSync, PaginatedKVStoreSync}; + + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_paginated_listing_skips_tmp_files_v2"); + let fs_store = FilesystemStoreV2::new(temp_path.clone()).unwrap(); + + let data = vec![42u8; 32]; + + // Write some real keys + KVStoreSync::write(&fs_store, "ns", "sub", "key0", data.clone()).unwrap(); + std::thread::sleep(std::time::Duration::from_millis(10)); + KVStoreSync::write(&fs_store, "ns", "sub", "key1", data.clone()).unwrap(); + + // Create a .tmp file and a subdirectory directly on disk + let dir = temp_path.join("ns").join("sub"); + fs::write(dir.join("inflight.tmp"), &data).unwrap(); + fs::create_dir_all(dir.join("stray_dir")).unwrap(); + + // Paginated listing should only return the two real keys + let response = PaginatedKVStoreSync::list_paginated(&fs_store, "ns", "sub", None).unwrap(); + assert_eq!(response.keys.len(), 2); + assert!(response.keys.contains(&"key0".to_string())); + assert!(response.keys.contains(&"key1".to_string())); + } + + #[test] + fn test_rejects_v1_data_directory() { + let mut temp_path = std::env::temp_dir(); + temp_path.push("test_rejects_v1_data_directory"); + let _ = fs::remove_dir_all(&temp_path); + fs::create_dir_all(&temp_path).unwrap(); + + // Create a file at the top level, as v1 would for an empty primary namespace + // and an empty secondary namespace. + fs::write(temp_path.join("some_key"), b"data").unwrap(); + + // V2 construction should fail + match FilesystemStoreV2::new(temp_path.clone()) { + Err(FilesystemStoreV2Error::V1DataDetected(path)) => { + assert_eq!(path, temp_path.join("some_key")); + }, + Err(err) => panic!("Expected V1DataDetected, got {:?}", err), + Ok(_) => panic!("Expected error for directory with top-level files"), + } + + // Clean up + let _ = fs::remove_dir_all(&temp_path); + + // Create a file one level down, as v1 would for a non-empty primary namespace + // and an empty secondary namespace. + fs::create_dir_all(temp_path.join("some_namespace")).unwrap(); + fs::write(temp_path.join("some_namespace").join("some_key"), b"data").unwrap(); + + match FilesystemStoreV2::new(temp_path.clone()) { + Err(FilesystemStoreV2Error::V1DataDetected(path)) => { + assert_eq!(path, temp_path.join("some_namespace").join("some_key")); + }, + Err(err) => panic!("Expected V1DataDetected, got {:?}", err), + Ok(_) => panic!("Expected error for directory with files one level down"), + } + + let _ = fs::remove_dir_all(&temp_path); + + // A v1 write with an empty primary namespace and non-empty secondary namespace + // is rejected by the KVStore API, but its filesystem layout would be the same + // one-level shape. + fs::create_dir_all(temp_path.join("some_secondary_namespace")).unwrap(); + fs::write(temp_path.join("some_secondary_namespace").join("some_key"), b"data").unwrap(); + + match FilesystemStoreV2::new(temp_path.clone()) { + Err(FilesystemStoreV2Error::V1DataDetected(path)) => { + assert_eq!(path, temp_path.join("some_secondary_namespace").join("some_key")); + }, + Err(err) => panic!("Expected V1DataDetected, got {:?}", err), + Ok(_) => panic!("Expected error for directory with files one level down"), + } + + let _ = fs::remove_dir_all(&temp_path); + + // An empty directory should succeed + fs::create_dir_all(&temp_path).unwrap(); + let result = FilesystemStoreV2::new(temp_path.clone()); + assert!(result.is_ok()); + + // A directory with only namespace subdirectories should succeed + fs::create_dir_all(temp_path.join("some_namespace").join("some_sub_namespace")).unwrap(); + let result = FilesystemStoreV2::new(temp_path.clone()); + assert!(result.is_ok()); + + // V1 data with non-empty primary and secondary namespaces has the same filesystem + // layout as valid v2 data, so construction must not reject this shape. + let fs_store = result.unwrap(); + KVStoreSync::write( + &fs_store, + "some_namespace", + "some_sub_namespace", + "some_key", + b"data".to_vec(), + ) + .unwrap(); + + let result = FilesystemStoreV2::new(temp_path); + assert!(result.is_ok()); + } +} diff --git a/lightning-persister/src/test_utils.rs b/lightning-persister/src/test_utils.rs index 48b383ad1ea..34e0619b34a 100644 --- a/lightning-persister/src/test_utils.rs +++ b/lightning-persister/src/test_utils.rs @@ -1,8 +1,7 @@ -use lightning::check_closed_broadcast; use lightning::events::ClosureReason; use lightning::ln::functional_test_utils::*; use lightning::util::persist::{ - migrate_kv_store_data, read_channel_monitors, KVStoreSync, MigratableKVStore, + migrate_kv_store_data, read_channel_monitors, KVStoreSync, MigratableKVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, }; use lightning::util::test_utils; @@ -60,15 +59,11 @@ pub(crate) fn do_read_write_remove_list_persist<K: KVStoreSync + RefUnwindSafe>( assert_eq!(listed_keys.len(), 0); } -pub(crate) fn do_test_data_migration<S: MigratableKVStore, T: MigratableKVStore>( - source_store: &mut S, target_store: &mut T, -) { - // We fill the source with some bogus keys. - let dummy_data = vec![42u8; 32]; +fn data_migration_test_keys() -> Vec<(String, String, String)> { let num_primary_namespaces = 3; let num_secondary_namespaces = 3; let num_keys = 3; - let mut expected_keys = Vec::new(); + let mut keys = Vec::new(); for i in 0..num_primary_namespaces { let primary_namespace = if i == 0 { String::new() @@ -84,13 +79,25 @@ pub(crate) fn do_test_data_migration<S: MigratableKVStore, T: MigratableKVStore> for k in 0..num_keys { let key = format!("testkey{}", KVSTORE_NAMESPACE_KEY_ALPHABET.chars().nth(k).unwrap()); - source_store - .write(&primary_namespace, &secondary_namespace, &key, dummy_data.clone()) - .unwrap(); - expected_keys.push((primary_namespace.clone(), secondary_namespace.clone(), key)); + keys.push((primary_namespace.clone(), secondary_namespace.clone(), key)); } } } + + keys +} + +pub(crate) fn do_test_data_migration<S: MigratableKVStoreSync, T: MigratableKVStoreSync>( + source_store: &mut S, target_store: &mut T, +) { + // We fill the source with some bogus keys. + let dummy_data = vec![42u8; 32]; + let mut expected_keys = data_migration_test_keys(); + for (primary_namespace, secondary_namespace, key) in &expected_keys { + source_store + .write(primary_namespace, secondary_namespace, key, dummy_data.clone()) + .unwrap(); + } expected_keys.sort(); expected_keys.dedup(); @@ -109,6 +116,47 @@ pub(crate) fn do_test_data_migration<S: MigratableKVStore, T: MigratableKVStore> } } +#[cfg(feature = "tokio")] +pub(crate) async fn do_test_data_migration_async< + S: lightning::util::persist::MigratableKVStore, + T: lightning::util::persist::MigratableKVStore, +>( + source_store: &S, target_store: &T, +) { + use lightning::util::persist::{migrate_kv_store_data_async, KVStore, MigratableKVStore}; + + // We fill the source with some bogus keys. + let dummy_data = vec![42u8; 32]; + let mut expected_keys = data_migration_test_keys(); + for (primary_namespace, secondary_namespace, key) in &expected_keys { + KVStore::write( + source_store, + primary_namespace, + secondary_namespace, + key, + dummy_data.clone(), + ) + .await + .unwrap(); + } + expected_keys.sort(); + expected_keys.dedup(); + + let mut source_list = MigratableKVStore::list_all_keys(source_store).await.unwrap(); + source_list.sort(); + assert_eq!(source_list, expected_keys); + + migrate_kv_store_data_async(source_store, target_store).await.unwrap(); + + let mut target_list = MigratableKVStore::list_all_keys(target_store).await.unwrap(); + target_list.sort(); + assert_eq!(target_list, expected_keys); + + for (p, s, k) in expected_keys.iter() { + assert_eq!(KVStore::read(target_store, p, s, k).await.unwrap(), dummy_data.clone()); + } +} + // 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<K: KVStoreSync + Sync>(store_0: &K, store_1: &K) { @@ -188,7 +236,7 @@ pub(crate) fn do_test_store<K: KVStoreSync + Sync>(store_0: &K, store_1: &K) { .unwrap(); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let node_txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap(); @@ -202,7 +250,7 @@ pub(crate) fn do_test_store<K: KVStoreSync + Sync>(store_0: &K, store_1: &K) { vec![node_txn[0].clone(), node_txn[0].clone()], ), ); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[nodes[0].node.get_our_node_id()], 100000); check_added_monitors(&nodes[1], 1); diff --git a/lightning-rapid-gossip-sync/src/lib.rs b/lightning-rapid-gossip-sync/src/lib.rs index a9653754655..70a2a79b618 100644 --- a/lightning-rapid-gossip-sync/src/lib.rs +++ b/lightning-rapid-gossip-sync/src/lib.rs @@ -147,6 +147,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> { /// Sync gossip data from a file. /// Returns the last sync timestamp to be used the next time rapid sync data is queried. /// + /// You should consider the gossip data source as semi-trusted. It is generally the case that it + /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating + /// the graph such that it leads to eventual OOM on the client. + /// /// `network_graph`: The network graph to apply the updates to /// /// `sync_path`: Path to the file where the gossip update data is located @@ -166,6 +170,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> { /// Update network graph from binary data. /// Returns the last sync timestamp to be used the next time rapid sync data is queried. /// + /// You should consider the gossip data source as semi-trusted. It is generally the case that it + /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating + /// the graph such that it leads to eventual OOM on the client. + /// /// `update_data`: `&[u8]` binary stream that comprises the update data #[cfg(feature = "std")] pub fn update_network_graph(&self, update_data: &[u8]) -> Result<u32, GraphSyncError> { @@ -176,6 +184,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> { /// Update network graph from binary data. /// Returns the last sync timestamp to be used the next time rapid sync data is queried. /// + /// You should consider the gossip data source as semi-trusted. It is generally the case that it + /// can DoS the client either by omitting data which leads to pathfinding failure or by bloating + /// the graph such that it leads to eventual OOM on the client. + /// /// `update_data`: `&[u8]` binary stream that comprises the update data /// `current_time_unix`: `Option<u64>` optional current timestamp to verify data age pub fn update_network_graph_no_std( diff --git a/lightning-rapid-gossip-sync/src/processing.rs b/lightning-rapid-gossip-sync/src/processing.rs index 9d3287969f2..45aa1a84486 100644 --- a/lightning-rapid-gossip-sync/src/processing.rs +++ b/lightning-rapid-gossip-sync/src/processing.rs @@ -9,7 +9,9 @@ use lightning::ln::msgs::{ DecodeError, ErrorAction, LightningError, SocketAddress, UnsignedChannelUpdate, UnsignedNodeAnnouncement, }; -use lightning::routing::gossip::{NetworkGraph, NodeAlias, NodeId}; +use lightning::routing::gossip::{ + NetworkGraph, NodeAlias, NodeId, CHAN_COUNT_ESTIMATE, NODE_COUNT_ESTIMATE, +}; use lightning::util::logger::Logger; use lightning::util::ser::{BigSize, FixedLengthReader, Readable}; use lightning::{log_debug, log_given_level, log_gossip, log_trace, log_warn}; @@ -112,17 +114,27 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> { } }; + const MAX_NODE_COUNT: u32 = (NODE_COUNT_ESTIMATE as u32) * 10; + const MAX_CHANNEL_COUNT: u64 = (CHAN_COUNT_ESTIMATE as u64) * 10; + let node_id_count: u32 = Readable::read(read_cursor)?; + if node_id_count > MAX_NODE_COUNT { + return Err(LightningError { + err: "RGS data contained nonsense number of nodes to update".to_owned(), + action: ErrorAction::IgnoreError, + } + .into()); + } let mut node_ids: Vec<NodeId> = Vec::with_capacity(core::cmp::min( node_id_count, MAX_INITIAL_NODE_ID_VECTOR_CAPACITY, ) as usize); - let network_graph = &self.network_graph; let mut node_modifications: Vec<UnsignedNodeAnnouncement> = Vec::new(); + let read_only_network_graph = network_graph.read_only(); + if parse_node_details { - let read_only_network_graph = network_graph.read_only(); for _ in 0..node_id_count { let mut pubkey_bytes = [0u8; 33]; read_cursor.read_exact(&mut pubkey_bytes)?; @@ -234,9 +246,12 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> { } } + let original_graph_channel_count = read_only_network_graph.channels().len() as u32; + core::mem::drop(read_only_network_graph); + let mut previous_scid: u64 = 0; let announcement_count: u32 = Readable::read(read_cursor)?; - for _ in 0..announcement_count { + for i in 0..announcement_count { let features = Readable::read(read_cursor)?; // handle SCID @@ -281,6 +296,10 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> { } } + if (original_graph_channel_count as u64) + (i as u64) > MAX_CHANNEL_COUNT { + continue; + } + let announcement_result = network_graph.add_channel_from_partial_announcement( short_channel_id, funding_sats, @@ -326,6 +345,13 @@ impl<NG: Deref<Target = NetworkGraph<L>>, L: Logger> RapidGossipSync<NG, L> { previous_scid = 0; let update_count: u32 = Readable::read(read_cursor)?; + if update_count as u64 > MAX_CHANNEL_COUNT { + return Err(LightningError { + err: "RGS data contained nonsense number of channels to update".to_owned(), + action: ErrorAction::IgnoreError, + } + .into()); + } log_debug!(self.logger, "Processing RGS update from {} with {} nodes, {} channel announcements and {} channel updates.", latest_seen_timestamp, node_id_count, announcement_count, update_count); if update_count == 0 { @@ -549,7 +575,7 @@ mod tests { 108, 101, 46, 99, 111, 109, 1, 187, 19, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 57, 13, 3, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 2, 23, 48, 62, 77, 75, 108, 209, 54, 16, 50, 202, 155, 210, 174, 185, 217, 0, 170, 77, 69, 217, 234, 216, 10, 201, - 66, 51, 116, 196, 81, 167, 37, 77, 7, 102, 0, 0, 2, 25, 48, 0, 0, 0, 1, 0, 0, 1, 0, 1, + 66, 51, 116, 196, 81, 167, 37, 77, 7, 102, 0, 0, 2, 25, 48, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, ]; @@ -669,7 +695,7 @@ mod tests { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 0, 0, 1, 0, 255, 128, 0, 0, 0, 0, 0, 0, 1, 0, 147, 42, 23, 23, 23, 23, 23, + 0, 0, 0, 1, 0, 0, 1, 1, 255, 128, 0, 0, 0, 0, 0, 0, 0, 0, 147, 42, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, 23, diff --git a/lightning-tests/Cargo.toml b/lightning-tests/Cargo.toml index 4e8d330089d..05a5bd55ce5 100644 --- a/lightning-tests/Cargo.toml +++ b/lightning-tests/Cargo.toml @@ -29,6 +29,4 @@ level = "forbid" # # Note that Cargo automatically declares corresponding cfgs for every feature # defined in the member-level [features] tables as "expected". -check-cfg = [ - "cfg(taproot)", -] +check-cfg = [] diff --git a/lightning-tests/src/lib.rs b/lightning-tests/src/lib.rs index c028193d692..80c95299d5b 100644 --- a/lightning-tests/src/lib.rs +++ b/lightning-tests/src/lib.rs @@ -1,5 +1,5 @@ #[cfg_attr(test, macro_use)] extern crate lightning; -#[cfg(all(test, not(taproot)))] +#[cfg(test)] pub mod upgrade_downgrade_tests; diff --git a/lightning-tests/src/upgrade_downgrade_tests.rs b/lightning-tests/src/upgrade_downgrade_tests.rs index 14b0a5c5822..0cc643b9c2d 100644 --- a/lightning-tests/src/upgrade_downgrade_tests.rs +++ b/lightning-tests/src/upgrade_downgrade_tests.rs @@ -11,13 +11,17 @@ //! LDK. use lightning_0_2::commitment_signed_dance as commitment_signed_dance_0_2; +use lightning_0_2::events::bump_transaction::sync::WalletSourceSync as WalletSourceSync_0_2; use lightning_0_2::events::Event as Event_0_2; use lightning_0_2::get_monitor as get_monitor_0_2; use lightning_0_2::ln::channelmanager::PaymentId as PaymentId_0_2; use lightning_0_2::ln::channelmanager::RecipientOnionFields as RecipientOnionFields_0_2; use lightning_0_2::ln::functional_test_utils as lightning_0_2_utils; use lightning_0_2::ln::msgs::ChannelMessageHandler as _; +use lightning_0_2::ln::msgs::OnionMessage as OnionMessage_0_2; +use lightning_0_2::onion_message::packet::Packet as Packet_0_2; use lightning_0_2::routing::router as router_0_2; +use lightning_0_2::util::ser::MaybeReadable as MaybeReadable_0_2; use lightning_0_2::util::ser::Writeable as _; use lightning_0_1::commitment_signed_dance as commitment_signed_dance_0_1; @@ -45,24 +49,30 @@ use lightning_0_0_125::ln::msgs::ChannelMessageHandler as _; use lightning_0_0_125::routing::router as router_0_0_125; use lightning_0_0_125::util::ser::Writeable as _; +use lightning::blinded_path::message::NextMessageHop; use lightning::chain::channelmonitor::{ANTI_REORG_DELAY, HTLC_FAIL_BACK_BUFFER}; -use lightning::events::bump_transaction::sync::WalletSourceSync; use lightning::events::{ClosureReason, Event, HTLCHandlingFailureType}; +use lightning::ln::channel_state::SpliceCandidateStatus; use lightning::ln::functional_test_utils::*; -use lightning::ln::funding::SpliceContribution; +use lightning::ln::msgs; use lightning::ln::msgs::BaseMessageHandler as _; use lightning::ln::msgs::ChannelMessageHandler as _; use lightning::ln::msgs::MessageSendEvent; use lightning::ln::splicing_tests::*; use lightning::ln::types::ChannelId; +use lightning::onion_message::packet::Packet; use lightning::sign::OutputSpender; +use lightning::util::ser::{MaybeReadable, Writeable}; +use lightning::util::wallet_utils::WalletSourceSync; use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use bitcoin::script::Builder; -use bitcoin::secp256k1::Secp256k1; +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; use bitcoin::{opcodes, Amount, TxOut}; +use lightning::io::Cursor; + use std::sync::Arc; #[test] @@ -453,18 +463,21 @@ fn do_test_0_1_htlc_forward_after_splice(fail_htlc: bool) { reconnect_b_c_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_b_c_args); - let contribution = SpliceContribution::splice_out(vec![TxOut { + let outputs = vec![TxOut { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); - let splice_tx = splice_channel(&nodes[0], &nodes[1], ChannelId(chan_id_bytes_a), contribution); + }]; + let channel_id = ChannelId(chan_id_bytes_a); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); for node in nodes.iter() { mine_transaction(node, &splice_tx); connect_blocks(node, ANTI_REORG_DELAY - 1); } let splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_b_id); - lock_splice(&nodes[0], &nodes[1], &splice_locked, false); + lock_splice(&nodes[0], &nodes[1], &splice_locked, false, &[]); for node in nodes.iter() { connect_blocks(node, EXTRA_BLOCKS_BEFORE_FAIL - ANTI_REORG_DELAY); @@ -538,11 +551,10 @@ fn upgrade_mid_htlc_intercept_forward() { } fn do_upgrade_mid_htlc_forward(test: MidHtlcForwardCase) { - // In 0.3, we started reconstructing the `ChannelManager`'s HTLC forwards maps from the HTLCs - // contained in `Channel`s, as part of removing the requirement to regularly persist the - // `ChannelManager`. However, HTLC forwards can only be reconstructed this way if they were - // received on 0.3 or higher. Test that HTLC forwards that were serialized on <=0.2 will still - // succeed when read on 0.3+. + // In an upcoming version, we plan to start reconstructing the `ChannelManager`'s HTLC forwards + // maps from the HTLCs contained in `Channel`s, as part of removing the requirement to regularly + // persist the `ChannelManager`. Preemptively test that HTLC forwards that were serialized on + // <=0.2 will still succeed when read on this upcoming version. let (node_a_ser, node_b_ser, node_c_ser, mon_a_1_ser, mon_b_1_ser, mon_b_2_ser, mon_c_1_ser); let (node_a_id, node_b_id, node_c_id); let (payment_secret_bytes, payment_hash_bytes, payment_preimage_bytes); @@ -699,3 +711,391 @@ fn do_upgrade_mid_htlc_forward(test: MidHtlcForwardCase) { expect_payment_claimable!(nodes[2], pay_hash, pay_secret, 1_000_000); claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], pay_preimage); } + +/// Constructs a dummy `OnionMessage` (current version) for use in serialization tests. +fn dummy_onion_message() -> msgs::OnionMessage { + let pubkey = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap()); + msgs::OnionMessage { + blinding_point: pubkey, + onion_routing_packet: Packet { + version: 0, + public_key: pubkey, + hop_data: vec![1; 64], + hmac: [2; 32], + }, + } +} + +/// Constructs a dummy `OnionMessage` (0.2 version) for use in serialization tests. +fn dummy_onion_message_0_2() -> OnionMessage_0_2 { + let pubkey = bitcoin::secp256k1::PublicKey::from_secret_key( + &Secp256k1::new(), + &SecretKey::from_slice(&[42; 32]).unwrap(), + ); + OnionMessage_0_2 { + blinding_point: pubkey, + onion_routing_packet: Packet_0_2 { + version: 0, + public_key: pubkey, + hop_data: vec![1; 64], + hmac: [2; 32], + }, + } +} + +#[test] +fn test_onion_message_intercepted_upgrade_from_0_2() { + // Ensure that an `Event::OnionMessageIntercepted` serialized by LDK 0.2 (which uses + // `peer_node_id: PublicKey` in TLV field 0) can be deserialized by the current version, + // producing `NextMessageHop::NodeId`. + let pubkey = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap()); + + let event_0_2 = Event_0_2::OnionMessageIntercepted { + peer_node_id: pubkey, + message: dummy_onion_message_0_2(), + }; + + let serialized = lightning_0_2::util::ser::Writeable::encode(&event_0_2); + + let mut reader = Cursor::new(&serialized); + let deserialized = <Event as MaybeReadable>::read(&mut reader).unwrap().unwrap(); + + match deserialized { + Event::OnionMessageIntercepted { prev_hop, next_hop, message } => { + // LDK 0.2 did not write a `prev_hop`, so it must default to `None`. + assert_eq!(prev_hop, None); + assert_eq!(next_hop, NextMessageHop::NodeId(pubkey)); + assert_eq!(message, dummy_onion_message()); + }, + _ => panic!("Expected OnionMessageIntercepted event"), + } +} + +#[test] +fn test_onion_message_intercepted_node_id_downgrade_to_0_2() { + // Ensure that an `Event::OnionMessageIntercepted` with a `NodeId` next hop serialized by + // the current version can be deserialized by LDK 0.2 (which expects `peer_node_id` in TLV + // field 0 and ignores the newer `prev_hop` in TLV field 3). + let pubkey = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap()); + let prev_hop = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[43; 32]).unwrap()); + + let event = Event::OnionMessageIntercepted { + prev_hop: Some(prev_hop), + next_hop: NextMessageHop::NodeId(pubkey), + message: dummy_onion_message(), + }; + + let serialized = event.encode(); + + let mut reader = Cursor::new(&serialized); + let deserialized = <Event_0_2 as MaybeReadable_0_2>::read(&mut reader).unwrap().unwrap(); + + match deserialized { + Event_0_2::OnionMessageIntercepted { peer_node_id, message } => { + assert_eq!(peer_node_id, pubkey); + assert_eq!(message, dummy_onion_message_0_2()); + }, + _ => panic!("Expected OnionMessageIntercepted event"), + } +} + +#[test] +fn test_onion_message_intercepted_scid_downgrade_to_0_2() { + // Ensure that an `Event::OnionMessageIntercepted` with a `ShortChannelId` next hop + // serialized by the current version cannot be deserialized by LDK 0.2, since the + // `peer_node_id` field (0) is not written for SCID variants and LDK 0.2 requires it. + let event = Event::OnionMessageIntercepted { + prev_hop: None, + next_hop: NextMessageHop::ShortChannelId(42), + message: dummy_onion_message(), + }; + + let serialized = event.encode(); + + // LDK 0.2 will try to read field 0 as required. Since it's absent, the read will fail. + let mut reader = Cursor::new(&serialized); + let result = <Event_0_2 as MaybeReadable_0_2>::read(&mut reader); + assert!(result.is_err(), "LDK 0.2 should fail to decode a ShortChannelId variant"); +} + +fn downgrade_setup_single_splice() -> (Vec<u8>, Vec<u8>, Vec<u8>, Vec<u8>, ChannelId) { + // Build a current node with a single pending (negotiated, not yet locked) splice that node 0 + // funded (so node 0 is contributory, node 1 is a non-contributory acceptor). Return both + // nodes' serialized ChannelManager + ChannelMonitor and the channel id. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + let node_0_ser = nodes[0].node.encode(); + let node_1_ser = nodes[1].node.encode(); + let mon_0_ser = get_monitor!(nodes[0], channel_id).encode(); + let mon_1_ser = get_monitor!(nodes[1], channel_id).encode(); + (node_0_ser, node_1_ser, mon_0_ser, mon_1_ser, channel_id) +} + +#[test] +fn downgrade_single_splice_loads_on_0_2() { + // A current node with a single pending splice serializes in a form LDK 0.2 can still read, + // whether or not we funded it: only odd TLVs are written (the even RBF gate is omitted for a + // single round), so 0.2 skips the contribution it can't track and loads the channel. RBF is + // the only state that blocks downgrade (see downgrade_rbf_refused_by_0_2). + let (node_0_ser, node_1_ser, mon_0_ser, mon_1_ser, _) = downgrade_setup_single_splice(); + + let mut chanmon_cfgs = lightning_0_2_utils::create_chanmon_cfgs(2); + chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true; + chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true; + let node_cfgs = lightning_0_2_utils::create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = lightning_0_2_utils::create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = lightning_0_2_utils::create_network(2, &node_cfgs, &node_chanmgrs); + let mut config = lightning_0_2_utils::test_default_channel_config(); + // The current side uses the anchors channel type by default; 0.2 only accepts a channel whose + // type it advertises support for, so enable anchors here too (otherwise the read is refused on + // the channel type, before the splice serialization is ever exercised). + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + + // Node 0 (contributory initiator): the contribution lives in an odd TLV that 0.2 skips. + let mgr_0 = lightning_0_2_utils::_reload_node( + &nodes[0], + config.clone(), + &node_0_ser, + &[&mon_0_ser[..]], + ); + assert_eq!(mgr_0.list_channels().len(), 1); + // Node 1 (non-contributory acceptor): nothing 0.2 can't represent. + let mgr_1 = + lightning_0_2_utils::_reload_node(&nodes[1], config, &node_1_ser, &[&mon_1_ser[..]]); + assert_eq!(mgr_1.list_channels().len(), 1); +} + +#[test] +fn downgrade_rbf_refused_by_0_2() { + // RBF (more than one negotiation round) is the one splice state LDK 0.2 cannot operate. Current + // writes the even RBF-gate TLV for it, which 0.2 rejects as an unknown even (required) field, + // so reading the ChannelManager fails rather than silently mishandling the extra candidate. + let (node_0_ser, mon_0_ser); + { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_1 = nodes[1].node.get_our_node_id(); + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + + // RBF the splice, producing a second negotiated candidate. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = bitcoin::FeeRate::from_sat_per_kwu(1000); + let rbf_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + rbf_contribution, + new_funding_script, + ); + let _ = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(first_splice_tx.compute_txid()), + ); + expect_splice_pending_event(&nodes[0], &node_id_1); + // The acceptor did not contribute, so it gets no `SpliceNegotiated` event. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + node_0_ser = nodes[0].node.encode(); + mon_0_ser = get_monitor!(nodes[0], channel_id).encode(); + } + + let mut chanmon_cfgs = lightning_0_2_utils::create_chanmon_cfgs(2); + chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true; + chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true; + let node_cfgs = lightning_0_2_utils::create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = lightning_0_2_utils::create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = lightning_0_2_utils::create_network(2, &node_cfgs, &node_chanmgrs); + let mut config = lightning_0_2_utils::test_default_channel_config(); + // Match the anchors channel type used on the current side, so the manager read reaches (and + // fails on) the even RBF-gate TLV rather than refusing the channel type itself. + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + // _reload_node unwraps the manager read, which fails on the even RBF-gate TLV. Catch the panic + // here so it stays contained to the read we expect to fail. + let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + lightning_0_2_utils::_reload_node(&nodes[0], config, &node_0_ser, &[&mon_0_ser[..]]); + })) + .expect_err("0.2 should refuse to read the RBF splice"); + let panic_msg = panic + .downcast_ref::<String>() + .map(String::as_str) + .or_else(|| panic.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + panic_msg.contains("UnknownRequiredFeature"), + "expected an UnknownRequiredFeature decode failure, got: {panic_msg}", + ); +} + +#[test] +fn upgrade_single_splice_from_0_2() { + // A pending single splice written by LDK 0.2 -- which never tracked our contribution -- is read + // by current: the candidate comes back via the TLV-3 fallback with `contribution: None`. + let (node_0_ser, node_1_ser, mon_0_ser, mon_1_ser, chan_id_bytes); + { + let chanmon_cfgs = lightning_0_2_utils::create_chanmon_cfgs(2); + let node_cfgs = lightning_0_2_utils::create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = lightning_0_2_utils::create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = lightning_0_2_utils::create_network(2, &node_cfgs, &node_chanmgrs); + let channel_id = lightning_0_2_utils::create_announced_chan_between_nodes_with_value( + &nodes, 0, 1, 100_000, 0, + ) + .2; + chan_id_bytes = channel_id.0; + + let contribution = lightning_0_2::ln::funding::SpliceContribution::SpliceOut { + outputs: vec![bitcoin::TxOut { + value: bitcoin::Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }], + }; + // 0.2 drives the splice through tx_signatures, leaving one negotiated (unlocked) candidate. + let _ = lightning_0_2::ln::splicing_tests::splice_channel( + &nodes[0], + &nodes[1], + channel_id, + contribution, + ); + + node_0_ser = nodes[0].node.encode(); + node_1_ser = nodes[1].node.encode(); + mon_0_ser = get_monitor_0_2!(nodes[0], channel_id).encode(); + mon_1_ser = get_monitor_0_2!(nodes[1], channel_id).encode(); + } + + let mut chanmon_cfgs = create_chanmon_cfgs(2); + chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true; + chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true; + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister_a, persister_b, chain_mon_a, chain_mon_b); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let (node_a, node_b); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let config = test_default_channel_config(); + reload_node!( + nodes[0], + config.clone(), + &node_0_ser, + &[&mon_0_ser[..]], + persister_a, + chain_mon_a, + node_a + ); + reload_node!( + nodes[1], + config, + &node_1_ser, + &[&mon_1_ser[..]], + persister_b, + chain_mon_b, + node_b + ); + + // Current reads the 0.2 splice: one negotiated candidate, no contribution recorded. + let channel_id = ChannelId(chan_id_bytes); + for node in nodes.iter() { + let channels = node.node.list_channels(); + let details = channels.iter().find(|c| c.channel_id == channel_id).unwrap(); + let splice = details.splice_details.as_ref().expect("pending splice"); + assert_eq!(splice.candidates.len(), 1); + assert_eq!(splice.candidates[0].contribution, None); + } + + // The inherited splice cannot be RBF'd -- 0.2 persisted neither its feerate nor our contribution + // to reconstruct the prior request -- so splice_channel returns a fresh template with no RBF + // feerate floor rather than refusing. The new splice is queued to begin once the inherited + // splice locks. + let node_id_1 = nodes[1].node.get_our_node_id(); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(funding_template.min_rbf_feerate().is_none()); +} + +#[test] +fn splice_inherited_across_0_2_queues_until_lock() { + // Negotiate a contributory splice on current, downgrade to LDK 0.2, then upgrade back. LDK 0.2 + // persists neither our contribution nor the splice feerate and does not retain the odd TLVs that + // carry them, so the splice returns to current without either. It therefore cannot be RBF'd; + // splicing again instead queues a new splice that begins once the inherited splice locks. + // Same single-splice setup as the downgrade tests; we only need node 0 here. + let (v3_mgr, _, v3_mon, _, channel_id) = downgrade_setup_single_splice(); + let chan_id_bytes = channel_id.0; + + // Downgrade node 0 to LDK 0.2 and re-serialize there, stripping the contribution and feerate. + let (v2_mgr, v2_mon); + { + let mut chanmon_cfgs = lightning_0_2_utils::create_chanmon_cfgs(2); + chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true; + chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true; + let node_cfgs = lightning_0_2_utils::create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = lightning_0_2_utils::create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = lightning_0_2_utils::create_network(2, &node_cfgs, &node_chanmgrs); + let mut config = lightning_0_2_utils::test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + let mgr = lightning_0_2_utils::_reload_node(&nodes[0], config, &v3_mgr, &[&v3_mon[..]]); + assert_eq!(mgr.list_channels().len(), 1); + let v2_channel_id = lightning_0_2::ln::types::ChannelId(chan_id_bytes); + v2_mgr = mgr.encode(); + v2_mon = get_monitor_0_2!(nodes[0], v2_channel_id).encode(); + } + + // Upgrade back to current and splice the channel carrying the inherited splice. + let mut chanmon_cfgs = create_chanmon_cfgs(2); + chanmon_cfgs[0].keys_manager.disable_all_state_policy_checks = true; + chanmon_cfgs[1].keys_manager.disable_all_state_policy_checks = true; + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister, chain_mon, new_node); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let config = test_default_channel_config(); + reload_node!(nodes[0], config, &v2_mgr, &[&v2_mon[..]], persister, chain_mon, new_node); + + let channel_id = ChannelId(chan_id_bytes); + let node_id_1 = nodes[1].node.get_our_node_id(); + + // splice_channel returns a fresh template with no RBF feerate floor rather than refusing. + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(funding_template.min_rbf_feerate().is_none()); + + // Contributing queues the splice as `WaitingOnLock`: it cannot replace the inherited splice via + // RBF (its feerate and our contribution are absent), so it will be spliced once that splice + // locks. A splice-out needs no wallet funds, letting us drive the queue without connecting + // blocks to the reloaded node. + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let channels = nodes[0].node.list_channels(); + let splice = channels[0].splice_details.as_ref().unwrap(); + assert!(matches!( + splice.candidates.last().unwrap().status, + SpliceCandidateStatus::WaitingOnLock, + )); +} diff --git a/lightning-transaction-sync/Cargo.toml b/lightning-transaction-sync/Cargo.toml index 4bc37d7ff48..077ac2c4405 100644 --- a/lightning-transaction-sync/Cargo.toml +++ b/lightning-transaction-sync/Cargo.toml @@ -37,16 +37,16 @@ lightning = { version = "0.3.0", path = "../lightning", default-features = false lightning-macros = { version = "0.2", path = "../lightning-macros", default-features = false } bitcoin = { version = "0.32.2", default-features = false } futures = { version = "0.3", optional = true } -esplora-client = { version = "0.12", default-features = false, optional = true } -electrum-client = { version = "0.24.0", optional = true, default-features = false, features = ["proxy"] } +esplora-client = { version = "0.13", default-features = false, optional = true } +electrum-client = { version = "0.25", optional = true, default-features = false, features = ["proxy"] } [dev-dependencies] lightning = { version = "0.3.0", path = "../lightning", default-features = false, features = ["std", "_test_utils"] } tokio = { version = "1.35.0", features = ["macros"] } [target.'cfg(not(target_os = "windows"))'.dev-dependencies] -electrsd = { version = "0.36.0", default-features = false, features = ["legacy"] } -corepc-node = { version = "0.10.0", default-features = false, features = ["28_0"] } +electrsd = { version = "0.38", default-features = false, features = ["legacy"] } +bitcoind = { version = "0.38", default-features = false, features = ["28_1"] } [lints.rust.unexpected_cfgs] level = "forbid" diff --git a/lightning-transaction-sync/src/common.rs b/lightning-transaction-sync/src/common.rs index 88e52de186d..bafc9dc8627 100644 --- a/lightning-transaction-sync/src/common.rs +++ b/lightning-transaction-sync/src/common.rs @@ -133,6 +133,10 @@ impl FilterQueue { } } +pub(crate) fn is_potentially_unsafe_merkle_leaf(tx: &Transaction) -> bool { + tx.base_size() == 64 +} + #[derive(Debug)] pub(crate) struct ConfirmedTx { pub tx: Transaction, diff --git a/lightning-transaction-sync/src/electrum.rs b/lightning-transaction-sync/src/electrum.rs index 1905456d281..0283b9f00ee 100644 --- a/lightning-transaction-sync/src/electrum.rs +++ b/lightning-transaction-sync/src/electrum.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::common::{ConfirmedTx, FilterQueue, SyncState}; +use crate::common::{is_potentially_unsafe_merkle_leaf, ConfirmedTx, FilterQueue, SyncState}; use crate::error::{InternalError, TxSyncError}; use electrum_client::utils::validate_merkle_proof; @@ -96,7 +96,13 @@ impl<L: Logger> ElectrumSyncClient<L> { let mut tip_header = tip_notification.header; let mut tip_height = tip_notification.height as u32; - loop { + for i in 0..100 { + if i >= 10 { + log_debug!(self.logger, "Giving up trying to sync transactions after 10 attempts."); + sync_state.pending_sync = true; + return Err(TxSyncError::Failed); + } + let pending_registrations = self.queue.lock().unwrap().process_queues(&mut sync_state); let tip_is_new = Some(tip_header.block_hash()) != sync_state.last_sync_hash; @@ -271,14 +277,14 @@ impl<L: Logger> ElectrumSyncClient<L> { for txid in &sync_state.watched_transactions { match self.client.transaction_get(&txid) { Ok(tx) => { - // Bitcoin Core's Merkle tree implementation has no way to discern between - // internal and leaf node entries. As a consequence it is susceptible to an - // attacker injecting additional transactions by crafting 64-byte - // transactions matching an inner Merkle node's hash (see - // https://web.archive.org/web/20240329003521/https://bitslog.com/2018/06/09/leaf-node-weakness-in-bitcoin-merkle-tree-design/). - // To protect against this (highly unlikely) attack vector, we check that the - // transaction is at least 65 bytes in length. - if tx.total_size() == 64 { + if tx.compute_txid() != *txid { + log_error!(self.logger, "Retrieved transaction for txid {} doesn't match expectations. This should not happen. Please verify server integrity.", txid); + return Err(InternalError::Failed); + } + + // Skip before using an arbitrary returned output to look up the + // transaction's script history. + if is_potentially_unsafe_merkle_leaf(&tx) { log_error!(self.logger, "Skipping transaction {} due to retrieving potentially invalid tx data.", txid); continue; } @@ -329,13 +335,14 @@ impl<L: Logger> ElectrumSyncClient<L> { let mut filtered_history = script_history.iter().filter(|h| h.tx_hash == **txid); if let Some(history) = filtered_history.next() { - let prob_conf_height = history.height as u32; - if prob_conf_height <= 0 { + if history.height <= 0 { // Skip if it's a an unconfirmed entry. continue; } - let confirmed_tx = self.get_confirmed_tx(tx, prob_conf_height)?; - confirmed_txs.push(confirmed_tx); + let prob_conf_height = history.height as u32; + if let Some(confirmed_tx) = self.get_confirmed_tx(tx, prob_conf_height)? { + confirmed_txs.push(confirmed_tx); + } } if filtered_history.next().is_some() { log_error!( @@ -363,6 +370,11 @@ impl<L: Logger> ElectrumSyncClient<L> { match self.client.transaction_get(&txid) { Ok(tx) => { + if tx.compute_txid() != txid { + log_error!(self.logger, "Retrieved transaction for txid {} doesn't match expectations. This should not happen. Please verify server integrity.", txid); + return Err(InternalError::Failed); + } + let mut is_spend = false; for txin in &tx.input { let watched_outpoint = @@ -378,8 +390,11 @@ impl<L: Logger> ElectrumSyncClient<L> { } let prob_conf_height = possible_output_spend.height as u32; - let confirmed_tx = self.get_confirmed_tx(&tx, prob_conf_height)?; - confirmed_txs.push(confirmed_tx); + if let Some(confirmed_tx) = + self.get_confirmed_tx(&tx, prob_conf_height)? + { + confirmed_txs.push(confirmed_tx); + } }, Err(e) => { log_trace!( @@ -444,8 +459,21 @@ impl<L: Logger> ElectrumSyncClient<L> { fn get_confirmed_tx( &self, tx: &Transaction, prob_conf_height: u32, - ) -> Result<ConfirmedTx, InternalError> { + ) -> Result<Option<ConfirmedTx>, InternalError> { let txid = tx.compute_txid(); + // Bitcoin Core's Merkle tree implementation has no way to discern between internal and + // leaf node entries. As a consequence it is susceptible to an attacker injecting + // additional transactions by crafting 64-byte transactions matching an inner Merkle + // node's hash (see https://web.archive.org/web/20240329003521/https://bitslog.com/2018/06/09/leaf-node-weakness-in-bitcoin-merkle-tree-design/). + if is_potentially_unsafe_merkle_leaf(tx) { + log_error!( + self.logger, + "Skipping transaction {} due to retrieving potentially invalid tx data.", + txid + ); + return Ok(None); + } + match self.client.transaction_get_merkle(&txid, prob_conf_height as usize) { Ok(merkle_res) => { debug_assert_eq!(prob_conf_height, merkle_res.block_height as u32); @@ -467,7 +495,7 @@ impl<L: Logger> ElectrumSyncClient<L> { block_height: prob_conf_height, pos, }; - Ok(confirmed_tx) + Ok(Some(confirmed_tx)) }, Err(e) => { log_error!( @@ -511,3 +539,23 @@ impl<L: Logger> Filter for ElectrumSyncClient<L> { locked_queue.outputs.insert(output.outpoint.into_bitcoin_outpoint(), output); } } + +#[cfg(test)] +mod tests { + #[test] + fn transaction_get_responses_are_verified_at_call_sites() { + let src = include_str!("electrum.rs"); + let watched_transaction_check = concat!("if tx.compute_", "txid() != *txid"); + let watched_output_spend_check = concat!("if tx.compute_", "txid() != txid"); + + assert!( + src.contains(watched_transaction_check), + "watched transaction_get responses must be verified against the requested txid" + ); + assert!( + src.contains(watched_output_spend_check), + "watched-output spend transaction_get responses must be verified against the \ + requested txid" + ); + } +} diff --git a/lightning-transaction-sync/src/esplora.rs b/lightning-transaction-sync/src/esplora.rs index 6caf7a6a7ee..07d2ba26219 100644 --- a/lightning-transaction-sync/src/esplora.rs +++ b/lightning-transaction-sync/src/esplora.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::common::{ConfirmedTx, FilterQueue, SyncState}; +use crate::common::{is_potentially_unsafe_merkle_leaf, ConfirmedTx, FilterQueue, SyncState}; use crate::error::{InternalError, TxSyncError}; use lightning::chain::WatchedOutput; @@ -100,7 +100,13 @@ impl<L: Logger> EsploraSyncClient<L> { let mut tip_hash = maybe_await!(self.client.get_tip_hash())?; - loop { + for i in 0..100 { + if i >= 10 { + log_debug!(self.logger, "Giving up trying to sync transactions after 10 attempts."); + sync_state.pending_sync = true; + return Err(TxSyncError::Failed); + } + let pending_registrations = self.queue.lock().unwrap().process_queues(&mut sync_state); let tip_is_new = Some(tip_hash) != sync_state.last_sync_hash; @@ -361,8 +367,13 @@ impl<L: Logger> EsploraSyncClient<L> { let mut matches = Vec::new(); let mut indexes = Vec::new(); - let _ = merkle_block.txn.extract_matches(&mut matches, &mut indexes); - if indexes.len() != 1 || matches.len() != 1 || matches[0] != txid { + let computed_merkle_root = + merkle_block.txn.extract_matches(&mut matches, &mut indexes).ok(); + if computed_merkle_root != Some(block_header.merkle_root) + || indexes.len() != 1 + || matches.len() != 1 + || matches[0] != txid + { log_error!(self.logger, "Retrieved Merkle block for txid {} doesn't match expectations. This should not happen. Please verify server integrity.", txid); return Err(InternalError::Failed); } @@ -382,7 +393,7 @@ impl<L: Logger> EsploraSyncClient<L> { // https://web.archive.org/web/20240329003521/https://bitslog.com/2018/06/09/leaf-node-weakness-in-bitcoin-merkle-tree-design/). // To protect against this (highly unlikely) attack vector, we check that the // transaction is at least 65 bytes in length. - if tx.total_size() == 64 { + if is_potentially_unsafe_merkle_leaf(&tx) { log_error!( self.logger, "Skipping transaction {} due to retrieving potentially invalid tx data.", diff --git a/lightning-transaction-sync/tests/integration_tests.rs b/lightning-transaction-sync/tests/integration_tests.rs index 07b190ad30b..a5b303fdba6 100644 --- a/lightning-transaction-sync/tests/integration_tests.rs +++ b/lightning-transaction-sync/tests/integration_tests.rs @@ -18,8 +18,8 @@ use bitcoin::constants::genesis_block; use bitcoin::network::Network; use bitcoin::{Amount, BlockHash, Txid}; -use electrsd::corepc_node::Node as BitcoinD; -use electrsd::{corepc_node, ElectrsD}; +use bitcoind::BitcoinD; +use electrsd::ElectrsD; use std::collections::{HashMap, HashSet}; use std::env; @@ -28,10 +28,10 @@ use std::time::Duration; pub fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let bitcoind_exe = - env::var("BITCOIND_EXE").ok().or_else(|| corepc_node::downloaded_exe_path().ok()).expect( + env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect( "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature", ); - let mut bitcoind_conf = corepc_node::Conf::default(); + let mut bitcoind_conf = bitcoind::Conf::default(); bitcoind_conf.network = "regtest"; let bitcoind = BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap(); diff --git a/lightning-types/src/features.rs b/lightning-types/src/features.rs index 22493efc556..a55e811e719 100644 --- a/lightning-types/src/features.rs +++ b/lightning-types/src/features.rs @@ -162,17 +162,13 @@ mod sealed { // Byte 4 Quiescence | OnionMessages, // Byte 5 - ProvideStorage | ChannelType | SCIDPrivacy, + AnchorZeroFeeCommitments | ProvideStorage | ChannelType | SCIDPrivacy, // Byte 6 ZeroConf, // Byte 7 Trampoline | SimpleClose | Splice, - // Byte 8 - 16 - ,,,,,,,,, - // Byte 17 - AnchorZeroFeeCommitmentsStaging, - // Byte 18 - , + // Byte 8 - 18 + ,,,,,,,,,,, // Byte 19 HtlcHold, ] @@ -191,17 +187,13 @@ mod sealed { // Byte 4 Quiescence | OnionMessages, // Byte 5 - ProvideStorage | ChannelType | SCIDPrivacy, + AnchorZeroFeeCommitments | ProvideStorage | ChannelType | SCIDPrivacy, // Byte 6 ZeroConf | Keysend, // Byte 7 Trampoline | SimpleClose | Splice, - // Byte 8 - 16 - ,,,,,,,,, - // Byte 17 - AnchorZeroFeeCommitmentsStaging, - // Byte 18 - , + // Byte 8 - 18 + ,,,,,,,,,,, // Byte 19 HtlcHold, // Byte 20 - 31 @@ -264,13 +256,9 @@ mod sealed { // Byte 4 , // Byte 5 - SCIDPrivacy, + AnchorZeroFeeCommitments | SCIDPrivacy, // Byte 6 ZeroConf, - // Byte 7 - 16 - ,,,,,,,,,, - // Byte 17 - AnchorZeroFeeCommitmentsStaging, ]); /// Defines a feature with the given bits for the specified [`Context`]s. The generated trait is @@ -606,6 +594,17 @@ mod sealed { supports_onion_messages, requires_onion_messages ); + define_feature!( + 41, + AnchorZeroFeeCommitments, + [InitContext, NodeContext, ChannelTypeContext], + "Feature flags for `option_zero_fee_commitments`.", + set_anchor_zero_fee_commitments_optional, + set_anchor_zero_fee_commitments_required, + clear_anchor_zero_fee_commitments, + supports_anchor_zero_fee_commitments, + requires_anchor_zero_fee_commitments + ); define_feature!( 43, ProvideStorage, @@ -649,9 +648,17 @@ mod sealed { supports_payment_metadata, requires_payment_metadata ); - define_feature!(51, ZeroConf, [InitContext, NodeContext, ChannelTypeContext], + define_feature!( + 51, + ZeroConf, + [InitContext, NodeContext, ChannelTypeContext], "Feature flags for accepting channels with zero confirmations. Called `option_zeroconf` in the BOLTs", - set_zero_conf_optional, set_zero_conf_required, supports_zero_conf, requires_zero_conf); + set_zero_conf_optional, + set_zero_conf_required, + clear_zero_conf, + supports_zero_conf, + requires_zero_conf + ); define_feature!( 55, Keysend, @@ -699,17 +706,6 @@ mod sealed { // By default, allocate enough bytes to cover up to Splice. Update this as new features are // added which we expect to appear commonly across contexts. pub(super) const MIN_FEATURES_ALLOCATION_BYTES: usize = 63_usize.div_ceil(8); - define_feature!( - 141, // The BOLTs PR uses feature bit 40/41, so add +100 for the experimental bit - AnchorZeroFeeCommitmentsStaging, - [InitContext, NodeContext, ChannelTypeContext], - "Feature flags for `option_zero_fee_commitments`.", - set_anchor_zero_fee_commitments_optional, - set_anchor_zero_fee_commitments_required, - clear_anchor_zero_fee_commitments, - supports_anchor_zero_fee_commitments, - requires_anchor_zero_fee_commitments - ); define_feature!( 153, // The BOLTs PR uses feature bit 52/53, so add +100 for the experimental bit HtlcHold, @@ -1086,7 +1082,7 @@ impl ChannelTypeFeatures { /// Constructs a ChannelTypeFeatures with zero fee commitment anchors support. pub fn anchors_zero_fee_commitments() -> Self { let mut ret = Self::empty(); - <sealed::ChannelTypeContext as sealed::AnchorZeroFeeCommitmentsStaging>::set_required_bit( + <sealed::ChannelTypeContext as sealed::AnchorZeroFeeCommitments>::set_required_bit( &mut ret, ); ret @@ -1272,6 +1268,9 @@ impl<T: sealed::Context> Features<T> { fn set_bit(&mut self, bit: usize, custom: bool) -> Result<(), ()> { let byte_offset = bit / 8; let mask = 1 << (bit - 8 * byte_offset); + if byte_offset >= u16::MAX as usize { + return Err(()); + } if byte_offset < T::KNOWN_FEATURE_MASK.len() && custom { if (T::KNOWN_FEATURE_MASK[byte_offset] & mask) != 0 { return Err(()); diff --git a/lightning-types/src/lib.rs b/lightning-types/src/lib.rs index 7f72d6d2671..6a526adaed2 100644 --- a/lightning-types/src/lib.rs +++ b/lightning-types/src/lib.rs @@ -27,3 +27,4 @@ pub mod features; pub mod payment; pub mod routing; pub mod string; +mod unicode; diff --git a/lightning-types/src/payment.rs b/lightning-types/src/payment.rs index 0f0fcf7b516..efdab8bbd44 100644 --- a/lightning-types/src/payment.rs +++ b/lightning-types/src/payment.rs @@ -10,15 +10,16 @@ //! Types which describe payments in lightning. use core::borrow::Borrow; +use core::hash::{Hash, Hasher}; -use bitcoin::hashes::{sha256::Hash as Sha256, Hash as _}; +use bitcoin::hashes::{sha256::Hash as Sha256, Hash as CryptoHash}; use bitcoin::hex::display::impl_fmt_traits; /// The payment hash is the hash of the [`PaymentPreimage`] which is the value used to lock funds /// in HTLCs while they transit the lightning network. /// /// This is not exported to bindings users as we just use [u8; 32] directly -#[derive(Hash, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct PaymentHash(pub [u8; 32]); impl Borrow<[u8]> for PaymentHash { @@ -27,6 +28,13 @@ impl Borrow<[u8]> for PaymentHash { } } +impl Hash for PaymentHash { + fn hash<H: Hasher>(&self, state: &mut H) { + let slice: &[u8] = self.borrow(); + Hash::hash(slice, state); + } +} + impl_fmt_traits! { impl fmt_traits for PaymentHash { const LENGTH: usize = 32; @@ -37,7 +45,7 @@ impl_fmt_traits! { /// or in a lightning channel. /// /// This is not exported to bindings users as we just use [u8; 32] directly -#[derive(Hash, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct PaymentPreimage(pub [u8; 32]); impl Borrow<[u8]> for PaymentPreimage { @@ -46,6 +54,13 @@ impl Borrow<[u8]> for PaymentPreimage { } } +impl Hash for PaymentPreimage { + fn hash<H: Hasher>(&self, state: &mut H) { + let slice: &[u8] = self.borrow(); + Hash::hash(slice, state); + } +} + impl_fmt_traits! { impl fmt_traits for PaymentPreimage { const LENGTH: usize = 32; @@ -55,7 +70,7 @@ impl_fmt_traits! { /// Converts a `PaymentPreimage` into a `PaymentHash` by hashing the preimage with SHA256. impl From<PaymentPreimage> for PaymentHash { fn from(value: PaymentPreimage) -> Self { - PaymentHash(Sha256::hash(&value.0).to_byte_array()) + PaymentHash(<Sha256 as CryptoHash>::hash(&value.0).to_byte_array()) } } @@ -63,7 +78,7 @@ impl From<PaymentPreimage> for PaymentHash { /// multi-part HTLCs together into a single payment. /// /// This is not exported to bindings users as we just use [u8; 32] directly -#[derive(Hash, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] +#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct PaymentSecret(pub [u8; 32]); impl Borrow<[u8]> for PaymentSecret { @@ -72,6 +87,13 @@ impl Borrow<[u8]> for PaymentSecret { } } +impl Hash for PaymentSecret { + fn hash<H: Hasher>(&self, state: &mut H) { + let slice: &[u8] = self.borrow(); + Hash::hash(slice, state); + } +} + impl_fmt_traits! { impl fmt_traits for PaymentSecret { const LENGTH: usize = 32; diff --git a/lightning-types/src/string.rs b/lightning-types/src/string.rs index ae5395a5289..a21cad411be 100644 --- a/lightning-types/src/string.rs +++ b/lightning-types/src/string.rs @@ -12,6 +12,8 @@ use alloc::string::String; use core::fmt; +use crate::unicode::*; + /// Struct to `Display` fields in a safe way using `PrintableString` #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub struct UntrustedString(pub String); @@ -31,7 +33,13 @@ impl<'a> fmt::Display for PrintableString<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { use core::fmt::Write; for c in self.0.chars() { - let c = if c.is_control() { core::char::REPLACEMENT_CHARACTER } else { c }; + let is_other = is_unicode_general_category_other(c); + let is_unassigned = is_unicode_general_category_unassigned(c); + let c = if c.is_control() || is_other || is_unassigned { + core::char::REPLACEMENT_CHARACTER + } else { + c + }; f.write_char(c)?; } @@ -50,4 +58,24 @@ mod tests { "I \u{1F496} LDK!\u{FFFD}\u{26A1}", ); } + + #[test] + fn sanitizes_unicode_bidi_override_characters() { + // U+202E RIGHT-TO-LEFT OVERRIDE and friends are Unicode general category + // `Cf` (Format), not `Cc` (Control). They enable "Trojan Source" / + // bidi-spoofing attacks where an attacker-supplied string (e.g. a node + // alias gossiped from a peer) renders to a human reader as something + // other than its byte content. `PrintableString` is the sanitiser used + // for exactly these untrusted strings, so it must replace them. + let rendered = format!("{}", PrintableString("safe\u{202E}cipsxe.exe")); + assert!( + !rendered.contains('\u{202E}'), + "PrintableString left a U+202E RLO override in its output: {:?}", + rendered + ); + + // U+13440 is in the Egyptian Hieroglyph Format Controls block, but its + // general category is `Mn`, not `Cf`, so the `Cf` range ends at U+1343F. + assert_eq!(format!("{}", PrintableString("x\u{1343F}y\u{13440}z")), "x\u{FFFD}y\u{13440}z"); + } } diff --git a/lightning-types/src/unicode.rs b/lightning-types/src/unicode.rs new file mode 100644 index 00000000000..22b21969365 --- /dev/null +++ b/lightning-types/src/unicode.rs @@ -0,0 +1,799 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +// Auto-generated from the Unicode Character Database (UnicodeData.txt) by +// contrib/gen_unicode_general_category.py. Do not edit by hand; rerun the +// generator with an updated UnicodeData.txt to refresh the table. + +/// Returns `true` if `c` is in Unicode general category `Cc` (Control), `Cf` +/// (Format), `Cs` (Surrogate), or `Co` (Private Use) — the assigned codepoints +/// in the top-level `C` ("Other") category. The `Cs` portion of the table is +/// unreachable for `char` input (a `char` cannot hold a surrogate) but is kept +/// so the table mirrors the source UCD data verbatim. The disjoint `Cn` +/// (Unassigned) bucket is `is_unicode_general_category_unassigned`. +#[allow(dead_code)] +pub(crate) fn is_unicode_general_category_other(c: char) -> bool { + matches!( + c as u32, + 0x0000..=0x001F // control + | 0x007F..=0x009F // control + | 0x00AD // SOFT HYPHEN + | 0x0600..=0x0605 // ARABIC + | 0x061C // ARABIC LETTER MARK + | 0x06DD // ARABIC END OF AYAH + | 0x070F // SYRIAC ABBREVIATION MARK + | 0x0890..=0x0891 // MARK ABOVE + | 0x08E2 // ARABIC DISPUTED END OF AYAH + | 0x180E // MONGOLIAN VOWEL SEPARATOR + | 0x200B..=0x200F // Cf + | 0x202A..=0x202E // Cf + | 0x2060..=0x2064 // Cf + | 0x2066..=0x206F // Cf + | 0xD800..=0xF8FF // Co / Cs + | 0xFEFF // ZERO WIDTH NO-BREAK SPACE + | 0xFFF9..=0xFFFB // INTERLINEAR ANNOTATION + | 0x110BD // KAITHI NUMBER SIGN + | 0x110CD // KAITHI NUMBER SIGN ABOVE + | 0x13430..=0x1343F // EGYPTIAN HIEROGLYPH + | 0x1BCA0..=0x1BCA3 // SHORTHAND FORMAT + | 0x1D173..=0x1D17A // MUSICAL SYMBOL + | 0xE0001 // LANGUAGE TAG + | 0xE0020..=0xE007F // Cf + | 0xF0000..=0xFFFFD // Plane 15 Private Use + | 0x100000..=0x10FFFD // Plane 16 Private Use + ) +} + +/// Returns `true` if `c` is in Unicode general category `Cn` (Unassigned), or +/// strictly above U+10FFFF. The trailing `0x110000..=u32::MAX` arm is +/// unreachable for `char` input (a `char` is bounded to U+10FFFF) but is kept +/// for defensive coverage of the underlying `u32`. The disjoint Cc / Cf / Cs / +/// Co bucket is `is_unicode_general_category_other`. +#[allow(dead_code)] +pub(crate) fn is_unicode_general_category_unassigned(c: char) -> bool { + matches!( + c as u32, + 0x0378..=0x0379 + | 0x0380..=0x0383 + | 0x038B + | 0x038D + | 0x03A2 + | 0x0530 + | 0x0557..=0x0558 + | 0x058B..=0x058C + | 0x0590 + | 0x05C8..=0x05CF + | 0x05EB..=0x05EE + | 0x05F5..=0x05FF + | 0x070E + | 0x074B..=0x074C + | 0x07B2..=0x07BF + | 0x07FB..=0x07FC + | 0x082E..=0x082F + | 0x083F + | 0x085C..=0x085D + | 0x085F + | 0x086B..=0x086F + | 0x0892..=0x0896 + | 0x0984 + | 0x098D..=0x098E + | 0x0991..=0x0992 + | 0x09A9 + | 0x09B1 + | 0x09B3..=0x09B5 + | 0x09BA..=0x09BB + | 0x09C5..=0x09C6 + | 0x09C9..=0x09CA + | 0x09CF..=0x09D6 + | 0x09D8..=0x09DB + | 0x09DE + | 0x09E4..=0x09E5 + | 0x09FF..=0x0A00 + | 0x0A04 + | 0x0A0B..=0x0A0E + | 0x0A11..=0x0A12 + | 0x0A29 + | 0x0A31 + | 0x0A34 + | 0x0A37 + | 0x0A3A..=0x0A3B + | 0x0A3D + | 0x0A43..=0x0A46 + | 0x0A49..=0x0A4A + | 0x0A4E..=0x0A50 + | 0x0A52..=0x0A58 + | 0x0A5D + | 0x0A5F..=0x0A65 + | 0x0A77..=0x0A80 + | 0x0A84 + | 0x0A8E + | 0x0A92 + | 0x0AA9 + | 0x0AB1 + | 0x0AB4 + | 0x0ABA..=0x0ABB + | 0x0AC6 + | 0x0ACA + | 0x0ACE..=0x0ACF + | 0x0AD1..=0x0ADF + | 0x0AE4..=0x0AE5 + | 0x0AF2..=0x0AF8 + | 0x0B00 + | 0x0B04 + | 0x0B0D..=0x0B0E + | 0x0B11..=0x0B12 + | 0x0B29 + | 0x0B31 + | 0x0B34 + | 0x0B3A..=0x0B3B + | 0x0B45..=0x0B46 + | 0x0B49..=0x0B4A + | 0x0B4E..=0x0B54 + | 0x0B58..=0x0B5B + | 0x0B5E + | 0x0B64..=0x0B65 + | 0x0B78..=0x0B81 + | 0x0B84 + | 0x0B8B..=0x0B8D + | 0x0B91 + | 0x0B96..=0x0B98 + | 0x0B9B + | 0x0B9D + | 0x0BA0..=0x0BA2 + | 0x0BA5..=0x0BA7 + | 0x0BAB..=0x0BAD + | 0x0BBA..=0x0BBD + | 0x0BC3..=0x0BC5 + | 0x0BC9 + | 0x0BCE..=0x0BCF + | 0x0BD1..=0x0BD6 + | 0x0BD8..=0x0BE5 + | 0x0BFB..=0x0BFF + | 0x0C0D + | 0x0C11 + | 0x0C29 + | 0x0C3A..=0x0C3B + | 0x0C45 + | 0x0C49 + | 0x0C4E..=0x0C54 + | 0x0C57 + | 0x0C5B + | 0x0C5E..=0x0C5F + | 0x0C64..=0x0C65 + | 0x0C70..=0x0C76 + | 0x0C8D + | 0x0C91 + | 0x0CA9 + | 0x0CB4 + | 0x0CBA..=0x0CBB + | 0x0CC5 + | 0x0CC9 + | 0x0CCE..=0x0CD4 + | 0x0CD7..=0x0CDB + | 0x0CDF + | 0x0CE4..=0x0CE5 + | 0x0CF0 + | 0x0CF4..=0x0CFF + | 0x0D0D + | 0x0D11 + | 0x0D45 + | 0x0D49 + | 0x0D50..=0x0D53 + | 0x0D64..=0x0D65 + | 0x0D80 + | 0x0D84 + | 0x0D97..=0x0D99 + | 0x0DB2 + | 0x0DBC + | 0x0DBE..=0x0DBF + | 0x0DC7..=0x0DC9 + | 0x0DCB..=0x0DCE + | 0x0DD5 + | 0x0DD7 + | 0x0DE0..=0x0DE5 + | 0x0DF0..=0x0DF1 + | 0x0DF5..=0x0E00 + | 0x0E3B..=0x0E3E + | 0x0E5C..=0x0E80 + | 0x0E83 + | 0x0E85 + | 0x0E8B + | 0x0EA4 + | 0x0EA6 + | 0x0EBE..=0x0EBF + | 0x0EC5 + | 0x0EC7 + | 0x0ECF + | 0x0EDA..=0x0EDB + | 0x0EE0..=0x0EFF + | 0x0F48 + | 0x0F6D..=0x0F70 + | 0x0F98 + | 0x0FBD + | 0x0FCD + | 0x0FDB..=0x0FFF + | 0x10C6 + | 0x10C8..=0x10CC + | 0x10CE..=0x10CF + | 0x1249 + | 0x124E..=0x124F + | 0x1257 + | 0x1259 + | 0x125E..=0x125F + | 0x1289 + | 0x128E..=0x128F + | 0x12B1 + | 0x12B6..=0x12B7 + | 0x12BF + | 0x12C1 + | 0x12C6..=0x12C7 + | 0x12D7 + | 0x1311 + | 0x1316..=0x1317 + | 0x135B..=0x135C + | 0x137D..=0x137F + | 0x139A..=0x139F + | 0x13F6..=0x13F7 + | 0x13FE..=0x13FF + | 0x169D..=0x169F + | 0x16F9..=0x16FF + | 0x1716..=0x171E + | 0x1737..=0x173F + | 0x1754..=0x175F + | 0x176D + | 0x1771 + | 0x1774..=0x177F + | 0x17DE..=0x17DF + | 0x17EA..=0x17EF + | 0x17FA..=0x17FF + | 0x181A..=0x181F + | 0x1879..=0x187F + | 0x18AB..=0x18AF + | 0x18F6..=0x18FF + | 0x191F + | 0x192C..=0x192F + | 0x193C..=0x193F + | 0x1941..=0x1943 + | 0x196E..=0x196F + | 0x1975..=0x197F + | 0x19AC..=0x19AF + | 0x19CA..=0x19CF + | 0x19DB..=0x19DD + | 0x1A1C..=0x1A1D + | 0x1A5F + | 0x1A7D..=0x1A7E + | 0x1A8A..=0x1A8F + | 0x1A9A..=0x1A9F + | 0x1AAE..=0x1AAF + | 0x1ADE..=0x1ADF + | 0x1AEC..=0x1AFF + | 0x1B4D + | 0x1BF4..=0x1BFB + | 0x1C38..=0x1C3A + | 0x1C4A..=0x1C4C + | 0x1C8B..=0x1C8F + | 0x1CBB..=0x1CBC + | 0x1CC8..=0x1CCF + | 0x1CFB..=0x1CFF + | 0x1F16..=0x1F17 + | 0x1F1E..=0x1F1F + | 0x1F46..=0x1F47 + | 0x1F4E..=0x1F4F + | 0x1F58 + | 0x1F5A + | 0x1F5C + | 0x1F5E + | 0x1F7E..=0x1F7F + | 0x1FB5 + | 0x1FC5 + | 0x1FD4..=0x1FD5 + | 0x1FDC + | 0x1FF0..=0x1FF1 + | 0x1FF5 + | 0x1FFF + | 0x2065 + | 0x2072..=0x2073 + | 0x208F + | 0x209D..=0x209F + | 0x20C2..=0x20CF + | 0x20F1..=0x20FF + | 0x218C..=0x218F + | 0x242A..=0x243F + | 0x244B..=0x245F + | 0x2B74..=0x2B75 + | 0x2CF4..=0x2CF8 + | 0x2D26 + | 0x2D28..=0x2D2C + | 0x2D2E..=0x2D2F + | 0x2D68..=0x2D6E + | 0x2D71..=0x2D7E + | 0x2D97..=0x2D9F + | 0x2DA7 + | 0x2DAF + | 0x2DB7 + | 0x2DBF + | 0x2DC7 + | 0x2DCF + | 0x2DD7 + | 0x2DDF + | 0x2E5E..=0x2E7F + | 0x2E9A + | 0x2EF4..=0x2EFF + | 0x2FD6..=0x2FEF + | 0x3040 + | 0x3097..=0x3098 + | 0x3100..=0x3104 + | 0x3130 + | 0x318F + | 0x31E6..=0x31EE + | 0x321F + | 0xA48D..=0xA48F + | 0xA4C7..=0xA4CF + | 0xA62C..=0xA63F + | 0xA6F8..=0xA6FF + | 0xA7DD..=0xA7F0 + | 0xA82D..=0xA82F + | 0xA83A..=0xA83F + | 0xA878..=0xA87F + | 0xA8C6..=0xA8CD + | 0xA8DA..=0xA8DF + | 0xA954..=0xA95E + | 0xA97D..=0xA97F + | 0xA9CE + | 0xA9DA..=0xA9DD + | 0xA9FF + | 0xAA37..=0xAA3F + | 0xAA4E..=0xAA4F + | 0xAA5A..=0xAA5B + | 0xAAC3..=0xAADA + | 0xAAF7..=0xAB00 + | 0xAB07..=0xAB08 + | 0xAB0F..=0xAB10 + | 0xAB17..=0xAB1F + | 0xAB27 + | 0xAB2F + | 0xAB6C..=0xAB6F + | 0xABEE..=0xABEF + | 0xABFA..=0xABFF + | 0xD7A4..=0xD7AF + | 0xD7C7..=0xD7CA + | 0xD7FC..=0xD7FF + | 0xFA6E..=0xFA6F + | 0xFADA..=0xFAFF + | 0xFB07..=0xFB12 + | 0xFB18..=0xFB1C + | 0xFB37 + | 0xFB3D + | 0xFB3F + | 0xFB42 + | 0xFB45 + | 0xFDD0..=0xFDEF + | 0xFE1A..=0xFE1F + | 0xFE53 + | 0xFE67 + | 0xFE6C..=0xFE6F + | 0xFE75 + | 0xFEFD..=0xFEFE + | 0xFF00 + | 0xFFBF..=0xFFC1 + | 0xFFC8..=0xFFC9 + | 0xFFD0..=0xFFD1 + | 0xFFD8..=0xFFD9 + | 0xFFDD..=0xFFDF + | 0xFFE7 + | 0xFFEF..=0xFFF8 + | 0xFFFE..=0xFFFF + | 0x1000C + | 0x10027 + | 0x1003B + | 0x1003E + | 0x1004E..=0x1004F + | 0x1005E..=0x1007F + | 0x100FB..=0x100FF + | 0x10103..=0x10106 + | 0x10134..=0x10136 + | 0x1018F + | 0x1019D..=0x1019F + | 0x101A1..=0x101CF + | 0x101FE..=0x1027F + | 0x1029D..=0x1029F + | 0x102D1..=0x102DF + | 0x102FC..=0x102FF + | 0x10324..=0x1032C + | 0x1034B..=0x1034F + | 0x1037B..=0x1037F + | 0x1039E + | 0x103C4..=0x103C7 + | 0x103D6..=0x103FF + | 0x1049E..=0x1049F + | 0x104AA..=0x104AF + | 0x104D4..=0x104D7 + | 0x104FC..=0x104FF + | 0x10528..=0x1052F + | 0x10564..=0x1056E + | 0x1057B + | 0x1058B + | 0x10593 + | 0x10596 + | 0x105A2 + | 0x105B2 + | 0x105BA + | 0x105BD..=0x105BF + | 0x105F4..=0x105FF + | 0x10737..=0x1073F + | 0x10756..=0x1075F + | 0x10768..=0x1077F + | 0x10786 + | 0x107B1 + | 0x107BB..=0x107FF + | 0x10806..=0x10807 + | 0x10809 + | 0x10836 + | 0x10839..=0x1083B + | 0x1083D..=0x1083E + | 0x10856 + | 0x1089F..=0x108A6 + | 0x108B0..=0x108DF + | 0x108F3 + | 0x108F6..=0x108FA + | 0x1091C..=0x1091E + | 0x1093A..=0x1093E + | 0x1095A..=0x1097F + | 0x109B8..=0x109BB + | 0x109D0..=0x109D1 + | 0x10A04 + | 0x10A07..=0x10A0B + | 0x10A14 + | 0x10A18 + | 0x10A36..=0x10A37 + | 0x10A3B..=0x10A3E + | 0x10A49..=0x10A4F + | 0x10A59..=0x10A5F + | 0x10AA0..=0x10ABF + | 0x10AE7..=0x10AEA + | 0x10AF7..=0x10AFF + | 0x10B36..=0x10B38 + | 0x10B56..=0x10B57 + | 0x10B73..=0x10B77 + | 0x10B92..=0x10B98 + | 0x10B9D..=0x10BA8 + | 0x10BB0..=0x10BFF + | 0x10C49..=0x10C7F + | 0x10CB3..=0x10CBF + | 0x10CF3..=0x10CF9 + | 0x10D28..=0x10D2F + | 0x10D3A..=0x10D3F + | 0x10D66..=0x10D68 + | 0x10D86..=0x10D8D + | 0x10D90..=0x10E5F + | 0x10E7F + | 0x10EAA + | 0x10EAE..=0x10EAF + | 0x10EB2..=0x10EC1 + | 0x10EC8..=0x10ECF + | 0x10ED9..=0x10EF9 + | 0x10F28..=0x10F2F + | 0x10F5A..=0x10F6F + | 0x10F8A..=0x10FAF + | 0x10FCC..=0x10FDF + | 0x10FF7..=0x10FFF + | 0x1104E..=0x11051 + | 0x11076..=0x1107E + | 0x110C3..=0x110CC + | 0x110CE..=0x110CF + | 0x110E9..=0x110EF + | 0x110FA..=0x110FF + | 0x11135 + | 0x11148..=0x1114F + | 0x11177..=0x1117F + | 0x111E0 + | 0x111F5..=0x111FF + | 0x11212 + | 0x11242..=0x1127F + | 0x11287 + | 0x11289 + | 0x1128E + | 0x1129E + | 0x112AA..=0x112AF + | 0x112EB..=0x112EF + | 0x112FA..=0x112FF + | 0x11304 + | 0x1130D..=0x1130E + | 0x11311..=0x11312 + | 0x11329 + | 0x11331 + | 0x11334 + | 0x1133A + | 0x11345..=0x11346 + | 0x11349..=0x1134A + | 0x1134E..=0x1134F + | 0x11351..=0x11356 + | 0x11358..=0x1135C + | 0x11364..=0x11365 + | 0x1136D..=0x1136F + | 0x11375..=0x1137F + | 0x1138A + | 0x1138C..=0x1138D + | 0x1138F + | 0x113B6 + | 0x113C1 + | 0x113C3..=0x113C4 + | 0x113C6 + | 0x113CB + | 0x113D6 + | 0x113D9..=0x113E0 + | 0x113E3..=0x113FF + | 0x1145C + | 0x11462..=0x1147F + | 0x114C8..=0x114CF + | 0x114DA..=0x1157F + | 0x115B6..=0x115B7 + | 0x115DE..=0x115FF + | 0x11645..=0x1164F + | 0x1165A..=0x1165F + | 0x1166D..=0x1167F + | 0x116BA..=0x116BF + | 0x116CA..=0x116CF + | 0x116E4..=0x116FF + | 0x1171B..=0x1171C + | 0x1172C..=0x1172F + | 0x11747..=0x117FF + | 0x1183C..=0x1189F + | 0x118F3..=0x118FE + | 0x11907..=0x11908 + | 0x1190A..=0x1190B + | 0x11914 + | 0x11917 + | 0x11936 + | 0x11939..=0x1193A + | 0x11947..=0x1194F + | 0x1195A..=0x1199F + | 0x119A8..=0x119A9 + | 0x119D8..=0x119D9 + | 0x119E5..=0x119FF + | 0x11A48..=0x11A4F + | 0x11AA3..=0x11AAF + | 0x11AF9..=0x11AFF + | 0x11B0A..=0x11B5F + | 0x11B68..=0x11BBF + | 0x11BE2..=0x11BEF + | 0x11BFA..=0x11BFF + | 0x11C09 + | 0x11C37 + | 0x11C46..=0x11C4F + | 0x11C6D..=0x11C6F + | 0x11C90..=0x11C91 + | 0x11CA8 + | 0x11CB7..=0x11CFF + | 0x11D07 + | 0x11D0A + | 0x11D37..=0x11D39 + | 0x11D3B + | 0x11D3E + | 0x11D48..=0x11D4F + | 0x11D5A..=0x11D5F + | 0x11D66 + | 0x11D69 + | 0x11D8F + | 0x11D92 + | 0x11D99..=0x11D9F + | 0x11DAA..=0x11DAF + | 0x11DDC..=0x11DDF + | 0x11DEA..=0x11EDF + | 0x11EF9..=0x11EFF + | 0x11F11 + | 0x11F3B..=0x11F3D + | 0x11F5B..=0x11FAF + | 0x11FB1..=0x11FBF + | 0x11FF2..=0x11FFE + | 0x1239A..=0x123FF + | 0x1246F + | 0x12475..=0x1247F + | 0x12544..=0x12F8F + | 0x12FF3..=0x12FFF + | 0x13456..=0x1345F + | 0x143FB..=0x143FF + | 0x14647..=0x160FF + | 0x1613A..=0x167FF + | 0x16A39..=0x16A3F + | 0x16A5F + | 0x16A6A..=0x16A6D + | 0x16ABF + | 0x16ACA..=0x16ACF + | 0x16AEE..=0x16AEF + | 0x16AF6..=0x16AFF + | 0x16B46..=0x16B4F + | 0x16B5A + | 0x16B62 + | 0x16B78..=0x16B7C + | 0x16B90..=0x16D3F + | 0x16D7A..=0x16E3F + | 0x16E9B..=0x16E9F + | 0x16EB9..=0x16EBA + | 0x16ED4..=0x16EFF + | 0x16F4B..=0x16F4E + | 0x16F88..=0x16F8E + | 0x16FA0..=0x16FDF + | 0x16FE5..=0x16FEF + | 0x16FF7..=0x16FFF + | 0x18CD6..=0x18CFE + | 0x18D1F..=0x18D7F + | 0x18DF3..=0x1AFEF + | 0x1AFF4 + | 0x1AFFC + | 0x1AFFF + | 0x1B123..=0x1B131 + | 0x1B133..=0x1B14F + | 0x1B153..=0x1B154 + | 0x1B156..=0x1B163 + | 0x1B168..=0x1B16F + | 0x1B2FC..=0x1BBFF + | 0x1BC6B..=0x1BC6F + | 0x1BC7D..=0x1BC7F + | 0x1BC89..=0x1BC8F + | 0x1BC9A..=0x1BC9B + | 0x1BCA4..=0x1CBFF + | 0x1CCFD..=0x1CCFF + | 0x1CEB4..=0x1CEB9 + | 0x1CED1..=0x1CEDF + | 0x1CEF1..=0x1CEFF + | 0x1CF2E..=0x1CF2F + | 0x1CF47..=0x1CF4F + | 0x1CFC4..=0x1CFFF + | 0x1D0F6..=0x1D0FF + | 0x1D127..=0x1D128 + | 0x1D1EB..=0x1D1FF + | 0x1D246..=0x1D2BF + | 0x1D2D4..=0x1D2DF + | 0x1D2F4..=0x1D2FF + | 0x1D357..=0x1D35F + | 0x1D379..=0x1D3FF + | 0x1D455 + | 0x1D49D + | 0x1D4A0..=0x1D4A1 + | 0x1D4A3..=0x1D4A4 + | 0x1D4A7..=0x1D4A8 + | 0x1D4AD + | 0x1D4BA + | 0x1D4BC + | 0x1D4C4 + | 0x1D506 + | 0x1D50B..=0x1D50C + | 0x1D515 + | 0x1D51D + | 0x1D53A + | 0x1D53F + | 0x1D545 + | 0x1D547..=0x1D549 + | 0x1D551 + | 0x1D6A6..=0x1D6A7 + | 0x1D7CC..=0x1D7CD + | 0x1DA8C..=0x1DA9A + | 0x1DAA0 + | 0x1DAB0..=0x1DEFF + | 0x1DF1F..=0x1DF24 + | 0x1DF2B..=0x1DFFF + | 0x1E007 + | 0x1E019..=0x1E01A + | 0x1E022 + | 0x1E025 + | 0x1E02B..=0x1E02F + | 0x1E06E..=0x1E08E + | 0x1E090..=0x1E0FF + | 0x1E12D..=0x1E12F + | 0x1E13E..=0x1E13F + | 0x1E14A..=0x1E14D + | 0x1E150..=0x1E28F + | 0x1E2AF..=0x1E2BF + | 0x1E2FA..=0x1E2FE + | 0x1E300..=0x1E4CF + | 0x1E4FA..=0x1E5CF + | 0x1E5FB..=0x1E5FE + | 0x1E600..=0x1E6BF + | 0x1E6DF + | 0x1E6F6..=0x1E6FD + | 0x1E700..=0x1E7DF + | 0x1E7E7 + | 0x1E7EC + | 0x1E7EF + | 0x1E7FF + | 0x1E8C5..=0x1E8C6 + | 0x1E8D7..=0x1E8FF + | 0x1E94C..=0x1E94F + | 0x1E95A..=0x1E95D + | 0x1E960..=0x1EC70 + | 0x1ECB5..=0x1ED00 + | 0x1ED3E..=0x1EDFF + | 0x1EE04 + | 0x1EE20 + | 0x1EE23 + | 0x1EE25..=0x1EE26 + | 0x1EE28 + | 0x1EE33 + | 0x1EE38 + | 0x1EE3A + | 0x1EE3C..=0x1EE41 + | 0x1EE43..=0x1EE46 + | 0x1EE48 + | 0x1EE4A + | 0x1EE4C + | 0x1EE50 + | 0x1EE53 + | 0x1EE55..=0x1EE56 + | 0x1EE58 + | 0x1EE5A + | 0x1EE5C + | 0x1EE5E + | 0x1EE60 + | 0x1EE63 + | 0x1EE65..=0x1EE66 + | 0x1EE6B + | 0x1EE73 + | 0x1EE78 + | 0x1EE7D + | 0x1EE7F + | 0x1EE8A + | 0x1EE9C..=0x1EEA0 + | 0x1EEA4 + | 0x1EEAA + | 0x1EEBC..=0x1EEEF + | 0x1EEF2..=0x1EFFF + | 0x1F02C..=0x1F02F + | 0x1F094..=0x1F09F + | 0x1F0AF..=0x1F0B0 + | 0x1F0C0 + | 0x1F0D0 + | 0x1F0F6..=0x1F0FF + | 0x1F1AE..=0x1F1E5 + | 0x1F203..=0x1F20F + | 0x1F23C..=0x1F23F + | 0x1F249..=0x1F24F + | 0x1F252..=0x1F25F + | 0x1F266..=0x1F2FF + | 0x1F6D9..=0x1F6DB + | 0x1F6ED..=0x1F6EF + | 0x1F6FD..=0x1F6FF + | 0x1F7DA..=0x1F7DF + | 0x1F7EC..=0x1F7EF + | 0x1F7F1..=0x1F7FF + | 0x1F80C..=0x1F80F + | 0x1F848..=0x1F84F + | 0x1F85A..=0x1F85F + | 0x1F888..=0x1F88F + | 0x1F8AE..=0x1F8AF + | 0x1F8BC..=0x1F8BF + | 0x1F8C2..=0x1F8CF + | 0x1F8D9..=0x1F8FF + | 0x1FA58..=0x1FA5F + | 0x1FA6E..=0x1FA6F + | 0x1FA7D..=0x1FA7F + | 0x1FA8B..=0x1FA8D + | 0x1FAC7 + | 0x1FAC9..=0x1FACC + | 0x1FADD..=0x1FADE + | 0x1FAEB..=0x1FAEE + | 0x1FAF9..=0x1FAFF + | 0x1FB93 + | 0x1FBFB..=0x1FFFF + | 0x2A6E0..=0x2A6FF + | 0x2B81E..=0x2B81F + | 0x2CEAE..=0x2CEAF + | 0x2EBE1..=0x2EBEF + | 0x2EE5E..=0x2F7FF + | 0x2FA1E..=0x2FFFF + | 0x3134B..=0x3134F + | 0x3347A..=0xE0000 + | 0xE0002..=0xE001F + | 0xE0080..=0xE00FF + | 0xE01F0..=0xEFFFF + | 0xFFFFE..=0xFFFFF + | 0x10FFFE..=0x10FFFF + | 0x110000..=u32::MAX // above U+10FFFF — unreachable for `char` + ) +} diff --git a/lightning/Cargo.toml b/lightning/Cargo.toml index fd6c5052359..661f8854f89 100644 --- a/lightning/Cargo.toml +++ b/lightning/Cargo.toml @@ -40,6 +40,7 @@ lightning-macros = { version = "0.2", path = "../lightning-macros" } bech32 = { version = "0.11.0", default-features = false } bitcoin = { version = "0.32.4", default-features = false, features = ["secp-recovery"] } +chacha20-poly1305 = { version = "0.2.0", default-features = false } dnssec-prover = { version = "0.6", default-features = false } hashbrown = { version = "0.13", default-features = false } @@ -65,8 +66,5 @@ features = ["bitcoinconsensus", "secp-recovery"] [target.'cfg(ldk_bench)'.dependencies] criterion = { version = "0.4", optional = true, default-features = false } -[target.'cfg(taproot)'.dependencies] -musig2 = { git = "https://github.com/arik-so/rust-musig2", rev = "6f95a05718cbb44d8fe3fa6021aea8117aa38d50" } - [lints] workspace = true diff --git a/lightning/src/blinded_path/message.rs b/lightning/src/blinded_path/message.rs index 7bcbe80a965..2f67cfdaca9 100644 --- a/lightning/src/blinded_path/message.rs +++ b/lightning/src/blinded_path/message.rs @@ -9,6 +9,8 @@ //! Data structures and methods for constructing [`BlindedMessagePath`]s to send a message over. +use alloc::collections::BTreeMap; + use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; #[allow(unused_imports)] @@ -29,7 +31,9 @@ use crate::routing::gossip::{NodeId, ReadOnlyNetworkGraph}; use crate::sign::{EntropySource, NodeSigner, ReceiveAuthKey, Recipient}; use crate::types::payment::PaymentHash; use crate::util::scid_utils; -use crate::util::ser::{FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer}; +use crate::util::ser::{ + BigSizeKeyedMap, FixedLengthReader, LengthReadableArgs, Readable, Writeable, Writer, +}; use core::time::Duration; use core::{cmp, mem}; @@ -143,10 +147,18 @@ impl BlindedMessagePath { if let IntroductionNode::NodeId(pubkey) = &self.0.introduction_node { let node_id = NodeId::from_pubkey(pubkey); if let Some(node_info) = network_graph.node(&node_id) { + // We don't consider channels that are disabled in either direction, as it may be + // an indication that the channel has closed and simply hasn't been removed from + // our graph yet. If no such channel is found, the `NodeId` representation is + // kept. if let Some((scid, channel_info)) = node_info .channels .iter() .filter_map(|scid| network_graph.channel(*scid).map(|info| (*scid, info))) + .filter(|(_, info)| { + info.one_to_two.as_ref().map(|dir| dir.enabled).unwrap_or(false) + && info.two_to_one.as_ref().map(|dir| dir.enabled).unwrap_or(false) + }) .min_by_key(|(scid, _)| scid_utils::block_from_scid(*scid)) { let direction = if node_id == channel_info.node_one { @@ -271,6 +283,11 @@ pub enum NextMessageHop { ShortChannelId(u64), } +impl_ser_tlv_based_enum!(NextMessageHop, + {0, NodeId} => (), + {2, ShortChannelId} => (), +); + /// An intermediate node, and possibly a short channel id leading to the next node. /// /// Note: @@ -391,6 +408,28 @@ pub enum OffersContext { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Offer`]: crate::offers::offer::Offer nonce: Nonce, + + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Payment metadata is stored as a map from a numeric key to an arbitrary byte array value. + /// This allows for several types of metadata to be stored attached to a single payment. In the + /// future some optional features of LDK may use some keys. For the sake of conflict + /// reduction, those features will attempt to use keys in the range 128-256. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). Further, any data placed here will increase + /// the size of the offer which may make it difficult to fit in QR codes. + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, }, /// Context used by a [`BlindedMessagePath`] within the [`Offer`] of an async recipient. /// @@ -440,15 +479,17 @@ pub enum OffersContext { OutboundPaymentForRefund { /// Payment ID used when creating a [`Refund`]. /// - /// [`Refund`]: crate::offers::refund::Refund - payment_id: PaymentId, - - /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid [`Refund`] and - /// for deriving its signing keys. + /// When a [`Bolt12Invoice`] is received, the payment id recovered from its payer metadata + /// must equal this one, confirming the invoice arrived over the blinded path included in the + /// refund for this payment. Without that check, an attacker holding that path could deliver + /// a different payment's invoice over it, and our paying it would reveal that both payments + /// came from us. That the invoice is for a refund we created is verified by + /// [`Bolt12Invoice::verify_using_metadata`] using its payer metadata. /// - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`Refund`]: crate::offers::refund::Refund - nonce: Nonce, + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`Bolt12Invoice::verify_using_metadata`]: crate::offers::invoice::Bolt12Invoice::verify_using_metadata + payment_id: PaymentId, }, /// Context used by a [`BlindedMessagePath`] as a reply path for an [`InvoiceRequest`]. /// @@ -461,15 +502,17 @@ pub enum OffersContext { OutboundPaymentForOffer { /// Payment ID used when creating an [`InvoiceRequest`]. /// - /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - payment_id: PaymentId, - - /// A nonce used for authenticating that a [`Bolt12Invoice`] is for a valid - /// [`InvoiceRequest`] and for deriving its signing keys. + /// When a [`Bolt12Invoice`] is received, the payment id recovered from its payer metadata + /// must equal this one, confirming the invoice arrived over the reply path created for this + /// payment. Without that check, an attacker holding this reply path could deliver a + /// different payment's invoice over it, and our paying it would reveal that both payments + /// came from us. That the invoice is for an invoice request we created is verified by + /// [`Bolt12Invoice::verify_using_metadata`] using its payer metadata. /// - /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest - nonce: Nonce, + /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice + /// [`Bolt12Invoice::verify_using_metadata`]: crate::offers::invoice::Bolt12Invoice::verify_using_metadata + payment_id: PaymentId, }, /// Context used by a [`BlindedMessagePath`] as a reply path for a [`Bolt12Invoice`]. /// @@ -634,7 +677,7 @@ pub enum AsyncPaymentsContext { }, } -impl_writeable_tlv_based_enum!(MessageContext, +impl_ser_tlv_based_enum!(MessageContext, {0, Offers} => (), {1, Custom} => (), {2, AsyncPayments} => (), @@ -645,13 +688,13 @@ impl_writeable_tlv_based_enum!(MessageContext, // introduction of `ReceiveAuthKey`-based authentication for inbound `BlindedMessagePath`s. Because // we do not support receiving to those contexts anymore (they will fail the `ReceiveAuthKey`-based // authentication checks), we can reuse those fields here. -impl_writeable_tlv_based_enum!(OffersContext, +impl_ser_tlv_based_enum!(OffersContext, (0, InvoiceRequest) => { (0, nonce, required), + (1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))), }, (1, OutboundPaymentForRefund) => { (0, payment_id, required), - (1, nonce, required), }, (2, InboundPayment) => { (0, payment_hash, required), @@ -663,11 +706,10 @@ impl_writeable_tlv_based_enum!(OffersContext, }, (4, OutboundPaymentForOffer) => { (0, payment_id, required), - (1, nonce, required), }, ); -impl_writeable_tlv_based_enum!(AsyncPaymentsContext, +impl_ser_tlv_based_enum!(AsyncPaymentsContext, (0, OutboundPayment) => { (0, payment_id, required), }, @@ -710,7 +752,7 @@ pub struct DNSResolverContext { pub nonce: [u8; 16], } -impl_writeable_tlv_based!(DNSResolverContext, { +impl_ser_tlv_based!(DNSResolverContext, { (0, nonce, required), }); @@ -789,3 +831,156 @@ pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>( let path = pks.zip(tlvs); utils::construct_blinded_hops(secp_ctx, path, session_priv) } + +#[cfg(test)] +mod tests { + use bitcoin::constants::ChainHash; + use bitcoin::network::Network; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + + use crate::blinded_path::message::{BlindedMessagePath, MessageContext, MessageForwardNode}; + use crate::blinded_path::IntroductionNode; + use crate::ln::msgs::{UnsignedChannelUpdate, MAX_VALUE_MSAT}; + use crate::routing::gossip::{NetworkGraph, P2PGossipSync}; + use crate::routing::test_utils::{add_channel, update_channel}; + use crate::sign::ReceiveAuthKey; + use crate::sync::Arc; + use crate::types::features::ChannelFeatures; + use crate::util::test_utils::{TestKeysInterface, TestLogger}; + + fn channel_update( + short_channel_id: u64, timestamp: u32, channel_flags: u8, + ) -> UnsignedChannelUpdate { + UnsignedChannelUpdate { + chain_hash: ChainHash::using_genesis_block(Network::Testnet), + short_channel_id, + timestamp, + message_flags: 1, // Only must_be_one + channel_flags, + cltv_expiry_delta: 0, + htlc_minimum_msat: 0, + htlc_maximum_msat: MAX_VALUE_MSAT, + fee_base_msat: 0, + fee_proportional_millionths: 0, + excess_data: Vec::new(), + } + } + + fn one_hop_path( + secp_ctx: &Secp256k1<bitcoin::secp256k1::All>, introduction_node_id: PublicKey, + recipient_node_id: PublicKey, entropy: &TestKeysInterface, + ) -> BlindedMessagePath { + let intermediate_nodes = + [MessageForwardNode { node_id: introduction_node_id, short_channel_id: None }]; + BlindedMessagePath::new( + &intermediate_nodes, + recipient_node_id, + ReceiveAuthKey([42; 32]), + MessageContext::Custom(Vec::new()), + false, + entropy, + secp_ctx, + ) + } + + #[test] + fn compact_introduction_node_skips_disabled_channels() { + // The compact (DirectedShortChannelId) introduction node encoding must only use + // channels that are enabled in both directions: disabled or closed channels may + // linger in the local network graph (e.g., when sourcing gossip from rapid gossip + // sync, which never removes them), and must not be selected, as senders would be + // unable to resolve (or route to) the introduction node. + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(TestLogger::new()); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger))); + let gossip_sync = P2PGossipSync::new(Arc::clone(&network_graph), None, Arc::clone(&logger)); + let entropy = TestKeysInterface::new(&[0; 32], Network::Testnet); + + let node_a_privkey = SecretKey::from_slice(&[41; 32]).unwrap(); + let node_b_privkey = SecretKey::from_slice(&[43; 32]).unwrap(); + let node_a_pubkey = PublicKey::from_secret_key(&secp_ctx, &node_a_privkey); + let recipient_pubkey = PublicKey::from_secret_key(&secp_ctx, &node_b_privkey); + + let disabled_scid = 100 << 40 | 1 << 16; + let enabled_scid = 200 << 40 | 1 << 16; + + // Add an older channel which is disabled in both directions, as is the case for a + // closed channel lingering in the local graph. + add_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + &node_b_privkey, + ChannelFeatures::from_le_bytes(vec![1]), + disabled_scid, + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + channel_update(disabled_scid, 1, 2), + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_b_privkey, + channel_update(disabled_scid, 1, 3), + ); + + // Add a newer channel which is enabled in both directions. + add_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + &node_b_privkey, + ChannelFeatures::from_le_bytes(vec![2]), + enabled_scid, + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + channel_update(enabled_scid, 2, 0), + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_b_privkey, + channel_update(enabled_scid, 2, 1), + ); + + // Even though the disabled channel is older, the enabled one is selected. + { + let network_graph = network_graph.read_only(); + let mut path = one_hop_path(&secp_ctx, node_a_pubkey, recipient_pubkey, &entropy); + path.use_compact_introduction_node(&network_graph); + match path.introduction_node() { + IntroductionNode::DirectedShortChannelId(_, scid) => { + assert_eq!(*scid, enabled_scid) + }, + IntroductionNode::NodeId(..) => panic!("expected a compact introduction node"), + } + } + + // Once the enabled channel is disabled as well, the `NodeId` encoding is kept. + update_channel( + &gossip_sync, + &secp_ctx, + &node_a_privkey, + channel_update(enabled_scid, 3, 2), + ); + update_channel( + &gossip_sync, + &secp_ctx, + &node_b_privkey, + channel_update(enabled_scid, 3, 3), + ); + + let network_graph = network_graph.read_only(); + let mut path = one_hop_path(&secp_ctx, node_a_pubkey, recipient_pubkey, &entropy); + path.use_compact_introduction_node(&network_graph); + assert!( + matches!(path.introduction_node(), IntroductionNode::NodeId(pubkey) if *pubkey == node_a_pubkey) + ); + } +} diff --git a/lightning/src/blinded_path/payment.rs b/lightning/src/blinded_path/payment.rs index 27292bacf4d..5fd608d6135 100644 --- a/lightning/src/blinded_path/payment.rs +++ b/lightning/src/blinded_path/payment.rs @@ -9,12 +9,14 @@ //! Data structures and methods for constructing [`BlindedPaymentPath`]s to send a payment over. +use alloc::collections::BTreeMap; + use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use crate::blinded_path::utils::{self, BlindedPathWithPadding}; use crate::blinded_path::{BlindedHop, BlindedPath, IntroductionNode, NodeIdLookUp}; -use crate::crypto::streams::ChaChaDualPolyReadAdapter; +use crate::crypto::streams::{ChaChaTriPolyReadAdapter, TriPolyAADUsed}; use crate::io; use crate::io::Cursor; use crate::ln::channel_state::CounterpartyForwardingInfo; @@ -29,8 +31,8 @@ use crate::types::features::BlindedHopFeatures; use crate::types::payment::PaymentSecret; use crate::types::routing::RoutingFees; use crate::util::ser::{ - FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable, WithoutLength, - Writeable, Writer, + BigSizeKeyedMap, FixedLengthReader, HighZeroBytesDroppedBigSize, LengthReadableArgs, Readable, + WithoutLength, Writeable, Writer, }; #[allow(unused_imports)] @@ -161,8 +163,35 @@ impl BlindedPaymentPath { ) } - fn new_inner<ES: EntropySource, T: secp256k1::Signing + secp256k1::Verification>( - intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, + /// Create a blinded path for a trampoline payment, to be forwarded along `intermediate_nodes`. + #[cfg(any(test, feature = "_test_utils"))] + pub(crate) fn new_for_trampoline< + ES: EntropySource, + T: secp256k1::Signing + secp256k1::Verification, + >( + intermediate_nodes: &[ForwardNode<TrampolineForwardTlvs>], payee_node_id: PublicKey, + local_node_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, + min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1<T>, + ) -> Result<Self, ()> { + Self::new_inner( + intermediate_nodes, + payee_node_id, + local_node_receive_key, + &[], + payee_tlvs, + htlc_maximum_msat, + min_final_cltv_expiry_delta, + entropy_source, + secp_ctx, + ) + } + + fn new_inner< + F: ForwardTlvsInfo, + ES: EntropySource, + T: secp256k1::Signing + secp256k1::Verification, + >( + intermediate_nodes: &[ForwardNode<F>], payee_node_id: PublicKey, local_node_receive_key: ReceiveAuthKey, dummy_tlvs: &[DummyTlvs], payee_tlvs: ReceiveTlvs, htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, entropy_source: ES, secp_ctx: &Secp256k1<T>, @@ -268,18 +297,20 @@ impl BlindedPaymentPath { node_signer.ecdh(Recipient::Node, &self.inner_path.blinding_point, None)?; let rho = onion_utils::gen_rho_from_shared_secret(&control_tlvs_ss.secret_bytes()); let receive_auth_key = node_signer.get_receive_auth_key(); + let phantom_auth_key = node_signer.get_expanded_key().phantom_node_blinded_path_key; + let read_arg = (rho, receive_auth_key.0, phantom_auth_key); + let encrypted_control_tlvs = &self.inner_path.blinded_hops.get(0).ok_or(())?.encrypted_payload; let mut s = Cursor::new(encrypted_control_tlvs); let mut reader = FixedLengthReader::new(&mut s, encrypted_control_tlvs.len() as u64); - let ChaChaDualPolyReadAdapter { readable, used_aad } = - ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0)) - .map_err(|_| ())?; - - match (&readable, used_aad) { - (BlindedPaymentTlvs::Forward(_), false) - | (BlindedPaymentTlvs::Dummy(_), true) - | (BlindedPaymentTlvs::Receive(_), true) => Ok((readable, control_tlvs_ss)), + let ChaChaTriPolyReadAdapter { readable, used_aad } = + ChaChaTriPolyReadAdapter::read(&mut reader, read_arg).map_err(|_| ())?; + + match (&readable, used_aad == TriPolyAADUsed::None) { + (BlindedPaymentTlvs::Forward(_), true) + | (BlindedPaymentTlvs::Dummy(_), false) + | (BlindedPaymentTlvs::Receive(_), false) => Ok((readable, control_tlvs_ss)), _ => Err(()), } } @@ -321,18 +352,42 @@ impl BlindedPaymentPath { } } -/// An intermediate node, its outbound channel, and relay parameters. +mod sealed { + pub trait ForwardTlvsInfo {} +} + +/// Common interface for forward TLV types used in blinded payment paths. +/// +/// Both [`ForwardTlvs`] (channel-based forwarding) and [`TrampolineForwardTlvs`] (trampoline +/// node-based forwarding) implement this trait, allowing blinded path construction to be generic +/// over the forwarding mechanism. +/// +/// This trait is sealed and is not intended for implementation outside of this crate. +pub trait ForwardTlvsInfo: Writeable + Clone + sealed::ForwardTlvsInfo { + /// The payment relay parameters for this hop. + fn payment_relay(&self) -> &PaymentRelay; + /// The payment constraints for this hop. + fn payment_constraints(&self) -> &PaymentConstraints; + /// The features for this hop. + fn features(&self) -> &BlindedHopFeatures; +} + +/// An intermediate node, its forwarding parameters, and its [`ForwardTlvsInfo`] for use in a +/// [`BlindedPaymentPath`]. #[derive(Clone, Debug)] -pub struct PaymentForwardNode { +pub struct ForwardNode<F: ForwardTlvsInfo> { /// The TLVs for this node's [`BlindedHop`], where the fee parameters contained within are also /// used for [`BlindedPayInfo`] construction. - pub tlvs: ForwardTlvs, + pub tlvs: F, /// This node's pubkey. pub node_id: PublicKey, /// The maximum value, in msat, that may be accepted by this node. pub htlc_maximum_msat: u64, } +/// An intermediate node for a regular (non-trampoline) [`BlindedPaymentPath`]. +pub type PaymentForwardNode = ForwardNode<ForwardTlvs>; + /// Data to construct a [`BlindedHop`] for forwarding a payment. #[derive(Clone, Debug)] pub struct ForwardTlvs { @@ -352,6 +407,20 @@ pub struct ForwardTlvs { pub next_blinding_override: Option<PublicKey>, } +impl sealed::ForwardTlvsInfo for ForwardTlvs {} + +impl ForwardTlvsInfo for ForwardTlvs { + fn payment_relay(&self) -> &PaymentRelay { + &self.payment_relay + } + fn payment_constraints(&self) -> &PaymentConstraints { + &self.payment_constraints + } + fn features(&self) -> &BlindedHopFeatures { + &self.features + } +} + /// Data to construct a [`BlindedHop`] for forwarding a Trampoline payment. #[derive(Clone, Debug)] pub struct TrampolineForwardTlvs { @@ -371,6 +440,20 @@ pub struct TrampolineForwardTlvs { pub next_blinding_override: Option<PublicKey>, } +impl sealed::ForwardTlvsInfo for TrampolineForwardTlvs {} + +impl ForwardTlvsInfo for TrampolineForwardTlvs { + fn payment_relay(&self) -> &PaymentRelay { + &self.payment_relay + } + fn payment_constraints(&self) -> &PaymentConstraints { + &self.payment_constraints + } + fn features(&self) -> &BlindedHopFeatures { + &self.features + } +} + /// TLVs carried by a dummy hop within a blinded payment path. /// /// Dummy hops do not correspond to real forwarding decisions, but are processed @@ -438,8 +521,8 @@ pub(crate) enum BlindedTrampolineTlvs { // Used to include forward and receive TLVs in the same iterator for encoding. #[derive(Clone)] -enum BlindedPaymentTlvsRef<'a> { - Forward(&'a ForwardTlvs), +enum BlindedPaymentTlvsRef<'a, F: ForwardTlvsInfo = ForwardTlvs> { + Forward(&'a F), Dummy(&'a DummyTlvs), Receive(&'a ReceiveTlvs), } @@ -491,6 +574,20 @@ pub enum PaymentContext { /// [`Refund`]: crate::offers::refund::Refund Bolt12Refund(Bolt12RefundContext), } +impl PaymentContext { + /// Returns the additional payment metadata stored alongside this payment context, if any. + /// + /// Payment metadata is stored as a map from a numeric key to an arbitrary byte array value. + /// This allows for several types of metadata to be stored attached to a single payment. In the + /// future some optional features of LDK may use some keys. + pub fn payment_metadata(&self) -> Option<&BTreeMap<u64, Vec<u8>>> { + match self { + Self::Bolt12Offer(Bolt12OfferContext { payment_metadata, .. }) + | Self::AsyncBolt12Offer(AsyncBolt12OfferContext { payment_metadata, .. }) + | Self::Bolt12Refund(Bolt12RefundContext { payment_metadata, .. }) => payment_metadata.as_ref(), + } + } +} // Used when writing PaymentContext in Event::PaymentClaimable to avoid cloning. pub(crate) enum PaymentContextRef<'a> { @@ -513,6 +610,27 @@ pub struct Bolt12OfferContext { /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice pub invoice_request: InvoiceRequestFields, + + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Payment metadata is stored as a map from a numeric key to an arbitrary byte array value. + /// This allows for several types of metadata to be stored attached to a single payment. In the + /// future some optional features of LDK may use some keys. For the sake of conflict + /// reduction, those features will attempt to use keys in the range 128-256. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + pub payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, } /// The context of a payment made for a static invoice requested from a BOLT 12 [`Offer`]. @@ -525,13 +643,55 @@ pub struct AsyncBolt12OfferContext { /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest pub offer_nonce: Nonce, + + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Payment metadata is stored as a map from a numeric key to an arbitrary byte array value. + /// This allows for several types of metadata to be stored attached to a single payment. In the + /// future some optional features of LDK may use some keys. For the sake of conflict + /// reduction, those features will attempt to use keys in the range 128-256. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + pub payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, } /// The context of a payment made for an invoice sent for a BOLT 12 [`Refund`]. /// /// [`Refund`]: crate::offers::refund::Refund #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Bolt12RefundContext {} +pub struct Bolt12RefundContext { + /// Additional data about this payment which is not used in LDK and can be used for any + /// purpose. + /// + /// This is analogous to the BOLT 11 [`RecipientOnionFields::payment_metadata`] (which is + /// provided to payers via [`Bolt11Invoice::payment_metadata`]) and can be used any time data + /// needs to be "stored" by a payment recipient for their own internal use, provided back to + /// them with the payment. + /// + /// Payment metadata is stored as a map from a numeric key to an arbitrary byte array value. + /// This allows for several types of metadata to be stored attached to a single payment. In the + /// future some optional features of LDK may use some keys. For the sake of conflict + /// reduction, those features will attempt to use keys in the range 128-256. + /// + /// Note that because this is included in the payment onion, its size must be tightly + /// constrained. More than a few hundred bytes and the payment will be entirely unpayable (with + /// limited routing options as size increases). + /// + /// [`RecipientOnionFields::payment_metadata`]: crate::ln::outbound_payment::RecipientOnionFields::payment_metadata + /// [`Bolt11Invoice::payment_metadata`]: lightning_invoice::Bolt11Invoice::payment_metadata + pub payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, +} impl TryFrom<CounterpartyForwardingInfo> for PaymentRelay { type Error = (); @@ -617,7 +777,7 @@ impl Writeable for ReceiveTlvs { } } -impl<'a> Writeable for BlindedPaymentTlvsRef<'a> { +impl<'a, F: ForwardTlvsInfo> Writeable for BlindedPaymentTlvsRef<'a, F> { fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { match self { Self::Forward(tlvs) => tlvs.write(w)?, @@ -721,8 +881,8 @@ impl Readable for BlindedTrampolineTlvs { pub(crate) const PAYMENT_PADDING_ROUND_OFF: usize = 30; /// Construct blinded payment hops for the given `intermediate_nodes` and payee info. -pub(super) fn blinded_hops<T: secp256k1::Signing + secp256k1::Verification>( - secp_ctx: &Secp256k1<T>, intermediate_nodes: &[PaymentForwardNode], payee_node_id: PublicKey, +pub(super) fn blinded_hops<F: ForwardTlvsInfo, T: secp256k1::Signing + secp256k1::Verification>( + secp_ctx: &Secp256k1<T>, intermediate_nodes: &[ForwardNode<F>], payee_node_id: PublicKey, dummy_tlvs: &[DummyTlvs], payee_tlvs: ReceiveTlvs, session_priv: &SecretKey, local_node_receive_key: ReceiveAuthKey, ) -> Vec<BlindedHop> { @@ -780,7 +940,7 @@ pub(crate) fn amt_to_forward_msat( (post_base_fee_inbound_amt * 1_000_000 + 1_000_000 + prop - 1) / (prop + 1_000_000); let fee = ((amt_to_forward * prop) / 1_000_000) + base; - if inbound_amt - fee < amt_to_forward { + if inbound_amt.checked_sub(fee)? < amt_to_forward { // Rounding up the forwarded amount resulted in underpaying this node, so take an extra 1 msat // in fee to compensate. amt_to_forward -= 1; @@ -821,15 +981,15 @@ where Ok((curr_base_fee, curr_prop_mil)) } -pub(super) fn compute_payinfo( - intermediate_nodes: &[PaymentForwardNode], dummy_tlvs: &[DummyTlvs], payee_tlvs: &ReceiveTlvs, +pub(super) fn compute_payinfo<F: ForwardTlvsInfo>( + intermediate_nodes: &[ForwardNode<F>], dummy_tlvs: &[DummyTlvs], payee_tlvs: &ReceiveTlvs, payee_htlc_maximum_msat: u64, min_final_cltv_expiry_delta: u16, ) -> Result<BlindedPayInfo, ()> { let routing_fees = intermediate_nodes .iter() .map(|node| RoutingFees { - base_msat: node.tlvs.payment_relay.fee_base_msat, - proportional_millionths: node.tlvs.payment_relay.fee_proportional_millionths, + base_msat: node.tlvs.payment_relay().fee_base_msat, + proportional_millionths: node.tlvs.payment_relay().fee_proportional_millionths, }) .chain(dummy_tlvs.iter().map(|tlvs| RoutingFees { base_msat: tlvs.payment_relay.fee_base_msat, @@ -845,24 +1005,24 @@ pub(super) fn compute_payinfo( for node in intermediate_nodes.iter() { // In the future, we'll want to take the intersection of all supported features for the // `BlindedPayInfo`, but there are no features in that context right now. - if node.tlvs.features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) { + if node.tlvs.features().requires_unknown_bits_from(&BlindedHopFeatures::empty()) { return Err(()); } cltv_expiry_delta = - cltv_expiry_delta.checked_add(node.tlvs.payment_relay.cltv_expiry_delta).ok_or(())?; + cltv_expiry_delta.checked_add(node.tlvs.payment_relay().cltv_expiry_delta).ok_or(())?; // The min htlc for an intermediate node is that node's min minus the fees charged by all of the // following hops for forwarding that min, since that fee amount will automatically be included // in the amount that this node receives and contribute towards reaching its min. htlc_minimum_msat = amt_to_forward_msat( - core::cmp::max(node.tlvs.payment_constraints.htlc_minimum_msat, htlc_minimum_msat), - &node.tlvs.payment_relay, + core::cmp::max(node.tlvs.payment_constraints().htlc_minimum_msat, htlc_minimum_msat), + node.tlvs.payment_relay(), ) .unwrap_or(1); // If underflow occurs, we definitely reached this node's min htlc_maximum_msat = amt_to_forward_msat( core::cmp::min(node.htlc_maximum_msat, htlc_maximum_msat), - &node.tlvs.payment_relay, + node.tlvs.payment_relay(), ) .ok_or(())?; // If underflow occurs, we cannot send to this hop without exceeding their max } @@ -923,7 +1083,7 @@ impl Readable for PaymentConstraints { } } -impl_writeable_tlv_based_enum_legacy!(PaymentContext, +impl_ser_tlv_based_enum_legacy!(PaymentContext, ; // 0 for Unknown removed in version 0.1. (1, Bolt12Offer), @@ -948,16 +1108,20 @@ impl<'a> Writeable for PaymentContextRef<'a> { } } -impl_writeable_tlv_based!(Bolt12OfferContext, { +impl_ser_tlv_based!(Bolt12OfferContext, { (0, offer_id, required), + (1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))), (2, invoice_request, required), }); -impl_writeable_tlv_based!(AsyncBolt12OfferContext, { +impl_ser_tlv_based!(AsyncBolt12OfferContext, { (0, offer_nonce, required), + (1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))), }); -impl_writeable_tlv_based!(Bolt12RefundContext, {}); +impl_ser_tlv_based!(Bolt12RefundContext, { + (1, payment_metadata, (option, encoding: (BTreeMap<u64, Vec<u8>>, BigSizeKeyedMap))), +}); #[cfg(test)] mod tests { @@ -1016,7 +1180,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let htlc_maximum_msat = 100_000; let blinded_payinfo = @@ -1034,10 +1200,18 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; - let blinded_payinfo = - super::compute_payinfo(&[], &[], &recv_tlvs, 4242, TEST_FINAL_CLTV as u16).unwrap(); + let blinded_payinfo = super::compute_payinfo::<ForwardTlvs>( + &[], + &[], + &recv_tlvs, + 4242, + TEST_FINAL_CLTV as u16, + ) + .unwrap(); assert_eq!(blinded_payinfo.fee_base_msat, 0); assert_eq!(blinded_payinfo.fee_proportional_millionths, 0); assert_eq!(blinded_payinfo.cltv_expiry_delta, TEST_FINAL_CLTV as u16); @@ -1091,7 +1265,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 3 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let htlc_maximum_msat = 100_000; let blinded_payinfo = super::compute_payinfo( @@ -1151,7 +1327,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let htlc_minimum_msat = 3798; assert!(super::compute_payinfo( @@ -1222,7 +1400,9 @@ mod tests { let recv_tlvs = ReceiveTlvs { payment_secret: PaymentSecret([0; 32]), payment_constraints: PaymentConstraints { max_cltv_expiry: 0, htlc_minimum_msat: 1 }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let blinded_payinfo = super::compute_payinfo( @@ -1235,4 +1415,19 @@ mod tests { .unwrap(); assert_eq!(blinded_payinfo.htlc_maximum_msat, 3997); } + + #[test] + fn amt_to_forward_msat_underflow() { + // `amt_to_forward_msat` is documented to return `None` if underflow occurs, but the + // `inbound_amt - fee` subtraction was previously unguarded. With a high proportional fee + // and a small inbound amount, rounding the forwarded amount up leaves `fee` larger than + // `inbound_amt`, so the subtraction underflows (panicking in debug builds and returning a + // nonsensical result in release). Ensure we instead return `None`. + let payment_relay = PaymentRelay { + cltv_expiry_delta: 0, + fee_proportional_millionths: u32::MAX, + fee_base_msat: 1, + }; + assert!(super::amt_to_forward_msat(2, &payment_relay).is_none()); + } } diff --git a/lightning/src/chain/chaininterface.rs b/lightning/src/chain/chaininterface.rs index 806e947c153..3bc7d20af03 100644 --- a/lightning/src/chain/chaininterface.rs +++ b/lightning/src/chain/chaininterface.rs @@ -15,9 +15,11 @@ use core::{cmp, ops::Deref}; +use crate::ln::funding::FundingContribution; use crate::ln::types::ChannelId; use crate::prelude::*; +use bitcoin::hash_types::Txid; use bitcoin::secp256k1::PublicKey; use bitcoin::transaction::Transaction; @@ -104,19 +106,76 @@ pub enum TransactionType { /// A single sweep transaction may aggregate outputs from multiple channels. channels: Vec<(PublicKey, ChannelId)>, }, - /// A splice transaction modifying an existing channel's funding. + /// An interactively-negotiated funding transaction. /// - /// A transaction of this type will be broadcast as a result of a [`ChannelManager::splice_channel`] operation. + /// A transaction of this type will be broadcast as a result of a + /// [`ChannelManager::splice_channel`] operation, or (once supported) V2 (dual-funded) channel + /// establishment. The same variant is used for batches of either or both. /// /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel - Splice { - /// The `node_id` of the channel counterparty. - counterparty_node_id: PublicKey, - /// The ID of the channel being spliced. - channel_id: ChannelId, + InteractiveFunding { + /// Every negotiated candidate for this funding in order: the original negotiation + /// followed by any RBF replacements. The last entry is the candidate being broadcast. + candidates: Vec<FundingCandidate>, }, } +/// A single negotiated candidate within a [`TransactionType::InteractiveFunding`] broadcast. +/// +/// The candidate is identified by its [`Txid`] and lists the channels participating in it. A +/// single candidate funds more than one channel only when batching splices and/or V2 channel +/// openings (not yet implemented). +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct FundingCandidate { + /// The txid of this candidate. + pub txid: Txid, + /// The channels participating in this candidate. + pub channels: Vec<ChannelFunding>, +} + +/// Information about a single channel's participation in a [`FundingCandidate`]. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct ChannelFunding { + /// The `node_id` of the channel counterparty. + pub counterparty_node_id: PublicKey, + /// The ID of the channel. + pub channel_id: ChannelId, + /// Whether this channel is being newly established or is an existing channel being spliced. + pub purpose: FundingPurpose, + /// The local node's contribution to this channel in this candidate, or `None` if we did + /// not contribute (e.g., a pure acceptor with zero value added, or a leading RBF round + /// before we began contributing). + pub contribution: Option<FundingContribution>, +} + +/// The role of a channel within a [`FundingCandidate`]. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub enum FundingPurpose { + /// The channel is being newly established (V2 dual-funded open). + Establishment, + /// An existing channel is being spliced. + Splice, +} + +// Needed so downstream consumers can persist these without needing to define wrapper types +// mirroring the type structure. +impl_ser_tlv_based!(FundingCandidate, { + (1, txid, required), + (3, channels, required_vec), +}); + +impl_ser_tlv_based!(ChannelFunding, { + (1, counterparty_node_id, required), + (3, channel_id, required), + (5, purpose, required), + (7, contribution, option), +}); + +impl_ser_tlv_based_enum!(FundingPurpose, + (0, Establishment) => {}, + (2, Splice) => {}, +); + // TODO: Define typed abstraction over feerates to handle their conversions. pub(crate) fn compute_feerate_sat_per_1000_weight(fee_sat: u64, weight: u64) -> u32 { (fee_sat * 1000 / weight).try_into().unwrap_or(u32::max_value()) diff --git a/lightning/src/chain/chainmonitor.rs b/lightning/src/chain/chainmonitor.rs index 7db1b697c2b..b3b69096997 100644 --- a/lightning/src/chain/chainmonitor.rs +++ b/lightning/src/chain/chainmonitor.rs @@ -37,7 +37,7 @@ use crate::chain::channelmonitor::{ WithChannelMonitor, }; use crate::chain::transaction::{OutPoint, TransactionData}; -use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, WatchedOutput}; +use crate::chain::{BlockLocator, ChannelMonitorUpdateStatus, WatchedOutput}; use crate::events::{self, Event, EventHandler, ReplayEvent}; use crate::ln::channel_state::ChannelDetails; #[cfg(peer_storage)] @@ -51,21 +51,29 @@ use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::{EntropySource, PeerStorageKey, SignerProvider}; use crate::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard}; use crate::types::features::{InitFeatures, NodeFeatures}; -use crate::util::async_poll::{MaybeSend, MaybeSync}; use crate::util::errors::APIError; use crate::util::logger::{Logger, WithContext}; -use crate::util::native_async::FutureSpawner; +use crate::util::native_async::{FutureSpawner, MaybeSend, MaybeSync}; use crate::util::persist::{KVStore, MonitorName, MonitorUpdatingPersisterAsync}; #[cfg(peer_storage)] use crate::util::ser::{VecWriter, Writeable}; use crate::util::wakers::{Future, Notifier}; +use alloc::collections::VecDeque; use alloc::sync::Arc; #[cfg(peer_storage)] use core::iter::Cycle; use core::ops::Deref; use core::sync::atomic::{AtomicUsize, Ordering}; +/// A pending operation queued for later execution when `ChainMonitor` is in deferred mode. +enum PendingMonitorOp<ChannelSigner: EcdsaChannelSigner> { + /// A new monitor to insert and persist. + NewMonitor { channel_id: ChannelId, monitor: ChannelMonitor<ChannelSigner> }, + /// An update to apply and persist. + Update { channel_id: ChannelId, update: ChannelMonitorUpdate }, +} + /// `Persist` defines behavior for persisting channel monitors: this could mean /// writing once to disk, and/or uploading to one or more backup services. /// @@ -83,8 +91,10 @@ use core::sync::atomic::{AtomicUsize, Ordering}; /// the background with [`ChainMonitor::list_pending_monitor_updates`] and /// [`ChainMonitor::get_monitor`]. /// -/// Once a full [`ChannelMonitor`] has been persisted, all pending updates for that channel can -/// be marked as complete via [`ChainMonitor::channel_monitor_updated`]. +/// Each pending update must be individually marked as complete by calling +/// [`ChainMonitor::channel_monitor_updated`] with the corresponding update ID. Note that +/// persisting a full [`ChannelMonitor`] covers all prior updates, but each update ID still +/// needs to be marked complete separately. /// /// If at some point no further progress can be made towards persisting the pending updates, the /// node should simply shut down. @@ -371,6 +381,13 @@ pub struct ChainMonitor< #[cfg(peer_storage)] our_peerstorage_encryption_key: PeerStorageKey, + + /// When `true`, [`chain::Watch`] operations are queued rather than executed immediately. + deferred: bool, + /// Queued monitor operations awaiting flush. Unused when `deferred` is `false`. + pending_ops: Mutex<VecDeque<PendingMonitorOp<ChannelSigner>>>, + /// Guards [`Self::flush`] so that concurrent calls are serialized. + flush_lock: Mutex<()>, } impl< @@ -393,11 +410,23 @@ where /// /// Note that async monitor updating is considered beta, and bugs may be triggered by its use. /// + /// When `deferred` is `true`, [`chain::Watch::watch_channel`] and + /// [`chain::Watch::update_channel`] calls are not executed immediately. Instead, they are + /// queued internally and must be flushed by the caller via [`Self::flush`]. Use + /// [`Self::pending_operation_count`] to check how many operations are queued, then call + /// [`Self::flush`] to process them. This allows the caller to ensure that the + /// [`ChannelManager`] is persisted before its associated monitors, avoiding the risk of + /// force closures from a crash between monitor and channel manager persistence. + /// + /// When `deferred` is `false`, monitor operations are executed inline as usual. + /// + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// /// This is not exported to bindings users as async is not supported outside of Rust. pub fn new_async_beta( chain_source: Option<C>, broadcaster: T, logger: L, feeest: F, persister: MonitorUpdatingPersisterAsync<K, S, L, ES, SP, T, F>, _entropy_source: ES, - _our_peerstorage_encryption_key: PeerStorageKey, + _our_peerstorage_encryption_key: PeerStorageKey, deferred: bool, ) -> Self { let event_notifier = Arc::new(Notifier::new()); Self { @@ -414,6 +443,9 @@ where pending_send_only_events: Mutex::new(Vec::new()), #[cfg(peer_storage)] our_peerstorage_encryption_key: _our_peerstorage_encryption_key, + deferred, + pending_ops: Mutex::new(VecDeque::new()), + flush_lock: Mutex::new(()), } } } @@ -523,7 +555,7 @@ where channel_id_bytes[2], channel_id_bytes[3], ]); - channel_id_u32.wrapping_add(best_height.unwrap_or_default()) + best_height.map(|height| channel_id_u32.wrapping_add(height)) }; let partition_factor = if channel_count < 15 { @@ -533,7 +565,9 @@ where }; let has_pending_claims = monitor_state.monitor.has_pending_claims(); - if has_pending_claims || get_partition_key(channel_id) % partition_factor == 0 { + if has_pending_claims + || get_partition_key(channel_id).is_some_and(|key| key % partition_factor == 0) + { log_trace!(logger, "Syncing Channel Monitor"); // Even though we don't track monitor updates from chain-sync as pending, we still want // updates per-channel to be well-ordered so that users don't see a @@ -598,12 +632,22 @@ where /// is obtained by the [`ChannelManager`] through [`NodeSigner`] to decrypt peer backups. /// Using an inconsistent or incorrect key will result in the inability to decrypt previously encrypted backups. /// + /// When `deferred` is `true`, [`chain::Watch::watch_channel`] and + /// [`chain::Watch::update_channel`] calls are not executed immediately. Instead, they are + /// queued internally and must be flushed by the caller via [`Self::flush`]. Use + /// [`Self::pending_operation_count`] to check how many operations are queued, then call + /// [`Self::flush`] to process them. This allows the caller to ensure that the + /// [`ChannelManager`] is persisted before its associated monitors, avoiding the risk of + /// force closures from a crash between monitor and channel manager persistence. + /// + /// When `deferred` is `false`, monitor operations are executed inline as usual. + /// /// [`NodeSigner`]: crate::sign::NodeSigner /// [`NodeSigner::get_peer_storage_key`]: crate::sign::NodeSigner::get_peer_storage_key /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager pub fn new( chain_source: Option<C>, broadcaster: T, logger: L, feeest: F, persister: P, - _entropy_source: ES, _our_peerstorage_encryption_key: PeerStorageKey, + _entropy_source: ES, _our_peerstorage_encryption_key: PeerStorageKey, deferred: bool, ) -> Self { Self { monitors: RwLock::new(new_hash_map()), @@ -619,6 +663,9 @@ where pending_send_only_events: Mutex::new(Vec::new()), #[cfg(peer_storage)] our_peerstorage_encryption_key: _our_peerstorage_encryption_key, + deferred, + pending_ops: Mutex::new(VecDeque::new()), + flush_lock: Mutex::new(()), } } @@ -646,7 +693,7 @@ where ret } - /// Gets the [`LockedChannelMonitor`] for a given funding outpoint, returning an `Err` if no + /// Gets the [`LockedChannelMonitor`] for a given channel ID, returning an `Err` if no /// such [`ChannelMonitor`] is currently being monitored for. /// /// Note that the result holds a mutex over our monitor set, and should not be held @@ -662,7 +709,7 @@ where } } - /// Lists the funding outpoint and channel ID of each [`ChannelMonitor`] being monitored. + /// Lists the channel ID of each [`ChannelMonitor`] being monitored. /// /// Note that [`ChannelMonitor`]s are not removed when a channel is closed as they are always /// monitoring for on-chain state resolutions. @@ -719,7 +766,7 @@ where /// Note that we don't care about calls to [`Persist::update_persisted_channel`] where no /// [`ChannelMonitorUpdate`] was provided. /// - /// Returns an [`APIError::APIMisuseError`] if `funding_txo` does not match any currently + /// Returns an [`APIError::APIMisuseError`] if `channel_id` does not match any currently /// registered [`ChannelMonitor`]s. pub fn channel_monitor_updated( &self, channel_id: ChannelId, completed_update_id: u64, @@ -1038,7 +1085,7 @@ where &self, channel_id: ChannelId, monitor: ChannelMonitor<ChannelSigner>, ) -> Result<ChannelMonitorUpdateStatus, ()> { if !monitor.written_by_0_1_or_later() { - return chain::Watch::watch_channel(self, channel_id, monitor); + return self.watch_channel_internal(channel_id, monitor); } let logger = WithChannelMonitor::from(&self.logger, &monitor, None); @@ -1058,6 +1105,284 @@ where Ok(ChannelMonitorUpdateStatus::Completed) } + + fn watch_channel_internal( + &self, channel_id: ChannelId, monitor: ChannelMonitor<ChannelSigner>, + ) -> Result<ChannelMonitorUpdateStatus, ()> { + let logger = WithChannelMonitor::from(&self.logger, &monitor, None); + let mut monitors = self.monitors.write().unwrap(); + let entry = match monitors.entry(channel_id) { + hash_map::Entry::Occupied(_) => { + log_error!(logger, "Failed to add new channel data: channel monitor for given channel ID is already present"); + return Err(()); + }, + hash_map::Entry::Vacant(e) => e, + }; + log_trace!(logger, "Got new ChannelMonitor"); + let update_id = monitor.get_latest_update_id(); + let mut pending_monitor_updates = Vec::new(); + let persist_res = self.persister.persist_new_channel(monitor.persistence_key(), &monitor); + match persist_res { + ChannelMonitorUpdateStatus::InProgress => { + log_info!(logger, "Persistence of new ChannelMonitor in progress",); + pending_monitor_updates.push(update_id); + }, + ChannelMonitorUpdateStatus::Completed => { + log_info!(logger, "Persistence of new ChannelMonitor completed",); + }, + ChannelMonitorUpdateStatus::UnrecoverableError => { + let err_str = "ChannelMonitor[Update] persistence failed unrecoverably. This indicates we cannot continue normal operation and must shut down."; + log_error!(logger, "{}", err_str); + panic!("{}", err_str); + }, + } + if let Some(ref chain_source) = self.chain_source { + monitor.load_outputs_to_watch(chain_source, &self.logger); + } + entry.insert(MonitorHolder { + monitor, + pending_monitor_updates: Mutex::new(pending_monitor_updates), + }); + Ok(persist_res) + } + + fn update_channel_internal( + &self, channel_id: ChannelId, update: &ChannelMonitorUpdate, + ) -> ChannelMonitorUpdateStatus { + // `ChannelMonitorUpdate`'s `channel_id` is `None` prior to 0.0.121 and all channels in those + // versions are V1-established. For 0.0.121+ the `channel_id` fields is always `Some`. + debug_assert_eq!(update.channel_id.unwrap(), channel_id); + // Update the monitor that watches the channel referred to by the given outpoint. + let monitors = self.monitors.read().unwrap(); + match monitors.get(&channel_id) { + None => { + let logger = WithContext::from(&self.logger, None, Some(channel_id), None); + log_error!(logger, "Failed to update channel monitor: no such monitor registered"); + + // We should never ever trigger this from within ChannelManager. Technically a + // user could use this object with some proxying in between which makes this + // possible, but in tests and fuzzing, this should be a panic. + #[cfg(debug_assertions)] + panic!("ChannelManager generated a channel update for a channel that was not yet registered!"); + #[cfg(not(debug_assertions))] + ChannelMonitorUpdateStatus::InProgress + }, + Some(monitor_state) => { + let monitor = &monitor_state.monitor; + let logger = WithChannelMonitor::from(&self.logger, &monitor, None); + log_trace!(logger, "Updating ChannelMonitor to id {}", update.update_id,); + + // We hold a `pending_monitor_updates` lock through `update_monitor` to ensure we + // have well-ordered updates from the users' point of view. See the + // `pending_monitor_updates` docs for more. + let mut pending_monitor_updates = + monitor_state.pending_monitor_updates.lock().unwrap(); + let update_res = monitor.update_monitor( + update, + &self.broadcaster, + &self.fee_estimator, + &self.logger, + ); + + let update_id = update.update_id; + let persist_res = if update_res.is_err() { + // Even if updating the monitor returns an error, the monitor's state will + // still be changed. Therefore, we should persist the updated monitor despite the error. + // We don't want to persist a `monitor_update` which results in a failure to apply later + // while reading `channel_monitor` with updates from storage. Instead, we should persist + // the entire `channel_monitor` here. + log_warn!(logger, "Failed to update ChannelMonitor. Going ahead and persisting the entire ChannelMonitor"); + self.persister.update_persisted_channel( + monitor.persistence_key(), + None, + monitor, + ) + } else { + self.persister.update_persisted_channel( + monitor.persistence_key(), + Some(update), + monitor, + ) + }; + match persist_res { + ChannelMonitorUpdateStatus::InProgress => { + pending_monitor_updates.push(update_id); + log_debug!( + logger, + "Persistence of ChannelMonitorUpdate id {:?} in progress", + update_id, + ); + }, + ChannelMonitorUpdateStatus::Completed => { + log_debug!( + logger, + "Persistence of ChannelMonitorUpdate id {:?} completed", + update_id, + ); + }, + ChannelMonitorUpdateStatus::UnrecoverableError => { + // Take the monitors lock for writing so that we poison it and any future + // operations going forward fail immediately. + core::mem::drop(pending_monitor_updates); + core::mem::drop(monitors); + let _poison = self.monitors.write().unwrap(); + let err_str = "ChannelMonitor[Update] persistence failed unrecoverably. This indicates we cannot continue normal operation and must shut down."; + log_error!(logger, "{}", err_str); + panic!("{}", err_str); + }, + } + + // We may need to start monitoring for any alternative funding transactions. + if let Some(ref chain_source) = self.chain_source { + for (funding_outpoint, funding_script) in + update.internal_renegotiated_funding_data() + { + log_trace!( + logger, + "Registering renegotiated funding outpoint {} with the filter to monitor confirmations and spends", + funding_outpoint + ); + chain_source.register_tx(&funding_outpoint.txid, &funding_script); + chain_source.register_output(WatchedOutput { + block_hash: None, + outpoint: funding_outpoint, + script_pubkey: funding_script, + }); + } + } + + debug_assert!( + update_res.is_ok() || monitor.no_further_updates_allowed(), + "update_monitor returned Err but channel is not post-close", + ); + + // We also check update_res.is_err() as a defensive measure: an + // error should only occur on a post-close monitor (validated by + // the debug_assert above), but we defer here regardless to avoid + // returning Completed for a failed update. + if (update_res.is_err() || monitor.no_further_updates_allowed()) + && persist_res == ChannelMonitorUpdateStatus::Completed + { + // The channel is post-close (funding spend seen, lockdown, or + // holder tx signed). Return InProgress so ChannelManager freezes + // the channel until the force-close MonitorEvents are processed. + // Push a Completed event into pending_monitor_events so it gets + // picked up after the per-monitor events in the next + // release_pending_monitor_events call. + let funding_txo = monitor.get_funding_txo(); + let channel_id = monitor.channel_id(); + self.pending_monitor_events.lock().unwrap().push(( + funding_txo, + channel_id, + vec![MonitorEvent::Completed { + funding_txo, + channel_id, + monitor_update_id: monitor.get_latest_update_id(), + }], + monitor.get_counterparty_node_id(), + )); + log_debug!( + logger, + "Deferring completion of ChannelMonitorUpdate id {:?} (channel is post-close)", + update_id, + ); + ChannelMonitorUpdateStatus::InProgress + } else { + persist_res + } + }, + } + } + + /// Returns the number of pending monitor operations queued for later execution. + /// + /// When the `ChainMonitor` is constructed with `deferred` set to `true`, + /// [`chain::Watch::watch_channel`] and [`chain::Watch::update_channel`] calls are queued + /// instead of being executed immediately. Call this method to determine how many operations + /// are waiting, then pass the result to [`Self::flush`] to process them. + pub fn pending_operation_count(&self) -> usize { + self.pending_ops.lock().unwrap().len() + } + + /// Flushes the first `count` pending monitor operations that were queued while the + /// `ChainMonitor` operates in deferred mode. `count` must not exceed the number of + /// pending operations returned by [`Self::pending_operation_count`]. + /// + /// A typical usage pattern is to call [`Self::pending_operation_count`], persist the + /// [`ChannelManager`], then pass the count to this method to flush the queued operations. + /// + /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + pub fn flush(&self, count: usize, logger: &L) { + let _guard = self.flush_lock.lock().unwrap(); + if count == 0 { + return; + } + log_info!(logger, "Flushing up to {} monitor operations", count); + for _ in 0..count { + let mut queue = self.pending_ops.lock().unwrap(); + let op = match queue.pop_front() { + Some(op) => op, + None => { + debug_assert!(false, "flush count exceeded queue length"); + log_error!(logger, "flush count exceeded queue length"); + return; + }, + }; + + let (channel_id, update_id, status) = match op { + PendingMonitorOp::NewMonitor { channel_id, monitor } => { + let logger = WithChannelMonitor::from(logger, &monitor, None); + let update_id = monitor.get_latest_update_id(); + log_trace!(logger, "Flushing new monitor"); + // Hold `pending_ops` across the internal call so that + // `watch_channel` (which checks `monitors` + `pending_ops` + // atomically) cannot race with this insertion. + match self.watch_channel_internal(channel_id, monitor) { + Ok(status) => { + drop(queue); + (channel_id, update_id, status) + }, + Err(()) => { + // `watch_channel` checks both `pending_ops` and `monitors` + // for duplicates before queueing, so this is unreachable. + unreachable!(); + }, + } + }, + PendingMonitorOp::Update { channel_id, update } => { + let logger = WithContext::from(logger, None, Some(channel_id), None); + log_trace!(logger, "Flushing monitor update {}", update.update_id); + // Release `pending_ops` before the internal call so that + // concurrent `update_channel` queuing is not blocked. + drop(queue); + let update_id = update.update_id; + let status = self.update_channel_internal(channel_id, &update); + (channel_id, update_id, status) + }, + }; + + match status { + ChannelMonitorUpdateStatus::Completed => { + let logger = WithContext::from(logger, None, Some(channel_id), None); + if let Err(e) = self.channel_monitor_updated(channel_id, update_id) { + debug_assert!(false, "channel_monitor_updated failed: {:?}", e); + log_error!(logger, "channel_monitor_updated failed: {:?}", e); + } + }, + ChannelMonitorUpdateStatus::InProgress => {}, + ChannelMonitorUpdateStatus::UnrecoverableError => { + // Neither watch_channel_internal nor update_channel_internal + // return UnrecoverableError; they panic on that variant + // before it can be returned. + unreachable!(); + }, + } + } + + // A flushed monitor update may have generated new events, so assume we have + // some and wake the event processor. + self.event_notifier.notify(); + } } impl< @@ -1148,7 +1473,7 @@ where self.event_notifier.notify(); } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { let monitor_states = self.monitors.read().unwrap(); log_debug!( self.logger, @@ -1272,155 +1597,50 @@ where fn watch_channel( &self, channel_id: ChannelId, monitor: ChannelMonitor<ChannelSigner>, ) -> Result<ChannelMonitorUpdateStatus, ()> { - let logger = WithChannelMonitor::from(&self.logger, &monitor, None); - let mut monitors = self.monitors.write().unwrap(); - let entry = match monitors.entry(channel_id) { - hash_map::Entry::Occupied(_) => { - log_error!(logger, "Failed to add new channel data: channel monitor for given channel ID is already present"); - return Err(()); - }, - hash_map::Entry::Vacant(e) => e, - }; - log_trace!(logger, "Got new ChannelMonitor"); - let update_id = monitor.get_latest_update_id(); - let mut pending_monitor_updates = Vec::new(); - let persist_res = self.persister.persist_new_channel(monitor.persistence_key(), &monitor); - match persist_res { - ChannelMonitorUpdateStatus::InProgress => { - log_info!(logger, "Persistence of new ChannelMonitor in progress",); - pending_monitor_updates.push(update_id); - }, - ChannelMonitorUpdateStatus::Completed => { - log_info!(logger, "Persistence of new ChannelMonitor completed",); - }, - ChannelMonitorUpdateStatus::UnrecoverableError => { - let err_str = "ChannelMonitor[Update] persistence failed unrecoverably. This indicates we cannot continue normal operation and must shut down."; - log_error!(logger, "{}", err_str); - panic!("{}", err_str); - }, + if !self.deferred { + return self.watch_channel_internal(channel_id, monitor); } - if let Some(ref chain_source) = self.chain_source { - monitor.load_outputs_to_watch(chain_source, &self.logger); + + // Atomically check for duplicates in both the pending queue and the + // flushed monitor set. + let mut pending_ops = self.pending_ops.lock().unwrap(); + let monitors = self.monitors.read().unwrap(); + if monitors.contains_key(&channel_id) { + return Err(()); } - entry.insert(MonitorHolder { - monitor, - pending_monitor_updates: Mutex::new(pending_monitor_updates), + let already_pending = pending_ops.iter().any(|op| match op { + PendingMonitorOp::NewMonitor { channel_id: id, .. } => *id == channel_id, + _ => false, }); - Ok(persist_res) + if already_pending { + return Err(()); + } + pending_ops.push_back(PendingMonitorOp::NewMonitor { channel_id, monitor }); + Ok(ChannelMonitorUpdateStatus::InProgress) } fn update_channel( &self, channel_id: ChannelId, update: &ChannelMonitorUpdate, ) -> ChannelMonitorUpdateStatus { - // `ChannelMonitorUpdate`'s `channel_id` is `None` prior to 0.0.121 and all channels in those - // versions are V1-established. For 0.0.121+ the `channel_id` fields is always `Some`. - debug_assert_eq!(update.channel_id.unwrap(), channel_id); - // Update the monitor that watches the channel referred to by the given outpoint. - let monitors = self.monitors.read().unwrap(); - match monitors.get(&channel_id) { - None => { - let logger = WithContext::from(&self.logger, None, Some(channel_id), None); - log_error!(logger, "Failed to update channel monitor: no such monitor registered"); - - // We should never ever trigger this from within ChannelManager. Technically a - // user could use this object with some proxying in between which makes this - // possible, but in tests and fuzzing, this should be a panic. - #[cfg(debug_assertions)] - panic!("ChannelManager generated a channel update for a channel that was not yet registered!"); - #[cfg(not(debug_assertions))] - ChannelMonitorUpdateStatus::InProgress - }, - Some(monitor_state) => { - let monitor = &monitor_state.monitor; - let logger = WithChannelMonitor::from(&self.logger, &monitor, None); - log_trace!(logger, "Updating ChannelMonitor to id {}", update.update_id,); - - // We hold a `pending_monitor_updates` lock through `update_monitor` to ensure we - // have well-ordered updates from the users' point of view. See the - // `pending_monitor_updates` docs for more. - let mut pending_monitor_updates = - monitor_state.pending_monitor_updates.lock().unwrap(); - let update_res = monitor.update_monitor( - update, - &self.broadcaster, - &self.fee_estimator, - &self.logger, - ); - - let update_id = update.update_id; - let persist_res = if update_res.is_err() { - // Even if updating the monitor returns an error, the monitor's state will - // still be changed. Therefore, we should persist the updated monitor despite the error. - // We don't want to persist a `monitor_update` which results in a failure to apply later - // while reading `channel_monitor` with updates from storage. Instead, we should persist - // the entire `channel_monitor` here. - log_warn!(logger, "Failed to update ChannelMonitor. Going ahead and persisting the entire ChannelMonitor"); - self.persister.update_persisted_channel( - monitor.persistence_key(), - None, - monitor, - ) - } else { - self.persister.update_persisted_channel( - monitor.persistence_key(), - Some(update), - monitor, - ) - }; - match persist_res { - ChannelMonitorUpdateStatus::InProgress => { - pending_monitor_updates.push(update_id); - log_debug!( - logger, - "Persistence of ChannelMonitorUpdate id {:?} in progress", - update_id, - ); - }, - ChannelMonitorUpdateStatus::Completed => { - log_debug!( - logger, - "Persistence of ChannelMonitorUpdate id {:?} completed", - update_id, - ); - }, - ChannelMonitorUpdateStatus::UnrecoverableError => { - // Take the monitors lock for writing so that we poison it and any future - // operations going forward fail immediately. - core::mem::drop(pending_monitor_updates); - core::mem::drop(monitors); - let _poison = self.monitors.write().unwrap(); - let err_str = "ChannelMonitor[Update] persistence failed unrecoverably. This indicates we cannot continue normal operation and must shut down."; - log_error!(logger, "{}", err_str); - panic!("{}", err_str); - }, - } - - // We may need to start monitoring for any alternative funding transactions. - if let Some(ref chain_source) = self.chain_source { - for (funding_outpoint, funding_script) in - update.internal_renegotiated_funding_data() - { - log_trace!( - logger, - "Registering renegotiated funding outpoint {} with the filter to monitor confirmations and spends", - funding_outpoint - ); - chain_source.register_tx(&funding_outpoint.txid, &funding_script); - chain_source.register_output(WatchedOutput { - block_hash: None, - outpoint: funding_outpoint, - script_pubkey: funding_script, - }); - } - } + if !self.deferred { + return self.update_channel_internal(channel_id, update); + } - if update_res.is_err() { - ChannelMonitorUpdateStatus::InProgress - } else { - persist_res - } + let mut pending_ops = self.pending_ops.lock().unwrap(); + debug_assert!( + { + let monitors = self.monitors.read().unwrap(); + let in_monitors = monitors.contains_key(&channel_id); + let in_pending = pending_ops.iter().any(|op| match op { + PendingMonitorOp::NewMonitor { channel_id: id, .. } => *id == channel_id, + _ => false, + }); + in_monitors || in_pending }, - } + "ChannelManager generated a channel update for a channel that was not yet registered!" + ); + pending_ops.push_back(PendingMonitorOp::Update { channel_id, update: update.clone() }); + ChannelMonitorUpdateStatus::InProgress } fn release_pending_monitor_events( @@ -1429,8 +1649,9 @@ where for (channel_id, update_id) in self.persister.get_and_clear_completed_updates() { let _ = self.channel_monitor_updated(channel_id, update_id); } - let mut pending_monitor_events = self.pending_monitor_events.lock().unwrap().split_off(0); - for monitor_state in self.monitors.read().unwrap().values() { + let monitors = self.monitors.read().unwrap(); + let mut pending_monitor_events = Vec::new(); + for monitor_state in monitors.values() { let monitor_events = monitor_state.monitor.get_and_clear_pending_monitor_events(); if monitor_events.len() > 0 { let monitor_funding_txo = monitor_state.monitor.get_funding_txo(); @@ -1444,6 +1665,10 @@ where )); } } + // Drain pending_monitor_events (which includes deferred post-close + // completions) after per-monitor events so that force-close + // MonitorEvents are processed by ChannelManager first. + pending_monitor_events.extend(self.pending_monitor_events.lock().unwrap().split_off(0)); pending_monitor_events } } @@ -1550,12 +1775,22 @@ where #[cfg(test)] mod tests { - use crate::chain::channelmonitor::ANTI_REORG_DELAY; + use super::ChainMonitor; + use crate::chain::channelmonitor::{ChannelMonitorUpdate, ANTI_REORG_DELAY}; use crate::chain::{ChannelMonitorUpdateStatus, Watch}; use crate::events::{ClosureReason, Event}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; + use crate::ln::types::ChannelId; + use crate::sign::NodeSigner; + use crate::util::dyn_signer::DynSigner; + use crate::util::test_channel_signer::TestChannelSigner; + use crate::util::test_utils::{ + TestBroadcaster, TestChainSource, TestFeeEstimator, TestKeysInterface, TestLogger, + TestPersister, + }; use crate::{expect_payment_path_successful, get_event_msg}; + use bitcoin::Network; const CHAINSYNC_MONITOR_PARTITION_FACTOR: u32 = 5; @@ -1813,4 +2048,171 @@ mod tests { }) .is_err()); } + + /// Concrete `ChainMonitor` type wired to the standard test utilities in deferred mode. + type TestDeferredChainMonitor<'a> = ChainMonitor< + TestChannelSigner, + &'a TestChainSource, + &'a TestBroadcaster, + &'a TestFeeEstimator, + &'a TestLogger, + &'a TestPersister, + &'a TestKeysInterface, + >; + + /// Creates a minimal `ChannelMonitorUpdate` with no actual update steps. + fn dummy_update(update_id: u64, channel_id: ChannelId) -> ChannelMonitorUpdate { + ChannelMonitorUpdate { updates: vec![], update_id, channel_id: Some(channel_id) } + } + + fn create_deferred_chain_monitor<'a>( + chain_source: &'a TestChainSource, broadcaster: &'a TestBroadcaster, + logger: &'a TestLogger, fee_est: &'a TestFeeEstimator, persister: &'a TestPersister, + keys: &'a TestKeysInterface, + ) -> TestDeferredChainMonitor<'a> { + ChainMonitor::new( + Some(chain_source), + broadcaster, + logger, + fee_est, + persister, + keys, + keys.get_peer_storage_key(), + true, + ) + } + + /// Tests queueing and flushing of both `watch_channel` and `update_channel` operations + /// when `ChainMonitor` is in deferred mode, verifying that operations flow through to + /// `Persist` and that `channel_monitor_updated` is called on `Completed` status. + #[test] + fn test_queue_and_flush() { + let broadcaster = TestBroadcaster::new(Network::Testnet); + let fee_est = TestFeeEstimator::new(253); + let logger = TestLogger::new(); + let persister = TestPersister::new(); + let chain_source = TestChainSource::new(Network::Testnet); + let keys = TestKeysInterface::new(&[0; 32], Network::Testnet); + let deferred = create_deferred_chain_monitor( + &chain_source, + &broadcaster, + &logger, + &fee_est, + &persister, + &keys, + ); + + // Queue starts empty. + assert_eq!(deferred.pending_operation_count(), 0); + + // Queue a watch_channel, verifying InProgress status. + let chan = ChannelId::from_bytes([1u8; 32]); + let monitor = crate::chain::channelmonitor::dummy_monitor(chan, |keys| { + TestChannelSigner::new(DynSigner::new(keys)) + }); + let status = Watch::watch_channel(&deferred, chan, monitor); + assert_eq!(status, Ok(ChannelMonitorUpdateStatus::InProgress)); + assert_eq!(deferred.pending_operation_count(), 1); + + // Nothing persisted yet — operations are only queued. + assert!(persister.new_channel_persistences.lock().unwrap().is_empty()); + + // Queue two updates after the watch. Update IDs must be sequential (starting + // from 1 since the initial monitor has update_id 0). + assert_eq!( + Watch::update_channel(&deferred, chan, &dummy_update(1, chan)), + ChannelMonitorUpdateStatus::InProgress + ); + assert_eq!( + Watch::update_channel(&deferred, chan, &dummy_update(2, chan)), + ChannelMonitorUpdateStatus::InProgress + ); + assert_eq!(deferred.pending_operation_count(), 3); + + // Flush 2 of 3: persist_new_channel returns Completed (triggers + // channel_monitor_updated), update_persisted_channel returns InProgress (does not). + persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + deferred.flush(2, &&logger); + + assert_eq!(deferred.pending_operation_count(), 1); + + // persist_new_channel was called for the watch. + assert_eq!(persister.new_channel_persistences.lock().unwrap().len(), 1); + + // Because persist_new_channel returned Completed, channel_monitor_updated was called, + // so update_id 0 should no longer be pending. + let pending = deferred.list_pending_monitor_updates(); + #[cfg(not(c_bindings))] + let pending_for_chan = pending.get(&chan).unwrap(); + #[cfg(c_bindings)] + let pending_for_chan = &pending.iter().find(|(chan_id, _)| *chan_id == chan).unwrap().1; + assert!(!pending_for_chan.contains(&0)); + + // update_persisted_channel was called for update_id 1, and because it returned + // InProgress, update_id 1 remains pending. + let monitor_name = deferred.get_monitor(chan).unwrap().persistence_key(); + assert!(persister + .offchain_monitor_updates + .lock() + .unwrap() + .get(&monitor_name) + .unwrap() + .contains(&1)); + assert!(pending_for_chan.contains(&1)); + + // Flush remaining: update_persisted_channel returns Completed (default), triggers + // channel_monitor_updated. + deferred.flush(1, &&logger); + assert_eq!(deferred.pending_operation_count(), 0); + + // update_persisted_channel was called for update_id 2. + assert!(persister + .offchain_monitor_updates + .lock() + .unwrap() + .get(&monitor_name) + .unwrap() + .contains(&2)); + + // update_id 1 is still pending from the InProgress earlier, but update_id 2 was + // completed in this flush so it is no longer pending. + let pending = deferred.list_pending_monitor_updates(); + #[cfg(not(c_bindings))] + let pending_for_chan = pending.get(&chan).unwrap(); + #[cfg(c_bindings)] + let pending_for_chan = &pending.iter().find(|(chan_id, _)| *chan_id == chan).unwrap().1; + assert!(pending_for_chan.contains(&1)); + assert!(!pending_for_chan.contains(&2)); + + // Flushing an empty queue is a no-op. + let persist_count_before = persister.new_channel_persistences.lock().unwrap().len(); + deferred.flush(0, &&logger); + assert_eq!(persister.new_channel_persistences.lock().unwrap().len(), persist_count_before); + } + + /// Tests that `ChainMonitor` in deferred mode properly defers `watch_channel` and + /// `update_channel` operations, verifying correctness through a complete channel open + /// and payment flow. Operations are auto-flushed via the `TestChainMonitor` + /// `release_pending_monitor_events` helper. + #[test] + fn test_deferred_monitor_payment() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs_deferred(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let chain_monitor_a = &nodes[0].chain_monitor.chain_monitor; + let chain_monitor_b = &nodes[1].chain_monitor.chain_monitor; + + create_announced_chan_between_nodes(&nodes, 0, 1); + + let (preimage, _hash, ..) = route_payment(&nodes[0], &[&nodes[1]], 10_000); + claim_payment(&nodes[0], &[&nodes[1]], preimage); + + assert_eq!(chain_monitor_a.list_monitors().len(), 1); + assert_eq!(chain_monitor_b.list_monitors().len(), 1); + assert_eq!(chain_monitor_a.pending_operation_count(), 0); + assert_eq!(chain_monitor_b.pending_operation_count(), 0); + } } diff --git a/lightning/src/chain/channelmonitor.rs b/lightning/src/chain/channelmonitor.rs index 37351460634..24c1031f0c3 100644 --- a/lightning/src/chain/channelmonitor.rs +++ b/lightning/src/chain/channelmonitor.rs @@ -42,9 +42,9 @@ use crate::chain::package::{ HolderHTLCOutput, PackageSolvingData, PackageTemplate, RevokedHTLCOutput, RevokedOutput, }; use crate::chain::transaction::{OutPoint, TransactionData}; -use crate::chain::{BestBlock, WatchedOutput}; +use crate::chain::{BlockLocator, WatchedOutput}; use crate::events::bump_transaction::{AnchorDescriptor, BumpTransactionEvent}; -use crate::events::{ClosureReason, Event, EventHandler, ReplayEvent}; +use crate::events::{ClosureReason, Event, EventHandler, FundingInfo, ReplayEvent}; use crate::ln::chan_utils::{ self, ChannelTransactionParameters, CommitmentTransaction, CounterpartyCommitmentSecrets, HTLCClaim, HTLCOutputInCommitment, HolderCommitmentTransaction, @@ -55,6 +55,7 @@ use crate::ln::channel_keys::{ RevocationKey, }; use crate::ln::channelmanager::{HTLCSource, PaymentClaimDetails, SentHTLCId}; +use crate::ln::funding::FundingContribution; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; use crate::sign::{ @@ -253,11 +254,11 @@ pub struct HTLCUpdate { pub(crate) payment_hash: PaymentHash, pub(crate) payment_preimage: Option<PaymentPreimage>, pub(crate) source: HTLCSource, - pub(crate) htlc_value_satoshis: Option<u64>, + pub(crate) htlc_value_satoshis: u64, } -impl_writeable_tlv_based!(HTLCUpdate, { +impl_ser_tlv_based!(HTLCUpdate, { (0, payment_hash, required), - (1, htlc_value_satoshis, option), + (1, htlc_value_satoshis, required), (2, source, required), (4, payment_preimage, option), }); @@ -344,7 +345,7 @@ struct HolderSignedTx { } // Any changes made here must also reflect in `write_legacy_holder_commitment_data`. -impl_writeable_tlv_based!(HolderSignedTx, { +impl_ser_tlv_based!(HolderSignedTx, { (0, txid, required), (1, to_self_value_sat, required), // Added in 0.0.100, required in 0.2. (2, revocation_key, required), @@ -505,7 +506,7 @@ impl OnchainEventEntry { conf_threshold } - fn has_reached_confirmation_threshold(&self, best_block: &BestBlock) -> bool { + fn has_reached_confirmation_threshold(&self, best_block: &BlockLocator) -> bool { best_block.height >= self.confirmation_threshold() } } @@ -528,7 +529,7 @@ enum OnchainEvent { HTLCUpdate { source: HTLCSource, payment_hash: PaymentHash, - htlc_value_satoshis: Option<u64>, + htlc_value_satoshis: u64, /// None in the second case, above, ie when there is no relevant output in the commitment /// transaction which appeared on chain. commitment_tx_output_idx: Option<u32>, @@ -613,7 +614,7 @@ impl MaybeReadable for OnchainEventEntry { impl_writeable_tlv_based_enum_upgradable!(OnchainEvent, (0, HTLCUpdate) => { (0, source, required), - (1, htlc_value_satoshis, option), + (1, htlc_value_satoshis, required), (2, payment_hash, required), (3, commitment_tx_output_idx, option), }, @@ -688,6 +689,7 @@ pub(crate) enum ChannelMonitorUpdateStep { channel_parameters: ChannelTransactionParameters, holder_commitment_tx: HolderCommitmentTransaction, counterparty_commitment_tx: CommitmentTransaction, + funding_contribution: Option<FundingContribution>, }, RenegotiatedFundingLocked { funding_txid: Txid, @@ -773,6 +775,7 @@ impl_writeable_tlv_based_enum_upgradable!(ChannelMonitorUpdateStep, (1, channel_parameters, (required: ReadableArgs, None)), (3, holder_commitment_tx, required), (5, counterparty_commitment_tx, required), + (7, funding_contribution, option), }, (12, RenegotiatedFundingLocked) => { (1, funding_txid, required), @@ -1058,15 +1061,15 @@ impl Readable for IrrevocablyResolvedHTLC { /// You MUST ensure that no ChannelMonitors for a given channel anywhere contain out-of-date /// information and are actively monitoring the chain. /// -/// Like the [`ChannelManager`], deserialization is implemented for `(BlockHash, ChannelMonitor)`, -/// providing you with the last block hash which was connected before shutting down. You must begin -/// syncing the chain from that point, disconnecting and connecting blocks as required to get to -/// the best chain on startup. Note that all [`ChannelMonitor`]s passed to a [`ChainMonitor`] must +/// Like the [`ChannelManager`], deserialization is implemented for `(BlockLocator, ChannelMonitor)`, +/// providing a locator for the best chain as of the last write before shutting down. You must +/// begin syncing the chain from that locator, disconnecting and connecting blocks as required to +/// get to the best chain on startup. Note that all [`ChannelMonitor`]s passed to a [`ChainMonitor`] must /// by synced as of the same block, so syncing must happen prior to [`ChainMonitor`] /// initialization. /// /// For those loading potentially-ancient [`ChannelMonitor`]s, deserialization is also implemented -/// for `Option<(BlockHash, ChannelMonitor)>`. LDK can no longer deserialize a [`ChannelMonitor`] +/// for `Option<(BlockLocator, ChannelMonitor)>`. LDK can no longer deserialize a [`ChannelMonitor`] /// that was first created in LDK prior to 0.0.110 and last updated prior to LDK 0.0.119. In such /// cases, the `Option<(..)>` deserialization option may return `Ok(None)` rather than failing to /// deserialize, allowing you to differentiate between the two cases. @@ -1101,7 +1104,7 @@ impl CommitmentHTLCData { } } -impl_writeable_tlv_based!(CommitmentHTLCData, { +impl_ser_tlv_based!(CommitmentHTLCData, { (1, nondust_htlc_sources, required_vec), (3, dust_htlcs, required_vec), }); @@ -1166,6 +1169,9 @@ struct FundingScope { // transaction for which we have deleted claim information on some watchtowers. current_holder_commitment_tx: HolderCommitmentTransaction, prev_holder_commitment_tx: Option<HolderCommitmentTransaction>, + + /// Our funding contribution when we negotiated the corresponding funding transaction. + contribution: Option<FundingContribution>, } impl FundingScope { @@ -1185,15 +1191,24 @@ impl FundingScope { fn channel_type_features(&self) -> &ChannelTypeFeatures { &self.channel_parameters.channel_type_features } + + fn contributed_inputs(&self) -> impl Iterator<Item = bitcoin::OutPoint> + '_ { + self.contribution.iter().flat_map(|contribution| contribution.contributed_inputs()) + } + + fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.contribution.iter().flat_map(|contribution| contribution.contributed_outputs()) + } } -impl_writeable_tlv_based!(FundingScope, { +impl_ser_tlv_based!(FundingScope, { (1, channel_parameters, (required: ReadableArgs, None)), (3, current_counterparty_commitment_txid, required), (5, prev_counterparty_commitment_txid, option), (7, current_holder_commitment_tx, required), (9, prev_holder_commitment_tx, option), (11, counterparty_claimable_outpoints, required), + (13, contribution, option), }); #[derive(Clone, PartialEq)] @@ -1354,7 +1369,7 @@ pub(crate) struct ChannelMonitorImpl<Signer: EcdsaChannelSigner> { // (we do *not*, however, update them in update_monitor to ensure any local user copies keep // their best_block from its state and not based on updated copies that didn't run through // the full block_connected). - best_block: BestBlock, + best_block: BlockLocator, /// The node_id of our counterparty counterparty_node_id: PublicKey, @@ -1378,8 +1393,8 @@ pub(crate) struct ChannelMonitorImpl<Signer: EcdsaChannelSigner> { /// In-memory only HTLC ids used to track upstream HTLCs that have been failed backwards due to /// a downstream channel force-close remaining unconfirmed by the time the upstream timeout /// expires. This is used to tell us we already generated an event to fail this HTLC back - /// during a previous block scan. - failed_back_htlc_ids: HashSet<SentHTLCId>, + /// during a previous block scan. Not serialized. + pub(crate) failed_back_htlc_ids: HashSet<SentHTLCId>, // The auxiliary HTLC data associated with a holder commitment transaction. This includes // non-dust HTLC sources, along with dust HTLCs and their sources. Note that this assumes any @@ -1755,6 +1770,8 @@ pub(crate) fn write_chanmon_internal<Signer: EcdsaChannelSigner, W: Writer>( (34, channel_monitor.alternative_funding_confirmed, option), (35, channel_monitor.is_manual_broadcast, required), (37, channel_monitor.funding_seen_onchain, required), + (39, channel_monitor.best_block.previous_blocks, required), + (41, channel_monitor.funding.contribution, option), }); Ok(()) @@ -1857,7 +1874,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> { on_counterparty_tx_csv: u16, destination_script: &Script, channel_parameters: &ChannelTransactionParameters, holder_pays_commitment_tx_fee: bool, commitment_transaction_number_obscure_factor: u64, - initial_holder_commitment_tx: HolderCommitmentTransaction, best_block: BestBlock, + initial_holder_commitment_tx: HolderCommitmentTransaction, best_block: BlockLocator, counterparty_node_id: PublicKey, channel_id: ChannelId, is_manual_broadcast: bool, ) -> ChannelMonitor<Signer> { @@ -1904,6 +1921,8 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> { current_holder_commitment_tx: initial_holder_commitment_tx, prev_holder_commitment_tx: None, + + contribution: None, }, pending_funding: vec![], @@ -2378,7 +2397,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> { /// Determines if the disconnected block contained any transactions of interest and updates /// appropriately. pub fn blocks_disconnected<B: BroadcasterInterface, F: FeeEstimator, L: Logger>( - &self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &L, + &self, fork_point: BlockLocator, broadcaster: B, fee_estimator: F, logger: &L, ) { let mut inner = self.inner.lock().unwrap(); let logger = WithChannelMonitor::from_impl(logger, &*inner, None); @@ -2471,7 +2490,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> { /// Gets the latest best block which was connected either via the [`chain::Listen`] or /// [`chain::Confirm`] interfaces. - pub fn current_best_block(&self) -> BestBlock { + pub fn current_best_block(&self) -> BlockLocator { self.inner.lock().unwrap().best_block.clone() } @@ -2669,7 +2688,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { debug_assert!(htlc_spend_tx_opt.is_none()); htlc_spend_tx_opt = event.transaction.as_ref(); debug_assert!(holder_timeout_spend_pending.is_none()); - debug_assert_eq!(htlc_value_satoshis.unwrap(), htlc.amount_msat / 1000); + debug_assert_eq!(htlc_value_satoshis, htlc.amount_msat / 1000); holder_timeout_spend_pending = Some(event.confirmation_threshold()); }, OnchainEvent::HTLCSpendConfirmation { commitment_tx_output_idx, preimage, .. } @@ -2795,6 +2814,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { let outbound_payment = match source { None => panic!("Outbound HTLCs should have a source"), Some(&HTLCSource::PreviousHopData(_)) => false, + Some(&HTLCSource::TrampolineForward { .. }) => false, Some(&HTLCSource::OutboundRoute { .. }) => true, }; return Some(Balance::MaybeTimeoutClaimableHTLC { @@ -3007,6 +3027,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitor<Signer> { let outbound_payment = match source { None => panic!("Outbound HTLCs should have a source"), Some(HTLCSource::PreviousHopData(_)) => false, + Some(HTLCSource::TrampolineForward { .. }) => false, Some(HTLCSource::OutboundRoute { .. }) => true, }; if outbound_payment { @@ -3322,7 +3343,7 @@ macro_rules! fail_unbroadcast_htlcs { event: OnchainEvent::HTLCUpdate { source: (**source).clone(), payment_hash: htlc.payment_hash.clone(), - htlc_value_satoshis: Some(htlc.amount_msat / 1000), + htlc_value_satoshis: htlc.amount_msat / 1000, commitment_tx_output_idx: None, }, }; @@ -3956,6 +3977,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { &mut self, logger: &WithContext<L>, channel_parameters: &ChannelTransactionParameters, alternative_holder_commitment_tx: &HolderCommitmentTransaction, alternative_counterparty_commitment_tx: &CommitmentTransaction, + funding_contribution: &Option<FundingContribution>, ) -> Result<(), ()> { let alternative_counterparty_commitment_txid = alternative_counterparty_commitment_tx.trust().txid(); @@ -4022,6 +4044,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { counterparty_claimable_outpoints, current_holder_commitment_tx: alternative_holder_commitment_tx.clone(), prev_holder_commitment_tx: None, + contribution: funding_contribution.clone(), }; let alternative_funding_outpoint = alternative_funding.funding_outpoint(); @@ -4039,9 +4062,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { } if let Some(parent_funding_txid) = channel_parameters.splice_parent_funding_txid.as_ref() { - // Only one splice can be negotiated at a time after we've exchanged `channel_ready` - // (implying our funding is confirmed) that spends our currently locked funding. - if !self.pending_funding.is_empty() { + // Multiple RBF candidates for the same splice are allowed (they share the same + // parent funding txid). A new splice with a different parent while one is pending + // is not allowed. This also ensures a dual-funded channel has exchanged + // `channel_ready` (implying funding is confirmed) before allowing a splice, + // since unconfirmed initial funding has no splice parent. + let has_different_parent = self.pending_funding.iter().any(|funding| { + funding.channel_parameters.splice_parent_funding_txid.as_ref() + != Some(parent_funding_txid) + }); + if has_different_parent { log_error!( logger, "Negotiated splice while channel is pending channel_ready/splice_locked" @@ -4071,6 +4101,29 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { Ok(()) } + fn queue_discard_funding_event( + &mut self, discarded_funding: impl Iterator<Item = FundingScope>, + ) { + for funding in discarded_funding { + if let Some(contribution) = funding.contribution { + if let Some((inputs, outputs)) = contribution.into_unique_contributions( + self.funding.contributed_inputs(), + self.funding.contributed_outputs(), + ) { + self.pending_events.push(Event::DiscardFunding { + channel_id: self.channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }); + } + } else { + self.pending_events.push(Event::DiscardFunding { + channel_id: self.channel_id, + funding_info: FundingInfo::OutPoint { outpoint: funding.funding_outpoint() }, + }); + } + } + } + fn promote_funding(&mut self, new_funding_txid: Txid) -> Result<(), ()> { let prev_funding_txid = self.funding.funding_txid(); @@ -4101,18 +4154,20 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { let no_further_updates_allowed = self.no_further_updates_allowed(); // The swap above places the previous `FundingScope` into `pending_funding`. - for funding in self.pending_funding.drain(..) { - let funding_txid = funding.funding_txid(); - self.outputs_to_watch.remove(&funding_txid); - if no_further_updates_allowed && funding_txid != prev_funding_txid { - self.pending_events.push(Event::DiscardFunding { - channel_id: self.channel_id, - funding_info: crate::events::FundingInfo::OutPoint { - outpoint: funding.funding_outpoint(), - }, - }); - } + for funding in &self.pending_funding { + self.outputs_to_watch.remove(&funding.funding_txid()); } + let mut discarded_funding = Vec::new(); + mem::swap(&mut self.pending_funding, &mut discarded_funding); + let discarded_funding = discarded_funding + .into_iter() + // The previous funding is filtered out since it was already locked, so nothing needs to + // be discarded. + .filter(|funding| { + no_further_updates_allowed && funding.funding_txid() != prev_funding_txid + }); + self.queue_discard_funding_event(discarded_funding); + if let Some((alternative_funding_txid, _)) = self.alternative_funding_confirmed.take() { // In exceedingly rare cases, it's possible there was a reorg that caused a potential funding to // be locked in that this `ChannelMonitor` has not yet seen. Thus, we avoid a runtime assertion @@ -4229,11 +4284,13 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { }, ChannelMonitorUpdateStep::RenegotiatedFunding { channel_parameters, holder_commitment_tx, counterparty_commitment_tx, + funding_contribution, } => { log_trace!(logger, "Updating ChannelMonitor with alternative holder and counterparty commitment transactions for funding txid {}", channel_parameters.funding_outpoint.unwrap().txid); if let Err(_) = self.renegotiated_funding( logger, channel_parameters, holder_commitment_tx, counterparty_commitment_tx, + funding_contribution, ) { ret = Err(()); } @@ -4290,6 +4347,55 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { self.latest_update_id = updates.update_id; + // If a counterparty commitment update was applied while the funding output has already + // been spent on-chain, fail back the outbound HTLCs from the update. This handles the + // race where a monitor update is dispatched before the channel force-closes but only + // applied after the commitment transaction confirms. + for update in updates.updates.iter() { + match update { + ChannelMonitorUpdateStep::LatestCounterpartyCommitmentTXInfo { + htlc_outputs, .. + } => { + // Only outbound HTLCs have a source; inbound ones are `None` + // and skipped by the `filter_map`. + self.fail_htlcs_from_update_after_funding_spend( + htlc_outputs.iter().filter_map(|(htlc, source)| { + source.as_ref().map(|s| (&**s, htlc.payment_hash, htlc.amount_msat)) + }), + logger, + ); + }, + ChannelMonitorUpdateStep::LatestCounterpartyCommitment { + commitment_txs, htlc_data, + } => { + // On a counterparty commitment, `offered=false` means offered by + // us (outbound). `nondust_htlc_sources` contains sources only for + // these outbound nondust HTLCs, matching the filter order. + debug_assert_eq!( + commitment_txs[0].nondust_htlcs().iter() + .filter(|htlc| !htlc.offered).count(), + htlc_data.nondust_htlc_sources.len(), + ); + let nondust = commitment_txs[0] + .nondust_htlcs() + .iter() + .filter(|htlc| !htlc.offered) + .zip(htlc_data.nondust_htlc_sources.iter()) + .map(|(htlc, source)| (source, htlc.payment_hash, htlc.amount_msat)); + // Only outbound dust HTLCs have a source; inbound ones are `None` + // and skipped by the `filter_map`. + let dust = htlc_data.dust_htlcs.iter().filter_map(|(htlc, source)| { + source.as_ref().map(|s| (s, htlc.payment_hash, htlc.amount_msat)) + }); + self.fail_htlcs_from_update_after_funding_spend( + nondust.chain(dust), + logger, + ); + }, + _ => {}, + } + } + // Refuse updates after we've detected a spend onchain (or if the channel was otherwise // closed), but only if the update isn't the kind of update we expect to see after channel // closure. @@ -4336,6 +4442,121 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { self.funding_spend_seen || self.lockdown_from_offchain || self.holder_tx_signed } + /// Given outbound HTLCs from a counterparty commitment update, checks if the funding output + /// has been spent on-chain. If so, creates `OnchainEvent::HTLCUpdate` entries to fail back + /// HTLCs that weren't already known to the monitor. + /// + /// This handles the race where a `ChannelMonitorUpdate` with a new counterparty commitment + /// is dispatched (e.g., via deferred writes) before the channel force-closes, but only + /// applied to the in-memory monitor after the commitment transaction has already confirmed. + /// + /// Only truly new HTLCs (not present in any previously-known commitment) need to be failed + /// here. HTLCs that were already tracked by the monitor will be handled by the existing + /// `fail_unbroadcast_htlcs` logic when the spending transaction confirms. + fn fail_htlcs_from_update_after_funding_spend<'a, L: Logger>( + &mut self, htlcs: impl Iterator<Item = (&'a HTLCSource, PaymentHash, u64)>, + logger: &WithContext<L>, + ) { + let pending_spend_entry = self + .onchain_events_awaiting_threshold_conf + .iter() + .find(|event| matches!(event.event, OnchainEvent::FundingSpendConfirmation { .. })) + .map(|entry| (entry.txid, entry.transaction.clone(), entry.height, entry.block_hash)); + if self.funding_spend_confirmed.is_none() && pending_spend_entry.is_none() { + return; + } + + // Check HTLC sources against all previously-known commitments to find truly new + // ones. After the update has been applied, `prev_counterparty_commitment_txid` holds + // what was `current` before this update, so it represents the already-known + // counterparty state. HTLCs already present in any of these will be handled by + // `fail_unbroadcast_htlcs` when the spending transaction confirms. + let is_source_known = |source: &HTLCSource| { + if let Some(ref txid) = self.funding.prev_counterparty_commitment_txid { + if let Some(htlc_list) = self.funding.counterparty_claimable_outpoints.get(txid) { + if htlc_list.iter().any(|(_, s)| s.as_ref().map(|s| s.as_ref()) == Some(source)) + { + return true; + } + } + } + // Note that we don't care about the case where a counterparty sent us a fresh local commitment transaction + // post-closure (with the `ChannelManager` still operating the channel). First of all we only care about + // resolving outbound HTLCs, which fundamentally have to be initiated by us. However we also don't mind + // looking at the current holder commitment transaction's HTLCs as any fresh outbound HTLCs will have to + // first come in a locally-initiated update to the counterparty's commitment transaction which we can, by + // refusing to apply the update, prevent the counterparty from ever seeing (as no messages can be sent until + // the monitor is updated). Thus, the HTLCs we care about can never appear in the holder commitment + // transaction. + if holder_commitment_htlcs!(self, CURRENT_WITH_SOURCES).any(|(_, s)| s == Some(source)) + { + return true; + } + if let Some(mut iter) = holder_commitment_htlcs!(self, PREV_WITH_SOURCES) { + if iter.any(|(_, s)| s == Some(source)) { + return true; + } + } + false + }; + for (source, payment_hash, amount_msat) in htlcs { + if is_source_known(source) { + continue; + } + if self.counterparty_fulfilled_htlcs.get(&SentHTLCId::from_source(source)).is_some() { + continue; + } + let htlc_value_satoshis = amount_msat / 1000; + let logger = WithContext::from(logger, None, None, Some(payment_hash)); + // Defensively mark the HTLC as failed back so the expiry-based failure + // path in `block_connected` doesn't generate a duplicate `HTLCUpdate` + // event for the same source. + self.failed_back_htlc_ids.insert(SentHTLCId::from_source(source)); + if let Some(confirmed_txid) = self.funding_spend_confirmed { + // Funding spend already confirmed past ANTI_REORG_DELAY: resolve immediately. + log_trace!( + logger, + "Failing HTLC from late counterparty commitment update immediately \ + (funding spend already confirmed)" + ); + self.pending_monitor_events.push(MonitorEvent::HTLCEvent(HTLCUpdate { + payment_hash, + payment_preimage: None, + source: source.clone(), + htlc_value_satoshis, + })); + self.htlcs_resolved_on_chain.push(IrrevocablyResolvedHTLC { + commitment_tx_output_idx: None, + resolving_txid: Some(confirmed_txid), + resolving_tx: None, + payment_preimage: None, + }); + } else { + // Funding spend still awaiting ANTI_REORG_DELAY: queue the failure. + let (txid, transaction, height, block_hash) = pending_spend_entry.clone().unwrap(); + let entry = OnchainEventEntry { + txid, + transaction, + height, + block_hash, + event: OnchainEvent::HTLCUpdate { + source: source.clone(), + payment_hash, + htlc_value_satoshis, + commitment_tx_output_idx: None, + }, + }; + log_trace!( + logger, + "Failing HTLC from late counterparty commitment update, \ + waiting for confirmation (at height {})", + entry.confirmation_threshold() + ); + self.onchain_events_awaiting_threshold_conf.push(entry); + } + } + } + fn get_latest_update_id(&self) -> u64 { self.latest_update_id } @@ -4693,13 +4914,16 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { } else if let Some(per_commitment_claimable_data) = per_commitment_option { assert_eq!(funding_spent.funding_txid(), funding_txid_spent); - // While this isn't useful yet, there is a potential race where if a counterparty - // revokes a state at the same time as the commitment transaction for that state is - // confirmed, and the watchtower receives the block before the user, the user could - // upload a new ChannelMonitor with the revocation secret but the watchtower has - // already processed the block, resulting in the counterparty_commitment_txn_on_chain entry - // not being generated by the above conditional. Thus, to be safe, we go ahead and - // insert it here. + // Track that this counterparty commitment tx appeared on-chain. This is + // used by `provide_payment_preimage` to look up the commitment number + // when a preimage arrives after the commitment tx is already confirmed. + // It also handles a race where a counterparty revokes a state at the + // same time as the commitment transaction for that state is confirmed, + // and the watchtower receives the block before the user. The user could + // upload a new ChannelMonitor with the revocation secret but the + // watchtower has already processed the block, resulting in the + // counterparty_commitment_txn_on_chain entry not being generated by + // the above conditional. self.counterparty_commitment_txn_on_chain.insert(commitment_txid, commitment_number); log_info!(logger, "Got broadcast of non-revoked counterparty commitment transaction {}", commitment_txid); @@ -5217,9 +5441,6 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { &mut self, header: &Header, txdata: &TransactionData, height: u32, broadcaster: B, fee_estimator: F, logger: &WithContext<L>, ) -> Vec<TransactionOutputs> { - let block_hash = header.block_hash(); - self.best_block = BestBlock::new(block_hash, height); - let bounded_fee_estimator = LowerBoundedFeeEstimator::new(fee_estimator); self.transactions_confirmed(header, txdata, height, broadcaster, &bounded_fee_estimator, logger) } @@ -5236,11 +5457,11 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { let block_hash = header.block_hash(); if height > self.best_block.height { - self.best_block = BestBlock::new(block_hash, height); + self.best_block.update_for_new_tip(block_hash, height); log_trace!(logger, "Connecting new block {} at height {}", block_hash, height); self.block_confirmed(height, block_hash, vec![], vec![], vec![], &broadcaster, &fee_estimator, logger) } else if block_hash != self.best_block.block_hash { - self.best_block = BestBlock::new(block_hash, height); + self.best_block = BlockLocator::new(block_hash, height); log_trace!(logger, "Best block re-orged, replaced with new block {} at height {}", block_hash, height); self.onchain_events_awaiting_threshold_conf.retain(|ref entry| entry.height <= height); let conf_target = self.closure_conf_target(); @@ -5510,7 +5731,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { } if height > self.best_block.height { - self.best_block = BestBlock::new(block_hash, height); + self.best_block.update_for_new_tip(block_hash, height); } if should_broadcast_commitment { @@ -5636,15 +5857,14 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { self.funding_spend_confirmed = Some(entry.txid); self.confirmed_commitment_tx_counterparty_output = commitment_tx_to_counterparty_output; if self.alternative_funding_confirmed.is_none() { - for funding in self.pending_funding.drain(..) { + // We saw a confirmed commitment for our currently locked funding, so + // discard all pending ones. + for funding in &self.pending_funding { self.outputs_to_watch.remove(&funding.funding_txid()); - self.pending_events.push(Event::DiscardFunding { - channel_id: self.channel_id, - funding_info: crate::events::FundingInfo::OutPoint { - outpoint: funding.funding_outpoint(), - }, - }); } + let mut discarded_funding = Vec::new(); + mem::swap(&mut self.pending_funding, &mut discarded_funding); + self.queue_discard_funding_event(discarded_funding.into_iter()); } }, OnchainEvent::AlternativeFundingConfirmation {} => { @@ -5716,7 +5936,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { source: source.clone(), payment_preimage: None, payment_hash: htlc.payment_hash, - htlc_value_satoshis: Some(htlc.amount_msat / 1000), + htlc_value_satoshis: htlc.amount_msat / 1000, })); } } @@ -5757,7 +5977,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { #[rustfmt::skip] fn blocks_disconnected<B: BroadcasterInterface, F: FeeEstimator, L: Logger>( - &mut self, fork_point: BestBlock, broadcaster: B, fee_estimator: F, logger: &WithContext<L> + &mut self, fork_point: BlockLocator, broadcaster: B, fee_estimator: F, logger: &WithContext<L> ) { let new_height = fork_point.height; log_trace!(logger, "Block(s) disconnected to height {}", new_height); @@ -6133,7 +6353,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { source, payment_preimage: Some(payment_preimage), payment_hash, - htlc_value_satoshis: Some(amount_msat / 1000), + htlc_value_satoshis: amount_msat / 1000, })); } } else if offered_preimage_claim { @@ -6157,7 +6377,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { source, payment_preimage: Some(payment_preimage), payment_hash, - htlc_value_satoshis: Some(amount_msat / 1000), + htlc_value_satoshis: amount_msat / 1000, })); } } else { @@ -6178,7 +6398,7 @@ impl<Signer: EcdsaChannelSigner> ChannelMonitorImpl<Signer> { event: OnchainEvent::HTLCUpdate { source, payment_hash, - htlc_value_satoshis: Some(amount_msat / 1000), + htlc_value_satoshis: amount_msat / 1000, commitment_tx_output_idx: Some(input.previous_output.vout), }, }; @@ -6266,7 +6486,7 @@ impl<Signer: EcdsaChannelSigner, T: BroadcasterInterface, F: FeeEstimator, L: Lo self.0.block_connected(header, txdata, height, &self.1, &self.2, &self.3); } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { self.0.blocks_disconnected(fork_point, &self.1, &self.2, &self.3); } } @@ -6296,7 +6516,7 @@ where const MAX_ALLOC_SIZE: usize = 64 * 1024; impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP)> - for (BlockHash, ChannelMonitor<SP::EcdsaSigner>) + for (BlockLocator, ChannelMonitor<SP::EcdsaSigner>) { fn read<R: io::Read>(reader: &mut R, args: (&'a ES, &'b SP)) -> Result<Self, DecodeError> { match <Option<Self>>::read(reader, args) { @@ -6308,7 +6528,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP } impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP)> - for Option<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)> + for Option<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)> { #[rustfmt::skip] fn read<R: io::Read>(reader: &mut R, args: (&'a ES, &'b SP)) -> Result<Self, DecodeError> { @@ -6471,7 +6691,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP } } - let best_block = BestBlock::new(Readable::read(reader)?, Readable::read(reader)?); + let mut best_block = BlockLocator::new(Readable::read(reader)?, Readable::read(reader)?); let waiting_threshold_conf_len: u64 = Readable::read(reader)?; let mut onchain_events_awaiting_threshold_conf = Vec::with_capacity(cmp::min(waiting_threshold_conf_len as usize, MAX_ALLOC_SIZE / 128)); @@ -6521,6 +6741,8 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP let mut alternative_funding_confirmed = None; let mut is_manual_broadcast = RequiredWrapper(None); let mut funding_seen_onchain = RequiredWrapper(None); + let mut best_block_previous_blocks = None; + let mut current_funding_contribution = None; read_tlv_fields!(reader, { (1, funding_spend_confirmed, option), (3, htlcs_resolved_on_chain, optional_vec), @@ -6543,7 +6765,13 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP (34, alternative_funding_confirmed, option), (35, is_manual_broadcast, (default_value, false)), (37, funding_seen_onchain, (default_value, true)), + (39, best_block_previous_blocks, option), // Added and always set in 0.3 + (41, current_funding_contribution, option), }); + if let Some(previous_blocks) = best_block_previous_blocks { + best_block.previous_blocks = previous_blocks; + } + // Note that `payment_preimages_with_info` was added (and is always written) in LDK 0.1, so // we can use it to determine if this monitor was last written by LDK 0.1 or later. let written_by_0_1_or_later = payment_preimages_with_info.is_some(); @@ -6657,6 +6885,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP current_holder_commitment_tx, prev_holder_commitment_tx, + contribution: current_funding_contribution, }, pending_funding: pending_funding.unwrap_or(vec![]), is_manual_broadcast: is_manual_broadcast.0.unwrap(), @@ -6736,14 +6965,79 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP To continue, run a v0.1 release, send/route a payment over the channel or close it."); } } - Ok(Some((best_block.block_hash, monitor))) + Ok(Some((best_block, monitor))) } } +#[cfg(test)] +pub(super) fn dummy_monitor<S: EcdsaChannelSigner + 'static>( + channel_id: ChannelId, wrap_signer: impl FnOnce(crate::sign::InMemorySigner) -> S, +) -> ChannelMonitor<S> { + use crate::ln::chan_utils::{ChannelPublicKeys, CounterpartyChannelTransactionParameters}; + use crate::sign::{ChannelSigner, InMemorySigner}; + use bitcoin::network::Network; + + let secp_ctx = Secp256k1::new(); + let dummy_key = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let keys = InMemorySigner::new( + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + true, + SecretKey::from_slice(&[41; 32]).unwrap(), + SecretKey::from_slice(&[41; 32]).unwrap(), + [41; 32], + [0; 32], + [0; 32], + ); + let counterparty_pubkeys = ChannelPublicKeys { + funding_pubkey: dummy_key, + revocation_basepoint: RevocationBasepoint::from(dummy_key), + payment_point: dummy_key, + delayed_payment_basepoint: DelayedPaymentBasepoint::from(dummy_key), + htlc_basepoint: HtlcBasepoint::from(dummy_key), + }; + let funding_outpoint = + crate::chain::transaction::OutPoint { txid: Txid::all_zeros(), index: u16::MAX }; + let channel_parameters = ChannelTransactionParameters { + holder_pubkeys: keys.pubkeys(&secp_ctx), + holder_selected_contest_delay: 66, + is_outbound_from_holder: true, + counterparty_parameters: Some(CounterpartyChannelTransactionParameters { + pubkeys: counterparty_pubkeys, + selected_contest_delay: 67, + }), + funding_outpoint: Some(funding_outpoint), + splice_parent_funding_txid: None, + channel_type_features: ChannelTypeFeatures::only_static_remote_key(), + channel_value_satoshis: 0, + }; + let shutdown_script = crate::ln::script::ShutdownScript::new_p2wpkh_from_pubkey(dummy_key); + let best_block = BlockLocator::from_network(Network::Testnet); + let signer = wrap_signer(keys); + ChannelMonitor::new( + secp_ctx, + signer, + Some(shutdown_script.into_inner()), + 0, + &ScriptBuf::new(), + &channel_parameters, + true, + 0, + HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), + best_block, + dummy_key, + channel_id, + false, + ) +} + #[cfg(test)] mod tests { use bitcoin::amount::Amount; - use bitcoin::hash_types::{BlockHash, Txid}; + use bitcoin::hash_types::Txid; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::hex::FromHex; @@ -6760,7 +7054,7 @@ mod tests { use bitcoin::{Sequence, Witness}; use crate::chain::chaininterface::LowerBoundedFeeEstimator; - use crate::events::ClosureReason; + use crate::events::{ClosureReason, Event}; use super::ChannelMonitorUpdateStep; use crate::chain::channelmonitor::{ChannelMonitor, WithChannelMonitor}; @@ -6769,23 +7063,16 @@ mod tests { weight_revoked_received_htlc, WEIGHT_REVOKED_OUTPUT, }; use crate::chain::transaction::OutPoint; - use crate::chain::{BestBlock, Confirm}; + use crate::chain::{BlockLocator, Confirm}; use crate::io; - use crate::ln::chan_utils::{ - self, ChannelPublicKeys, ChannelTransactionParameters, - CounterpartyChannelTransactionParameters, HTLCOutputInCommitment, - HolderCommitmentTransaction, - }; + use crate::ln::chan_utils::{self, HTLCOutputInCommitment, HolderCommitmentTransaction}; use crate::ln::channel_keys::{ - DelayedPaymentBasepoint, DelayedPaymentKey, HtlcBasepoint, RevocationBasepoint, - RevocationKey, + DelayedPaymentBasepoint, DelayedPaymentKey, RevocationBasepoint, RevocationKey, }; use crate::ln::channelmanager::{HTLCSource, PaymentId}; use crate::ln::functional_test_utils::*; use crate::ln::outbound_payment::RecipientOnionFields; - use crate::ln::script::ShutdownScript; use crate::ln::types::ChannelId; - use crate::sign::{ChannelSigner, InMemorySigner}; use crate::sync::Arc; use crate::types::features::ChannelTypeFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -6817,6 +7104,7 @@ mod tests { let legacy_cfg = test_legacy_channel_config(); let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[Some(legacy_cfg.clone()), Some(legacy_cfg.clone()), Some(legacy_cfg)]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + nodes[1].disable_monitor_completeness_assertion(); let channel = create_announced_chan_between_nodes(&nodes, 0, 1); create_announced_chan_between_nodes(&nodes, 1, 2); @@ -6842,7 +7130,7 @@ mod tests { nodes[1].chain_monitor.chain_monitor.transactions_confirmed(&new_header, &[(0, broadcast_tx)], conf_height); - let (_, pre_update_monitor) = <(BlockHash, ChannelMonitor<_>)>::read( + let (_, pre_update_monitor) = <(BlockLocator, ChannelMonitor<_>)>::read( &mut io::Cursor::new(&get_monitor!(nodes[1], channel.2).encode()), (&nodes[1].keys_manager.backing, &nodes[1].keys_manager.backing)).unwrap(); @@ -6850,7 +7138,7 @@ mod tests { // the update through to the ChannelMonitor which will refuse it (as the channel is closed). let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 100_000); nodes[1].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0) + RecipientOnionFields::secret_only(payment_secret, 100_000), PaymentId(payment_hash.0) ).unwrap(); check_added_monitors(&nodes[1], 1); @@ -6890,8 +7178,21 @@ mod tests { check_spends!(htlc_txn[1], broadcast_tx); check_closed_broadcast(&nodes[1], 1, true); - check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000); - check_added_monitors(&nodes[1], 1); + if !use_local_txn { + // When the counterparty commitment confirms, FundingSpendConfirmation matures + // immediately (no CSV delay), so funding_spend_confirmed is set. The new payment's + // commitment update then triggers immediate HTLC failure, generating payment events + // alongside the channel close event. + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 3); + assert!(events.iter().any(|e| matches!(e, Event::PaymentPathFailed { .. }))); + assert!(events.iter().any(|e| matches!(e, Event::PaymentFailed { .. }))); + assert!(events.iter().any(|e| matches!(e, Event::ChannelClosed { .. }))); + check_added_monitors(&nodes[1], 2); + } else { + check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000); + check_added_monitors(&nodes[1], 1); + } } #[test] @@ -6955,51 +7256,11 @@ mod tests { } } - let keys = InMemorySigner::new( - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - true, - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - [41; 32], - [0; 32], - [0; 32], - ); - - let counterparty_pubkeys = ChannelPublicKeys { - funding_pubkey: PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[44; 32]).unwrap()), - revocation_basepoint: RevocationBasepoint::from(PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[45; 32]).unwrap())), - payment_point: PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[46; 32]).unwrap()), - delayed_payment_basepoint: DelayedPaymentBasepoint::from(PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[47; 32]).unwrap())), - htlc_basepoint: HtlcBasepoint::from(PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[48; 32]).unwrap())) - }; let funding_outpoint = OutPoint { txid: Txid::all_zeros(), index: u16::MAX }; let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); - let channel_parameters = ChannelTransactionParameters { - holder_pubkeys: keys.pubkeys(&secp_ctx), - holder_selected_contest_delay: 66, - is_outbound_from_holder: true, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { - pubkeys: counterparty_pubkeys, - selected_contest_delay: 67, - }), - funding_outpoint: Some(funding_outpoint), - splice_parent_funding_txid: None, - channel_type_features: ChannelTypeFeatures::only_static_remote_key(), - channel_value_satoshis: 0, - }; // Prune with one old state and a holder commitment tx holding a few overlaps with the // old state. - let shutdown_pubkey = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - let shutdown_script = ShutdownScript::new_p2wpkh_from_pubkey(shutdown_pubkey); - let best_block = BestBlock::from_network(Network::Testnet); - let monitor = ChannelMonitor::new( - Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), - &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, false, - ); + let monitor = super::dummy_monitor(channel_id, |keys| keys); let nondust_htlcs = preimages_slice_to_htlcs!(preimages[0..10]); let dummy_commitment_tx = HolderCommitmentTransaction::dummy(0, funding_outpoint, nondust_htlcs); @@ -7218,49 +7479,9 @@ mod tests { let dummy_key = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - let keys = InMemorySigner::new( - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - true, - SecretKey::from_slice(&[41; 32]).unwrap(), - SecretKey::from_slice(&[41; 32]).unwrap(), - [41; 32], - [0; 32], - [0; 32], - ); - - let counterparty_pubkeys = ChannelPublicKeys { - funding_pubkey: PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[44; 32]).unwrap()), - revocation_basepoint: RevocationBasepoint::from(PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[45; 32]).unwrap())), - payment_point: PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[46; 32]).unwrap()), - delayed_payment_basepoint: DelayedPaymentBasepoint::from(PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[47; 32]).unwrap())), - htlc_basepoint: HtlcBasepoint::from(PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[48; 32]).unwrap())), - }; let funding_outpoint = OutPoint { txid: Txid::all_zeros(), index: u16::MAX }; let channel_id = ChannelId::v1_from_funding_outpoint(funding_outpoint); - let channel_parameters = ChannelTransactionParameters { - holder_pubkeys: keys.pubkeys(&secp_ctx), - holder_selected_contest_delay: 66, - is_outbound_from_holder: true, - counterparty_parameters: Some(CounterpartyChannelTransactionParameters { - pubkeys: counterparty_pubkeys, - selected_contest_delay: 67, - }), - funding_outpoint: Some(funding_outpoint), - splice_parent_funding_txid: None, - channel_type_features: ChannelTypeFeatures::only_static_remote_key(), - channel_value_satoshis: 0, - }; - let shutdown_pubkey = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); - let shutdown_script = ShutdownScript::new_p2wpkh_from_pubkey(shutdown_pubkey); - let best_block = BestBlock::from_network(Network::Testnet); - let monitor = ChannelMonitor::new( - Secp256k1::new(), keys, Some(shutdown_script.into_inner()), 0, &ScriptBuf::new(), - &channel_parameters, true, 0, HolderCommitmentTransaction::dummy(0, funding_outpoint, Vec::new()), - best_block, dummy_key, channel_id, false, - ); + let monitor = super::dummy_monitor(channel_id, |keys| keys); let chan_id = monitor.inner.lock().unwrap().channel_id(); let payment_hash = PaymentHash([1; 32]); diff --git a/lightning/src/chain/mod.rs b/lightning/src/chain/mod.rs index bc47f1b1db6..72006f78205 100644 --- a/lightning/src/chain/mod.rs +++ b/lightning/src/chain/mod.rs @@ -18,7 +18,9 @@ use bitcoin::network::Network; use bitcoin::script::{Script, ScriptBuf}; use bitcoin::secp256k1::PublicKey; -use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate, MonitorEvent}; +use crate::chain::channelmonitor::{ + ChannelMonitor, ChannelMonitorUpdate, MonitorEvent, ANTI_REORG_DELAY, +}; use crate::chain::transaction::{OutPoint, TransactionData}; use crate::ln::types::ChannelId; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -36,34 +38,118 @@ pub(crate) mod onchaintx; pub(crate) mod package; pub mod transaction; -/// The best known block as identified by its hash and height. +/// Identifies a position in the chain by its block hash and height, along with recent ancestor +/// hashes used to locate the fork point of a reorg. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -pub struct BestBlock { - /// The block's hash +pub struct BlockLocator { + /// The block's hash. pub block_hash: BlockHash, - /// The height at which the block was confirmed. + /// The block's height. pub height: u32, + /// Ancestor block hashes immediately before [`Self::block_hash`], in reverse chronological + /// order. + /// + /// These ensure we can find the fork point of a reorg if our block source no longer has the + /// previous tip after a restart. + pub previous_blocks: [Option<BlockHash>; ANTI_REORG_DELAY as usize * 2], } -impl BestBlock { - /// Constructs a `BestBlock` that represents the genesis block at height 0 of the given +impl BlockLocator { + /// Constructs a `BlockLocator` that represents the genesis block at height 0 of the given /// network. pub fn from_network(network: Network) -> Self { - BestBlock { block_hash: genesis_block(network).header.block_hash(), height: 0 } + let block_hash = genesis_block(network).header.block_hash(); + let previous_blocks = [None; ANTI_REORG_DELAY as usize * 2]; + BlockLocator { block_hash, height: 0, previous_blocks } } - /// Returns a `BestBlock` as identified by the given block hash and height. + /// Returns a `BlockLocator` as identified by the given block hash and height. /// /// This is not exported to bindings users directly as the bindings auto-generate an /// equivalent `new`. pub fn new(block_hash: BlockHash, height: u32) -> Self { - BestBlock { block_hash, height } + let previous_blocks = [None; ANTI_REORG_DELAY as usize * 2]; + BlockLocator { block_hash, height, previous_blocks } + } + + /// Advances to a new block at height [`Self::height`] + 1. + pub fn advance(&mut self, new_hash: BlockHash) { + // Shift all block hashes to the right (making room for the old tip at index 0) + for i in (1..self.previous_blocks.len()).rev() { + self.previous_blocks[i] = self.previous_blocks[i - 1]; + } + + // The old tip becomes the new index 0 (tip-1) + self.previous_blocks[0] = Some(self.block_hash); + + // Update to the new tip + self.block_hash = new_hash; + self.height += 1; + } + + /// Updates this locator for a new chain tip, either delegating to [`Self::advance`] if the new + /// block is simply one higher than the current tip and wiping [`Self::previous_blocks`] if a + /// few blocks have been skipped. + pub fn update_for_new_tip(&mut self, new_tip_hash: BlockHash, new_tip_height: u32) { + if new_tip_height == self.height + 1 { + self.advance(new_tip_hash); + } else { + *self = BlockLocator::new(new_tip_hash, new_tip_height); + } + } + + /// Returns the block hash at the given height, if available in our history. + pub fn get_hash_at_height(&self, height: u32) -> Option<BlockHash> { + if height > self.height { + return None; + } + if height == self.height { + return Some(self.block_hash); + } + + // offset = 1 means we want tip-1, which is block_hashes[0] + // offset = 2 means we want tip-2, which is block_hashes[1], etc. + let offset = self.height.saturating_sub(height) as usize; + if offset >= 1 && offset <= self.previous_blocks.len() { + self.previous_blocks[offset - 1] + } else { + None + } + } + + /// Finds the most recent common ancestor between two [`BlockLocator`]s by searching their + /// ancestor hash histories. + /// + /// Returns the common block hash and height, or None if no common block is found in the + /// available histories. + pub fn find_common_ancestor(&self, other: &BlockLocator) -> Option<(BlockHash, u32)> { + // First check if either tip matches + if self.block_hash == other.block_hash && self.height == other.height { + return Some((self.block_hash, self.height)); + } + + // Check all heights covered by self's history + let min_height = self.height.saturating_sub(self.previous_blocks.len() as u32); + for check_height in (min_height..=self.height).rev() { + if let Some(self_hash) = self.get_hash_at_height(check_height) { + if let Some(other_hash) = other.get_hash_at_height(check_height) { + if self_hash == other_hash { + return Some((self_hash, check_height)); + } + } + } + } + None } } -impl_writeable_tlv_based!(BestBlock, { +impl_ser_tlv_based!(BlockLocator, { (0, block_hash, required), + // Note that any change to the previous_blocks array length will change the serialization + // format and thus it is specified without constants here. + (1, previous_blocks_read, (legacy, [Option<BlockHash>; 6 * 2], |_| Ok(()), |us: &BlockLocator| Some(us.previous_blocks))), (2, height, required), + (unused, previous_blocks, (static_value, previous_blocks_read.unwrap_or([None; 6 * 2]))), }); /// The `Listen` trait is used to notify when blocks have been connected or disconnected from the @@ -93,8 +179,8 @@ impl_writeable_tlv_based!(BestBlock, { /// /// # Object Birthday /// -/// Note that most implementations take a [`BestBlock`] on construction and blocks only need to be -/// applied starting from that point. +/// Note that most implementations take a [`BlockLocator`] on construction identifying the best +/// block at that time, and blocks only need to be applied starting from that point. pub trait Listen { /// Notifies the listener that a block was added at the given height, with the transaction data /// possibly filtered. @@ -108,11 +194,11 @@ pub trait Listen { /// Notifies the listener that one or more blocks were removed in anticipation of a reorg. /// - /// The provided [`BestBlock`] is the new best block after disconnecting blocks in the reorg - /// but before connecting new ones (i.e. the "fork point" block). For backwards compatibility, - /// you may instead walk the chain backwards, calling `blocks_disconnected` for each block - /// that is disconnected in a reorg. - fn blocks_disconnected(&self, fork_point_block: BestBlock); + /// The provided [`BlockLocator`] identifies the new best block after disconnecting blocks in + /// the reorg but before connecting new ones (i.e. the "fork point" block). For backwards + /// compatibility, you may instead walk the chain backwards, calling `blocks_disconnected` for + /// each block that is disconnected in a reorg. + fn blocks_disconnected(&self, fork_point_block: BlockLocator); } /// The `Confirm` trait is used to notify LDK when relevant transactions have been confirmed on @@ -233,11 +319,10 @@ pub enum ChannelMonitorUpdateStatus { /// This includes performing any `fsync()` calls required to ensure the update is guaranteed to /// be available on restart even if the application crashes. /// - /// If you return this variant, you cannot later return [`InProgress`] from the same instance of - /// [`Persist`]/[`Watch`] without first restarting. + /// You cannot switch from [`InProgress`] to this variant for the same channel without first + /// restarting. However, switching from this variant to [`InProgress`] is always allowed. /// /// [`InProgress`]: ChannelMonitorUpdateStatus::InProgress - /// [`Persist`]: chainmonitor::Persist Completed, /// Indicates that the update will happen asynchronously in the background or that a transient /// failure occurred which is being retried in the background and will eventually complete. @@ -263,12 +348,7 @@ pub enum ChannelMonitorUpdateStatus { /// reliable, this feature is considered beta, and a handful of edge-cases remain. Until the /// remaining cases are fixed, in rare cases, *using this feature may lead to funds loss*. /// - /// If you return this variant, you cannot later return [`Completed`] from the same instance of - /// [`Persist`]/[`Watch`] without first restarting. - /// /// [`InProgress`]: ChannelMonitorUpdateStatus::InProgress - /// [`Completed`]: ChannelMonitorUpdateStatus::Completed - /// [`Persist`]: chainmonitor::Persist InProgress, /// Indicates that an update has failed and will not complete at any point in the future. /// @@ -328,6 +408,8 @@ pub trait Watch<ChannelSigner: EcdsaChannelSigner> { /// cannot be retried, the node should shut down immediately after returning /// [`ChannelMonitorUpdateStatus::UnrecoverableError`], see its documentation for more info. /// + /// See [`ChannelMonitorUpdateStatus`] for requirements on when each variant may be returned. + /// /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager fn update_channel( &self, channel_id: ChannelId, update: &ChannelMonitorUpdate, @@ -452,7 +534,7 @@ impl<T: Listen> Listen for dyn core::ops::Deref<Target = T> { (**self).filtered_block_connected(header, txdata, height); } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { (**self).blocks_disconnected(fork_point); } } @@ -467,7 +549,7 @@ where self.1.filtered_block_connected(header, txdata, height); } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { self.0.blocks_disconnected(fork_point); self.1.blocks_disconnected(fork_point); } @@ -495,3 +577,45 @@ impl ClaimId { ClaimId(Sha256::from_engine(engine).to_byte_array()) } } + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::hashes::Hash; + + #[test] + fn test_best_block() { + let hash1 = BlockHash::from_slice(&[1; 32]).unwrap(); + let mut chain_a = BlockLocator::new(hash1, 100); + let mut chain_b = BlockLocator::new(hash1, 100); + + // Test get_hash_at_height on initial block + assert_eq!(chain_a.get_hash_at_height(100), Some(hash1)); + assert_eq!(chain_a.get_hash_at_height(101), None); + assert_eq!(chain_a.get_hash_at_height(99), None); + + // Test find_common_ancestor with identical blocks + assert_eq!(chain_a.find_common_ancestor(&chain_b), Some((hash1, 100))); + + let hash2 = BlockHash::from_slice(&[2; 32]).unwrap(); + chain_a.advance(hash2); + assert_eq!(chain_a.height, 101); + assert_eq!(chain_a.block_hash, hash2); + assert_eq!(chain_a.previous_blocks[0], Some(hash1)); + assert_eq!(chain_a.get_hash_at_height(101), Some(hash2)); + assert_eq!(chain_a.get_hash_at_height(100), Some(hash1)); + + // Test find_common_ancestor with different heights + assert_eq!(chain_a.find_common_ancestor(&chain_b), Some((hash1, 100))); + + // Test find_common_ancestor with diverged chains but the same height + let hash_b3 = BlockHash::from_slice(&[33; 32]).unwrap(); + chain_b.advance(hash_b3); + assert_eq!(chain_a.find_common_ancestor(&chain_b), Some((hash1, 100))); + + // Test find_common_ancestor with no common history + let hash_other = BlockHash::from_slice(&[99; 32]).unwrap(); + let chain_c = BlockLocator::new(hash_other, 200); + assert_eq!(chain_a.find_common_ancestor(&chain_c), None); + } +} diff --git a/lightning/src/chain/onchaintx.rs b/lightning/src/chain/onchaintx.rs index 3eb6d64f3a2..75a4e1977d5 100644 --- a/lightning/src/chain/onchaintx.rs +++ b/lightning/src/chain/onchaintx.rs @@ -413,7 +413,7 @@ impl<'a, 'b, ES: EntropySource, SP: SignerProvider> ReadableArgs<(&'a ES, &'b SP } let claimable_outpoints_len: u64 = Readable::read(reader)?; - let mut claimable_outpoints = hash_map_with_capacity(cmp::min(pending_claim_requests_len as usize, MAX_ALLOC_SIZE / 128)); + let mut claimable_outpoints = hash_map_with_capacity(cmp::min(claimable_outpoints_len as usize, MAX_ALLOC_SIZE / 128)); for _ in 0..claimable_outpoints_len { let outpoint = Readable::read(reader)?; let ancestor_claim_txid = Readable::read(reader)?; diff --git a/lightning/src/chain/package.rs b/lightning/src/chain/package.rs index 0ef8855242b..269a8dd1d7d 100644 --- a/lightning/src/chain/package.rs +++ b/lightning/src/chain/package.rs @@ -172,7 +172,7 @@ impl RevokedOutput { } } -impl_writeable_tlv_based!(RevokedOutput, { +impl_ser_tlv_based!(RevokedOutput, { (0, per_commitment_point, required), (1, outpoint_confirmation_height, option), // Added in 0.1.4/0.2 and always set (2, counterparty_delayed_payment_base_key, required), @@ -238,7 +238,7 @@ impl RevokedHTLCOutput { } } -impl_writeable_tlv_based!(RevokedHTLCOutput, { +impl_ser_tlv_based!(RevokedHTLCOutput, { (0, per_commitment_point, required), (1, outpoint_confirmation_height, option), // Added in 0.1.4/0.2 and always set (2, counterparty_delayed_payment_base_key, required), @@ -1066,7 +1066,7 @@ impl PackageSolvingData { } } -impl_writeable_tlv_based_enum_legacy!(PackageSolvingData, ; +impl_ser_tlv_based_enum_legacy!(PackageSolvingData, ; (0, RevokedOutput), (1, RevokedHTLCOutput), (2, CounterpartyOfferedHTLCOutput), diff --git a/lightning/src/crypto/chacha20.rs b/lightning/src/crypto/chacha20.rs deleted file mode 100644 index 67f9e93c480..00000000000 --- a/lightning/src/crypto/chacha20.rs +++ /dev/null @@ -1,639 +0,0 @@ -// This file was stolen from rust-crypto. -// Copyright 2012-2013 The Rust Project Developers. See the COPYRIGHT -// file at the top-level directory of this distribution and at -// http://rust-lang.org/COPYRIGHT. -// -// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE -// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license -// <LICENSE-MIT or 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(not(fuzzing))] -mod real_chacha { - use core::cmp; - - #[derive(Clone, Copy, PartialEq, Eq)] - #[allow(non_camel_case_types)] - struct u32x4(pub u32, pub u32, pub u32, pub u32); - impl ::core::ops::Add for u32x4 { - type Output = u32x4; - #[inline] - fn add(self, rhs: u32x4) -> u32x4 { - u32x4( - self.0.wrapping_add(rhs.0), - self.1.wrapping_add(rhs.1), - self.2.wrapping_add(rhs.2), - self.3.wrapping_add(rhs.3), - ) - } - } - impl ::core::ops::Sub for u32x4 { - type Output = u32x4; - #[inline] - fn sub(self, rhs: u32x4) -> u32x4 { - u32x4( - self.0.wrapping_sub(rhs.0), - self.1.wrapping_sub(rhs.1), - self.2.wrapping_sub(rhs.2), - self.3.wrapping_sub(rhs.3), - ) - } - } - impl ::core::ops::BitXor for u32x4 { - type Output = u32x4; - #[inline] - fn bitxor(self, rhs: u32x4) -> u32x4 { - u32x4(self.0 ^ rhs.0, self.1 ^ rhs.1, self.2 ^ rhs.2, self.3 ^ rhs.3) - } - } - impl ::core::ops::Shr<u8> for u32x4 { - type Output = u32x4; - #[inline] - fn shr(self, shr: u8) -> u32x4 { - u32x4(self.0 >> shr, self.1 >> shr, self.2 >> shr, self.3 >> shr) - } - } - impl ::core::ops::Shl<u8> for u32x4 { - type Output = u32x4; - #[inline] - fn shl(self, shl: u8) -> u32x4 { - u32x4(self.0 << shl, self.1 << shl, self.2 << shl, self.3 << shl) - } - } - impl u32x4 { - #[inline] - fn from_bytes(bytes: &[u8]) -> Self { - assert_eq!(bytes.len(), 4 * 4); - Self( - u32::from_le_bytes(bytes[0 * 4..1 * 4].try_into().expect("len is 4")), - u32::from_le_bytes(bytes[1 * 4..2 * 4].try_into().expect("len is 4")), - u32::from_le_bytes(bytes[2 * 4..3 * 4].try_into().expect("len is 4")), - u32::from_le_bytes(bytes[3 * 4..4 * 4].try_into().expect("len is 4")), - ) - } - } - - const BLOCK_SIZE: usize = 64; - - #[derive(Clone, Copy)] - struct ChaChaState { - a: u32x4, - b: u32x4, - c: u32x4, - d: u32x4, - } - - #[derive(Copy)] - pub struct ChaCha20 { - state: ChaChaState, - output: [u8; BLOCK_SIZE], - offset: usize, - } - - impl Clone for ChaCha20 { - fn clone(&self) -> ChaCha20 { - *self - } - } - - macro_rules! swizzle { - ($b: expr, $c: expr, $d: expr) => {{ - let u32x4(b10, b11, b12, b13) = $b; - $b = u32x4(b11, b12, b13, b10); - let u32x4(c10, c11, c12, c13) = $c; - $c = u32x4(c12, c13, c10, c11); - let u32x4(d10, d11, d12, d13) = $d; - $d = u32x4(d13, d10, d11, d12); - }}; - } - - macro_rules! state_to_buffer { - ($state: expr, $output: expr) => {{ - let u32x4(a1, a2, a3, a4) = $state.a; - let u32x4(b1, b2, b3, b4) = $state.b; - let u32x4(c1, c2, c3, c4) = $state.c; - let u32x4(d1, d2, d3, d4) = $state.d; - let lens = [a1, a2, a3, a4, b1, b2, b3, b4, c1, c2, c3, c4, d1, d2, d3, d4]; - for i in 0..lens.len() { - $output[i * 4..(i + 1) * 4].copy_from_slice(&lens[i].to_le_bytes()); - } - }}; - } - - macro_rules! round { - ($state: expr) => {{ - $state.a = $state.a + $state.b; - rotate!($state.d, $state.a, 16); - $state.c = $state.c + $state.d; - rotate!($state.b, $state.c, 12); - $state.a = $state.a + $state.b; - rotate!($state.d, $state.a, 8); - $state.c = $state.c + $state.d; - rotate!($state.b, $state.c, 7); - }}; - } - - macro_rules! rotate { - ($a: expr, $b: expr, $rot: expr) => {{ - let v = $a ^ $b; - let r = 32 - $rot; - let right = v >> r; - $a = (v << $rot) ^ right - }}; - } - - impl ChaCha20 { - pub fn new(key: &[u8], nonce: &[u8]) -> ChaCha20 { - assert!(key.len() == 16 || key.len() == 32); - assert!(nonce.len() == 8 || nonce.len() == 12); - - ChaCha20 { state: ChaCha20::expand(key, nonce), output: [0u8; BLOCK_SIZE], offset: 64 } - } - - /// Get one block from a ChaCha stream. - pub fn get_single_block(key: &[u8; 32], nonce: &[u8; 16]) -> [u8; 32] { - let mut chacha = ChaCha20 { - state: ChaCha20::expand(key, nonce), - output: [0u8; BLOCK_SIZE], - offset: 64, - }; - let mut chacha_bytes = [0; 32]; - chacha.process_in_place(&mut chacha_bytes); - chacha_bytes - } - - /// Encrypts `src` into `dest` using a single block from a ChaCha stream. Passing `dest` as - /// `src` in a second call will decrypt it. - pub fn encrypt_single_block(key: &[u8; 32], nonce: &[u8; 16], dest: &mut [u8], src: &[u8]) { - debug_assert_eq!(dest.len(), src.len()); - debug_assert!(dest.len() <= 32); - - let block = ChaCha20::get_single_block(key, nonce); - for i in 0..dest.len() { - dest[i] = block[i] ^ src[i]; - } - } - - /// Same as `encrypt_single_block` only operates on a fixed-size input in-place. - pub fn encrypt_single_block_in_place( - key: &[u8; 32], nonce: &[u8; 16], bytes: &mut [u8; 32], - ) { - let block = ChaCha20::get_single_block(key, nonce); - for i in 0..bytes.len() { - bytes[i] ^= block[i]; - } - } - - fn expand(key: &[u8], nonce: &[u8]) -> ChaChaState { - let constant = match key.len() { - 16 => b"expand 16-byte k", - 32 => b"expand 32-byte k", - _ => unreachable!(), - }; - ChaChaState { - a: u32x4::from_bytes(&constant[0..16]), - b: u32x4::from_bytes(&key[0..16]), - c: if key.len() == 16 { - u32x4::from_bytes(&key[0..16]) - } else { - u32x4::from_bytes(&key[16..32]) - }, - d: if nonce.len() == 16 { - u32x4::from_bytes(&nonce[0..16]) - } else if nonce.len() == 12 { - let mut nonce4 = [0; 4 * 4]; - nonce4[4..].copy_from_slice(nonce); - u32x4::from_bytes(&nonce4) - } else { - let mut nonce4 = [0; 4 * 4]; - nonce4[8..].copy_from_slice(nonce); - u32x4::from_bytes(&nonce4) - }, - } - } - - // put the the next BLOCK_SIZE keystream bytes into self.output - fn update(&mut self) { - let mut state = self.state; - - for _ in 0..10 { - round!(state); - swizzle!(state.b, state.c, state.d); - round!(state); - swizzle!(state.d, state.c, state.b); - } - state.a = state.a + self.state.a; - state.b = state.b + self.state.b; - state.c = state.c + self.state.c; - state.d = state.d + self.state.d; - - state_to_buffer!(state, self.output); - - self.state.d = self.state.d + u32x4(1, 0, 0, 0); - let u32x4(c12, _, _, _) = self.state.d; - if c12 == 0 { - // we could increment the other counter word with an 8 byte nonce - // but other implementations like boringssl have this same - // limitation - panic!("counter is exhausted"); - } - - self.offset = 0; - } - - #[inline] // Useful cause input may be 0s on stack that should be optimized out - pub fn process(&mut self, input: &[u8], output: &mut [u8]) { - assert!(input.len() == output.len()); - let len = input.len(); - let mut i = 0; - while i < len { - // If there is no keystream available in the output buffer, - // generate the next block. - if self.offset == BLOCK_SIZE { - self.update(); - } - - // Process the min(available keystream, remaining input length). - let count = cmp::min(BLOCK_SIZE - self.offset, len - i); - // explicitly assert lengths to avoid bounds checks: - assert!(output.len() >= i + count); - assert!(input.len() >= i + count); - assert!(self.output.len() >= self.offset + count); - for j in 0..count { - output[i + j] = input[i + j] ^ self.output[self.offset + j]; - } - i += count; - self.offset += count; - } - } - - pub fn process_in_place(&mut self, input_output: &mut [u8]) { - let len = input_output.len(); - let mut i = 0; - while i < len { - // If there is no keystream available in the output buffer, - // generate the next block. - if self.offset == BLOCK_SIZE { - self.update(); - } - - // Process the min(available keystream, remaining input length). - let count = cmp::min(BLOCK_SIZE - self.offset, len - i); - // explicitly assert lengths to avoid bounds checks: - assert!(input_output.len() >= i + count); - assert!(self.output.len() >= self.offset + count); - for j in 0..count { - input_output[i + j] ^= self.output[self.offset + j]; - } - i += count; - self.offset += count; - } - } - - #[cfg(test)] - pub fn seek_to_block(&mut self, block_offset: u32) { - self.state.d.0 = block_offset; - self.update(); - } - } -} -#[cfg(not(fuzzing))] -pub use self::real_chacha::ChaCha20; - -#[cfg(fuzzing)] -mod fuzzy_chacha { - pub struct ChaCha20 {} - - impl ChaCha20 { - pub fn new(key: &[u8], nonce: &[u8]) -> ChaCha20 { - assert!(key.len() == 16 || key.len() == 32); - assert!(nonce.len() == 8 || nonce.len() == 12); - Self {} - } - - pub fn get_single_block(_key: &[u8; 32], _nonce: &[u8; 16]) -> [u8; 32] { - [0; 32] - } - - pub fn encrypt_single_block( - _key: &[u8; 32], _nonce: &[u8; 16], dest: &mut [u8], src: &[u8], - ) { - debug_assert_eq!(dest.len(), src.len()); - debug_assert!(dest.len() <= 32); - dest.copy_from_slice(src); - } - - pub fn encrypt_single_block_in_place( - _key: &[u8; 32], _nonce: &[u8; 16], _bytes: &mut [u8; 32], - ) { - } - - pub fn process(&mut self, input: &[u8], output: &mut [u8]) { - output.copy_from_slice(input); - } - - pub fn process_in_place(&mut self, _input_output: &mut [u8]) {} - } -} -#[cfg(fuzzing)] -pub use self::fuzzy_chacha::ChaCha20; - -#[cfg(test)] -mod test { - use core::iter::repeat; - - use crate::prelude::*; - - use super::ChaCha20; - - #[test] - fn test_chacha20_256_tls_vectors() { - struct TestVector { - key: [u8; 32], - nonce: [u8; 8], - keystream: Vec<u8>, - } - // taken from http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 - let test_vectors = [ - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - keystream: vec![ - 0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90, 0x40, 0x5d, 0x6a, 0xe5, 0x53, - 0x86, 0xbd, 0x28, 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a, 0xa8, 0x36, - 0xef, 0xcc, 0x8b, 0x77, 0x0d, 0xc7, 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, - 0x8d, 0x77, 0x24, 0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37, 0x6a, 0x43, 0xb8, 0xf4, - 0x15, 0x18, 0xa1, 0x1c, 0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86, - ], - }, - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - keystream: vec![ - 0x45, 0x40, 0xf0, 0x5a, 0x9f, 0x1f, 0xb2, 0x96, 0xd7, 0x73, 0x6e, 0x7b, 0x20, - 0x8e, 0x3c, 0x96, 0xeb, 0x4f, 0xe1, 0x83, 0x46, 0x88, 0xd2, 0x60, 0x4f, 0x45, - 0x09, 0x52, 0xed, 0x43, 0x2d, 0x41, 0xbb, 0xe2, 0xa0, 0xb6, 0xea, 0x75, 0x66, - 0xd2, 0xa5, 0xd1, 0xe7, 0xe2, 0x0d, 0x42, 0xaf, 0x2c, 0x53, 0xd7, 0x92, 0xb1, - 0xc4, 0x3f, 0xea, 0x81, 0x7e, 0x9a, 0xd2, 0x75, 0xae, 0x54, 0x69, 0x63, - ], - }, - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], - keystream: vec![ - 0xde, 0x9c, 0xba, 0x7b, 0xf3, 0xd6, 0x9e, 0xf5, 0xe7, 0x86, 0xdc, 0x63, 0x97, - 0x3f, 0x65, 0x3a, 0x0b, 0x49, 0xe0, 0x15, 0xad, 0xbf, 0xf7, 0x13, 0x4f, 0xcb, - 0x7d, 0xf1, 0x37, 0x82, 0x10, 0x31, 0xe8, 0x5a, 0x05, 0x02, 0x78, 0xa7, 0x08, - 0x45, 0x27, 0x21, 0x4f, 0x73, 0xef, 0xc7, 0xfa, 0x5b, 0x52, 0x77, 0x06, 0x2e, - 0xb7, 0xa0, 0x43, 0x3e, 0x44, 0x5f, 0x41, 0xe3, - ], - }, - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - nonce: [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - keystream: vec![ - 0xef, 0x3f, 0xdf, 0xd6, 0xc6, 0x15, 0x78, 0xfb, 0xf5, 0xcf, 0x35, 0xbd, 0x3d, - 0xd3, 0x3b, 0x80, 0x09, 0x63, 0x16, 0x34, 0xd2, 0x1e, 0x42, 0xac, 0x33, 0x96, - 0x0b, 0xd1, 0x38, 0xe5, 0x0d, 0x32, 0x11, 0x1e, 0x4c, 0xaf, 0x23, 0x7e, 0xe5, - 0x3c, 0xa8, 0xad, 0x64, 0x26, 0x19, 0x4a, 0x88, 0x54, 0x5d, 0xdc, 0x49, 0x7a, - 0x0b, 0x46, 0x6e, 0x7d, 0x6b, 0xbd, 0xb0, 0x04, 0x1b, 0x2f, 0x58, 0x6b, - ], - }, - TestVector { - key: [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, - 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, - 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - ], - nonce: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], - keystream: vec![ - 0xf7, 0x98, 0xa1, 0x89, 0xf1, 0x95, 0xe6, 0x69, 0x82, 0x10, 0x5f, 0xfb, 0x64, - 0x0b, 0xb7, 0x75, 0x7f, 0x57, 0x9d, 0xa3, 0x16, 0x02, 0xfc, 0x93, 0xec, 0x01, - 0xac, 0x56, 0xf8, 0x5a, 0xc3, 0xc1, 0x34, 0xa4, 0x54, 0x7b, 0x73, 0x3b, 0x46, - 0x41, 0x30, 0x42, 0xc9, 0x44, 0x00, 0x49, 0x17, 0x69, 0x05, 0xd3, 0xbe, 0x59, - 0xea, 0x1c, 0x53, 0xf1, 0x59, 0x16, 0x15, 0x5c, 0x2b, 0xe8, 0x24, 0x1a, 0x38, - 0x00, 0x8b, 0x9a, 0x26, 0xbc, 0x35, 0x94, 0x1e, 0x24, 0x44, 0x17, 0x7c, 0x8a, - 0xde, 0x66, 0x89, 0xde, 0x95, 0x26, 0x49, 0x86, 0xd9, 0x58, 0x89, 0xfb, 0x60, - 0xe8, 0x46, 0x29, 0xc9, 0xbd, 0x9a, 0x5a, 0xcb, 0x1c, 0xc1, 0x18, 0xbe, 0x56, - 0x3e, 0xb9, 0xb3, 0xa4, 0xa4, 0x72, 0xf8, 0x2e, 0x09, 0xa7, 0xe7, 0x78, 0x49, - 0x2b, 0x56, 0x2e, 0xf7, 0x13, 0x0e, 0x88, 0xdf, 0xe0, 0x31, 0xc7, 0x9d, 0xb9, - 0xd4, 0xf7, 0xc7, 0xa8, 0x99, 0x15, 0x1b, 0x9a, 0x47, 0x50, 0x32, 0xb6, 0x3f, - 0xc3, 0x85, 0x24, 0x5f, 0xe0, 0x54, 0xe3, 0xdd, 0x5a, 0x97, 0xa5, 0xf5, 0x76, - 0xfe, 0x06, 0x40, 0x25, 0xd3, 0xce, 0x04, 0x2c, 0x56, 0x6a, 0xb2, 0xc5, 0x07, - 0xb1, 0x38, 0xdb, 0x85, 0x3e, 0x3d, 0x69, 0x59, 0x66, 0x09, 0x96, 0x54, 0x6c, - 0xc9, 0xc4, 0xa6, 0xea, 0xfd, 0xc7, 0x77, 0xc0, 0x40, 0xd7, 0x0e, 0xaf, 0x46, - 0xf7, 0x6d, 0xad, 0x39, 0x79, 0xe5, 0xc5, 0x36, 0x0c, 0x33, 0x17, 0x16, 0x6a, - 0x1c, 0x89, 0x4c, 0x94, 0xa3, 0x71, 0x87, 0x6a, 0x94, 0xdf, 0x76, 0x28, 0xfe, - 0x4e, 0xaa, 0xf2, 0xcc, 0xb2, 0x7d, 0x5a, 0xaa, 0xe0, 0xad, 0x7a, 0xd0, 0xf9, - 0xd4, 0xb6, 0xad, 0x3b, 0x54, 0x09, 0x87, 0x46, 0xd4, 0x52, 0x4d, 0x38, 0x40, - 0x7a, 0x6d, 0xeb, 0x3a, 0xb7, 0x8f, 0xab, 0x78, 0xc9, - ], - }, - ]; - - for tv in test_vectors.iter() { - let mut c = ChaCha20::new(&tv.key, &tv.nonce); - let input: Vec<u8> = repeat(0).take(tv.keystream.len()).collect(); - let mut output: Vec<u8> = repeat(0).take(input.len()).collect(); - c.process(&input[..], &mut output[..]); - assert_eq!(output, tv.keystream); - } - } - - #[test] - fn test_chacha20_256_tls_vectors_96_nonce() { - struct TestVector { - key: [u8; 32], - nonce: [u8; 12], - keystream: Vec<u8>, - } - // taken from http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 - let test_vectors = [ - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - keystream: vec![ - 0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90, 0x40, 0x5d, 0x6a, 0xe5, 0x53, - 0x86, 0xbd, 0x28, 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a, 0xa8, 0x36, - 0xef, 0xcc, 0x8b, 0x77, 0x0d, 0xc7, 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, - 0x8d, 0x77, 0x24, 0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37, 0x6a, 0x43, 0xb8, 0xf4, - 0x15, 0x18, 0xa1, 0x1c, 0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86, - ], - }, - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - keystream: vec![ - 0x45, 0x40, 0xf0, 0x5a, 0x9f, 0x1f, 0xb2, 0x96, 0xd7, 0x73, 0x6e, 0x7b, 0x20, - 0x8e, 0x3c, 0x96, 0xeb, 0x4f, 0xe1, 0x83, 0x46, 0x88, 0xd2, 0x60, 0x4f, 0x45, - 0x09, 0x52, 0xed, 0x43, 0x2d, 0x41, 0xbb, 0xe2, 0xa0, 0xb6, 0xea, 0x75, 0x66, - 0xd2, 0xa5, 0xd1, 0xe7, 0xe2, 0x0d, 0x42, 0xaf, 0x2c, 0x53, 0xd7, 0x92, 0xb1, - 0xc4, 0x3f, 0xea, 0x81, 0x7e, 0x9a, 0xd2, 0x75, 0xae, 0x54, 0x69, 0x63, - ], - }, - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], - keystream: vec![ - 0xde, 0x9c, 0xba, 0x7b, 0xf3, 0xd6, 0x9e, 0xf5, 0xe7, 0x86, 0xdc, 0x63, 0x97, - 0x3f, 0x65, 0x3a, 0x0b, 0x49, 0xe0, 0x15, 0xad, 0xbf, 0xf7, 0x13, 0x4f, 0xcb, - 0x7d, 0xf1, 0x37, 0x82, 0x10, 0x31, 0xe8, 0x5a, 0x05, 0x02, 0x78, 0xa7, 0x08, - 0x45, 0x27, 0x21, 0x4f, 0x73, 0xef, 0xc7, 0xfa, 0x5b, 0x52, 0x77, 0x06, 0x2e, - 0xb7, 0xa0, 0x43, 0x3e, 0x44, 0x5f, 0x41, 0xe3, - ], - }, - TestVector { - key: [ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - keystream: vec![ - 0xef, 0x3f, 0xdf, 0xd6, 0xc6, 0x15, 0x78, 0xfb, 0xf5, 0xcf, 0x35, 0xbd, 0x3d, - 0xd3, 0x3b, 0x80, 0x09, 0x63, 0x16, 0x34, 0xd2, 0x1e, 0x42, 0xac, 0x33, 0x96, - 0x0b, 0xd1, 0x38, 0xe5, 0x0d, 0x32, 0x11, 0x1e, 0x4c, 0xaf, 0x23, 0x7e, 0xe5, - 0x3c, 0xa8, 0xad, 0x64, 0x26, 0x19, 0x4a, 0x88, 0x54, 0x5d, 0xdc, 0x49, 0x7a, - 0x0b, 0x46, 0x6e, 0x7d, 0x6b, 0xbd, 0xb0, 0x04, 0x1b, 0x2f, 0x58, 0x6b, - ], - }, - TestVector { - key: [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, - 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, - 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - ], - nonce: [0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], - keystream: vec![ - 0xf7, 0x98, 0xa1, 0x89, 0xf1, 0x95, 0xe6, 0x69, 0x82, 0x10, 0x5f, 0xfb, 0x64, - 0x0b, 0xb7, 0x75, 0x7f, 0x57, 0x9d, 0xa3, 0x16, 0x02, 0xfc, 0x93, 0xec, 0x01, - 0xac, 0x56, 0xf8, 0x5a, 0xc3, 0xc1, 0x34, 0xa4, 0x54, 0x7b, 0x73, 0x3b, 0x46, - 0x41, 0x30, 0x42, 0xc9, 0x44, 0x00, 0x49, 0x17, 0x69, 0x05, 0xd3, 0xbe, 0x59, - 0xea, 0x1c, 0x53, 0xf1, 0x59, 0x16, 0x15, 0x5c, 0x2b, 0xe8, 0x24, 0x1a, 0x38, - 0x00, 0x8b, 0x9a, 0x26, 0xbc, 0x35, 0x94, 0x1e, 0x24, 0x44, 0x17, 0x7c, 0x8a, - 0xde, 0x66, 0x89, 0xde, 0x95, 0x26, 0x49, 0x86, 0xd9, 0x58, 0x89, 0xfb, 0x60, - 0xe8, 0x46, 0x29, 0xc9, 0xbd, 0x9a, 0x5a, 0xcb, 0x1c, 0xc1, 0x18, 0xbe, 0x56, - 0x3e, 0xb9, 0xb3, 0xa4, 0xa4, 0x72, 0xf8, 0x2e, 0x09, 0xa7, 0xe7, 0x78, 0x49, - 0x2b, 0x56, 0x2e, 0xf7, 0x13, 0x0e, 0x88, 0xdf, 0xe0, 0x31, 0xc7, 0x9d, 0xb9, - 0xd4, 0xf7, 0xc7, 0xa8, 0x99, 0x15, 0x1b, 0x9a, 0x47, 0x50, 0x32, 0xb6, 0x3f, - 0xc3, 0x85, 0x24, 0x5f, 0xe0, 0x54, 0xe3, 0xdd, 0x5a, 0x97, 0xa5, 0xf5, 0x76, - 0xfe, 0x06, 0x40, 0x25, 0xd3, 0xce, 0x04, 0x2c, 0x56, 0x6a, 0xb2, 0xc5, 0x07, - 0xb1, 0x38, 0xdb, 0x85, 0x3e, 0x3d, 0x69, 0x59, 0x66, 0x09, 0x96, 0x54, 0x6c, - 0xc9, 0xc4, 0xa6, 0xea, 0xfd, 0xc7, 0x77, 0xc0, 0x40, 0xd7, 0x0e, 0xaf, 0x46, - 0xf7, 0x6d, 0xad, 0x39, 0x79, 0xe5, 0xc5, 0x36, 0x0c, 0x33, 0x17, 0x16, 0x6a, - 0x1c, 0x89, 0x4c, 0x94, 0xa3, 0x71, 0x87, 0x6a, 0x94, 0xdf, 0x76, 0x28, 0xfe, - 0x4e, 0xaa, 0xf2, 0xcc, 0xb2, 0x7d, 0x5a, 0xaa, 0xe0, 0xad, 0x7a, 0xd0, 0xf9, - 0xd4, 0xb6, 0xad, 0x3b, 0x54, 0x09, 0x87, 0x46, 0xd4, 0x52, 0x4d, 0x38, 0x40, - 0x7a, 0x6d, 0xeb, 0x3a, 0xb7, 0x8f, 0xab, 0x78, 0xc9, - ], - }, - ]; - - for tv in test_vectors.iter() { - let mut c = ChaCha20::new(&tv.key, &tv.nonce); - let input: Vec<u8> = repeat(0).take(tv.keystream.len()).collect(); - let mut output: Vec<u8> = repeat(0).take(input.len()).collect(); - c.process(&input[..], &mut output[..]); - assert_eq!(output, tv.keystream); - } - } - - #[test] - fn get_single_block() { - // Test that `get_single_block` (which takes a 16-byte nonce) is equivalent to getting a block - // using a 12-byte nonce, with the block starting at the counter offset given by the remaining 4 - // bytes. - let key = [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, - 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, - 0x1c, 0x1d, 0x1e, 0x1f, - ]; - let nonce_16bytes = [ - 0x00, 0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, - 0x0a, 0x0b, - ]; - let counter_pos = &nonce_16bytes[..4]; - let nonce_12bytes = &nonce_16bytes[4..]; - - // Initialize a ChaCha20 instance with its counter starting at 0. - let mut chacha20 = ChaCha20::new(&key, nonce_12bytes); - // Seek its counter to the block at counter_pos. - chacha20.seek_to_block(u32::from_le_bytes(counter_pos.try_into().unwrap())); - let mut block_bytes = [0; 32]; - chacha20.process_in_place(&mut block_bytes); - - assert_eq!(ChaCha20::get_single_block(&key, &nonce_16bytes), block_bytes); - } - - #[test] - fn encrypt_single_block() { - let key = [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, - 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, - 0x1c, 0x1d, 0x1e, 0x1f, - ]; - let nonce = [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, - 0x0e, 0x0f, - ]; - let bytes = [1; 32]; - - let mut encrypted_bytes = [0; 32]; - ChaCha20::encrypt_single_block(&key, &nonce, &mut encrypted_bytes, &bytes); - - let mut decrypted_bytes = [0; 32]; - ChaCha20::encrypt_single_block(&key, &nonce, &mut decrypted_bytes, &encrypted_bytes); - - assert_eq!(bytes, decrypted_bytes); - } - - #[test] - fn encrypt_single_block_in_place() { - let key = [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, - 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, - 0x1c, 0x1d, 0x1e, 0x1f, - ]; - let nonce = [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, - 0x0e, 0x0f, - ]; - let unencrypted_bytes = [1; 32]; - let mut bytes = unencrypted_bytes; - - ChaCha20::encrypt_single_block_in_place(&key, &nonce, &mut bytes); - assert_ne!(bytes, unencrypted_bytes); - - ChaCha20::encrypt_single_block_in_place(&key, &nonce, &mut bytes); - assert_eq!(bytes, unencrypted_bytes); - } -} diff --git a/lightning/src/crypto/chacha20poly1305rfc.rs b/lightning/src/crypto/chacha20poly1305rfc.rs deleted file mode 100644 index 839fad9ce6c..00000000000 --- a/lightning/src/crypto/chacha20poly1305rfc.rs +++ /dev/null @@ -1,157 +0,0 @@ -// ring has a garbage API so its use is avoided, but rust-crypto doesn't have RFC-variant poly1305 -// Instead, we steal rust-crypto's implementation and tweak it to match the RFC. -// -// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE -// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license -// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. -// You may not use this file except in accordance with one or both of these -// licenses. -// -// This is a port of Andrew Moons poly1305-donna -// https://github.com/floodyberry/poly1305-donna - -use super::chacha20::ChaCha20; -use super::fixed_time_eq; -use super::poly1305::Poly1305; - -pub struct ChaCha20Poly1305RFC { - cipher: ChaCha20, - mac: Poly1305, - finished: bool, - data_len: usize, - aad_len: u64, -} - -impl ChaCha20Poly1305RFC { - #[inline] - fn pad_mac_16(mac: &mut Poly1305, len: usize) { - if len % 16 != 0 { - mac.input(&[0; 16][0..16 - (len % 16)]); - } - } - pub fn new(key: &[u8], nonce: &[u8], aad: &[u8]) -> ChaCha20Poly1305RFC { - assert!(key.len() == 16 || key.len() == 32); - assert!(nonce.len() == 12); - - // Ehh, I'm too lazy to *also* tweak ChaCha20 to make it RFC-compliant - assert!(nonce[0] == 0 && nonce[1] == 0 && nonce[2] == 0 && nonce[3] == 0); - - let mut cipher = ChaCha20::new(key, &nonce[4..]); - let mut mac_key = [0u8; 64]; - let zero_key = [0u8; 64]; - cipher.process(&zero_key, &mut mac_key); - - #[cfg(not(fuzzing))] - let mut mac = Poly1305::new(&mac_key[..32]); - #[cfg(fuzzing)] - let mut mac = Poly1305::new(&key); - mac.input(aad); - ChaCha20Poly1305RFC::pad_mac_16(&mut mac, aad.len()); - - ChaCha20Poly1305RFC { cipher, mac, finished: false, data_len: 0, aad_len: aad.len() as u64 } - } - - pub fn encrypt(&mut self, input: &[u8], output: &mut [u8], out_tag: &mut [u8]) { - assert!(input.len() == output.len()); - assert!(!self.finished); - self.cipher.process(input, output); - self.data_len += input.len(); - self.mac.input(output); - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.finished = true; - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); - out_tag.copy_from_slice(&self.mac.result()); - } - - pub fn encrypt_full_message_in_place(&mut self, input_output: &mut [u8], out_tag: &mut [u8]) { - self.encrypt_in_place(input_output); - self.finish_and_get_tag(out_tag); - } - - // Encrypt `input_output` in-place. To finish and calculate the tag, use `finish_and_get_tag` - // below. - pub(in super::super) fn encrypt_in_place(&mut self, input_output: &mut [u8]) { - debug_assert!(!self.finished); - self.cipher.process_in_place(input_output); - self.data_len += input_output.len(); - self.mac.input(input_output); - } - - // If we were previously encrypting with `encrypt_in_place`, this method can be used to finish - // encrypting and calculate the tag. - pub(in super::super) fn finish_and_get_tag(&mut self, out_tag: &mut [u8]) { - debug_assert!(!self.finished); - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.finished = true; - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); - out_tag.copy_from_slice(&self.mac.result()); - } - - /// Decrypt the `input`, checking the given `tag` prior to writing the decrypted contents - /// into `output`. Note that, because `output` is not touched until the `tag` is checked, - /// this decryption is *variable time*. - pub fn variable_time_decrypt( - &mut self, input: &[u8], output: &mut [u8], tag: &[u8], - ) -> Result<(), ()> { - assert!(input.len() == output.len()); - assert!(!self.finished); - - self.finished = true; - - self.mac.input(input); - - self.data_len += input.len(); - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); - - let calc_tag = self.mac.result(); - if fixed_time_eq(&calc_tag, tag) { - self.cipher.process(input, output); - Ok(()) - } else { - Err(()) - } - } - - pub fn check_decrypt_in_place( - &mut self, input_output: &mut [u8], tag: &[u8], - ) -> Result<(), ()> { - self.decrypt_in_place(input_output); - if self.finish_and_check_tag(tag) { - Ok(()) - } else { - Err(()) - } - } - - /// Decrypt in place, without checking the tag. Use `finish_and_check_tag` to check it - /// later when decryption finishes. - /// - /// Should never be `pub` because the public API should always enforce tag checking. - pub(in super::super) fn decrypt_in_place(&mut self, input_output: &mut [u8]) { - debug_assert!(!self.finished); - self.mac.input(input_output); - self.data_len += input_output.len(); - self.cipher.process_in_place(input_output); - } - - /// If we were previously decrypting with `just_decrypt_in_place`, this method must be used - /// to check the tag. Returns whether or not the tag is valid. - pub(in super::super) fn finish_and_check_tag(&mut self, tag: &[u8]) -> bool { - debug_assert!(!self.finished); - self.finished = true; - ChaCha20Poly1305RFC::pad_mac_16(&mut self.mac, self.data_len); - self.mac.input(&self.aad_len.to_le_bytes()); - self.mac.input(&(self.data_len as u64).to_le_bytes()); - - let calc_tag = self.mac.result(); - if fixed_time_eq(&calc_tag, tag) { - true - } else { - false - } - } -} diff --git a/lightning/src/crypto/mod.rs b/lightning/src/crypto/mod.rs index 478918a49a8..73d7ad64685 100644 --- a/lightning/src/crypto/mod.rs +++ b/lightning/src/crypto/mod.rs @@ -7,8 +7,5 @@ fn fixed_time_eq(a: &[u8], b: &[u8]) -> bool { a == b } -pub(crate) mod chacha20; -pub(crate) mod chacha20poly1305rfc; -pub(crate) mod poly1305; pub(crate) mod streams; pub(crate) mod utils; diff --git a/lightning/src/crypto/poly1305.rs b/lightning/src/crypto/poly1305.rs deleted file mode 100644 index a71e39ed773..00000000000 --- a/lightning/src/crypto/poly1305.rs +++ /dev/null @@ -1,434 +0,0 @@ -// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE -// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license -// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -// This is a port of Andrew Moons poly1305-donna -// https://github.com/floodyberry/poly1305-donna - -#[cfg(not(fuzzing))] -mod real_poly1305 { - use core::cmp::min; - - #[derive(Clone, Copy)] - pub struct Poly1305 { - r: [u32; 5], - h: [u32; 5], - pad: [u32; 4], - leftover: usize, - buffer: [u8; 16], - finalized: bool, - } - - impl Poly1305 { - pub fn new(key: &[u8]) -> Poly1305 { - assert!(key.len() == 32); - let mut poly = Poly1305 { - r: [0u32; 5], - h: [0u32; 5], - pad: [0u32; 4], - leftover: 0, - buffer: [0u8; 16], - finalized: false, - }; - - // r &= 0xffffffc0ffffffc0ffffffc0fffffff - poly.r[0] = (u32::from_le_bytes(key[0..4].try_into().expect("len is 4"))) & 0x3ffffff; - poly.r[1] = - (u32::from_le_bytes(key[3..7].try_into().expect("len is 4")) >> 2) & 0x3ffff03; - poly.r[2] = - (u32::from_le_bytes(key[6..10].try_into().expect("len is 4")) >> 4) & 0x3ffc0ff; - poly.r[3] = - (u32::from_le_bytes(key[9..13].try_into().expect("len is 4")) >> 6) & 0x3f03fff; - poly.r[4] = - (u32::from_le_bytes(key[12..16].try_into().expect("len is 4")) >> 8) & 0x00fffff; - - poly.pad[0] = u32::from_le_bytes(key[16..20].try_into().expect("len is 4")); - poly.pad[1] = u32::from_le_bytes(key[20..24].try_into().expect("len is 4")); - poly.pad[2] = u32::from_le_bytes(key[24..28].try_into().expect("len is 4")); - poly.pad[3] = u32::from_le_bytes(key[28..32].try_into().expect("len is 4")); - - poly - } - - fn block(&mut self, m: &[u8]) { - let hibit: u32 = if self.finalized { 0 } else { 1 << 24 }; - - let r0 = self.r[0]; - let r1 = self.r[1]; - let r2 = self.r[2]; - let r3 = self.r[3]; - let r4 = self.r[4]; - - let s1 = r1 * 5; - let s2 = r2 * 5; - let s3 = r3 * 5; - let s4 = r4 * 5; - - let mut h0 = self.h[0]; - let mut h1 = self.h[1]; - let mut h2 = self.h[2]; - let mut h3 = self.h[3]; - let mut h4 = self.h[4]; - - // h += m - h0 += (u32::from_le_bytes(m[0..4].try_into().expect("len is 4"))) & 0x3ffffff; - h1 += (u32::from_le_bytes(m[3..7].try_into().expect("len is 4")) >> 2) & 0x3ffffff; - h2 += (u32::from_le_bytes(m[6..10].try_into().expect("len is 4")) >> 4) & 0x3ffffff; - h3 += (u32::from_le_bytes(m[9..13].try_into().expect("len is 4")) >> 6) & 0x3ffffff; - h4 += (u32::from_le_bytes(m[12..16].try_into().expect("len is 4")) >> 8) | hibit; - - // h *= r - let d0 = (h0 as u64 * r0 as u64) - + (h1 as u64 * s4 as u64) - + (h2 as u64 * s3 as u64) - + (h3 as u64 * s2 as u64) - + (h4 as u64 * s1 as u64); - let mut d1 = (h0 as u64 * r1 as u64) - + (h1 as u64 * r0 as u64) - + (h2 as u64 * s4 as u64) - + (h3 as u64 * s3 as u64) - + (h4 as u64 * s2 as u64); - let mut d2 = (h0 as u64 * r2 as u64) - + (h1 as u64 * r1 as u64) - + (h2 as u64 * r0 as u64) - + (h3 as u64 * s4 as u64) - + (h4 as u64 * s3 as u64); - let mut d3 = (h0 as u64 * r3 as u64) - + (h1 as u64 * r2 as u64) - + (h2 as u64 * r1 as u64) - + (h3 as u64 * r0 as u64) - + (h4 as u64 * s4 as u64); - let mut d4 = (h0 as u64 * r4 as u64) - + (h1 as u64 * r3 as u64) - + (h2 as u64 * r2 as u64) - + (h3 as u64 * r1 as u64) - + (h4 as u64 * r0 as u64); - - // (partial) h %= p - let mut c: u32; - c = (d0 >> 26) as u32; - h0 = d0 as u32 & 0x3ffffff; - d1 += c as u64; - c = (d1 >> 26) as u32; - h1 = d1 as u32 & 0x3ffffff; - d2 += c as u64; - c = (d2 >> 26) as u32; - h2 = d2 as u32 & 0x3ffffff; - d3 += c as u64; - c = (d3 >> 26) as u32; - h3 = d3 as u32 & 0x3ffffff; - d4 += c as u64; - c = (d4 >> 26) as u32; - h4 = d4 as u32 & 0x3ffffff; - h0 += c * 5; - c = h0 >> 26; - h0 &= 0x3ffffff; - h1 += c; - - self.h[0] = h0; - self.h[1] = h1; - self.h[2] = h2; - self.h[3] = h3; - self.h[4] = h4; - } - - pub fn finish(&mut self) { - if self.leftover > 0 { - self.buffer[self.leftover] = 1; - for i in self.leftover + 1..16 { - self.buffer[i] = 0; - } - self.finalized = true; - let tmp = self.buffer; - self.block(&tmp); - } - - // fully carry h - let mut h0 = self.h[0]; - let mut h1 = self.h[1]; - let mut h2 = self.h[2]; - let mut h3 = self.h[3]; - let mut h4 = self.h[4]; - - let mut c: u32; - c = h1 >> 26; - h1 &= 0x3ffffff; - h2 += c; - c = h2 >> 26; - h2 &= 0x3ffffff; - h3 += c; - c = h3 >> 26; - h3 &= 0x3ffffff; - h4 += c; - c = h4 >> 26; - h4 &= 0x3ffffff; - h0 += c * 5; - c = h0 >> 26; - h0 &= 0x3ffffff; - h1 += c; - - // compute h + -p - let mut g0 = h0.wrapping_add(5); - c = g0 >> 26; - g0 &= 0x3ffffff; - let mut g1 = h1.wrapping_add(c); - c = g1 >> 26; - g1 &= 0x3ffffff; - let mut g2 = h2.wrapping_add(c); - c = g2 >> 26; - g2 &= 0x3ffffff; - let mut g3 = h3.wrapping_add(c); - c = g3 >> 26; - g3 &= 0x3ffffff; - let mut g4 = h4.wrapping_add(c).wrapping_sub(1 << 26); - - // select h if h < p, or h + -p if h >= p - let mut mask = (g4 >> (32 - 1)).wrapping_sub(1); - g0 &= mask; - g1 &= mask; - g2 &= mask; - g3 &= mask; - g4 &= mask; - mask = !mask; - h0 = (h0 & mask) | g0; - h1 = (h1 & mask) | g1; - h2 = (h2 & mask) | g2; - h3 = (h3 & mask) | g3; - h4 = (h4 & mask) | g4; - - // h = h % (2^128) - h0 = ((h0) | (h1 << 26)) & 0xffffffff; - h1 = ((h1 >> 6) | (h2 << 20)) & 0xffffffff; - h2 = ((h2 >> 12) | (h3 << 14)) & 0xffffffff; - h3 = ((h3 >> 18) | (h4 << 8)) & 0xffffffff; - - // h = mac = (h + pad) % (2^128) - let mut f: u64; - f = h0 as u64 + self.pad[0] as u64; - h0 = f as u32; - f = h1 as u64 + self.pad[1] as u64 + (f >> 32); - h1 = f as u32; - f = h2 as u64 + self.pad[2] as u64 + (f >> 32); - h2 = f as u32; - f = h3 as u64 + self.pad[3] as u64 + (f >> 32); - h3 = f as u32; - - self.h[0] = h0; - self.h[1] = h1; - self.h[2] = h2; - self.h[3] = h3; - } - - pub fn input(&mut self, data: &[u8]) { - assert!(!self.finalized); - let mut m = data; - - if self.leftover > 0 { - let want = min(16 - self.leftover, m.len()); - for i in 0..want { - self.buffer[self.leftover + i] = m[i]; - } - m = &m[want..]; - self.leftover += want; - - if self.leftover < 16 { - return; - } - - // self.block(self.buffer[..]); - let tmp = self.buffer; - self.block(&tmp); - - self.leftover = 0; - } - - while m.len() >= 16 { - self.block(&m[0..16]); - m = &m[16..]; - } - - for i in 0..m.len() { - self.buffer[i] = m[i]; - } - self.leftover = m.len(); - } - - pub fn result(&mut self) -> [u8; 16] { - if !self.finalized { - self.finish(); - } - let mut output = [0; 16]; - output[0..4].copy_from_slice(&self.h[0].to_le_bytes()); - output[4..8].copy_from_slice(&self.h[1].to_le_bytes()); - output[8..12].copy_from_slice(&self.h[2].to_le_bytes()); - output[12..16].copy_from_slice(&self.h[3].to_le_bytes()); - output - } - } - - #[cfg(test)] - mod test { - use core::iter::repeat; - - use super::Poly1305; - - fn poly1305(key: &[u8], msg: &[u8], mac: &mut [u8; 16]) { - let mut poly = Poly1305::new(key); - poly.input(msg); - *mac = poly.result(); - } - - #[test] - fn test_nacl_vector() { - let key = [ - 0xee, 0xa6, 0xa7, 0x25, 0x1c, 0x1e, 0x72, 0x91, 0x6d, 0x11, 0xc2, 0xcb, 0x21, 0x4d, - 0x3c, 0x25, 0x25, 0x39, 0x12, 0x1d, 0x8e, 0x23, 0x4e, 0x65, 0x2d, 0x65, 0x1f, 0xa4, - 0xc8, 0xcf, 0xf8, 0x80, - ]; - - let msg = [ - 0x8e, 0x99, 0x3b, 0x9f, 0x48, 0x68, 0x12, 0x73, 0xc2, 0x96, 0x50, 0xba, 0x32, 0xfc, - 0x76, 0xce, 0x48, 0x33, 0x2e, 0xa7, 0x16, 0x4d, 0x96, 0xa4, 0x47, 0x6f, 0xb8, 0xc5, - 0x31, 0xa1, 0x18, 0x6a, 0xc0, 0xdf, 0xc1, 0x7c, 0x98, 0xdc, 0xe8, 0x7b, 0x4d, 0xa7, - 0xf0, 0x11, 0xec, 0x48, 0xc9, 0x72, 0x71, 0xd2, 0xc2, 0x0f, 0x9b, 0x92, 0x8f, 0xe2, - 0x27, 0x0d, 0x6f, 0xb8, 0x63, 0xd5, 0x17, 0x38, 0xb4, 0x8e, 0xee, 0xe3, 0x14, 0xa7, - 0xcc, 0x8a, 0xb9, 0x32, 0x16, 0x45, 0x48, 0xe5, 0x26, 0xae, 0x90, 0x22, 0x43, 0x68, - 0x51, 0x7a, 0xcf, 0xea, 0xbd, 0x6b, 0xb3, 0x73, 0x2b, 0xc0, 0xe9, 0xda, 0x99, 0x83, - 0x2b, 0x61, 0xca, 0x01, 0xb6, 0xde, 0x56, 0x24, 0x4a, 0x9e, 0x88, 0xd5, 0xf9, 0xb3, - 0x79, 0x73, 0xf6, 0x22, 0xa4, 0x3d, 0x14, 0xa6, 0x59, 0x9b, 0x1f, 0x65, 0x4c, 0xb4, - 0x5a, 0x74, 0xe3, 0x55, 0xa5, - ]; - - let expected = [ - 0xf3, 0xff, 0xc7, 0x70, 0x3f, 0x94, 0x00, 0xe5, 0x2a, 0x7d, 0xfb, 0x4b, 0x3d, 0x33, - 0x05, 0xd9, - ]; - - let mut mac = [0u8; 16]; - poly1305(&key, &msg, &mut mac); - assert_eq!(&mac[..], &expected[..]); - - let mut poly = Poly1305::new(&key); - poly.input(&msg[0..32]); - poly.input(&msg[32..96]); - poly.input(&msg[96..112]); - poly.input(&msg[112..120]); - poly.input(&msg[120..124]); - poly.input(&msg[124..126]); - poly.input(&msg[126..127]); - poly.input(&msg[127..128]); - poly.input(&msg[128..129]); - poly.input(&msg[129..130]); - poly.input(&msg[130..131]); - let mac = poly.result(); - assert_eq!(&mac[..], &expected[..]); - } - - #[test] - fn donna_self_test() { - let wrap_key = [ - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - ]; - - let wrap_msg = [ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0xff, 0xff, - ]; - - let wrap_mac = [ - 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]; - - let mut mac = [0u8; 16]; - poly1305(&wrap_key, &wrap_msg, &mut mac); - assert_eq!(&mac[..], &wrap_mac[..]); - - let total_key = [ - 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - 0x00, 0x00, 0x00, 0x00, - ]; - - let total_mac = [ - 0x64, 0xaf, 0xe2, 0xe8, 0xd6, 0xad, 0x7b, 0xbd, 0xd2, 0x87, 0xf9, 0x7c, 0x44, 0x62, - 0x3d, 0x39, - ]; - - let mut tpoly = Poly1305::new(&total_key); - for i in 0..256 { - let key: Vec<u8> = repeat(i as u8).take(32).collect(); - let msg: Vec<u8> = repeat(i as u8).take(256).collect(); - let mut mac = [0u8; 16]; - poly1305(&key[..], &msg[0..i], &mut mac); - tpoly.input(&mac); - } - let mac = tpoly.result(); - assert_eq!(&mac[..], &total_mac[..]); - } - - #[test] - fn test_tls_vectors() { - // from http://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 - let key = b"this is 32-byte key for Poly1305"; - let msg = [0u8; 32]; - let expected = [ - 0x49, 0xec, 0x78, 0x09, 0x0e, 0x48, 0x1e, 0xc6, 0xc2, 0x6b, 0x33, 0xb9, 0x1c, 0xcc, - 0x03, 0x07, - ]; - let mut mac = [0u8; 16]; - poly1305(key, &msg, &mut mac); - assert_eq!(&mac[..], &expected[..]); - - let msg = b"Hello world!"; - let expected = [ - 0xa6, 0xf7, 0x45, 0x00, 0x8f, 0x81, 0xc9, 0x16, 0xa2, 0x0d, 0xcc, 0x74, 0xee, 0xf2, - 0xb2, 0xf0, - ]; - poly1305(key, msg, &mut mac); - assert_eq!(&mac[..], &expected[..]); - } - } -} -#[cfg(not(fuzzing))] -pub use real_poly1305::*; - -#[cfg(fuzzing)] -mod fuzzy_poly1305 { - #[derive(Clone, Copy)] - pub struct Poly1305 { - tag: [u8; 16], - finalized: bool, - } - - impl Poly1305 { - pub fn new(key: &[u8]) -> Poly1305 { - assert_eq!(key.len(), 32); - let mut poly = Poly1305 { tag: [0; 16], finalized: false }; - poly.tag.copy_from_slice(&key[..16]); - - poly - } - - pub fn finish(&mut self) { - self.finalized = true; - } - - pub fn input(&mut self, _data: &[u8]) { - assert!(!self.finalized); - } - - pub fn result(&mut self) -> [u8; 16] { - if !self.finalized { - self.finish(); - } - self.tag - } - } -} -#[cfg(fuzzing)] -pub use fuzzy_poly1305::*; diff --git a/lightning/src/crypto/streams.rs b/lightning/src/crypto/streams.rs index c406e933bc9..ff34b86755e 100644 --- a/lightning/src/crypto/streams.rs +++ b/lightning/src/crypto/streams.rs @@ -1,7 +1,4 @@ -use crate::crypto::chacha20::ChaCha20; -use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; use crate::crypto::fixed_time_eq; -use crate::crypto::poly1305::Poly1305; use crate::io::{self, Read, Write}; use crate::ln::msgs::DecodeError; @@ -10,6 +7,10 @@ use crate::util::ser::{ }; use alloc::vec::Vec; +use chacha20_poly1305::{ + chacha20::{ChaCha20, Key, Nonce}, + poly1305::Poly1305, +}; pub(crate) struct ChaChaReader<'a, R: io::Read> { pub chacha: &'a mut ChaCha20, @@ -19,7 +20,7 @@ impl<'a, R: io::Read> io::Read for ChaChaReader<'a, R> { fn read(&mut self, dest: &mut [u8]) -> Result<usize, io::Error> { let res = self.read.read(dest)?; if res > 0 { - self.chacha.process_in_place(&mut dest[0..res]); + self.chacha.apply_keystream(&mut dest[..res]); } Ok(res) } @@ -42,11 +43,20 @@ impl<'a, W: Writeable> ChaChaPolyWriteAdapter<'a, W> { impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> { // Simultaneously write and encrypt Self::writeable. fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { - let mut chacha = ChaCha20Poly1305RFC::new(&self.rho, &[0; 12], &[]); - let mut chacha_stream = ChaChaPolyWriter { chacha: &mut chacha, write: w }; + let mut chacha = ChaCha20::new(Key::new(self.rho), Nonce::new([0; 12]), 0); + let mut mac_key = [0u8; 64]; + chacha.apply_keystream(&mut mac_key); + + #[cfg(not(fuzzing))] + let mac = Poly1305::new(mac_key[..32].try_into().unwrap()); + #[cfg(fuzzing)] + let mac = Poly1305::new(self.rho); + + let mut chacha_stream = + ChaChaPolyWriter { chacha: &mut chacha, poly: mac, write_len: 0, write: w }; self.writeable.write(&mut chacha_stream)?; - let mut tag = [0 as u8; 16]; - chacha.finish_and_get_tag(&mut tag); + + let tag = chacha_stream.finish_and_get_tag(); tag.write(w)?; Ok(()) @@ -58,16 +68,19 @@ impl<'a, T: Writeable> Writeable for ChaChaPolyWriteAdapter<'a, T> { } /// Encrypts the provided plaintext with the given key using ChaCha20Poly1305 in the modified -/// with-AAD form used in [`ChaChaDualPolyReadAdapter`]. +/// with-AAD form used in [`ChaChaTriPolyReadAdapter`]. pub(crate) fn chachapoly_encrypt_with_swapped_aad( mut plaintext: Vec<u8>, key: [u8; 32], aad: [u8; 32], ) -> Vec<u8> { - let mut chacha = ChaCha20::new(&key[..], &[0; 12]); + let mut chacha = ChaCha20::new(Key::new(key), Nonce::new([0; 12]), 0); let mut mac_key = [0u8; 64]; - chacha.process_in_place(&mut mac_key); + chacha.apply_keystream(&mut mac_key); - let mut mac = Poly1305::new(&mac_key[..32]); - chacha.process_in_place(&mut plaintext[..]); + #[cfg(not(fuzzing))] + let mut mac = Poly1305::new(mac_key[..32].try_into().unwrap()); + #[cfg(fuzzing)] + let mut mac = Poly1305::new(key); + chacha.apply_keystream(&mut plaintext[..]); mac.input(&plaintext[..]); if plaintext.len() % 16 != 0 { @@ -80,58 +93,71 @@ pub(crate) fn chachapoly_encrypt_with_swapped_aad( mac.input(&(plaintext.len() as u64).to_le_bytes()); mac.input(&32u64.to_le_bytes()); - plaintext.extend_from_slice(&mac.result()); + plaintext.extend_from_slice(&mac.tag()); plaintext } +#[derive(PartialEq, Eq)] +pub(crate) enum TriPolyAADUsed { + /// No AAD was used. + /// + /// The HMAC validated with standard ChaCha20Poly1305. + None, + /// The HMAC vlidated using the first AAD provided. + First, + /// The HMAC vlidated using the second AAD provided. + Second, +} + /// Enables the use of the serialization macros for objects that need to be simultaneously decrypted /// and deserialized. This allows us to avoid an intermediate Vec allocation. /// -/// This variant of [`ChaChaPolyReadAdapter`] calculates Poly1305 tags twice, once using the given -/// key and once with the given 32-byte AAD appended after the encrypted stream, accepting either -/// being correct as sufficient. +/// This variant of [`ChaChaPolyReadAdapter`] calculates Poly1305 tags thrice, once using the given +/// key and once each for the two given 32-byte AADs appended after the encrypted stream, accepting +/// any being correct as sufficient. /// -/// Note that we do *not* use the provided AAD as the standard ChaCha20Poly1305 AAD as that would +/// Note that we do *not* use the provided AADs as the standard ChaCha20Poly1305 AAD as that would /// require placing it first and prevent us from avoiding redundant Poly1305 rounds. Instead, the -/// ChaCha20Poly1305 MAC check is tweaked to move the AAD to *after* the the contents being +/// ChaCha20Poly1305 MAC check is tweaked to move the AAD to *after* the contents being /// checked, effectively treating the contents as the AAD for the AAD-containing MAC but behaving /// like classic ChaCha20Poly1305 for the non-AAD-containing MAC. -pub(crate) struct ChaChaDualPolyReadAdapter<R: Readable> { +pub(crate) struct ChaChaTriPolyReadAdapter<R: Readable> { pub readable: R, - pub used_aad: bool, + pub used_aad: TriPolyAADUsed, } -impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyReadAdapter<T> { +impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32], [u8; 32])> + for ChaChaTriPolyReadAdapter<T> +{ // Simultaneously read and decrypt an object from a LengthLimitedRead storing it in // Self::readable. LengthLimitedRead must be used instead of std::io::Read because we need the // total length to separate out the tag at the end. fn read<R: LengthLimitedRead>( - r: &mut R, params: ([u8; 32], [u8; 32]), + r: &mut R, params: ([u8; 32], [u8; 32], [u8; 32]), ) -> Result<Self, DecodeError> { if r.remaining_bytes() < 16 { return Err(DecodeError::InvalidValue); } - let (key, aad) = params; + let (key, aad_a, aad_b) = params; - let mut chacha = ChaCha20::new(&key[..], &[0; 12]); + let mut chacha = ChaCha20::new(Key::new(key), Nonce::new([0; 12]), 0); let mut mac_key = [0u8; 64]; - chacha.process_in_place(&mut mac_key); + chacha.apply_keystream(&mut mac_key); #[cfg(not(fuzzing))] - let mut mac = Poly1305::new(&mac_key[..32]); + let mut mac = Poly1305::new(mac_key[..32].try_into().unwrap()); #[cfg(fuzzing)] - let mut mac = Poly1305::new(&key); + let mut mac = Poly1305::new(key); let decrypted_len = r.remaining_bytes() - 16; let s = FixedLengthReader::new(r, decrypted_len); let mut chacha_stream = - ChaChaDualPolyReader { chacha: &mut chacha, poly: &mut mac, read_len: 0, read: s }; + ChaChaTriPolyReader { chacha: &mut chacha, poly: &mut mac, read_len: 0, read: s }; let readable: T = Readable::read(&mut chacha_stream)?; while chacha_stream.read.bytes_remain() { let mut buf = [0; 256]; if chacha_stream.read(&mut buf)? == 0 { - // Reached EOF return Err(DecodeError::ShortRead); } } @@ -142,47 +168,53 @@ impl<T: Readable> LengthReadableArgs<([u8; 32], [u8; 32])> for ChaChaDualPolyRea mac.input(&[0; 16][0..16 - (read_len % 16)]); } - let mut mac_aad = mac; + let mut mac_aad_a = mac; + let mut mac_aad_b = mac; - mac_aad.input(&aad[..]); + mac_aad_a.input(&aad_a[..]); + mac_aad_b.input(&aad_b[..]); // Note that we don't need to pad the AAD since its a multiple of 16 bytes // For the AAD-containing MAC, swap the AAD and the read data, effectively. - mac_aad.input(&(read_len as u64).to_le_bytes()); - mac_aad.input(&32u64.to_le_bytes()); + mac_aad_a.input(&(read_len as u64).to_le_bytes()); + mac_aad_b.input(&(read_len as u64).to_le_bytes()); + mac_aad_a.input(&32u64.to_le_bytes()); + mac_aad_b.input(&32u64.to_le_bytes()); // For the non-AAD-containing MAC, leave the data and AAD where they belong. mac.input(&0u64.to_le_bytes()); mac.input(&(read_len as u64).to_le_bytes()); - let mut tag = [0 as u8; 16]; + let mut tag = [0u8; 16]; r.read_exact(&mut tag)?; - if fixed_time_eq(&mac.result(), &tag) { - Ok(Self { readable, used_aad: false }) - } else if fixed_time_eq(&mac_aad.result(), &tag) { - Ok(Self { readable, used_aad: true }) + if fixed_time_eq(&mac.tag(), &tag) { + Ok(Self { readable, used_aad: TriPolyAADUsed::None }) + } else if fixed_time_eq(&mac_aad_a.tag(), &tag) { + Ok(Self { readable, used_aad: TriPolyAADUsed::First }) + } else if fixed_time_eq(&mac_aad_b.tag(), &tag) { + Ok(Self { readable, used_aad: TriPolyAADUsed::Second }) } else { return Err(DecodeError::InvalidValue); } } } -struct ChaChaDualPolyReader<'a, R: Read> { +struct ChaChaTriPolyReader<'a, R: Read> { chacha: &'a mut ChaCha20, poly: &'a mut Poly1305, read_len: usize, pub read: R, } -impl<'a, R: Read> Read for ChaChaDualPolyReader<'a, R> { +impl<'a, R: Read> Read for ChaChaTriPolyReader<'a, R> { // Decrypts bytes from Self::read into `dest`. // After all reads complete, the caller must compare the expected tag with - // the result of `Poly1305::result()`. + // the result of `Poly1305::tag()` fn read(&mut self, dest: &mut [u8]) -> Result<usize, io::Error> { let res = self.read.read(dest)?; if res > 0 { - self.poly.input(&dest[0..res]); - self.chacha.process_in_place(&mut dest[0..res]); + self.poly.input(&dest[..res]); + self.chacha.apply_keystream(&mut dest[..res]); self.read_len += res; } Ok(res) @@ -204,19 +236,38 @@ impl<T: Readable> LengthReadableArgs<[u8; 32]> for ChaChaPolyReadAdapter<T> { return Err(DecodeError::InvalidValue); } - let mut chacha = ChaCha20Poly1305RFC::new(&secret, &[0; 12], &[]); + let mut chacha = ChaCha20::new(Key::new(secret), Nonce::new([0; 12]), 0); + let mut mac_key = [0u8; 64]; + chacha.apply_keystream(&mut mac_key); + + #[cfg(not(fuzzing))] + let mut mac = Poly1305::new(mac_key[..32].try_into().unwrap()); + #[cfg(fuzzing)] + let mut mac = Poly1305::new(secret); + let decrypted_len = r.remaining_bytes() - 16; let s = FixedLengthReader::new(r, decrypted_len); - let mut chacha_stream = ChaChaPolyReader { chacha: &mut chacha, read: s }; + let mut chacha_stream = ChaChaPolyReader::new(&mut chacha, &mut mac, s); let readable: T = Readable::read(&mut chacha_stream)?; while chacha_stream.read.bytes_remain() { let mut buf = [0; 256]; - chacha_stream.read(&mut buf)?; + if chacha_stream.read(&mut buf)? == 0 { + return Err(DecodeError::ShortRead); + } } - let mut tag = [0 as u8; 16]; + let read_len = chacha_stream.read_len(); + drop(chacha_stream); + + if read_len % 16 != 0 { + mac.input(&[0; 16][0..16 - (read_len % 16)]); + } + mac.input(&0u64.to_le_bytes()); + mac.input(&(read_len as u64).to_le_bytes()); + + let mut tag = [0u8; 16]; r.read_exact(&mut tag)?; - if !chacha.finish_and_check_tag(&tag) { + if !fixed_time_eq(&mac.tag(), &tag) { return Err(DecodeError::InvalidValue); } @@ -224,20 +275,32 @@ impl<T: Readable> LengthReadableArgs<[u8; 32]> for ChaChaPolyReadAdapter<T> { } } -/// Enables simultaneously reading and decrypting a ChaCha20Poly1305RFC stream from a std::io::Read. +/// Enables simultaneously reading and decrypting a ChaCha20Poly1305 stream from a std::io::Read. struct ChaChaPolyReader<'a, R: Read> { - pub chacha: &'a mut ChaCha20Poly1305RFC, + chacha: &'a mut ChaCha20, + poly: &'a mut Poly1305, + read_len: usize, pub read: R, } +impl<'a, R: Read> ChaChaPolyReader<'a, R> { + fn new(chacha: &'a mut ChaCha20, poly: &'a mut Poly1305, read: R) -> Self { + Self { chacha, poly, read_len: 0, read } + } + + fn read_len(&self) -> usize { + self.read_len + } +} + impl<'a, R: Read> Read for ChaChaPolyReader<'a, R> { // Decrypt bytes from Self::read into `dest`. - // `ChaCha20Poly1305RFC::finish_and_check_tag` must be called to check the tag after all reads - // complete. fn read(&mut self, dest: &mut [u8]) -> Result<usize, io::Error> { let res = self.read.read(dest)?; if res > 0 { - self.chacha.decrypt_in_place(&mut dest[0..res]); + self.poly.input(&dest[..res]); + self.chacha.apply_keystream(&mut dest[..res]); + self.read_len += res; } Ok(res) } @@ -245,14 +308,26 @@ impl<'a, R: Read> Read for ChaChaPolyReader<'a, R> { /// Enables simultaneously writing and encrypting a byte stream into a Writer. struct ChaChaPolyWriter<'a, W: Writer> { - pub chacha: &'a mut ChaCha20Poly1305RFC, + chacha: &'a mut ChaCha20, + poly: Poly1305, + write_len: usize, pub write: &'a mut W, } +impl<'a, W: Writer> ChaChaPolyWriter<'a, W> { + /// Finish encrypting and return the 16-byte authentication tag. + fn finish_and_get_tag(mut self) -> [u8; 16] { + if self.write_len % 16 != 0 { + self.poly.input(&[0; 16][0..16 - (self.write_len % 16)]); + } + self.poly.input(&0u64.to_le_bytes()); + self.poly.input(&(self.write_len as u64).to_le_bytes()); + self.poly.tag() + } +} + impl<'a, W: Writer> Writer for ChaChaPolyWriter<'a, W> { // Encrypt then write bytes from `src` into Self::write. - // `ChaCha20Poly1305RFC::finish_and_get_tag` can be called to retrieve the tag after all writes - // complete. fn write_all(&mut self, src: &[u8]) -> Result<(), io::Error> { let mut src_idx = 0; while src_idx < src.len() { @@ -260,8 +335,10 @@ impl<'a, W: Writer> Writer for ChaChaPolyWriter<'a, W> { let bytes_written = (&mut write_buffer[..]) .write(&src[src_idx..]) .expect("In-memory writes can't fail"); - self.chacha.encrypt_in_place(&mut write_buffer[..bytes_written]); + self.chacha.apply_keystream(&mut write_buffer[..bytes_written]); + self.poly.input(&write_buffer[..bytes_written]); self.write.write_all(&write_buffer[..bytes_written])?; + self.write_len += bytes_written; src_idx += bytes_written; } Ok(()) @@ -281,7 +358,7 @@ mod tests { field2: Vec<u8>, field3: Vec<u8>, } - impl_writeable_tlv_based!(TestWriteable, { + impl_ser_tlv_based!(TestWriteable, { (1, field1, required_vec), (2, field2, required_vec), (3, field3, required_vec), @@ -349,15 +426,15 @@ mod tests { } #[test] - fn short_read_chacha_dual_read_adapter() { - // Previously, if we attempted to read from a ChaChaDualPolyReadAdapter but the object + fn short_read_chacha_tri_read_adapter() { + // Previously, if we attempted to read from a ChaChaTriPolyReadAdapter but the object // being read is shorter than the available buffer while the buffer passed to - // ChaChaDualPolyReadAdapter itself always thinks it has room, we'd end up + // ChaChaTriPolyReadAdapter itself always thinks it has room, we'd end up // infinite-looping as we didn't handle `Read::read`'s 0 return values at EOF. let mut stream = &[0; 1024][..]; let mut too_long_stream = FixedLengthReader::new(&mut stream, 2048); - let keys = ([42; 32], [99; 32]); - let res = super::ChaChaDualPolyReadAdapter::<u8>::read(&mut too_long_stream, keys); + let keys = ([42; 32], [98; 32], [99; 32]); + let res = super::ChaChaTriPolyReadAdapter::<u8>::read(&mut too_long_stream, keys); match res { Ok(_) => panic!(), Err(e) => assert_eq!(e, DecodeError::ShortRead), diff --git a/lightning/src/crypto/utils.rs b/lightning/src/crypto/utils.rs index 1570b3a0b2f..d6fa2044d79 100644 --- a/lightning/src/crypto/utils.rs +++ b/lightning/src/crypto/utils.rs @@ -3,6 +3,8 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; use bitcoin::secp256k1::{ecdsa::Signature, Message, Secp256k1, SecretKey, Signing}; +use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; + use crate::sign::EntropySource; macro_rules! hkdf_extract_expand { @@ -22,7 +24,7 @@ macro_rules! hkdf_extract_expand { let (k1, k2, _) = hkdf_extract_expand!($salt, $ikm); (k1, k2) }}; - ($salt: expr, $ikm: expr, 6) => {{ + ($salt: expr, $ikm: expr, 8) => {{ let (k1, k2, prk) = hkdf_extract_expand!($salt, $ikm); let mut hmac = HmacEngine::<Sha256>::new(&prk[..]); @@ -45,7 +47,17 @@ macro_rules! hkdf_extract_expand { hmac.input(&[6; 1]); let k6 = Hmac::from_engine(hmac).to_byte_array(); - (k1, k2, k3, k4, k5, k6) + let mut hmac = HmacEngine::<Sha256>::new(&prk[..]); + hmac.input(&k6); + hmac.input(&[7; 1]); + let k7 = Hmac::from_engine(hmac).to_byte_array(); + + let mut hmac = HmacEngine::<Sha256>::new(&prk[..]); + hmac.input(&k7); + hmac.input(&[8; 1]); + let k8 = Hmac::from_engine(hmac).to_byte_array(); + + (k1, k2, k3, k4, k5, k6, k7, k8) }}; } @@ -53,10 +65,10 @@ pub fn hkdf_extract_expand_twice(salt: &[u8], ikm: &[u8]) -> ([u8; 32], [u8; 32] hkdf_extract_expand!(salt, ikm, 2) } -pub fn hkdf_extract_expand_6x( +pub fn hkdf_extract_expand_8x( salt: &[u8], ikm: &[u8], -) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) { - hkdf_extract_expand!(salt, ikm, 6) +) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32], [u8; 32]) { + hkdf_extract_expand!(salt, ikm, 8) } #[inline] @@ -86,3 +98,12 @@ pub fn sign_with_aux_rand<C: Signing, ES: EntropySource>( let sig = sign(ctx, msg, sk); sig } + +pub fn apply_chacha20(key: [u8; 32], nonce: [u8; 16], data: &mut [u8]) { + ChaCha20::new_from_block( + Key::new(key), + Nonce::new(nonce[4..].try_into().unwrap()), + u32::from_le_bytes(nonce[..4].try_into().unwrap()), + ) + .apply_keystream(data); +} diff --git a/lightning/src/events/bump_transaction/mod.rs b/lightning/src/events/bump_transaction/mod.rs index ff034176385..af2709c335c 100644 --- a/lightning/src/events/bump_transaction/mod.rs +++ b/lightning/src/events/bump_transaction/mod.rs @@ -14,46 +14,33 @@ pub mod sync; use alloc::collections::BTreeMap; -use core::future::Future; -use core::ops::Deref; use crate::chain::chaininterface::{ compute_feerate_sat_per_1000_weight, fee_for_weight, BroadcasterInterface, TransactionType, }; use crate::chain::ClaimId; -use crate::io_extras::sink; use crate::ln::chan_utils; use crate::ln::chan_utils::{ shared_anchor_script_pubkey, HTLCOutputInCommitment, ANCHOR_INPUT_WITNESS_WEIGHT, - BASE_INPUT_WEIGHT, BASE_TX_SIZE, EMPTY_SCRIPT_SIG_WEIGHT, EMPTY_WITNESS_WEIGHT, - HTLC_SUCCESS_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_SUCCESS_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, - HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, - P2WSH_TXOUT_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, TRUC_CHILD_MAX_WEIGHT, TRUC_MAX_WEIGHT, + EMPTY_SCRIPT_SIG_WEIGHT, EMPTY_WITNESS_WEIGHT, HTLC_SUCCESS_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, + HTLC_SUCCESS_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, HTLC_TIMEOUT_INPUT_KEYED_ANCHOR_WITNESS_WEIGHT, + HTLC_TIMEOUT_INPUT_P2A_ANCHOR_WITNESS_WEIGHT, TRUC_CHILD_MAX_WEIGHT, TRUC_MAX_WEIGHT, }; use crate::ln::types::ChannelId; use crate::prelude::*; use crate::sign::ecdsa::EcdsaChannelSigner; -use crate::sign::{ - ChannelDerivationParameters, HTLCDescriptor, SignerProvider, P2TR_KEY_PATH_WITNESS_WEIGHT, - P2WPKH_WITNESS_WEIGHT, -}; -use crate::sync::Mutex; -use crate::util::async_poll::{MaybeSend, MaybeSync}; +use crate::sign::{ChannelDerivationParameters, HTLCDescriptor, SignerProvider}; use crate::util::logger::Logger; +use crate::util::wallet_utils::{CoinSelection, CoinSelectionSource, ConfirmedUtxo, Input}; use bitcoin::amount::Amount; -use bitcoin::consensus::Encodable; -use bitcoin::constants::WITNESS_SCALE_FACTOR; -use bitcoin::key::TweakedPublicKey; use bitcoin::locktime::absolute::LockTime; use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; use bitcoin::secp256k1; use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use bitcoin::transaction::Version; -use bitcoin::{ - OutPoint, Psbt, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, Witness, -}; +use bitcoin::{OutPoint, Psbt, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; /// A descriptor used to sign for a commitment transaction's anchor output. #[derive(Clone, Debug, PartialEq, Eq)] @@ -77,6 +64,7 @@ impl AnchorDescriptor { chan_utils::get_keyed_anchor_redeemscript( &channel_params.broadcaster_pubkeys().funding_pubkey, ) + .to_p2wsh() } else { assert!(tx_params.channel_type_features.supports_anchor_zero_fee_commitments()); shared_anchor_script_pubkey() @@ -257,443 +245,6 @@ pub enum BumpTransactionEvent { }, } -/// An input that must be included in a transaction when performing coin selection through -/// [`CoinSelectionSource::select_confirmed_utxos`]. It is guaranteed to be a SegWit input, so it -/// must have an empty [`TxIn::script_sig`] when spent. -#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] -pub struct Input { - /// The unique identifier of the input. - pub outpoint: OutPoint, - /// The UTXO being spent by the input. - pub previous_utxo: TxOut, - /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and - /// [`TxIn::witness`], each with their lengths included, required to satisfy the output's - /// script. - pub satisfaction_weight: u64, -} - -/// An unspent transaction output that is available to spend resulting from a successful -/// [`CoinSelection`] attempt. -#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] -pub struct Utxo { - /// The unique identifier of the output. - pub outpoint: OutPoint, - /// The output to spend. - pub output: TxOut, - /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and [`TxIn::witness`], each - /// with their lengths included, required to satisfy the output's script. The weight consumed by - /// the input's `script_sig` must account for [`WITNESS_SCALE_FACTOR`]. - pub satisfaction_weight: u64, -} - -impl_writeable_tlv_based!(Utxo, { - (1, outpoint, required), - (3, output, required), - (5, satisfaction_weight, required), -}); - -impl Utxo { - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a legacy P2PKH output. - pub fn new_p2pkh(outpoint: OutPoint, value: Amount, pubkey_hash: &PubkeyHash) -> Self { - let script_sig_size = 1 /* script_sig length */ + - 1 /* OP_PUSH73 */ + - 73 /* sig including sighash flag */ + - 1 /* OP_PUSH33 */ + - 33 /* pubkey */; - Self { - outpoint, - output: TxOut { value, script_pubkey: ScriptBuf::new_p2pkh(pubkey_hash) }, - satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + 1, /* empty witness */ - } - } - - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a P2WPKH nested in P2SH output. - pub fn new_nested_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { - let script_sig_size = 1 /* script_sig length */ + - 1 /* OP_0 */ + - 1 /* OP_PUSH20 */ + - 20 /* pubkey_hash */; - Self { - outpoint, - output: TxOut { - value, - script_pubkey: ScriptBuf::new_p2sh( - &ScriptBuf::new_p2wpkh(pubkey_hash).script_hash(), - ), - }, - satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 - + P2WPKH_WITNESS_WEIGHT, - } - } - - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a SegWit v0 P2WPKH output. - pub fn new_v0_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { - Self { - outpoint, - output: TxOut { value, script_pubkey: ScriptBuf::new_p2wpkh(pubkey_hash) }, - satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + P2WPKH_WITNESS_WEIGHT, - } - } - - /// Returns a `Utxo` with the `satisfaction_weight` estimate for a keypath spend of a SegWit v1 P2TR output. - pub fn new_v1_p2tr( - outpoint: OutPoint, value: Amount, tweaked_public_key: TweakedPublicKey, - ) -> Self { - Self { - outpoint, - output: TxOut { value, script_pubkey: ScriptBuf::new_p2tr_tweaked(tweaked_public_key) }, - satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + P2TR_KEY_PATH_WITNESS_WEIGHT, - } - } -} - -/// The result of a successful coin selection attempt for a transaction requiring additional UTXOs -/// to cover its fees. -#[derive(Clone, Debug)] -pub struct CoinSelection { - /// The set of UTXOs (with at least 1 confirmation) to spend and use within a transaction - /// requiring additional fees. - pub confirmed_utxos: Vec<Utxo>, - /// An additional output tracking whether any change remained after coin selection. This output - /// should always have a value above dust for its given `script_pubkey`. It should not be - /// spent until the transaction it belongs to confirms to ensure mempool descendant limits are - /// not met. This implies no other party should be able to spend it except us. - pub change_output: Option<TxOut>, -} - -/// An abstraction over a bitcoin wallet that can perform coin selection over a set of UTXOs and can -/// sign for them. The coin selection method aims to mimic Bitcoin Core's `fundrawtransaction` RPC, -/// which most wallets should be able to satisfy. Otherwise, consider implementing [`WalletSource`], -/// which can provide a default implementation of this trait when used with [`Wallet`]. -/// -/// For a synchronous version of this trait, see [`sync::CoinSelectionSourceSync`]. -/// -/// This is not exported to bindings users as async is only supported in Rust. -// Note that updates to documentation on this trait should be copied to the synchronous version. -pub trait CoinSelectionSource { - /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are - /// available to spend. Implementations are free to pick their coin selection algorithm of - /// choice, as long as the following requirements are met: - /// - /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction - /// throughout coin selection, but must not be returned as part of the result. - /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction - /// throughout coin selection. In some cases, like when funding an anchor transaction, this - /// set is empty. Implementations should ensure they handle this correctly on their end, - /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be - /// provided, in which case a zero-value empty OP_RETURN output can be used instead. - /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the - /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. - /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this - /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC - /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for - /// anchor transactions, we will try your coin selection again with the same input-output - /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions - /// cannot be downsized. - /// - /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of - /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require - /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and - /// delaying block inclusion. - /// - /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they - /// can be re-used within new fee-bumped iterations of the original claiming transaction, - /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a - /// transaction associated with it, and all of the available UTXOs have already been assigned to - /// other claims, implementations must be willing to double spend their UTXOs. The choice of - /// which UTXOs to double spend is left to the implementation, but it must strive to keep the - /// set of other claims being double spent to a minimum. - /// - /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims - fn select_confirmed_utxos<'a>( - &'a self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &'a [TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a; - /// Signs and provides the full witness for all inputs within the transaction known to the - /// trait (i.e., any provided via [`CoinSelectionSource::select_confirmed_utxos`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a; -} - -/// An alternative to [`CoinSelectionSource`] that can be implemented and used along [`Wallet`] to -/// provide a default implementation to [`CoinSelectionSource`]. -/// -/// For a synchronous version of this trait, see [`sync::WalletSourceSync`]. -/// -/// This is not exported to bindings users as async is only supported in Rust. -// Note that updates to documentation on this trait should be copied to the synchronous version. -pub trait WalletSource { - /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. - fn list_confirmed_utxos<'a>( - &'a self, - ) -> impl Future<Output = Result<Vec<Utxo>, ()>> + MaybeSend + 'a; - /// Returns a script to use for change above dust resulting from a successful coin selection - /// attempt. - fn get_change_script<'a>( - &'a self, - ) -> impl Future<Output = Result<ScriptBuf, ()>> + MaybeSend + 'a; - /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within - /// the transaction known to the wallet (i.e., any provided via - /// [`WalletSource::list_confirmed_utxos`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a; -} - -/// A wrapper over [`WalletSource`] that implements [`CoinSelectionSource`] by preferring UTXOs -/// that would avoid conflicting double spends. If not enough UTXOs are available to do so, -/// conflicting double spends may happen. -/// -/// For a synchronous version of this wrapper, see [`sync::WalletSync`]. -/// -/// This is not exported to bindings users as async is only supported in Rust. -// Note that updates to documentation on this struct should be copied to the synchronous version. -pub struct Wallet<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> -where - W::Target: WalletSource + MaybeSend, -{ - source: W, - logger: L, - // TODO: Do we care about cleaning this up once the UTXOs have a confirmed spend? We can do so - // by checking whether any UTXOs that exist in the map are no longer returned in - // `list_confirmed_utxos`. - locked_utxos: Mutex<HashMap<OutPoint, ClaimId>>, -} - -impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> Wallet<W, L> -where - W::Target: WalletSource + MaybeSend, -{ - /// Returns a new instance backed by the given [`WalletSource`] that serves as an implementation - /// of [`CoinSelectionSource`]. - pub fn new(source: W, logger: L) -> Self { - Self { source, logger, locked_utxos: Mutex::new(new_hash_map()) } - } - - /// Performs coin selection on the set of UTXOs obtained from - /// [`WalletSource::list_confirmed_utxos`]. Its algorithm can be described as "smallest - /// above-dust-after-spend first", with a slight twist: we may skip UTXOs that are above dust at - /// the target feerate after having spent them in a separate claim transaction if - /// `force_conflicting_utxo_spend` is unset to avoid producing conflicting transactions. If - /// `tolerate_high_network_feerates` is set, we'll attempt to spend UTXOs that contribute at - /// least 1 satoshi at the current feerate, otherwise, we'll only attempt to spend those which - /// contribute at least twice their fee. - async fn select_confirmed_utxos_internal( - &self, utxos: &[Utxo], claim_id: ClaimId, force_conflicting_utxo_spend: bool, - tolerate_high_network_feerates: bool, target_feerate_sat_per_1000_weight: u32, - preexisting_tx_weight: u64, input_amount_sat: Amount, target_amount_sat: Amount, - max_tx_weight: u64, - ) -> Result<CoinSelection, ()> { - // P2WSH and P2TR outputs are both the heaviest-weight standard outputs at 34 bytes - let max_coin_selection_weight = max_tx_weight - .checked_sub(preexisting_tx_weight + P2WSH_TXOUT_WEIGHT) - .ok_or_else(|| { - log_debug!( - self.logger, - "max_tx_weight is too small to accommodate the preexisting tx weight plus a P2WSH/P2TR output" - ); - })?; - - let mut selected_amount; - let mut total_fees; - let mut selected_utxos; - { - let mut locked_utxos = self.locked_utxos.lock().unwrap(); - let mut eligible_utxos = utxos - .iter() - .filter_map(|utxo| { - if let Some(utxo_claim_id) = locked_utxos.get(&utxo.outpoint) { - if *utxo_claim_id != claim_id && !force_conflicting_utxo_spend { - log_trace!( - self.logger, - "Skipping UTXO {} to prevent conflicting spend", - utxo.outpoint - ); - return None; - } - } - let fee_to_spend_utxo = Amount::from_sat(fee_for_weight( - target_feerate_sat_per_1000_weight, - BASE_INPUT_WEIGHT + utxo.satisfaction_weight, - )); - let should_spend = if tolerate_high_network_feerates { - utxo.output.value > fee_to_spend_utxo - } else { - utxo.output.value >= fee_to_spend_utxo * 2 - }; - if should_spend { - Some((utxo, fee_to_spend_utxo)) - } else { - log_trace!( - self.logger, - "Skipping UTXO {} due to dust proximity after spend", - utxo.outpoint - ); - None - } - }) - .collect::<Vec<_>>(); - eligible_utxos.sort_unstable_by_key(|(utxo, fee_to_spend_utxo)| { - utxo.output.value - *fee_to_spend_utxo - }); - - selected_amount = input_amount_sat; - total_fees = Amount::from_sat(fee_for_weight( - target_feerate_sat_per_1000_weight, - preexisting_tx_weight, - )); - selected_utxos = VecDeque::new(); - // Invariant: `selected_utxos_weight` is never greater than `max_coin_selection_weight` - let mut selected_utxos_weight = 0; - for (utxo, fee_to_spend_utxo) in eligible_utxos { - if selected_amount >= target_amount_sat + total_fees { - break; - } - // First skip any UTXOs with prohibitive satisfaction weights - if BASE_INPUT_WEIGHT + utxo.satisfaction_weight > max_coin_selection_weight { - continue; - } - // If adding this UTXO to `selected_utxos` would push us over the - // `max_coin_selection_weight`, remove UTXOs from the front to make room - // for this new UTXO. - while selected_utxos_weight + BASE_INPUT_WEIGHT + utxo.satisfaction_weight - > max_coin_selection_weight - && !selected_utxos.is_empty() - { - let (smallest_value_after_spend_utxo, fee_to_spend_utxo): (Utxo, Amount) = - selected_utxos.pop_front().unwrap(); - selected_amount -= smallest_value_after_spend_utxo.output.value; - total_fees -= fee_to_spend_utxo; - selected_utxos_weight -= - BASE_INPUT_WEIGHT + smallest_value_after_spend_utxo.satisfaction_weight; - } - selected_amount += utxo.output.value; - total_fees += fee_to_spend_utxo; - selected_utxos_weight += BASE_INPUT_WEIGHT + utxo.satisfaction_weight; - selected_utxos.push_back((utxo.clone(), fee_to_spend_utxo)); - } - if selected_amount < target_amount_sat + total_fees { - log_debug!( - self.logger, - "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", - target_feerate_sat_per_1000_weight, - max_coin_selection_weight, - ); - return Err(()); - } - // Once we've selected enough UTXOs to cover `target_amount_sat + total_fees`, - // we may be able to remove some small-value ones while still covering - // `target_amount_sat + total_fees`. - while !selected_utxos.is_empty() - && selected_amount - selected_utxos.front().unwrap().0.output.value - >= target_amount_sat + total_fees - selected_utxos.front().unwrap().1 - { - let (smallest_value_after_spend_utxo, fee_to_spend_utxo) = - selected_utxos.pop_front().unwrap(); - selected_amount -= smallest_value_after_spend_utxo.output.value; - total_fees -= fee_to_spend_utxo; - } - for (utxo, _) in &selected_utxos { - locked_utxos.insert(utxo.outpoint, claim_id); - } - } - - let remaining_amount = selected_amount - target_amount_sat - total_fees; - let change_script = self.source.get_change_script().await?; - let change_output_fee = fee_for_weight( - target_feerate_sat_per_1000_weight, - (8 /* value */ + change_script.consensus_encode(&mut sink()).unwrap() as u64) - * WITNESS_SCALE_FACTOR as u64, - ); - let change_output_amount = - Amount::from_sat(remaining_amount.to_sat().saturating_sub(change_output_fee)); - let change_output = if change_output_amount < change_script.minimal_non_dust() { - log_debug!(self.logger, "Coin selection attempt did not yield change output"); - None - } else { - Some(TxOut { script_pubkey: change_script, value: change_output_amount }) - }; - - Ok(CoinSelection { - confirmed_utxos: selected_utxos.into_iter().map(|(utxo, _)| utxo).collect(), - change_output, - }) - } -} - -impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> CoinSelectionSource - for Wallet<W, L> -where - W::Target: WalletSource + MaybeSend + MaybeSync, -{ - fn select_confirmed_utxos<'a>( - &'a self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &'a [TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a { - async move { - let utxos = self.source.list_confirmed_utxos().await?; - // TODO: Use fee estimation utils when we upgrade to bitcoin v0.30.0. - let total_output_size: u64 = must_pay_to - .iter() - .map( - |output| 8 /* value */ + 1 /* script len */ + output.script_pubkey.len() as u64, - ) - .sum(); - let total_satisfaction_weight: u64 = - must_spend.iter().map(|input| input.satisfaction_weight).sum(); - let total_input_weight = - (BASE_INPUT_WEIGHT * must_spend.len() as u64) + total_satisfaction_weight; - - let preexisting_tx_weight = SEGWIT_MARKER_FLAG_WEIGHT - + total_input_weight - + ((BASE_TX_SIZE + total_output_size) * WITNESS_SCALE_FACTOR as u64); - let input_amount_sat = must_spend.iter().map(|input| input.previous_utxo.value).sum(); - let target_amount_sat = must_pay_to.iter().map(|output| output.value).sum(); - - let configs = [(false, false), (false, true), (true, false), (true, true)]; - for (force_conflicting_utxo_spend, tolerate_high_network_feerates) in configs { - log_debug!( - self.logger, - "Attempting coin selection targeting {} sat/kW (force_conflicting_utxo_spend = {}, tolerate_high_network_feerates = {})", - target_feerate_sat_per_1000_weight, - force_conflicting_utxo_spend, - tolerate_high_network_feerates - ); - let attempt = self - .select_confirmed_utxos_internal( - &utxos, - claim_id, - force_conflicting_utxo_spend, - tolerate_high_network_feerates, - target_feerate_sat_per_1000_weight, - preexisting_tx_weight, - input_amount_sat, - target_amount_sat, - max_tx_weight, - ) - .await; - if attempt.is_ok() { - return attempt; - } - } - Err(()) - } - } - - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { - self.source.sign_psbt(psbt) - } -} - /// A handler for [`Event::BumpTransaction`] events that sources confirmed UTXOs from a /// [`CoinSelectionSource`] to fee bump transactions via Child-Pays-For-Parent (CPFP) or /// Replace-By-Fee (RBF). @@ -706,12 +257,10 @@ where // Note that updates to documentation on this struct should be copied to the synchronous version. pub struct BumpTransactionEventHandler< B: BroadcasterInterface, - C: Deref, + C: CoinSelectionSource, SP: SignerProvider, L: Logger, -> where - C::Target: CoinSelectionSource, -{ +> { broadcaster: B, utxo_source: C, signer_provider: SP, @@ -719,10 +268,8 @@ pub struct BumpTransactionEventHandler< secp: Secp256k1<secp256k1::All>, } -impl<B: BroadcasterInterface, C: Deref, SP: SignerProvider, L: Logger> +impl<B: BroadcasterInterface, C: CoinSelectionSource, SP: SignerProvider, L: Logger> BumpTransactionEventHandler<B, C, SP, L> -where - C::Target: CoinSelectionSource, { /// Returns a new instance capable of handling [`Event::BumpTransaction`] events. /// @@ -733,11 +280,11 @@ where /// Updates a transaction with the result of a successful coin selection attempt. fn process_coin_selection(&self, tx: &mut Transaction, coin_selection: &CoinSelection) { - for utxo in coin_selection.confirmed_utxos.iter() { + for ConfirmedUtxo { utxo, .. } in coin_selection.confirmed_utxos.iter() { tx.input.push(TxIn { previous_output: utxo.outpoint, script_sig: ScriptBuf::new(), - sequence: Sequence::ZERO, + sequence: utxo.sequence, witness: Witness::new(), }); } @@ -785,7 +332,13 @@ where let anchor_input_witness_weight = if channel_type.supports_anchor_zero_fee_commitments() { EMPTY_WITNESS_WEIGHT } else { - ANCHOR_INPUT_WITNESS_WEIGHT + let weight = ANCHOR_INPUT_WITNESS_WEIGHT; + #[cfg(secp256k1_fuzz)] + let weight = { + // The secp256k1 fuzz signer does not low-S normalize dummy signatures. + weight + 1 + }; + weight }; // First, check if the commitment transaction has sufficient fees on its own. @@ -830,7 +383,7 @@ where let coin_selection: CoinSelection = self .utxo_source .select_confirmed_utxos( - claim_id, + Some(claim_id), must_spend, &[], package_target_feerate_sat_per_1000_weight, @@ -858,12 +411,10 @@ where output: vec![], }; - let input_satisfaction_weight: u64 = - coin_selection.confirmed_utxos.iter().map(|utxo| utxo.satisfaction_weight).sum(); + let input_satisfaction_weight = coin_selection.satisfaction_weight(); let total_satisfaction_weight = anchor_input_witness_weight + EMPTY_SCRIPT_SIG_WEIGHT + input_satisfaction_weight; - let total_input_amount = must_spend_amount - + coin_selection.confirmed_utxos.iter().map(|utxo| utxo.output.value).sum(); + let total_input_amount = must_spend_amount + coin_selection.input_amount(); self.process_coin_selection(&mut anchor_tx, &coin_selection); let anchor_txid = anchor_tx.compute_txid(); @@ -878,10 +429,10 @@ where let index = idx + 1; debug_assert_eq!( anchor_psbt.unsigned_tx.input[index].previous_output, - utxo.outpoint + utxo.outpoint() ); - if utxo.output.script_pubkey.is_witness_program() { - anchor_psbt.inputs[index].witness_utxo = Some(utxo.output); + if utxo.output().script_pubkey.is_witness_program() { + anchor_psbt.inputs[index].witness_utxo = Some(utxo.into_output()); } } @@ -1003,6 +554,18 @@ where } else { panic!("channel type should be either zero-fee HTLCs, or zero-fee commitments"); }; + // The secp256k1 fuzz signer emits dummy signatures without low-S normalization, so + // DER+sighash can be one byte larger for each of the two HTLC signatures. + #[cfg(secp256k1_fuzz)] + let (htlc_success_witness_weight, htlc_timeout_witness_weight) = + (htlc_success_witness_weight + 2, htlc_timeout_witness_weight + 2); + let (htlc_success_input_output_pair_weight, htlc_timeout_input_output_pair_weight) = ( + chan_utils::aggregated_htlc_success_input_output_pair_weight(channel_type), + chan_utils::aggregated_htlc_timeout_input_output_pair_weight(channel_type), + ); + #[cfg(secp256k1_fuzz)] + let (htlc_success_input_output_pair_weight, htlc_timeout_input_output_pair_weight) = + (htlc_success_input_output_pair_weight + 2, htlc_timeout_input_output_pair_weight + 2); let max_tx_weight = if channel_type.supports_anchor_zero_fee_commitments() { // Cap the size of transactions claiming `HolderHTLCOutput` in 0FC channels. @@ -1043,9 +606,9 @@ where &htlc_descriptors[broadcasted_htlcs..broadcasted_htlcs + batch_size] { let input_output_weight = if htlc_descriptor.preimage.is_some() { - chan_utils::aggregated_htlc_success_input_output_pair_weight(channel_type) + htlc_success_input_output_pair_weight } else { - chan_utils::aggregated_htlc_timeout_input_output_pair_weight(channel_type) + htlc_timeout_input_output_pair_weight }; if htlc_weight_sum + input_output_weight >= max_tx_weight - USER_COINS_WEIGHT_BUDGET { @@ -1095,7 +658,7 @@ where let coin_selection: CoinSelection = match self .utxo_source .select_confirmed_utxos( - utxo_id, + Some(utxo_id), must_spend, &htlc_tx.output, target_feerate_sat_per_1000_weight, @@ -1105,9 +668,8 @@ where { Ok(selection) => selection, Err(()) => { - let htlcs_to_remove = USER_COINS_WEIGHT_BUDGET.div_ceil( - chan_utils::aggregated_htlc_timeout_input_output_pair_weight(channel_type), - ); + let htlcs_to_remove = + USER_COINS_WEIGHT_BUDGET.div_ceil(htlc_timeout_input_output_pair_weight); batch_size = batch_size.checked_sub(htlcs_to_remove as usize).ok_or(())?; if batch_size == 0 { return Err(()); @@ -1120,13 +682,11 @@ where utxo_id = claim_id.step_with_bytes(&broadcasted_htlcs.to_be_bytes()); #[cfg(debug_assertions)] - let input_satisfaction_weight: u64 = - coin_selection.confirmed_utxos.iter().map(|utxo| utxo.satisfaction_weight).sum(); + let input_satisfaction_weight = coin_selection.satisfaction_weight(); #[cfg(debug_assertions)] let total_satisfaction_weight = must_spend_satisfaction_weight + input_satisfaction_weight; #[cfg(debug_assertions)] - let input_value: u64 = - coin_selection.confirmed_utxos.iter().map(|utxo| utxo.output.value.to_sat()).sum(); + let input_value = coin_selection.input_amount().to_sat(); #[cfg(debug_assertions)] let total_input_amount = must_spend_amount + input_value; @@ -1147,9 +707,12 @@ where for (idx, utxo) in coin_selection.confirmed_utxos.into_iter().enumerate() { // offset to skip the htlc inputs let index = idx + selected_htlcs.len(); - debug_assert_eq!(htlc_psbt.unsigned_tx.input[index].previous_output, utxo.outpoint); - if utxo.output.script_pubkey.is_witness_program() { - htlc_psbt.inputs[index].witness_utxo = Some(utxo.output); + debug_assert_eq!( + htlc_psbt.unsigned_tx.input[index].previous_output, + utxo.outpoint() + ); + if utxo.output().script_pubkey.is_witness_program() { + htlc_psbt.inputs[index].witness_utxo = Some(utxo.into_output()); } } @@ -1293,21 +856,23 @@ where mod tests { use super::*; - use crate::events::bump_transaction::sync::{ - BumpTransactionEventHandlerSync, CoinSelectionSourceSync, - }; + use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync; use crate::io::Cursor; use crate::ln::chan_utils::ChannelTransactionParameters; use crate::ln::channel::ANCHOR_OUTPUT_VALUE_SATOSHI; use crate::sign::KeysManager; + use crate::sync::Mutex; use crate::types::features::ChannelTypeFeatures; use crate::util::ser::Readable; use crate::util::test_utils::{TestBroadcaster, TestLogger}; + use crate::util::wallet_utils::CoinSelectionSourceSync; + use crate::util::wallet_utils::Utxo; - use bitcoin::hashes::Hash; + use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::hex::FromHex; + use bitcoin::key::TweakedPublicKey; use bitcoin::{ - Network, ScriptBuf, Transaction, Txid, WitnessProgram, WitnessVersion, XOnlyPublicKey, + Network, ScriptBuf, Transaction, WitnessProgram, WitnessVersion, XOnlyPublicKey, }; struct TestCoinSelectionSource { @@ -1316,7 +881,7 @@ mod tests { } impl CoinSelectionSourceSync for TestCoinSelectionSource { fn select_confirmed_utxos( - &self, _claim_id: ClaimId, must_spend: Vec<Input>, _must_pay_to: &[TxOut], + &self, _claim_id: Option<ClaimId>, must_spend: Vec<Input>, _must_pay_to: &[TxOut], target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64, ) -> Result<CoinSelection, ()> { let mut expected_selects = self.expected_selects.lock().unwrap(); @@ -1328,9 +893,17 @@ mod tests { Ok(res) } fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()> { + let prevtx_ids: Vec<_> = self + .expected_selects + .lock() + .unwrap() + .iter() + .flat_map(|selection| selection.3.confirmed_utxos.iter()) + .map(|utxo| utxo.prevtx.compute_txid()) + .collect(); let mut tx = psbt.unsigned_tx; for input in tx.input.iter_mut() { - if input.previous_output.txid != Txid::from_byte_array([44; 32]) { + if prevtx_ids.contains(&input.previous_output.txid) { // Channel output, add a realistic size witness to make the assertions happy input.witness = Witness::from_slice(&[vec![42; 162]]); } @@ -1371,6 +944,13 @@ mod tests { .weight() .to_wu(); + let prevtx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![TxOut { value: Amount::from_sat(200), script_pubkey: ScriptBuf::new() }], + }; + let broadcaster = TestBroadcaster::new(Network::Testnet); let source = TestCoinSelectionSource { expected_selects: Mutex::new(vec![ @@ -1385,13 +965,14 @@ mod tests { commitment_and_anchor_fee, 868, CoinSelection { - confirmed_utxos: vec![Utxo { - outpoint: OutPoint { txid: Txid::from_byte_array([44; 32]), vout: 0 }, - output: TxOut { - value: Amount::from_sat(200), - script_pubkey: ScriptBuf::new(), + confirmed_utxos: vec![ConfirmedUtxo { + utxo: Utxo { + outpoint: OutPoint { txid: prevtx.compute_txid(), vout: 0 }, + output: prevtx.output[0].clone(), + satisfaction_weight: 5, // Just the script_sig and witness lengths + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, }, - satisfaction_weight: 5, // Just the script_sig and witness lengths + prevtx, }], change_output: None, }, @@ -1451,4 +1032,27 @@ mod tests { 1 /* witness items */ + 1 /* schnorr sig len */ + 64 /* schnorr sig */ ); } + + #[test] + fn test_anchor_descriptor_previous_utxo_script_pubkey_uses_p2wsh() { + let mut transaction_parameters = ChannelTransactionParameters::test_dummy(42_000_000); + transaction_parameters.channel_type_features = + ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + + let funding_pubkey = transaction_parameters.holder_pubkeys.funding_pubkey; + let expected_script_pubkey = + chan_utils::get_keyed_anchor_redeemscript(&funding_pubkey).to_p2wsh(); + + let anchor_descriptor = AnchorDescriptor { + channel_derivation_parameters: ChannelDerivationParameters { + value_satoshis: 42_000_000, + keys_id: [42; 32], + transaction_parameters, + }, + outpoint: OutPoint::null(), + value: Amount::from_sat(ANCHOR_OUTPUT_VALUE_SATOSHI), + }; + + assert_eq!(anchor_descriptor.previous_utxo().script_pubkey, expected_script_pubkey); + } } diff --git a/lightning/src/events/bump_transaction/sync.rs b/lightning/src/events/bump_transaction/sync.rs index f4245cd5194..f2e1be1590c 100644 --- a/lightning/src/events/bump_transaction/sync.rs +++ b/lightning/src/events/bump_transaction/sync.rs @@ -15,246 +15,12 @@ use core::pin::pin; use core::task; use crate::chain::chaininterface::BroadcasterInterface; -use crate::chain::ClaimId; -use crate::prelude::*; use crate::sign::SignerProvider; -use crate::util::async_poll::{dummy_waker, MaybeSend, MaybeSync}; +use crate::util::async_poll::dummy_waker; use crate::util::logger::Logger; +use crate::util::wallet_utils::{CoinSelectionSourceSync, CoinSelectionSourceSyncWrapper}; -use bitcoin::{Psbt, ScriptBuf, Transaction, TxOut}; - -use super::BumpTransactionEvent; -use super::{ - BumpTransactionEventHandler, CoinSelection, CoinSelectionSource, Input, Utxo, Wallet, - WalletSource, -}; - -/// An alternative to [`CoinSelectionSourceSync`] that can be implemented and used along -/// [`WalletSync`] to provide a default implementation to [`CoinSelectionSourceSync`]. -/// -/// For an asynchronous version of this trait, see [`WalletSource`]. -// Note that updates to documentation on this trait should be copied to the asynchronous version. -pub trait WalletSourceSync { - /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. - fn list_confirmed_utxos(&self) -> Result<Vec<Utxo>, ()>; - /// Returns a script to use for change above dust resulting from a successful coin selection - /// attempt. - fn get_change_script(&self) -> Result<ScriptBuf, ()>; - /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within - /// the transaction known to the wallet (i.e., any provided via - /// [`WalletSource::list_confirmed_utxos`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - /// - /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig - /// [`TxIn::witness`]: bitcoin::TxIn::witness - fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()>; -} - -pub(crate) struct WalletSourceSyncWrapper<T: Deref>(T) -where - T::Target: WalletSourceSync; - -// Implement `Deref` directly on WalletSourceSyncWrapper so that it can be used directly -// below, rather than via a wrapper. -impl<T: Deref> Deref for WalletSourceSyncWrapper<T> -where - T::Target: WalletSourceSync, -{ - type Target = Self; - fn deref(&self) -> &Self { - self - } -} - -impl<T: Deref> WalletSource for WalletSourceSyncWrapper<T> -where - T::Target: WalletSourceSync, -{ - fn list_confirmed_utxos<'a>( - &'a self, - ) -> impl Future<Output = Result<Vec<Utxo>, ()>> + MaybeSend + 'a { - let utxos = self.0.list_confirmed_utxos(); - async move { utxos } - } - - fn get_change_script<'a>( - &'a self, - ) -> impl Future<Output = Result<ScriptBuf, ()>> + MaybeSend + 'a { - let script = self.0.get_change_script(); - async move { script } - } - - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { - let signed_psbt = self.0.sign_psbt(psbt); - async move { signed_psbt } - } -} - -/// A wrapper over [`WalletSourceSync`] that implements [`CoinSelectionSourceSync`] by preferring -/// UTXOs that would avoid conflicting double spends. If not enough UTXOs are available to do so, -/// conflicting double spends may happen. -/// -/// For an asynchronous version of this wrapper, see [`Wallet`]. -// Note that updates to documentation on this struct should be copied to the asynchronous version. -pub struct WalletSync<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> -where - W::Target: WalletSourceSync + MaybeSend, -{ - wallet: Wallet<WalletSourceSyncWrapper<W>, L>, -} - -impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> WalletSync<W, L> -where - W::Target: WalletSourceSync + MaybeSend, -{ - /// Constructs a new [`WalletSync`] instance. - pub fn new(source: W, logger: L) -> Self { - Self { wallet: Wallet::new(WalletSourceSyncWrapper(source), logger) } - } -} - -impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> CoinSelectionSourceSync - for WalletSync<W, L> -where - W::Target: WalletSourceSync + MaybeSend + MaybeSync, -{ - fn select_confirmed_utxos( - &self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &[TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> Result<CoinSelection, ()> { - let fut = self.wallet.select_confirmed_utxos( - claim_id, - must_spend, - must_pay_to, - target_feerate_sat_per_1000_weight, - max_tx_weight, - ); - let mut waker = dummy_waker(); - let mut ctx = task::Context::from_waker(&mut waker); - match pin!(fut).poll(&mut ctx) { - task::Poll::Ready(result) => result, - task::Poll::Pending => { - unreachable!( - "Wallet::select_confirmed_utxos should not be pending in a sync context" - ); - }, - } - } - - fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()> { - let fut = self.wallet.sign_psbt(psbt); - let mut waker = dummy_waker(); - let mut ctx = task::Context::from_waker(&mut waker); - match pin!(fut).poll(&mut ctx) { - task::Poll::Ready(result) => result, - task::Poll::Pending => { - unreachable!("Wallet::sign_psbt should not be pending in a sync context"); - }, - } - } -} - -/// An abstraction over a bitcoin wallet that can perform coin selection over a set of UTXOs and can -/// sign for them. The coin selection method aims to mimic Bitcoin Core's `fundrawtransaction` RPC, -/// which most wallets should be able to satisfy. Otherwise, consider implementing -/// [`WalletSourceSync`], which can provide a default implementation of this trait when used with -/// [`WalletSync`]. -/// -/// For an asynchronous version of this trait, see [`CoinSelectionSource`]. -// Note that updates to documentation on this trait should be copied to the asynchronous version. -pub trait CoinSelectionSourceSync { - /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are - /// available to spend. Implementations are free to pick their coin selection algorithm of - /// choice, as long as the following requirements are met: - /// - /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction - /// throughout coin selection, but must not be returned as part of the result. - /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction - /// throughout coin selection. In some cases, like when funding an anchor transaction, this - /// set is empty. Implementations should ensure they handle this correctly on their end, - /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be - /// provided, in which case a zero-value empty OP_RETURN output can be used instead. - /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the - /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. - /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this - /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC - /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for - /// anchor transactions, we will try your coin selection again with the same input-output - /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions - /// cannot be downsized. - /// - /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of - /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require - /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and - /// delaying block inclusion. - /// - /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they - /// can be re-used within new fee-bumped iterations of the original claiming transaction, - /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a - /// transaction associated with it, and all of the available UTXOs have already been assigned to - /// other claims, implementations must be willing to double spend their UTXOs. The choice of - /// which UTXOs to double spend is left to the implementation, but it must strive to keep the - /// set of other claims being double spent to a minimum. - /// - /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims - fn select_confirmed_utxos( - &self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &[TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> Result<CoinSelection, ()>; - - /// Signs and provides the full witness for all inputs within the transaction known to the - /// trait (i.e., any provided via [`CoinSelectionSourceSync::select_confirmed_utxos`]). - /// - /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the - /// unsigned transaction and then sign it with your wallet. - fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()>; -} - -struct CoinSelectionSourceSyncWrapper<T: Deref>(T) -where - T::Target: CoinSelectionSourceSync; - -// Implement `Deref` directly on CoinSelectionSourceSyncWrapper so that it can be used directly -// below, rather than via a wrapper. -impl<T: Deref> Deref for CoinSelectionSourceSyncWrapper<T> -where - T::Target: CoinSelectionSourceSync, -{ - type Target = Self; - fn deref(&self) -> &Self { - self - } -} - -impl<T: Deref> CoinSelectionSource for CoinSelectionSourceSyncWrapper<T> -where - T::Target: CoinSelectionSourceSync, -{ - fn select_confirmed_utxos<'a>( - &'a self, claim_id: ClaimId, must_spend: Vec<Input>, must_pay_to: &'a [TxOut], - target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, - ) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a { - let coins = self.0.select_confirmed_utxos( - claim_id, - must_spend, - must_pay_to, - target_feerate_sat_per_1000_weight, - max_tx_weight, - ); - async move { coins } - } - - fn sign_psbt<'a>( - &'a self, psbt: Psbt, - ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { - let psbt = self.0.sign_psbt(psbt); - async move { psbt } - } -} +use super::{BumpTransactionEvent, BumpTransactionEventHandler}; /// A handler for [`Event::BumpTransaction`] events that sources confirmed UTXOs from a /// [`CoinSelectionSourceSync`] to fee bump transactions via Child-Pays-For-Parent (CPFP) or diff --git a/lightning/src/events/mod.rs b/lightning/src/events/mod.rs index 3dfed10d5c8..6bbcf4f15ae 100644 --- a/lightning/src/events/mod.rs +++ b/lightning/src/events/mod.rs @@ -18,19 +18,21 @@ pub mod bump_transaction; pub use bump_transaction::BumpTransactionEvent; -use crate::blinded_path::message::{BlindedMessagePath, OffersContext}; +use crate::blinded_path::message::{BlindedMessagePath, NextMessageHop, OffersContext}; use crate::blinded_path::payment::{ Bolt12OfferContext, Bolt12RefundContext, PaymentContext, PaymentContextRef, }; use crate::chain::transaction; use crate::ln::channel::FUNDING_CONF_DEADLINE_BLOCKS; use crate::ln::channelmanager::{InterceptId, PaymentId}; +use crate::ln::funding::FundingContribution; use crate::ln::msgs; use crate::ln::onion_utils::LocalHTLCFailureReason; use crate::ln::outbound_payment::RecipientOnionFields; use crate::ln::types::ChannelId; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_request::InvoiceRequest; +pub use crate::offers::payer_proof::PaidBolt12Invoice; use crate::offers::static_invoice::StaticInvoice; use crate::onion_message::messenger::Responder; use crate::routing::gossip::NetworkUpdate; @@ -41,8 +43,8 @@ use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::types::string::UntrustedString; use crate::util::errors::APIError; use crate::util::ser::{ - BigSize, FixedLengthReader, MaybeReadable, Readable, RequiredWrapper, UpgradableRequired, - WithoutLength, Writeable, Writer, + BigSize, FixedLengthReader, MaybeReadable, Readable, ReadableArgs, RequiredWrapper, + UpgradableRequired, WithoutLength, Writeable, Writer, }; use crate::io; @@ -51,7 +53,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::script::ScriptBuf; use bitcoin::secp256k1::PublicKey; -use bitcoin::{OutPoint, Transaction, TxOut}; +use bitcoin::{OutPoint, Transaction}; use core::ops::Deref; #[allow(unused_imports)] @@ -77,15 +79,141 @@ pub enum FundingInfo { /// The outpoint of the funding outpoint: transaction::OutPoint, }, + /// The contributions used for a dual funding or splice funding transaction. + Contribution { + /// UTXOs spent as inputs contributed to the funding transaction. + inputs: Vec<OutPoint>, + /// Output scripts contributed to the funding transaction. + outputs: Vec<ScriptBuf>, + }, } -impl_writeable_tlv_based_enum!(FundingInfo, +impl_ser_tlv_based_enum!(FundingInfo, (0, Tx) => { (0, transaction, required) }, (1, OutPoint) => { (1, outpoint, required) + }, + (2, Contribution) => { + (1, inputs, optional_vec), + (3, outputs, optional_vec), + } +); + +/// The reason a funding negotiation round failed. +/// +/// Each negotiation attempt (initial or RBF) resolves to either success or failure. This enum +/// indicates what caused the failure. Use [`is_retriable`] to determine whether the splice can +/// be reattempted on this channel by calling [`ChannelManager::splice_channel`]. +/// +/// [`is_retriable`]: Self::is_retriable +/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NegotiationFailureReason { + /// The reason was not available (e.g., from an older serialization). + Unknown, + /// The peer disconnected during negotiation. Wait for the peer to reconnect, then retry. + PeerDisconnected, + /// The counterparty explicitly aborted the negotiation by sending `tx_abort`. Retrying with + /// the same parameters is unlikely to succeed — consider adjusting the contribution or + /// waiting for the counterparty to initiate. + CounterpartyAborted { + /// The counterparty's abort message. + /// + /// This is counterparty-provided data. Use `Display` on [`UntrustedString`] for safe + /// logging. + msg: UntrustedString, + }, + /// An error occurred during interactive transaction negotiation (e.g., the counterparty sent + /// an invalid message). The negotiation was aborted. + NegotiationError { + /// A developer-readable error message. + msg: String, + }, + /// The funding contribution was invalid (e.g., insufficient balance for the splice amount). + /// Call [`ChannelManager::splice_channel`] for a fresh [`FundingTemplate`] and build a new + /// contribution with adjusted parameters. + /// + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate + ContributionInvalid, + /// The negotiation was locally canceled via [`ChannelManager::cancel_funding_contributed`]. + /// + /// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed + LocallyCanceled, + /// The channel is closing, so the negotiation cannot continue. See [`Event::ChannelClosed`] + /// for the closure reason. + ChannelClosing, + /// The contribution's feerate was too low for RBF. Call [`ChannelManager::splice_channel`] + /// for a fresh [`FundingTemplate`] (which includes the updated minimum feerate) and build a + /// new contribution with a higher feerate. + /// + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate + FeeRateTooLow, + /// An RBF attempt could not be initiated (e.g., a prior splice transaction already + /// confirmed). The channel remains operational — start a new splice with + /// [`ChannelManager::splice_channel`] if further changes are needed. + /// + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + CannotInitiateRbf, +} + +impl NegotiationFailureReason { + /// Whether the splice negotiation is likely to succeed if retried on this channel. When `true`, + /// call [`ChannelManager::splice_channel`] to obtain a fresh [`FundingTemplate`] and retry. + /// + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate + pub fn is_retriable(&self) -> bool { + match self { + Self::Unknown + | Self::PeerDisconnected + | Self::ContributionInvalid + | Self::FeeRateTooLow => true, + Self::CounterpartyAborted { .. } + | Self::NegotiationError { .. } + | Self::LocallyCanceled + | Self::ChannelClosing + | Self::CannotInitiateRbf => false, + } + } +} + +impl core::fmt::Display for NegotiationFailureReason { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Unknown => f.write_str("unknown reason"), + Self::PeerDisconnected => f.write_str("peer disconnected during negotiation"), + Self::CounterpartyAborted { msg } => { + write!(f, "counterparty aborted: {}", msg) + }, + Self::NegotiationError { msg } => write!(f, "negotiation error: {}", msg), + Self::ContributionInvalid => f.write_str("funding contribution was invalid"), + Self::LocallyCanceled => f.write_str("splice locally canceled"), + + Self::ChannelClosing => f.write_str("channel is closing"), + Self::FeeRateTooLow => f.write_str("feerate too low for RBF"), + Self::CannotInitiateRbf => f.write_str("cannot initiate RBF"), + } } +} + +impl_writeable_tlv_based_enum_upgradable!(NegotiationFailureReason, + (1, Unknown) => {}, + (3, PeerDisconnected) => {}, + (5, CounterpartyAborted) => { + (1, msg, required), + }, + (7, NegotiationError) => { + (1, msg, required), + }, + (9, ContributionInvalid) => {}, + (11, LocallyCanceled) => {}, + (13, ChannelClosing) => {}, + (15, FeeRateTooLow) => {}, + (17, CannotInitiateRbf) => {}, ); /// Some information provided on receipt of payment depends on whether the payment received is a @@ -214,7 +342,7 @@ impl PaymentPurpose { } } -impl_writeable_tlv_based_enum_legacy!(PaymentPurpose, +impl_ser_tlv_based_enum_legacy!(PaymentPurpose, (0, Bolt11InvoicePayment) => { (0, payment_preimage, option), (2, payment_secret, required), @@ -264,7 +392,7 @@ pub struct ClaimedHTLC { /// 0.0.119. pub counterparty_skimmed_fee_msat: u64, } -impl_writeable_tlv_based!(ClaimedHTLC, { +impl_ser_tlv_based!(ClaimedHTLC, { (0, channel_id, required), (1, counterparty_skimmed_fee_msat, (default_value, 0u64)), (2, user_channel_id, required), @@ -573,6 +701,10 @@ pub enum HTLCHandlingFailureType { /// The payment hash of the payment we attempted to process. payment_hash: PaymentHash, }, + /// We were responsible for pathfinding and forwarding of a trampoline payment, but failed to + /// do so. An example of such an instance is when we can't find a route to the specified + /// trampoline destination. + TrampolineForward {}, } impl_writeable_tlv_based_enum_upgradable!(HTLCHandlingFailureType, @@ -590,6 +722,7 @@ impl_writeable_tlv_based_enum_upgradable!(HTLCHandlingFailureType, (4, Receive) => { (0, payment_hash, required), }, + (5, TrampolineForward) => {}, ); /// The reason for HTLC failures in [`Event::HTLCHandlingFailed`]. @@ -604,7 +737,7 @@ pub enum HTLCHandlingFailureReason { }, } -impl_writeable_tlv_based_enum!(HTLCHandlingFailureReason, +impl_ser_tlv_based_enum!(HTLCHandlingFailureReason, (1, Downstream) => {}, (3, Local) => { (0, reason, required), @@ -625,7 +758,7 @@ enum InterceptNextHop { FakeScid { requested_next_hop_scid: u64 }, } -impl_writeable_tlv_based_enum!(InterceptNextHop, +impl_ser_tlv_based_enum!(InterceptNextHop, (0, FakeScid) => { (0, requested_next_hop_scid, required), }, @@ -727,6 +860,35 @@ pub enum InboundChannelFunds { DualFunded, } +/// Identifies the channel and peer committed to a HTLC, used for both incoming and outgoing HTLCs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HTLCLocator { + /// The channel that the HTLC was sent or received on. + pub channel_id: ChannelId, + + /// The amount, in milli-satoshis, of the HTLC that was sent or received, if known. + pub amount_msat: Option<u64>, + + /// The `user_channel_id` for `channel_id`. + /// + /// This will be `None` if the payment was settled via an on-chain transaction. It will also + /// be `None` for events serialized by versions prior to 0.0.122. + pub user_channel_id: Option<u128>, + + /// The public key identity of the node that the HTLC was sent to or received from. + /// + /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by versions + /// prior to 0.1. + pub node_id: Option<PublicKey>, +} + +impl_ser_tlv_based!(HTLCLocator, { + (1, channel_id, required), + (3, user_channel_id, option), + (5, node_id, option), + (7, amount_msat, option), +}); + /// An Event which you should probably take some action in response to. /// /// Note that while Writeable and Readable are implemented for Event, you probably shouldn't use @@ -1044,21 +1206,18 @@ pub enum Event { /// If the recipient or an intermediate node misbehaves and gives us free money, this may /// overstate the amount paid, though this is unlikely. /// - /// This is only `None` for payments initiated on LDK versions prior to 0.0.103. + /// This is only `None` for payments abandoned but ultimately claimed when using LDK versions + /// prior to 0.3, 0.2.3, or 0.1.10. /// /// [`Route::get_total_fees`]: crate::routing::router::Route::get_total_fees fee_paid_msat: Option<u64>, - /// The BOLT 12 invoice that was paid. `None` if the payment was a non BOLT 12 payment. - /// - /// The BOLT 12 invoice is useful for proof of payment because it contains the - /// payment hash. A third party can verify that the payment was made by - /// showing the invoice and confirming that the payment hash matches - /// the hash of the payment preimage. + /// The paid BOLT 12 invoice bundled with the data needed to construct a + /// [`PayerProof`], which selectively discloses invoice fields to prove payment to a + /// third party. /// - /// However, the [`PaidBolt12Invoice`] can also be of type [`StaticInvoice`], which - /// is a special [`Bolt12Invoice`] where proof of payment is not possible. + /// `None` for non-BOLT 12 payments. /// - /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + /// [`PayerProof`]: crate::offers::payer_proof::PayerProof bolt12_invoice: Option<PaidBolt12Invoice>, }, /// Indicates an outbound payment failed. Individual [`Event::PaymentPathFailed`] events @@ -1320,38 +1479,22 @@ pub enum Event { /// This event is generated when a payment has been successfully forwarded through us and a /// forwarding fee earned. /// + /// Note that downgrading from 0.3 and above with pending trampoline forwards that use multipart + /// payments will produce an event that only provides information about the first htlc that was + /// received/dispatched. + /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. PaymentForwarded { - /// The channel id of the incoming channel between the previous node and us. - /// - /// This is only `None` for events generated or serialized by versions prior to 0.0.107. - prev_channel_id: Option<ChannelId>, - /// The channel id of the outgoing channel between the next node and us. - /// - /// This is only `None` for events generated or serialized by versions prior to 0.0.107. - next_channel_id: Option<ChannelId>, - /// The `user_channel_id` of the incoming channel between the previous node and us. - /// - /// This is only `None` for events generated or serialized by versions prior to 0.0.122. - prev_user_channel_id: Option<u128>, - /// The `user_channel_id` of the outgoing channel between the next node and us. - /// - /// This will be `None` if the payment was settled via an on-chain transaction. See the - /// caveat described for the `total_fee_earned_msat` field. Moreover it will be `None` for - /// events generated or serialized by versions prior to 0.0.122. - next_user_channel_id: Option<u128>, - /// The node id of the previous node. - /// - /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by - /// versions prior to 0.1 - prev_node_id: Option<PublicKey>, - /// The node id of the next node. - /// - /// This is only `None` for HTLCs received prior to 0.1 or for events serialized by - /// versions prior to 0.1 - next_node_id: Option<PublicKey>, + /// The set of HTLCs forwarded to our node that will be claimed by this forward. Contains a + /// single HTLC for source-routed payments, and may contain multiple HTLCs when we acted as + /// a trampoline router, responsible for pathfinding within the route. + prev_htlcs: Vec<HTLCLocator>, + /// The set of HTLCs forwarded by our node that have been claimed by this forward. Contains + /// a single HTLC for regular source-routed payments, and may contain multiple HTLCs when + /// we acted as a trampoline router, responsible for pathfinding within the route. + next_htlcs: Vec<HTLCLocator>, /// The total fee, in milli-satoshis, which was earned as a result of the payment. /// /// Note that if we force-closed the channel over which we forwarded an HTLC while the HTLC @@ -1386,7 +1529,7 @@ pub enum Event { /// The final amount forwarded, in milli-satoshis, after the fee is deducted. /// /// The caveat described above the `total_fee_earned_msat` field applies here as well. - outbound_amount_forwarded_msat: Option<u64>, + outbound_amount_forwarded_msat: u64, }, /// Used to indicate that a channel with the given `channel_id` is being opened and pending /// confirmation on-chain. @@ -1505,8 +1648,12 @@ pub enum Event { /// [`ChainMonitor::get_claimable_balances`]: crate::chain::chainmonitor::ChainMonitor::get_claimable_balances last_local_balance_msat: Option<u64>, }, - /// Used to indicate that a splice for the given `channel_id` has been negotiated and its - /// funding transaction has been broadcast. + /// Used to indicate that a splice for the given `channel_id` has been negotiated, its + /// funding transaction has been broadcast, and local inputs or outputs were contributed to + /// it. + /// + /// This event is not emitted if the counterparty negotiated a splice without using a local + /// contribution. /// /// The splice is then considered pending until both parties have seen enough confirmations to /// consider the funding locked. Once this occurs, an [`Event::ChannelReady`] will be emitted. @@ -1516,8 +1663,8 @@ pub enum Event { /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. - SplicePending { - /// The `channel_id` of the channel that has a pending splice funding transaction. + SpliceNegotiated { + /// The `channel_id` of the channel with the negotiated splice funding transaction. channel_id: ChannelId, /// The `user_channel_id` value passed in to [`ChannelManager::create_channel`] for outbound /// channels, or to [`ChannelManager::accept_inbound_channel`] for inbound channels. @@ -1535,19 +1682,20 @@ pub enum Event { /// The witness script that is used to lock the channel's funding output to commitment transactions. new_funding_redeem_script: ScriptBuf, }, - /// Used to indicate that a splice for the given `channel_id` has failed. + /// Used to indicate that a splice negotiation round for the given `channel_id` has failed. /// - /// This event may be emitted if a splice fails after it has been initiated but prior to signing - /// any negotiated funding transaction. + /// Each splice attempt (initial or RBF) resolves to this event on failure. On success, + /// [`Event::SpliceNegotiated`] is emitted if the negotiated transaction includes local + /// inputs or outputs. Prior successfully negotiated splice transactions are unaffected. /// - /// Any UTXOs contributed to be spent by the funding transaction may be reused and will be - /// given in `contributed_inputs`. + /// Any UTXOs contributed to the failed round that are not committed to a prior negotiated + /// splice transaction will be returned via a preceding [`Event::DiscardFunding`]. /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. - SpliceFailed { - /// The `channel_id` of the channel for which the splice failed. + SpliceNegotiationFailed { + /// The `channel_id` of the channel for which the splice negotiation round failed. channel_id: ChannelId, /// The `user_channel_id` value passed in to [`ChannelManager::create_channel`] for outbound /// channels, or to [`ChannelManager::accept_inbound_channel`] for inbound channels. @@ -1557,14 +1705,23 @@ pub enum Event { user_channel_id: u128, /// The `node_id` of the channel counterparty. counterparty_node_id: PublicKey, - /// The outpoint of the channel's splice funding transaction, if one was created. - abandoned_funding_txo: Option<OutPoint>, - /// The features that this channel will operate with, if available. - channel_type: Option<ChannelTypeFeatures>, - /// UTXOs spent as inputs contributed to the splice transaction. - contributed_inputs: Vec<OutPoint>, - /// Outputs contributed to the splice transaction. - contributed_outputs: Vec<TxOut>, + /// The reason the splice negotiation failed. + reason: NegotiationFailureReason, + /// The funding contribution from the failed negotiation round, if available. This can be + /// fed back to [`ChannelManager::funding_contributed`] to retry with the same parameters. + /// Alternatively, call [`ChannelManager::splice_channel`] to obtain a fresh + /// [`FundingTemplate`] and build a new contribution. + /// + /// The contribution preserves the full set of inputs and outputs from the failed round, + /// including any that were also committed to a prior negotiated (but not yet locked) + /// splice transaction. Those overlapping inputs and outputs are intentionally omitted + /// from the preceding [`Event::DiscardFunding`], since they remain committed to that + /// prior splice. + /// + /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + /// [`FundingTemplate`]: crate::ln::funding::FundingTemplate + contribution: Option<FundingContribution>, }, /// Used to indicate to the user that they can abandon the funding transaction and recycle the /// inputs for another purpose. @@ -1636,7 +1793,7 @@ pub enum Event { /// Furthermore, note that if [`ChannelTypeFeatures::supports_zero_conf`] returns true on this type, /// the resulting [`ChannelManager`] will not be readable by versions of LDK prior to /// 0.0.107. Channels setting this type also need to get manually accepted via - /// [`crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer_0conf`], + /// [`crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer`], /// or will be rejected otherwise. /// /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager @@ -1649,12 +1806,17 @@ pub enum Event { /// Indicates that the HTLC was accepted, but could not be processed when or after attempting to /// forward it. /// + /// Note that downgrading from 0.3 with pending trampoline forwards that have incoming multipart + /// payments will produce an event that only provides information about the first htlc that was + /// received/dispatched. + /// /// # Failure Behavior and Persistence /// This event will eventually be replayed after failures-to-handle (i.e., the event handler /// returning `Err(ReplayEvent ())`) and will be persisted across restarts. HTLCHandlingFailed { - /// The channel over which the HTLC was received. - prev_channel_id: ChannelId, + /// The channel(s) over which the HTLC(s) was received. May contain multiple entries for + /// trampoline forwards. + prev_channel_ids: Vec<ChannelId>, /// The type of HTLC handling that failed. failure_type: HTLCHandlingFailureType, /// The reason that the HTLC failed. @@ -1679,9 +1841,13 @@ pub enum Event { /// [`ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments`]: crate::util::config::ChannelHandshakeConfig::negotiate_anchor_zero_fee_commitments BumpTransaction(BumpTransactionEvent), /// We received an onion message that is intended to be forwarded to a peer - /// that is currently offline. This event will only be generated if the - /// `OnionMessenger` was initialized with - /// [`OnionMessenger::new_with_offline_peer_interception`], see its docs. + /// that is currently offline *or* that is intended to be forwarded along a channel with an + /// SCID unknown to us. + /// + /// This event will only be generated if the `OnionMessenger` was initialized with + /// [`OnionMessenger::new_with_offline_peer_interception`], see its docs. The + /// [`NextMessageHop::ShortChannelId`] variant is only generated if `intercept_for_unknown_scids` + /// was set when constructing the `OnionMessenger`. /// /// The offline peer should be awoken if possible on receipt of this event, such as via the LSPS5 /// protocol. @@ -1695,9 +1861,21 @@ pub enum Event { /// /// [`OnionMessenger::new_with_offline_peer_interception`]: crate::onion_message::messenger::OnionMessenger::new_with_offline_peer_interception OnionMessageIntercepted { - /// The node id of the offline peer. - peer_node_id: PublicKey, - /// The onion message intended to be forwarded to `peer_node_id`. + /// The node id of the peer that sent the message, if known. + /// + /// This is `None` when the message is sent with + /// [`MessageSendInstructions::ForwardedMessage`] (e.g., when calling + /// [`OffersMessageFlow::enqueue_invoice_request_to_forward`]) rather than forwarded + /// internally by the `OnionMessenger`, as well as for events serialized prior to LDK 0.3. + /// Otherwise it is the node we received the message from. + /// + /// [`MessageSendInstructions::ForwardedMessage`]: crate::onion_message::messenger::MessageSendInstructions::ForwardedMessage + /// [`OffersMessageFlow::enqueue_invoice_request_to_forward`]: crate::offers::flow::OffersMessageFlow::enqueue_invoice_request_to_forward + prev_hop: Option<PublicKey>, + /// The next hop (offline peer or unknown SCID). + next_hop: NextMessageHop, + /// The onion message intended to be forwarded to the offline peer or via the unknown + /// channel once established. message: msgs::OnionMessage, }, /// Indicates that an onion message supporting peer has come online and any messages previously @@ -1807,7 +1985,7 @@ pub enum Event { invoice_request: InvoiceRequest, }, /// Indicates that a channel funding transaction constructed interactively is ready to be - /// signed. This event will only be triggered if at least one input was contributed. + /// signed. This event will only be triggered if a contribution was made to the transaction. /// /// The transaction contains all inputs and outputs provided by both parties including the /// channel's funding output and a change output if applicable. @@ -1818,8 +1996,9 @@ pub enum Event { /// Each signature MUST use the `SIGHASH_ALL` flag to avoid invalidation of the initial commitment and /// hence possible loss of funds. /// - /// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) signed - /// funding transaction. + /// After signing, call [`ChannelManager::funding_transaction_signed`] with the (partially) + /// signed funding transaction. For splices where you contributed inputs or outputs, call + /// [`ChannelManager::cancel_funding_contributed`] instead if you no longer wish to proceed. /// /// Generated in [`ChannelManager`] message handling. /// @@ -1828,6 +2007,7 @@ pub enum Event { /// returning `Err(ReplayEvent ())`), but will only be regenerated as needed after restarts. /// /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager + /// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed /// [`ChannelManager::funding_transaction_signed`]: crate::ln::channelmanager::ChannelManager::funding_transaction_signed FundingTransactionReadyForSigning { /// The `channel_id` of the channel which you'll need to pass back into @@ -2019,29 +2199,48 @@ impl Writeable for Event { }); }, &Event::PaymentForwarded { - prev_channel_id, - next_channel_id, - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id, + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { 7u8.write(writer)?; + // Fields 1, 3, 9, 11, 13 and 15 are written for backwards compatibility. We don't + // want to fail writes, so we write garbage data if we don't have at least on htlc. + debug_assert!( + !prev_htlcs.is_empty(), + "at least one prev_htlc required for PaymentForwarded", + ); + debug_assert!( + !next_htlcs.is_empty(), + "at least one next_htlc required for PaymentForwarded", + ); + let empty_locator = HTLCLocator { + channel_id: ChannelId::new_zero(), + amount_msat: None, + user_channel_id: None, + node_id: None, + }; + let legacy_prev = prev_htlcs.first().unwrap_or(&empty_locator); + let legacy_next = next_htlcs.first().unwrap_or(&empty_locator); write_tlv_fields!(writer, { (0, total_fee_earned_msat, option), - (1, prev_channel_id, option), + (1, Some(legacy_prev.channel_id), option), (2, claim_from_onchain_tx, required), - (3, next_channel_id, option), - (5, outbound_amount_forwarded_msat, option), + (3, Some(legacy_next.channel_id), option), + (5, outbound_amount_forwarded_msat, required), (7, skimmed_fee_msat, option), - (9, prev_user_channel_id, option), - (11, next_user_channel_id, option), - (13, prev_node_id, option), - (15, next_node_id, option), + (9, legacy_prev.user_channel_id, option), + (11, legacy_next.user_channel_id, option), + (13, legacy_prev.node_id, option), + (15, legacy_next.node_id, option), + // HTLCs are written as required, rather than required_vec, so that they can be + // deserialized using default_value to fill in legacy fields which expects + // LengthReadable (required_vec is WithoutLength). + (17, *prev_htlcs, required), + (19, *next_htlcs, required), }); }, &Event::ChannelClosed { @@ -2189,15 +2388,24 @@ impl Writeable for Event { }) }, &Event::HTLCHandlingFailed { - ref prev_channel_id, + ref prev_channel_ids, ref failure_type, ref failure_reason, } => { 25u8.write(writer)?; + // Legacy field is written for backwards compatibility. We don't want to fail writes + // so we write garbage data if we don't have the data we expect. + debug_assert!( + !prev_channel_ids.is_empty(), + "at least one prev_channel_id required for HTLCHandlingFailed" + ); + let zero_id = ChannelId::new_zero(); + let legacy_chan_id = prev_channel_ids.first().unwrap_or(&zero_id); write_tlv_fields!(writer, { - (0, prev_channel_id, required), + (0, legacy_chan_id, required), (1, failure_reason, option), (2, failure_type, required), + (3, *prev_channel_ids, required), }) }, &Event::BumpTransaction(ref event) => { @@ -2250,11 +2458,19 @@ impl Writeable for Event { 35u8.write(writer)?; // Never write ConnectionNeeded events as buffered onion messages aren't serialized. }, - &Event::OnionMessageIntercepted { ref peer_node_id, ref message } => { + &Event::OnionMessageIntercepted { ref prev_hop, ref next_hop, ref message } => { 37u8.write(writer)?; + // 0 used to be peer_node_id in LDK v0.2 and prior; we keep writing it when the next + // hop is a node id for backwards compatibility. + let legacy_peer_node_id = match next_hop { + NextMessageHop::NodeId(node_id) => Some(node_id), + NextMessageHop::ShortChannelId(_) => None, + }; write_tlv_fields!(writer, { - (0, peer_node_id, required), + (0, legacy_peer_node_id, option), + (1, next_hop, required), (2, message, required), + (3, prev_hop, option), }); }, &Event::OnionMessagePeerConnected { ref peer_node_id } => { @@ -2302,7 +2518,7 @@ impl Writeable for Event { // We never write out FundingTransactionReadyForSigning events as they will be regenerated when // necessary. }, - &Event::SplicePending { + &Event::SpliceNegotiated { ref channel_id, ref user_channel_id, ref counterparty_node_id, @@ -2320,24 +2536,20 @@ impl Writeable for Event { (11, new_funding_redeem_script, required), }); }, - &Event::SpliceFailed { + &Event::SpliceNegotiationFailed { ref channel_id, ref user_channel_id, ref counterparty_node_id, - ref abandoned_funding_txo, - ref channel_type, - ref contributed_inputs, - ref contributed_outputs, + ref reason, + ref contribution, } => { 52u8.write(writer)?; write_tlv_fields!(writer, { (1, channel_id, required), - (3, channel_type, option), (5, user_channel_id, required), (7, counterparty_node_id, required), - (9, abandoned_funding_txo, option), - (11, *contributed_inputs, optional_vec), - (13, *contributed_outputs, optional_vec), + (11, reason, required), + (13, contribution, option), }); }, // Note that, going forward, all new events must only write data inside of @@ -2378,7 +2590,7 @@ impl MaybeReadable for Event { (6, _user_payment_id, option), (7, claim_deadline, option), (8, payment_preimage, option), - (9, onion_fields, option), + (9, onion_fields, (option: ReadableArgs, amount_msat)), (10, counterparty_skimmed_fee_msat_opt, option), (11, payment_context, option), (13, payment_id, option), @@ -2545,35 +2757,51 @@ impl MaybeReadable for Event { }, 7u8 => { let mut f = || { - let mut prev_channel_id = None; - let mut next_channel_id = None; - let mut prev_user_channel_id = None; - let mut next_user_channel_id = None; - let mut prev_node_id = None; - let mut next_node_id = None; + // Legacy values that have been replaced by prev_htlcs and next_htlcs. + let mut prev_channel_id_legacy = None; + let mut next_channel_id_legacy = None; + let mut prev_user_channel_id_legacy = None; + let mut next_user_channel_id_legacy = None; + let mut prev_node_id_legacy = None; + let mut next_node_id_legacy = None; + let mut total_fee_earned_msat = None; let mut skimmed_fee_msat = None; let mut claim_from_onchain_tx = false; - let mut outbound_amount_forwarded_msat = None; + let mut outbound_amount_forwarded_msat = 0; + let mut prev_htlcs = vec![]; + let mut next_htlcs = vec![]; read_tlv_fields!(reader, { (0, total_fee_earned_msat, option), - (1, prev_channel_id, option), + (1, prev_channel_id_legacy, option), (2, claim_from_onchain_tx, required), - (3, next_channel_id, option), - (5, outbound_amount_forwarded_msat, option), + (3, next_channel_id_legacy, option), + (5, outbound_amount_forwarded_msat, required), (7, skimmed_fee_msat, option), - (9, prev_user_channel_id, option), - (11, next_user_channel_id, option), - (13, prev_node_id, option), - (15, next_node_id, option), + (9, prev_user_channel_id_legacy, option), + (11, next_user_channel_id_legacy, option), + (13, prev_node_id_legacy, option), + (15, next_node_id_legacy, option), + // We never expect prev/next_channel_id_legacy to be None because this field + // was only None for versions before 0.0.107 and we do not allow upgrades + // with pending forwards to 0.1 for any version 0.0.123 or earlier. + (17, prev_htlcs, (default_value, vec![HTLCLocator{ + channel_id: prev_channel_id_legacy.ok_or(DecodeError::InvalidValue)?, + amount_msat: total_fee_earned_msat + .map(|fee| outbound_amount_forwarded_msat + fee), + user_channel_id: prev_user_channel_id_legacy, + node_id: prev_node_id_legacy, + }])), + (19, next_htlcs, (default_value, vec![HTLCLocator{ + channel_id: next_channel_id_legacy.ok_or(DecodeError::InvalidValue)?, + amount_msat: Some(outbound_amount_forwarded_msat), + user_channel_id: next_user_channel_id_legacy, + node_id: next_node_id_legacy, + }])), }); Ok(Some(Event::PaymentForwarded { - prev_channel_id, - next_channel_id, - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id, + prev_htlcs, + next_htlcs, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, @@ -2710,7 +2938,8 @@ impl MaybeReadable for Event { (4, amount_msat, required), (5, htlcs, optional_vec), (7, sender_intended_total_msat, option), - (9, onion_fields, option), + (9, onion_fields, (option: ReadableArgs, + sender_intended_total_msat.unwrap_or(amount_msat))), (11, payment_id, option), }); Ok(Some(Event::PaymentClaimed { @@ -2762,13 +2991,17 @@ impl MaybeReadable for Event { }, 25u8 => { let mut f = || { - let mut prev_channel_id = ChannelId::new_zero(); + let mut prev_channel_id_legacy = ChannelId::new_zero(); let mut failure_reason = None; let mut failure_type_opt = UpgradableRequired(None); + let mut prev_channel_ids = vec![]; read_tlv_fields!(reader, { - (0, prev_channel_id, required), + (0, prev_channel_id_legacy, required), (1, failure_reason, option), (2, failure_type_opt, upgradable_required), + (3, prev_channel_ids, (default_value, vec![ + prev_channel_id_legacy, + ])), }); // If a legacy HTLCHandlingFailureType::UnknownNextHop was written, upgrade @@ -2783,7 +3016,7 @@ impl MaybeReadable for Event { failure_reason = Some(LocalHTLCFailureReason::UnknownNextPeer.into()); } Ok(Some(Event::HTLCHandlingFailed { - prev_channel_id, + prev_channel_ids, failure_type: _init_tlv_based_struct_field!( failure_type_opt, upgradable_required @@ -2869,11 +3102,18 @@ impl MaybeReadable for Event { 37u8 => { let mut f = || { _init_and_read_len_prefixed_tlv_fields!(reader, { - (0, peer_node_id, required), + (0, peer_node_id, option), + (1, next_hop, option), (2, message, required), + (3, prev_hop, option), }); + + let next_hop = next_hop + .or(peer_node_id.map(NextMessageHop::NodeId)) + .ok_or(msgs::DecodeError::InvalidValue)?; Ok(Some(Event::OnionMessageIntercepted { - peer_node_id: peer_node_id.0.unwrap(), + prev_hop, + next_hop, message: message.0.unwrap(), })) }; @@ -2945,7 +3185,7 @@ impl MaybeReadable for Event { (11, new_funding_redeem_script, required), }); - Ok(Some(Event::SplicePending { + Ok(Some(Event::SpliceNegotiated { channel_id: channel_id.0.unwrap(), user_channel_id: user_channel_id.0.unwrap(), counterparty_node_id: counterparty_node_id.0.unwrap(), @@ -2960,22 +3200,18 @@ impl MaybeReadable for Event { let mut f = || { _init_and_read_len_prefixed_tlv_fields!(reader, { (1, channel_id, required), - (3, channel_type, option), (5, user_channel_id, required), (7, counterparty_node_id, required), - (9, abandoned_funding_txo, option), - (11, contributed_inputs, optional_vec), - (13, contributed_outputs, optional_vec), + (11, reason, upgradable_option), + (13, contribution, option), }); - Ok(Some(Event::SpliceFailed { + Ok(Some(Event::SpliceNegotiationFailed { channel_id: channel_id.0.unwrap(), user_channel_id: user_channel_id.0.unwrap(), counterparty_node_id: counterparty_node_id.0.unwrap(), - abandoned_funding_txo, - channel_type, - contributed_inputs: contributed_inputs.unwrap_or_default(), - contributed_outputs: contributed_outputs.unwrap_or_default(), + reason: reason.unwrap_or(NegotiationFailureReason::Unknown), + contribution, })) }; f() @@ -3000,6 +3236,48 @@ impl MaybeReadable for Event { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn legacy_payment_forwarded_preserves_unknown_inbound_htlc_amount() { + let prev_channel_id = ChannelId::from_bytes([1; 32]); + let next_channel_id = ChannelId::from_bytes([2; 32]); + let mut encoded_legacy_event = vec![ + 7, // Event::PaymentForwarded + 81, // TLV stream length + 1, 32, // prev_channel_id + ]; + encoded_legacy_event.extend_from_slice(&[1; 32]); + encoded_legacy_event.extend_from_slice(&[2, 1, 0]); // claim_from_onchain_tx + encoded_legacy_event.extend_from_slice(&[3, 32]); // next_channel_id + encoded_legacy_event.extend_from_slice(&[2; 32]); + // outbound_amount_forwarded_msat + encoded_legacy_event.extend_from_slice(&[5, 8, 0, 0, 0, 0, 0, 45, 198, 192]); + + match Event::read(&mut &encoded_legacy_event[..]).unwrap().unwrap() { + Event::PaymentForwarded { + prev_htlcs, + next_htlcs, + total_fee_earned_msat, + outbound_amount_forwarded_msat, + .. + } => { + assert_eq!(total_fee_earned_msat, None); + assert_eq!(outbound_amount_forwarded_msat, 3_000_000); + assert_eq!(prev_htlcs.len(), 1); + assert_eq!(prev_htlcs[0].channel_id, prev_channel_id); + assert_eq!(prev_htlcs[0].amount_msat, None); + assert_eq!(next_htlcs.len(), 1); + assert_eq!(next_htlcs[0].channel_id, next_channel_id); + assert_eq!(next_htlcs[0].amount_msat, Some(3_000_000)); + }, + _ => panic!("expected PaymentForwarded event"), + } + } +} + /// A trait indicating an object may generate events. /// /// Events are processed by passing an [`EventHandler`] to [`process_pending_events`]. @@ -3083,19 +3361,3 @@ impl<T: EventHandler> EventHandler for Arc<T> { self.deref().handle_event(event) } } - -/// The BOLT 12 invoice that was paid, surfaced in [`Event::PaymentSent::bolt12_invoice`]. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum PaidBolt12Invoice { - /// The BOLT 12 invoice specified by the BOLT 12 specification, - /// allowing the user to perform proof of payment. - Bolt12Invoice(Bolt12Invoice), - /// The Static invoice, used in the async payment specification update proposal, - /// where the user cannot perform proof of payment. - StaticInvoice(StaticInvoice), -} - -impl_writeable_tlv_based_enum!(PaidBolt12Invoice, - {0, Bolt12Invoice} => (), - {2, StaticInvoice} => (), -); diff --git a/lightning/src/lib.rs b/lightning/src/lib.rs index ee3b0f47a4d..496d1e5bb45 100644 --- a/lightning/src/lib.rs +++ b/lightning/src/lib.rs @@ -43,6 +43,9 @@ #[cfg(all(fuzzing, test))] compile_error!("Tests will always fail with cfg=fuzzing"); +#[cfg(all(fuzzing, feature = "grind_signatures"))] +compile_error!("Fuzz builds must not enable grind_signatures"); + #[macro_use] extern crate alloc; diff --git a/lightning/src/ln/accountable_tests.rs b/lightning/src/ln/accountable_tests.rs index 16ca1425817..a2b918a3e14 100644 --- a/lightning/src/ln/accountable_tests.rs +++ b/lightning/src/ln/accountable_tests.rs @@ -26,12 +26,13 @@ fn test_accountable_forwarding_with_override( let _chan_ab = create_announced_chan_between_nodes(&nodes, 0, 1); let _chan_bc = create_announced_chan_between_nodes(&nodes, 1, 2); - let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[2]); + let (payment_preimage, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[2], None, None); let route_params = RouteParameters::from_payment_params_and_value( PaymentParameters::from_node_id(nodes[2].node.get_our_node_id(), TEST_FINAL_CLTV), 100_000, ); - let onion_fields = RecipientOnionFields::secret_only(payment_secret); + let onion_fields = RecipientOnionFields::secret_only(payment_secret, 100_000); let payment_id = PaymentId(payment_hash.0); nodes[0] .node diff --git a/lightning/src/ln/async_payments_tests.rs b/lightning/src/ln/async_payments_tests.rs index 8a991b1d98d..6e8f38f847a 100644 --- a/lightning/src/ln/async_payments_tests.rs +++ b/lightning/src/ln/async_payments_tests.rs @@ -7,6 +7,8 @@ // You may not use this file except in accordance with one or both of these // licenses. +use alloc::collections::BTreeMap; + use crate::blinded_path::message::{ BlindedMessagePath, MessageContext, NextMessageHop, OffersContext, }; @@ -299,6 +301,7 @@ fn create_static_invoice_builder<'a>( relative_expiry_secs, recipient.node.list_usable_channels(), recipient.node.test_get_peers_for_blinded_path(), + None, ) .unwrap() } @@ -314,7 +317,10 @@ fn create_static_invoice<T: secp256k1::Signing + secp256k1::Verification>( .create_blinded_paths( always_online_counterparty.node.get_our_node_id(), always_online_counterparty.keys_manager.get_receive_auth_key(), - MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + MessageContext::Offers(OffersContext::InvoiceRequest { + nonce: Nonce([42; 16]), + payment_metadata: None, + }), Vec::new(), &secp_ctx, ) @@ -504,6 +510,9 @@ fn often_offline_node_cfg() -> UserConfig { cfg.channel_handshake_config.announce_for_forwarding = false; cfg.channel_handshake_limits.force_announced_channel_preference = true; cfg.hold_outbound_htlcs_at_next_hop = true; + // Use the setting that matches the default at the time these tests were written + cfg.channel_handshake_config.unannounced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; cfg } @@ -615,7 +624,7 @@ fn invalid_keysend_payment_secret() { .node .send_spontaneous_payment( Some(keysend_preimage), - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(keysend_preimage.0), route_params, Retry::Attempts(0), @@ -682,7 +691,10 @@ fn static_invoice_unknown_required_features() { .create_blinded_paths( nodes[1].node.get_our_node_id(), nodes[1].keys_manager.get_receive_auth_key(), - MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + MessageContext::Offers(OffersContext::InvoiceRequest { + nonce: Nonce([42; 16]), + payment_metadata: None, + }), Vec::new(), &secp_ctx, ) @@ -1147,6 +1159,88 @@ fn async_receive_flow_success() { assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } +#[test] +fn async_payment_delivers_payment_metadata() { + // Test that `payment_metadata` set in the `AsyncBolt12OfferContext` of a static invoice's + // blinded payment paths is surfaced via `Event::PaymentClaimable` when the async recipient + // receives the keysend payment. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + + let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); + allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + let node_chanmgrs = + create_node_chanmgrs(3, &node_cfgs, &[None, Some(allow_priv_chan_fwds_cfg), None]); + + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + let recipient_id = vec![42; 32]; + let inv_server_paths = + nodes[1].node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + nodes[2].node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + expect_offer_paths_requests(&nodes[2], &[&nodes[0], &nodes[1]]); + + // Configure the recipient's router to inject `payment_metadata` into the + // `AsyncBolt12OfferContext` of the static invoice's blinded payment paths. The + // `pass_static_invoice_server_messages` flow below builds the static invoice via this router, + // at which point the override is consumed. + let mut expected_metadata = BTreeMap::new(); + expected_metadata.insert(0u64, vec![1, 2, 3, 4]); + expected_metadata.insert(7u64, vec![0xab, 0xcd]); + nodes[2].router.set_next_payment_context_metadata(expected_metadata.clone()); + + let invoice_flow_res = + pass_static_invoice_server_messages(&nodes[1], &nodes[2], recipient_id.clone()); + let static_invoice = invoice_flow_res.invoice; + let offer = nodes[2].node.get_async_receive_offer().unwrap(); + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + nodes[0].node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap(); + let release_held_htlc_om = pass_async_payments_oms( + static_invoice.clone(), + &nodes[0], + &nodes[1], + &nodes[2], + recipient_id, + invoice_flow_res.invoice_request_path, + ) + .1; + nodes[0] + .onion_messenger + .handle_onion_message(nodes[2].node.get_our_node_id(), &release_held_htlc_om); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); + let payment_hash = extract_payment_hash(&ev); + check_added_monitors(&nodes[0], 1); + + let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; + let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + let claimable_ev = do_pass_along_path(args).unwrap(); + + // Verify the `payment_metadata` we injected is surfaced via the `Bolt12OfferContext` of + // the `PaymentPurpose`. The recipient converts `AsyncBolt12OfferContext` to + // `Bolt12OfferContext` when constructing the `PaymentPurpose` for keysend payments. + match &claimable_ev { + Event::PaymentClaimable { + purpose: PaymentPurpose::Bolt12OfferPayment { payment_context, .. }, + .. + } => { + assert_eq!(payment_context.payment_metadata.as_ref(), Some(&expected_metadata)); + }, + _ => panic!("Unexpected event: {:?}", claimable_ev), + } + + let keysend_preimage = extract_payment_preimage(&claimable_ev); + let (res, _) = + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); +} + #[cfg_attr(feature = "std", ignore)] #[test] fn expired_static_invoice_fail() { @@ -1310,6 +1404,10 @@ fn async_receive_mpp() { let mut allow_priv_chan_fwds_cfg = test_default_channel_config(); allow_priv_chan_fwds_cfg.accept_forwards_to_priv_channels = true; + // Set the percentage to the default value at the time this test was written + allow_priv_chan_fwds_cfg + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 10; let node_chanmgrs = create_node_chanmgrs( 4, @@ -1584,6 +1682,7 @@ fn reject_bad_payment_secret() { PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { // We don't reach the point of checking the invreq nonce due to the invalid payment secret offer_nonce: Nonce([i; Nonce::LENGTH]), + payment_metadata: None, }), u32::MAX, ) @@ -1662,7 +1761,10 @@ fn invalid_async_receive_with_retry<F1, F2>( .create_blinded_paths( nodes[1].node.get_our_node_id(), nodes[1].keys_manager.get_receive_auth_key(), - MessageContext::Offers(OffersContext::InvoiceRequest { nonce: Nonce([42; 16]) }), + MessageContext::Offers(OffersContext::InvoiceRequest { + nonce: Nonce([42; 16]), + payment_metadata: None, + }), Vec::new(), &secp_ctx, ) @@ -1879,8 +1981,9 @@ fn expired_static_invoice_payment_path() { } }; - // Mine a bunch of blocks so the hardcoded path's `max_cltv_expiry` is expired at the recipient's - // end by the time the payment arrives. + // Mine a bunch of blocks on the sender so the hardcoded path's `max_cltv_expiry` is expired. + // Note that the path expires "all at once" and will be invalid at the intro point so will be + // rejected before it reaches the destination. let min_cltv_expiry_delta = test_default_channel_config().channel_config.cltv_expiry_delta; connect_blocks( &nodes[0], @@ -1895,7 +1998,6 @@ fn expired_static_invoice_payment_path() { &nodes[1], final_max_cltv_expiry - nodes[1].best_block_info().1 - // Don't expire the path for nodes[1] - min_cltv_expiry_delta as u32 - HTLC_FAIL_BACK_BUFFER - LATENCY_GRACE_PERIOD_BLOCKS @@ -1932,18 +2034,17 @@ fn expired_static_invoice_payment_path() { let payment_hash = extract_payment_hash(&ev); check_added_monitors(&nodes[0], 1); - let route: &[&[&Node]] = &[&[&nodes[1], &nodes[2]]]; - let args = PassAlongPathArgs::new(&nodes[0], route[0], amt_msat, payment_hash, ev) - .without_claimable_event() - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) - .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); - do_pass_along_path(args); - fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1], &nodes[2]], false); - nodes[2].logger.assert_log_contains( - "lightning::ln::channelmanager", - "violated blinded payment constraints", - 1, + let payment_event = SendEvent::from_event(ev); + nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &payment_event.msgs[0]); + check_added_monitors(&nodes[1], 0); + do_commitment_signed_dance(&nodes[1], &nodes[0], &payment_event.commitment_msg, false, true); + expect_and_process_pending_htlcs(&nodes[1], false); + expect_htlc_handling_failed_destinations!( + nodes[1].node.get_and_clear_pending_events(), + &[HTLCHandlingFailureType::InvalidOnion] ); + check_added_monitors(&nodes[1], 1); + fail_blinded_htlc_backwards(payment_hash, 1, &[&nodes[0], &nodes[1]], false); } #[cfg_attr(feature = "std", ignore)] @@ -2349,6 +2450,25 @@ fn refresh_static_invoices_for_used_offers() { .handle_onion_message(server.node.get_our_node_id(), &invoice_persisted_om); assert_eq!(recipient.node.flow.test_get_async_receive_offers().len(), 1); + // The invoice was just refreshed and persisted. A later timer tick must wait until the next + // refresh threshold before generating another invoice for the same offer. + recipient.node.timer_tick_occurred(); + let pending_oms_after = recipient.onion_messenger.release_pending_msgs(); + let mut extra_serve_invoices = 0; + if let Some(msgs) = pending_oms_after.get(&server.node.get_our_node_id()) { + for msg in msgs { + if let PeeledOnion::AsyncPayments(AsyncPaymentsMessage::ServeStaticInvoice(_), _, _) = + server.onion_messenger.peel_onion_message(&msg).unwrap() + { + extra_serve_invoices += 1; + } + } + } + assert_eq!( + extra_serve_invoices, 0, + "used offer invoice was refreshed again immediately after a successful refresh" + ); + // Remove the peer restriction added above. server.message_router.peers_override.lock().unwrap().clear(); recipient.message_router.peers_override.lock().unwrap().clear(); @@ -3117,7 +3237,10 @@ fn intercepted_hold_htlc() { .unwrap(); let mut offer_nonce = Nonce([0; Nonce::LENGTH]); offer_nonce.0.copy_from_slice(&hardcoded_random_bytes[..Nonce::LENGTH]); - let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { offer_nonce }); + let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { + offer_nonce, + payment_metadata: None, + }); let blinded_payment_path_with_jit_channel_scid = recipient .node .flow @@ -3451,3 +3574,167 @@ fn release_htlc_races_htlc_onion_decode() { claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); } + +#[test] +fn async_payment_e2e_release_before_hold_registered() { + // Tests that an LSP will release a held htlc if the `ReleaseHeldHtlc` message was received + // before the HTLC was fully committed to the channel, which was previously broken. + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + + let (sender_cfg, recipient_cfg) = (often_offline_node_cfg(), often_offline_node_cfg()); + let mut sender_lsp_cfg = test_default_channel_config(); + sender_lsp_cfg.enable_htlc_hold = true; + let mut invoice_server_cfg = test_default_channel_config(); + invoice_server_cfg.accept_forwards_to_priv_channels = true; + + let node_chanmgrs = create_node_chanmgrs( + 4, + &node_cfgs, + &[Some(sender_cfg), Some(sender_lsp_cfg), Some(invoice_server_cfg), Some(recipient_cfg)], + ); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); + create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 1_000_000, 0); + unify_blockheight_across_nodes(&nodes); + let sender = &nodes[0]; + let sender_lsp = &nodes[1]; + let invoice_server = &nodes[2]; + let recipient = &nodes[3]; + + let recipient_id = vec![42; 32]; + let inv_server_paths = + invoice_server.node.blinded_paths_for_async_recipient(recipient_id.clone(), None).unwrap(); + recipient.node.set_paths_to_static_invoice_server(inv_server_paths).unwrap(); + expect_offer_paths_requests(recipient, &[invoice_server, sender_lsp]); + let invoice_flow_res = + pass_static_invoice_server_messages(invoice_server, recipient, recipient_id.clone()); + let invoice = invoice_flow_res.invoice; + let invreq_path = invoice_flow_res.invoice_request_path; + + let offer = recipient.node.get_async_receive_offer().unwrap(); + recipient.node.peer_disconnected(invoice_server.node.get_our_node_id()); + recipient.onion_messenger.peer_disconnected(invoice_server.node.get_our_node_id()); + invoice_server.node.peer_disconnected(recipient.node.get_our_node_id()); + invoice_server.onion_messenger.peer_disconnected(recipient.node.get_our_node_id()); + + let amt_msat = 5000; + let payment_id = PaymentId([1; 32]); + sender.node.pay_for_offer(&offer, Some(amt_msat), payment_id, Default::default()).unwrap(); + + let (peer_id, invreq_om) = extract_invoice_request_om(sender, &[sender_lsp, invoice_server]); + invoice_server.onion_messenger.handle_onion_message(peer_id, &invreq_om); + + let mut events = invoice_server.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (reply_path, invreq) = match events.pop().unwrap() { + Event::StaticInvoiceRequested { + recipient_id: ev_id, reply_path, invoice_request, .. + } => { + assert_eq!(recipient_id, ev_id); + (reply_path, invoice_request) + }, + _ => panic!(), + }; + + invoice_server + .node + .respond_to_static_invoice_request(invoice, reply_path, invreq, invreq_path) + .unwrap(); + let (peer_node_id, static_invoice_om, static_invoice) = + extract_static_invoice_om(invoice_server, &[sender_lsp, sender]); + + // Lock the HTLC in with the sender LSP, but stop before the sender's revoke_and_ack is handed + // back to the sender LSP. This reproduces the real LSPS2 timing where ReleaseHeldHtlc can + // arrive before the held HTLC is queued for decode on the sender LSP. + sender.onion_messenger.handle_onion_message(peer_node_id, &static_invoice_om); + check_added_monitors(sender, 1); + let commitment_update = get_htlc_update_msgs(&sender, &sender_lsp.node.get_our_node_id()); + let update_add = commitment_update.update_add_htlcs[0].clone(); + let payment_hash = update_add.payment_hash; + assert!(update_add.hold_htlc.is_some()); + sender_lsp.node.handle_update_add_htlc(sender.node.get_our_node_id(), &update_add); + sender_lsp.node.handle_commitment_signed_batch_test( + sender.node.get_our_node_id(), + &commitment_update.commitment_signed, + ); + check_added_monitors(sender_lsp, 1); + let (_extra_msg_option, sender_raa, sender_holding_cell_htlcs) = + do_main_commitment_signed_dance(sender_lsp, sender, false); + assert!(sender_holding_cell_htlcs.is_empty()); + + let held_htlc_om_to_inv_server = sender + .onion_messenger + .next_onion_message_for_peer(invoice_server.node.get_our_node_id()) + .unwrap(); + invoice_server + .onion_messenger + .handle_onion_message(sender_lsp.node.get_our_node_id(), &held_htlc_om_to_inv_server); + + let mut events_rc = core::cell::RefCell::new(Vec::new()); + invoice_server.onion_messenger.process_pending_events(&|e| Ok(events_rc.borrow_mut().push(e))); + let events = events_rc.into_inner(); + let held_htlc_om = events + .into_iter() + .find_map(|ev| { + if let Event::OnionMessageIntercepted { message, .. } = ev { + let peeled_onion = recipient.onion_messenger.peel_onion_message(&message).unwrap(); + if matches!( + peeled_onion, + PeeledOnion::Offers(OffersMessage::InvoiceRequest { .. }, _, _) + ) { + return None; + } + + assert!(matches!( + peeled_onion, + PeeledOnion::AsyncPayments(AsyncPaymentsMessage::HeldHtlcAvailable(_), _, _) + )); + Some(message) + } else { + None + } + }) + .unwrap(); + + let mut reconnect_args = ReconnectArgs::new(invoice_server, recipient); + reconnect_args.send_channel_ready = (true, true); + reconnect_nodes(reconnect_args); + + let events = core::cell::RefCell::new(Vec::new()); + invoice_server.onion_messenger.process_pending_events(&|e| Ok(events.borrow_mut().push(e))); + assert_eq!(events.borrow().len(), 1); + assert!(matches!(events.into_inner().pop().unwrap(), Event::OnionMessagePeerConnected { .. })); + expect_offer_paths_requests(recipient, &[invoice_server]); + + recipient + .onion_messenger + .handle_onion_message(invoice_server.node.get_our_node_id(), &held_htlc_om); + let (peer_id, release_htlc_om) = + extract_release_htlc_oms(recipient, &[sender, sender_lsp, invoice_server]).pop().unwrap(); + sender_lsp.onion_messenger.handle_onion_message(peer_id, &release_htlc_om); + + // Now let the sender LSP receive the sender's revoke_and_ack and continue processing the held + // HTLC, which previously would've resulted in holding the HTLC even though the release message + // was already received. + sender_lsp.node.handle_revoke_and_ack(sender.node.get_our_node_id(), &sender_raa); + check_added_monitors(sender_lsp, 1); + assert!(sender_lsp.node.get_and_clear_pending_msg_events().is_empty()); + sender_lsp.node.process_pending_htlc_forwards(); + let mut events = sender_lsp.node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = remove_first_msg_event_to_node(&invoice_server.node.get_our_node_id(), &mut events); + check_added_monitors(&sender_lsp, 1); + + let path: &[&Node] = &[invoice_server, recipient]; + let args = PassAlongPathArgs::new(sender_lsp, path, amt_msat, payment_hash, ev) + .with_dummy_tlvs(&[DummyTlvs::default(); DEFAULT_PAYMENT_DUMMY_HOPS]); + let claimable_ev = do_pass_along_path(args).unwrap(); + + let route: &[&[&Node]] = &[&[sender_lsp, invoice_server, recipient]]; + let keysend_preimage = extract_payment_preimage(&claimable_ev); + let (res, _) = + claim_payment_along_route(ClaimAlongRouteArgs::new(sender, route, keysend_preimage)); + assert_eq!(res, Some(PaidBolt12Invoice::StaticInvoice(static_invoice))); +} diff --git a/lightning/src/ln/async_signer_tests.rs b/lightning/src/ln/async_signer_tests.rs index 7d28a137d0a..05508a42b0e 100644 --- a/lightning/src/ln/async_signer_tests.rs +++ b/lightning/src/ln/async_signer_tests.rs @@ -10,9 +10,7 @@ //! Tests for asynchronous signing. These tests verify that the channel state machine behaves //! properly with a signer implementation that asynchronously derives signatures. -use crate::events::bump_transaction::sync::WalletSourceSync; -use crate::ln::funding::SpliceContribution; -use crate::ln::splicing_tests::negotiate_splice_tx; +use crate::ln::splicing_tests::{initiate_splice_out, negotiate_splice_tx}; use crate::prelude::*; use crate::util::ser::Writeable; use bitcoin::secp256k1::Secp256k1; @@ -20,11 +18,11 @@ use bitcoin::{Amount, TxOut}; use crate::chain::channelmonitor::LATENCY_GRACE_PERIOD_BLOCKS; use crate::chain::ChannelMonitorUpdateStatus; -use crate::events::{ClosureReason, Event}; +use crate::events::{ClosureReason, Event, HTLCHandlingFailureType}; use crate::ln::chan_utils::ClosingTransaction; use crate::ln::channel::DISCONNECT_PEER_AWAITING_RESPONSE_TICKS; use crate::ln::channel_state::{ChannelDetails, ChannelShutdownState}; -use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder}; +use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder, TrustedChannelFeatures}; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent}; use crate::ln::outbound_payment::RecipientOnionFields; use crate::ln::{functional_test_utils::*, msgs}; @@ -32,6 +30,7 @@ use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::SignerProvider; use crate::util::logger::Logger; use crate::util::test_channel_signer::SignerOp; +use crate::util::wallet_utils::WalletSourceSync; #[test] fn test_open_channel() { @@ -70,19 +69,20 @@ fn do_test_open_channel(zero_conf: bool) { // Handle an inbound channel simulating an async signer. nodes[1].disable_next_channel_signer_op(SignerOp::GetPerCommitmentPoint); - nodes[1].node.handle_open_channel(node_a_id, &open_chan_msg); if zero_conf { + nodes[1].node.handle_open_channel(node_a_id, &open_chan_msg); let events = nodes[1].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1, "Expected one event, got {}", events.len()); match &events[0] { Event::OpenChannelRequest { temporary_channel_id, .. } => { nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( temporary_channel_id, &node_a_id, 0, + TrustedChannelFeatures::ZeroConf, None, ) .expect("Unable to accept inbound zero-conf channel"); @@ -90,15 +90,7 @@ fn do_test_open_channel(zero_conf: bool) { ev => panic!("Expected OpenChannelRequest, not {:?}", ev), } } else { - let events = nodes[1].node.get_and_clear_pending_events(); - assert_eq!(events.len(), 1, "Expected one event, got {}", events.len()); - match &events[0] { - Event::OpenChannelRequest { temporary_channel_id, .. } => nodes[1] - .node - .accept_inbound_channel(temporary_channel_id, &node_a_id, 0, None) - .unwrap(), - ev => panic!("Expected OpenChannelRequest, not {:?}", ev), - } + handle_and_accept_open_channel(&nodes[1], node_a_id, &open_chan_msg); } let channel_id_1 = { @@ -305,7 +297,7 @@ fn do_test_async_commitment_signature_for_commitment_signed_revoke_and_ack( let (route, our_payment_hash, _our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(src, dst, 8000000); - let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret); + let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret, 8000000); let payment_id = PaymentId(our_payment_hash.0); src.node .send_payment_with_route(route, our_payment_hash, recipient_fields, payment_id) @@ -372,11 +364,9 @@ fn test_funding_signed_0conf() { fn do_test_funding_signed_0conf(signer_ops: Vec<SignerOp>) { // Simulate acquiring the signature for `funding_signed` asynchronously for a zero-conf channel. - let mut manually_accept_config = test_default_channel_config(); - let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_accept_config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); @@ -394,10 +384,11 @@ fn do_test_funding_signed_0conf(signer_ops: Vec<SignerOp>) { Event::OpenChannelRequest { temporary_channel_id, .. } => { nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( temporary_channel_id, &node_a_id, 0, + TrustedChannelFeatures::ZeroConf, None, ) .expect("Unable to accept inbound zero-conf channel"); @@ -507,6 +498,211 @@ fn test_async_raa_peer_disconnect() { do_test_async_raa_peer_disconnect(UnblockSignerAcrossDisconnectCase::BeforeReestablish, false); } +#[test] +fn test_signer_unblocked_clears_monitor_pending_raa_after_reestablish() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + + create_announced_chan_between_nodes(&nodes, 0, 1); + let chan_bc = create_announced_chan_between_nodes(&nodes, 1, 2); + + // Rebalance so that node C can send a payment back through node B later in the test. + send_payment(&nodes[0], &[&nodes[1], &nodes[2]], 5_000_000); + + // Put the B-C channel into AwaitingRAA by having C fail a payment backwards and retaining C's + // final RAA instead of delivering it to B immediately. + let (_, payment_hash_1, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 1_000_000); + nodes[2].node.fail_htlc_backwards(&payment_hash_1); + expect_and_process_pending_htlcs_and_htlc_handling_failed( + &nodes[2], + &[HTLCHandlingFailureType::Receive { payment_hash: payment_hash_1 }], + ); + check_added_monitors(&nodes[2], 1); + + let updates = get_htlc_update_msgs(&nodes[2], &node_b_id); + assert!(updates.update_add_htlcs.is_empty()); + assert_eq!(updates.update_fail_htlcs.len(), 1); + assert!(updates.update_fail_malformed_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + nodes[1].node.handle_update_fail_htlc(node_c_id, &updates.update_fail_htlcs[0]); + + let pending_c_raa = + commitment_signed_dance_return_raa(&nodes[1], &nodes[2], &updates.commitment_signed, false); + check_added_monitors(&nodes[0], 0); + + // While B is waiting for C's RAA, forward another A-to-C payment. B accepts it on the A-B + // channel, but cannot forward it over B-C yet, so it is held in B's holding cell. + let (route, payment_hash_2, _payment_preimage_2, payment_secret_2) = + get_route_and_payment_hash!(nodes[0], nodes[2], 1_000_000); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1_000_000); + let id_2 = PaymentId(payment_hash_2.0); + nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); + check_added_monitors(&nodes[0], 1); + + let send_event = SendEvent::from_node(&nodes[0]); + assert_eq!(send_event.node_id, node_b_id); + assert_eq!(send_event.msgs.len(), 1); + nodes[1].node.handle_update_add_htlc(node_a_id, &send_event.msgs[0]); + do_commitment_signed_dance(&nodes[1], &nodes[0], &send_event.commitment_msg, false, false); + + expect_and_process_pending_htlcs(&nodes[1], false); + check_added_monitors(&nodes[1], 0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Now make B owe C an RAA whose monitor update has already completed, but whose RAA cannot be + // constructed because B's signer is unavailable. + let (route, payment_hash_3, _payment_preimage_3, payment_secret_3) = + get_route_and_payment_hash!(nodes[2], nodes[0], 1_000_000); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3, 1_000_000); + let id_3 = PaymentId(payment_hash_3.0); + nodes[2].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); + check_added_monitors(&nodes[2], 1); + + let send_event = SendEvent::from_node(&nodes[2]); + assert_eq!(send_event.node_id, node_b_id); + assert_eq!(send_event.msgs.len(), 1); + nodes[1].node.handle_update_add_htlc(node_c_id, &send_event.msgs[0]); + nodes[1].disable_channel_signer_op(&node_c_id, &chan_bc.2, SignerOp::ReleaseCommitmentSecret); + nodes[1].node.handle_commitment_signed_batch_test(node_c_id, &send_event.commitment_msg); + check_added_monitors(&nodes[1], 1); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Deliver C's earlier RAA to B while monitor updating is blocked. This frees B's holding-cell + // HTLC and leaves a monitor update in flight. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.handle_revoke_and_ack(node_c_id, &pending_c_raa); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + check_added_monitors(&nodes[1], 1); + + nodes[1].node.peer_disconnected(node_c_id); + nodes[2].node.peer_disconnected(node_b_id); + + let init_msg = msgs::Init { + features: nodes[2].node.init_features(), + networks: None, + remote_network_address: None, + }; + nodes[1].node.peer_connected(node_c_id, &init_msg, true).unwrap(); + let bs_reestablish = get_chan_reestablish_msgs!(nodes[1], nodes[2]); + assert_eq!(bs_reestablish.len(), 1); + let init_msg = msgs::Init { + features: nodes[1].node.init_features(), + networks: None, + remote_network_address: None, + }; + nodes[2].node.peer_connected(node_b_id, &init_msg, false).unwrap(); + let cs_reestablish = get_chan_reestablish_msgs!(nodes[2], nodes[1]); + assert_eq!(cs_reestablish.len(), 1); + + nodes[1].node.handle_channel_reestablish(node_c_id, &cs_reestablish[0]); + + // The signer-pending path now generates the owed RAA before the held monitor update + // completes. + nodes[1].enable_channel_signer_op(&node_c_id, &chan_bc.2, SignerOp::ReleaseCommitmentSecret); + nodes[1].node.signer_unblocked(Some((node_c_id, chan_bc.2))); + let (_, signer_revoke_and_ack, signer_commitment_update, _, _, _, _, _, _) = + handle_chan_reestablish_msgs!(nodes[1], nodes[2]); + assert!(signer_revoke_and_ack.is_some()); + + // Once the held monitor update completes, B must not generate the same RAA a second time via + // the monitor-pending path. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + let (latest_update, _) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_bc.2); + nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_bc.2, latest_update); + check_added_monitors(&nodes[1], 0); + let (_, duplicate_revoke_and_ack, monitor_commitment_update, _, _, _, _, _, _) = + handle_chan_reestablish_msgs!(nodes[1], nodes[2]); + assert!(duplicate_revoke_and_ack.is_none()); + + nodes[2].node.handle_channel_reestablish(node_b_id, &bs_reestablish[0]); + let (_, c_revoke_and_ack, c_commitment_update, _, _, _, _, _, _) = + handle_chan_reestablish_msgs!(nodes[2], nodes[1]); + assert!(c_revoke_and_ack.is_none()); + assert!(c_commitment_update.is_none()); + + nodes[2].node.handle_revoke_and_ack(node_b_id, &signer_revoke_and_ack.unwrap()); + check_added_monitors(&nodes[2], 1); + + let commitment_update = signer_commitment_update.or(monitor_commitment_update); + if let Some(commitment_update) = commitment_update { + let send_event = SendEvent::from_commitment_update(node_c_id, chan_bc.2, commitment_update); + assert_eq!(send_event.node_id, node_c_id); + for update_add in send_event.msgs { + nodes[2].node.handle_update_add_htlc(node_b_id, &update_add); + } + nodes[2].node.handle_commitment_signed_batch_test(node_b_id, &send_event.commitment_msg); + check_added_monitors(&nodes[2], 1); + let (c_raa, c_commitment_signed) = get_revoke_commit_msgs(&nodes[2], &node_b_id); + nodes[1].node.handle_revoke_and_ack(node_c_id, &c_raa); + check_added_monitors(&nodes[1], 1); + nodes[1].node.handle_commitment_signed_batch_test(node_c_id, &c_commitment_signed); + check_added_monitors(&nodes[1], 1); + let b_raa = get_event_msg!(nodes[1], MessageSendEvent::SendRevokeAndACK, node_c_id); + nodes[2].node.handle_revoke_and_ack(node_b_id, &b_raa); + check_added_monitors(&nodes[2], 1); + } + + let (route, final_payment_hash, _final_payment_preimage, final_payment_secret) = + get_route_and_payment_hash!(nodes[1], nodes[2], 100_000); + let final_payment_id = PaymentId(final_payment_hash.0); + nodes[1] + .node + .send_payment_with_route( + route, + final_payment_hash, + RecipientOnionFields::secret_only(final_payment_secret, 100_000), + final_payment_id, + ) + .unwrap(); + check_added_monitors(&nodes[1], 1); + let final_payment_event = nodes[1].node.get_and_clear_pending_msg_events().remove(0); + match &final_payment_event { + MessageSendEvent::UpdateHTLCs { node_id, .. } => assert_eq!(*node_id, node_c_id), + _ => panic!("Unexpected event"), + } + do_pass_along_path( + PassAlongPathArgs::new( + &nodes[1], + &[&nodes[2]], + 100_000, + final_payment_hash, + final_payment_event, + ) + .with_payment_secret(final_payment_secret) + .without_clearing_recipient_events(), + ); + + let claimable_events = nodes[2].node.get_and_clear_pending_events(); + let final_claimable = claimable_events + .iter() + .find(|event| { + matches!( + event, + Event::PaymentClaimable { payment_hash, .. } if *payment_hash == final_payment_hash + ) + }) + .unwrap(); + check_payment_claimable( + final_claimable, + final_payment_hash, + final_payment_secret, + 100_000, + None, + node_c_id, + ); + expect_htlc_failure_conditions( + nodes[1].node.get_and_clear_pending_events(), + &[HTLCHandlingFailureType::Forward { node_id: Some(node_c_id), channel_id: chan_bc.2 }], + ); +} + fn do_test_async_raa_peer_disconnect( test_case: UnblockSignerAcrossDisconnectCase, raa_blocked_by_commit_point: bool, ) { @@ -531,7 +727,7 @@ fn do_test_async_raa_peer_disconnect( let (route, our_payment_hash, _our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(src, dst, 8000000); - let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret); + let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret, 8000000); let payment_id = PaymentId(our_payment_hash.0); src.node .send_payment_with_route(route, our_payment_hash, recipient_fields, payment_id) @@ -605,7 +801,7 @@ fn do_test_async_raa_peer_disconnect( } // Expect the RAA - let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _) = + let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); if test_case == UnblockSignerAcrossDisconnectCase::AtEnd { assert!(revoke_and_ack.is_none()); @@ -621,14 +817,14 @@ fn do_test_async_raa_peer_disconnect( dst.node.signer_unblocked(Some((src_node_id, chan_id))); if test_case == UnblockSignerAcrossDisconnectCase::AtEnd { - let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _) = + let (_, revoke_and_ack, commitment_signed, resend_order, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); assert!(revoke_and_ack.is_some()); assert!(commitment_signed.is_some()); assert!(resend_order == RAACommitmentOrder::RevokeAndACKFirst); } else { // Make sure we don't double send the RAA. - let (_, revoke_and_ack, commitment_signed, _, _, _, _, _) = + let (_, revoke_and_ack, commitment_signed, _, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); assert!(revoke_and_ack.is_none()); assert!(commitment_signed.is_none()); @@ -680,7 +876,7 @@ fn do_test_async_commitment_signature_peer_disconnect( let (route, our_payment_hash, _our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(src, dst, 8000000); - let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret); + let recipient_fields = RecipientOnionFields::secret_only(our_payment_secret, 8000000); let payment_id = PaymentId(our_payment_hash.0); src.node .send_payment_with_route(route, our_payment_hash, recipient_fields, payment_id) @@ -755,7 +951,7 @@ fn do_test_async_commitment_signature_peer_disconnect( } // Expect the RAA - let (_, revoke_and_ack, commitment_signed, _, _, _, _, _) = + let (_, revoke_and_ack, commitment_signed, _, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); assert!(revoke_and_ack.is_some()); if test_case == UnblockSignerAcrossDisconnectCase::AtEnd { @@ -769,11 +965,11 @@ fn do_test_async_commitment_signature_peer_disconnect( dst.node.signer_unblocked(Some((src_node_id, chan_id))); if test_case == UnblockSignerAcrossDisconnectCase::AtEnd { - let (_, _, commitment_signed, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); + let (_, _, commitment_signed, _, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); assert!(commitment_signed.is_some()); } else { // Make sure we don't double send the CS. - let (_, _, commitment_signed, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); + let (_, _, commitment_signed, _, _, _, _, _, _) = handle_chan_reestablish_msgs!(dst, src); assert!(commitment_signed.is_none()); } } @@ -815,7 +1011,7 @@ fn do_test_async_commitment_signature_ordering(monitor_update_failure: bool) { // to the peer. let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let recipient_fields = RecipientOnionFields::secret_only(payment_secret_2); + let recipient_fields = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let payment_id = PaymentId(payment_hash_2.0); nodes[0] .node @@ -1257,7 +1453,7 @@ fn do_test_closing_signed(extra_closing_signed: bool, reconnect: bool) { let channel = chan_lock.channel_by_id.get_mut(&chan_id).unwrap(); let (funding, context) = channel.funding_and_context_mut(); - let signer = context.get_mut_signer().as_mut_ecdsa().unwrap(); + let signer = context.get_mut_signer(); let signature = signer .sign_closing_transaction( &funding.channel_transaction_parameters, @@ -1319,9 +1515,9 @@ fn do_test_closing_signed(extra_closing_signed: bool, reconnect: bool) { } nodes[0].node.signer_unblocked(None); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_closing_signed) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_closing_signed) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_closing_signed.is_none()); assert!(nodes[0].node.list_channels().is_empty()); @@ -1354,14 +1550,14 @@ fn test_no_disconnect_while_async_revoke_and_ack_expecting_remote_commitment_sig // We'll send a payment from both nodes to each other. let (route1, payment_hash1, _, payment_secret1) = get_route_and_payment_hash!(&nodes[0], &nodes[1], payment_amount); - let onion1 = RecipientOnionFields::secret_only(payment_secret1); + let onion1 = RecipientOnionFields::secret_only(payment_secret1, payment_amount); let payment_id1 = PaymentId(payment_hash1.0); nodes[0].node.send_payment_with_route(route1, payment_hash1, onion1, payment_id1).unwrap(); check_added_monitors(&nodes[0], 1); let (route2, payment_hash2, _, payment_secret2) = get_route_and_payment_hash!(&nodes[1], &nodes[0], payment_amount); - let onion2 = RecipientOnionFields::secret_only(payment_secret2); + let onion2 = RecipientOnionFields::secret_only(payment_secret2, payment_amount); let payment_id2 = PaymentId(payment_hash2.0); nodes[1].node.send_payment_with_route(route2, payment_hash2, onion2, payment_id2).unwrap(); check_added_monitors(&nodes[1], 1); @@ -1583,10 +1779,11 @@ fn test_async_splice_initial_commit_sig() { ); // Negotiate a splice up until the signature exchange. - let contribution = SpliceContribution::splice_out(vec![TxOut { + let outputs = vec![TxOut { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); + }]; + let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); negotiate_splice_tx(initiator, acceptor, channel_id, contribution); assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); @@ -1655,6 +1852,175 @@ fn test_async_splice_initial_commit_sig() { get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, acceptor_node_id); acceptor.node.handle_tx_signatures(initiator_node_id, &tx_signatures); - let _ = get_event!(initiator, Event::SplicePending); - let _ = get_event!(acceptor, Event::SplicePending); + let _ = get_event!(initiator, Event::SpliceNegotiated); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn test_async_splice_initial_commit_sig_waits_for_monitor_before_tx_signatures() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + + let (initiator, acceptor) = (&nodes[0], &nodes[1]); + let initiator_node_id = initiator.node.get_our_node_id(); + let acceptor_node_id = acceptor.node.get_our_node_id(); + + acceptor.disable_channel_signer_op( + &initiator_node_id, + &channel_id, + SignerOp::SignCounterpartyCommitment, + ); + + // Negotiate a splice up until the signature exchange. + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + negotiate_splice_tx(initiator, acceptor, channel_id, contribution); + + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event { + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &acceptor_node_id, partially_signed_tx) + .unwrap(); + } + + let initiator_commit_sig = get_htlc_update_msgs(initiator, &acceptor_node_id); + + // Keep the monitor update from processing the initiator's initial commitment signed pending on + // the acceptor. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + acceptor + .node + .handle_commitment_signed(initiator_node_id, &initiator_commit_sig.commitment_signed[0]); + check_added_monitors(acceptor, 1); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + // Once the async signer is unblocked, we should send the initial commitment_signed, but still + // hold back tx_signatures until the monitor update is completed. + acceptor.enable_channel_signer_op( + &initiator_node_id, + &channel_id, + SignerOp::SignCounterpartyCommitment, + ); + acceptor.node.signer_unblocked(None); + + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] { + initiator.node.handle_commitment_signed(acceptor_node_id, &updates.commitment_signed[0]); + check_added_monitors(initiator, 1); + } else { + panic!("Unexpected event"); + } + + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + // Reestablishing before the monitor update completes should still not release `tx_signatures`. + initiator.node.peer_disconnected(acceptor_node_id); + acceptor.node.peer_disconnected(initiator_node_id); + let mut reconnect_args = ReconnectArgs::new(initiator, acceptor); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + acceptor.chain_monitor.complete_sole_pending_chan_update(&channel_id); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + let tx_signatures = + get_event_msg!(acceptor, MessageSendEvent::SendTxSignatures, initiator_node_id); + initiator.node.handle_tx_signatures(acceptor_node_id, &tx_signatures); + + let tx_signatures = + get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, acceptor_node_id); + acceptor.node.handle_tx_signatures(initiator_node_id, &tx_signatures); + + let _ = get_event!(initiator, Event::SpliceNegotiated); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn test_async_splice_shared_input_signature_released_on_unblock() { + // Test that we can provide the signature of a splice's shared input asynchronously, and check + // that the holding cell is freed after exiting quiescence due to exchanging `tx_signatures`. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + + let (initiator, acceptor) = (&nodes[0], &nodes[1]); + let initiator_node_id = initiator.node.get_our_node_id(); + let acceptor_node_id = acceptor.node.get_our_node_id(); + + initiator.disable_channel_signer_op( + &acceptor_node_id, + &channel_id, + SignerOp::SignSpliceSharedInput, + ); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + negotiate_splice_tx(initiator, acceptor, channel_id, contribution); + + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event { + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &acceptor_node_id, partially_signed_tx) + .unwrap(); + } + + let initiator_commit_sig = get_htlc_update_msgs(initiator, &acceptor_node_id); + acceptor + .node + .handle_commitment_signed(initiator_node_id, &initiator_commit_sig.commitment_signed[0]); + check_added_monitors(acceptor, 1); + + let acceptor_msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(acceptor_msg_events.len(), 2, "{acceptor_msg_events:?}"); + for msg_event in &acceptor_msg_events { + match msg_event { + MessageSendEvent::UpdateHTLCs { updates, .. } => { + initiator + .node + .handle_commitment_signed(acceptor_node_id, &updates.commitment_signed[0]); + check_added_monitors(initiator, 1); + }, + MessageSendEvent::SendTxSignatures { msg, .. } => { + initiator.node.handle_tx_signatures(acceptor_node_id, msg); + }, + _ => panic!("Unexpected event"), + } + } + + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + + initiator.enable_channel_signer_op( + &acceptor_node_id, + &channel_id, + SignerOp::SignSpliceSharedInput, + ); + initiator.node.signer_unblocked(None); + + let tx_signatures = + get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, acceptor_node_id); + acceptor.node.handle_tx_signatures(initiator_node_id, &tx_signatures); + + let _ = get_event!(initiator, Event::SpliceNegotiated); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); } diff --git a/lightning/src/ln/blinded_payment_tests.rs b/lightning/src/ln/blinded_payment_tests.rs index d78b9dfa4f2..3ecf4ae6344 100644 --- a/lightning/src/ln/blinded_payment_tests.rs +++ b/lightning/src/ln/blinded_payment_tests.rs @@ -8,13 +8,15 @@ // licenses. use crate::blinded_path::payment::{ - BlindedPaymentPath, Bolt12RefundContext, DummyTlvs, ForwardTlvs, PaymentConstraints, - PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, PAYMENT_PADDING_ROUND_OFF, + BlindedPaymentPath, Bolt12RefundContext, DummyTlvs, ForwardNode, ForwardTlvs, + PaymentConstraints, PaymentContext, PaymentForwardNode, PaymentRelay, ReceiveTlvs, + PAYMENT_PADDING_ROUND_OFF, }; use crate::blinded_path::utils::is_padded; use crate::blinded_path::{self, BlindedHop}; +use crate::chain::channelmonitor::HTLC_FAIL_BACK_BUFFER; use crate::events::{Event, HTLCHandlingFailureType, PaymentFailureReason}; -use crate::ln::channelmanager::{self, HTLCFailureMsg, PaymentId}; +use crate::ln::channelmanager::{self, HTLCFailureMsg, PaymentId, MPP_TIMEOUT_TICKS}; use crate::ln::functional_test_utils::*; use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{ @@ -34,7 +36,7 @@ use crate::routing::router::{ use crate::sign::{NodeSigner, PeerStorageKey, ReceiveAuthKey, Recipient}; use crate::types::features::{BlindedHopFeatures, ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentSecret}; -use crate::util::config::{HTLCInterceptionFlags, UserConfig}; +use crate::util::config::{ChannelConfig, HTLCInterceptionFlags, UserConfig}; use crate::util::ser::{WithoutLength, Writeable}; use crate::util::test_utils::{self, bytes_from_hex, pubkey_from_hex, secret_from_hex}; use bitcoin::hex::DisplayHex; @@ -83,7 +85,7 @@ pub fn blinded_payment_path( htlc_minimum_msat: intro_node_min_htlc_opt.unwrap_or_else(|| channel_upds.last().unwrap().htlc_minimum_msat), }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = keys_manager.get_receive_auth_key(); @@ -172,7 +174,7 @@ fn do_one_hop_blinded_path(success: bool) { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); @@ -187,7 +189,7 @@ fn do_one_hop_blinded_path(success: bool) { PaymentParameters::blinded(vec![blinded_path]), amt_msat, ); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], amt_msat, payment_hash, payment_secret); @@ -216,7 +218,9 @@ fn one_hop_blinded_path_with_dummy_hops() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); let dummy_tlvs = [DummyTlvs::default(); 2]; @@ -243,7 +247,7 @@ fn one_hop_blinded_path_with_dummy_hops() { .node .send_payment( payment_hash, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0), @@ -269,7 +273,11 @@ fn one_hop_blinded_path_with_dummy_hops() { fn mpp_to_one_hop_blinded_path() { let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = 10; + let configs: [Option<UserConfig>; 4] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let nodes = create_network(4, &node_cfgs, &node_chanmgrs); let mut secp_ctx = Secp256k1::new(); @@ -292,7 +300,7 @@ fn mpp_to_one_hop_blinded_path() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd_1_3.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[3].keys_manager.get_receive_auth_key(); let blinded_path = BlindedPaymentPath::new( @@ -307,7 +315,7 @@ fn mpp_to_one_hop_blinded_path() { PaymentParameters::blinded(vec![blinded_path]).with_bolt12_features(bolt12_features).unwrap(), amt_msat, ); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 2); let expected_route: &[&[&Node]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; @@ -349,7 +357,11 @@ fn mpp_to_one_hop_blinded_path() { fn mpp_to_three_hop_blinded_paths() { let chanmon_cfgs = create_chanmon_cfgs(6); let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(6, &node_cfgs, &[None, None, None, None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = 10; + let configs: [Option<UserConfig>; 6] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(6, &node_cfgs, &configs); let nodes = create_network(6, &node_cfgs, &node_chanmgrs); // Create this network topology so node 0 MPP's over 2 3-hop blinded paths: @@ -399,7 +411,7 @@ fn mpp_to_three_hop_blinded_paths() { RouteParameters::from_payment_params_and_value(pay_params, amt_msat) }; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 2); @@ -464,7 +476,7 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { let route = get_route(&nodes[0], &route_params).unwrap(); node_cfgs[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); macro_rules! cause_error { @@ -474,12 +486,12 @@ fn do_forward_checks_failure(check: ForwardCheckFail, intro_fails: bool) { $update_add.cltv_expiry = 10; // causes outbound CLTV expiry to underflow }, ForwardCheckFail::ForwardPayloadEncodedAsReceive => { - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); let session_priv = SecretKey::from_slice(&[3; 32]).unwrap(); let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); let cur_height = nodes[0].best_block_info().1; - let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( - &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); + let (mut onion_payloads, ..) = onion_utils::test_build_onion_payloads( + &route.paths[0], &recipient_onion_fields, cur_height, &None, None, None).unwrap(); // Remove the receive payload so the blinded forward payload is encoded as a final payload // (i.e. next_hop_hmac == [0; 32]) onion_payloads.pop(); @@ -594,7 +606,7 @@ fn failed_backwards_to_intro_node() { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -680,7 +692,7 @@ fn do_forward_fail_in_process_pending_htlc_fwds(check: ProcessPendingHTLCsCheck, nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2, &chan_upd_2_3], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -790,7 +802,7 @@ fn do_blinded_intercept_payment(intercept_node_fails: bool) { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&intercept_chan_upd], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let payment_event = { @@ -865,7 +877,7 @@ fn two_hop_blinded_path_success() { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); @@ -895,7 +907,7 @@ fn three_hop_blinded_path_success() { nodes.iter().skip(2).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_2_3, &chan_upd_3_4], &chanmon_cfgs[4].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3], &nodes[4]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3], &nodes[4]], payment_preimage); @@ -920,7 +932,7 @@ fn three_hop_blinded_path_fail() { nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2, &chan_upd_2_3], &chanmon_cfgs[3].keys_manager); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3]]], amt_msat, payment_hash, payment_secret); @@ -981,11 +993,11 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { }; let amt_msat = 5000; - let excess_final_cltv_delta_opt = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck { - // Set the final CLTV expiry too low to trigger the failure in process_pending_htlc_forwards. - Some(TEST_FINAL_CLTV as u16 - 2) + let required_final_cltv = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck { + // Set the final CLTV required much too high to trigger the failure in process_pending_htlc_forwards. + Some((TEST_FINAL_CLTV as u16) * 10) } else { None }; - let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), excess_final_cltv_delta_opt); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), required_final_cltv); let mut route_params = get_blinded_route_parameters(amt_msat, payment_secret, 1, 1_0000_0000, nodes.iter().skip(1).map(|n| n.node.get_our_node_id()).collect(), &[&chan_upd_1_2], &chanmon_cfgs[2].keys_manager); @@ -993,11 +1005,7 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { route_params.payment_params.max_path_length = 17; let route = if check == ReceiveCheckFail::ProcessPendingHTLCsCheck { - let mut route = get_route(&nodes[0], &route_params).unwrap(); - // Set the final CLTV expiry too low to trigger the failure in process_pending_htlc_forwards. - route.paths[0].hops.last_mut().map(|h| h.cltv_expiry_delta += excess_final_cltv_delta_opt.unwrap() as u32); - route.paths[0].blinded_tail.as_mut().map(|bt| bt.excess_final_cltv_expiry_delta = excess_final_cltv_delta_opt.unwrap() as u32); - route + get_route(&nodes[0], &route_params).unwrap() } else if check == ReceiveCheckFail::PaymentConstraints { // Create a blinded path where the receiver's encrypted payload has an htlc_minimum_msat that is // violated by `amt_msat`, and stick it in the route_params without changing the corresponding @@ -1021,7 +1029,7 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { find_route(&nodes[0], &route_params).unwrap() }; node_cfgs[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut payment_event_0_1 = { @@ -1064,9 +1072,9 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { let session_priv = SecretKey::from_slice(&session_priv).unwrap(); let mut onion_keys = onion_utils::construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); let cur_height = nodes[0].best_block_info().1; - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - let (mut onion_payloads, ..) = onion_utils::build_onion_payloads( - &route.paths[0], amt_msat, &recipient_onion_fields, cur_height, &None, None, None).unwrap(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); + let (mut onion_payloads, ..) = onion_utils::test_build_onion_payloads( + &route.paths[0], &recipient_onion_fields, cur_height, &None, None, None).unwrap(); let update_add = &mut payment_event_1_2.msgs[0]; onion_payloads.last_mut().map(|p| { @@ -1115,7 +1123,6 @@ fn do_multi_hop_receiver_fail(check: ReceiveCheckFail) { check_added_monitors(&nodes[2], 1); }, ReceiveCheckFail::ProcessPendingHTLCsCheck => { - assert_eq!(payment_event_1_2.msgs[0].cltv_expiry, nodes[0].best_block_info().1 + 1 + excess_final_cltv_delta_opt.unwrap() as u32 + TEST_FINAL_CLTV); nodes[2].node.handle_update_add_htlc(nodes[1].node.get_our_node_id(), &payment_event_1_2.msgs[0]); check_added_monitors(&nodes[2], 0); do_commitment_signed_dance(&nodes[2], &nodes[1], &payment_event_1_2.commitment_msg, true, true); @@ -1210,7 +1217,7 @@ fn blinded_path_retries() { RouteParameters::from_payment_params_and_value(pay_params, amt_msat) }; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(2)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(2)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[3]]], amt_msat, payment_hash, payment_secret); @@ -1309,7 +1316,7 @@ fn min_htlc() { assert_eq!(min_htlc_msat, route_params.payment_params.payee.blinded_route_hints()[0].payinfo.htlc_minimum_msat); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(min_htlc_msat), PaymentId(payment_hash.0), route_params.clone(), Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3]]], min_htlc_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3]], payment_preimage); @@ -1322,7 +1329,7 @@ fn min_htlc() { route_hints[0].payinfo.htlc_minimum_msat -= 1; } else { panic!() } route_params.final_value_msat -= 1; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(route_params.final_value_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut payment_event_0_1 = { @@ -1387,7 +1394,7 @@ fn conditionally_round_fwd_amt() { &chanmon_cfgs[4].keys_manager); route_params.max_total_routing_fee_msat = None; - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3], &nodes[4]]], amt_msat, payment_hash, payment_secret); nodes[4].node.claim_funds(payment_preimage); @@ -1416,7 +1423,7 @@ fn custom_tlvs_to_blinded_path() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); @@ -1432,7 +1439,7 @@ fn custom_tlvs_to_blinded_path() { amt_msat, ); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty() + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat) .with_custom_tlvs(RecipientCustomTlvs::new(vec![((1 << 16) + 1, vec![42, 42])]).unwrap()); nodes[0].node.send_payment(payment_hash, recipient_onion_fields.clone(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); @@ -1470,7 +1477,7 @@ fn fails_receive_tlvs_authentication() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = chanmon_cfgs[1].keys_manager.get_receive_auth_key(); @@ -1487,7 +1494,7 @@ fn fails_receive_tlvs_authentication() { ); // Test authentication works normally. - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); @@ -1500,7 +1507,7 @@ fn fails_receive_tlvs_authentication() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; // Use a mismatched ReceiveAuthKey to force auth failure: let mismatched_receive_auth_key = ReceiveAuthKey([0u8; 32]); @@ -1517,7 +1524,7 @@ fn fails_receive_tlvs_authentication() { amt_msat, ); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -1574,7 +1581,7 @@ fn blinded_payment_path_padding() { let route_params = RouteParameters::from_payment_params_and_value(PaymentParameters::blinded(vec![blinded_path]), amt_msat); - nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_payment(payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2], &nodes[3], &nodes[4]]], amt_msat, payment_hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3], &nodes[4]], payment_preimage); @@ -1681,7 +1688,7 @@ fn route_blinding_spec_test_vector() { }), }; let cur_height = 747_000; - let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &RecipientOnionFields::spontaneous_empty(), cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); + let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, &RecipientOnionFields::spontaneous_empty(amt_msat), cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); struct TestEcdhSigner { node_secret: SecretKey, @@ -1696,7 +1703,7 @@ fn route_blinding_spec_test_vector() { } Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } + fn get_expanded_key(&self) -> ExpandedKey { ExpandedKey::new([42; 32]) } fn get_node_id(&self, _recipient: Recipient) -> Result<PublicKey, ()> { unreachable!() } fn sign_invoice( &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, @@ -1857,7 +1864,7 @@ fn test_combined_trampoline_onion_creation_vectors() { short_channel_id: (572330 << 40) + (42 << 16) + 2821, channel_features: ChannelFeatures::empty(), fee_msat: 153_000, - cltv_expiry_delta: 0, + cltv_expiry_delta: 24 + 36, maybe_announced_channel: false, }, ], @@ -1904,8 +1911,8 @@ fn test_combined_trampoline_onion_creation_vectors() { let amt_msat = 150_000_000; let cur_height = 800_000; - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (bob_onion, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion_internal(&secp_ctx, &path, &outer_session_key, amt_msat, &recipient_onion_fields, cur_height, &associated_data, &None, None, outer_onion_prng_seed, Some(session_priv), Some([0; 32])).unwrap(); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); + let (bob_onion, htlc_msat, htlc_cltv) = onion_utils::create_payment_onion_internal(&secp_ctx, &path, &outer_session_key, &recipient_onion_fields, cur_height, &associated_data, &None, None, outer_onion_prng_seed, Some(session_priv), Some([0; 32])).unwrap(); let outer_onion_packet_hex = bob_onion.encode().to_lower_hex_string(); assert_eq!(outer_onion_packet_hex, "00025fd60556c134ae97e4baedba220a644037754ee67c54fd05e93bf40c17cbb73362fb9dee96001ff229945595b6edb59437a6bc143406d3f90f749892a84d8d430c6890437d26d5bfc599d565316ef51347521075bbab87c59c57bcf20af7e63d7192b46cf171e4f73cb11f9f603915389105d91ad630224bea95d735e3988add1e24b5bf28f1d7128db64284d90a839ba340d088c74b1fb1bd21136b1809428ec5399c8649e9bdf92d2dcfc694deae5046fa5b2bdf646847aaad73f5e95275763091c90e71031cae1f9a770fdea559642c9c02f424a2a28163dd0957e3874bd28a97bec67d18c0321b0e68bc804aa8345b17cb626e2348ca06c8312a167c989521056b0f25c55559d446507d6c491d50605cb79fa87929ce64b0a9860926eeaec2c431d926a1cadb9a1186e4061cb01671a122fc1f57602cbef06d6c194ec4b715c2e3dd4120baca3172cd81900b49fef857fb6d6afd24c983b608108b0a5ac0c1c6c52011f23b8778059ffadd1bb7cd06e2525417365f485a7fd1d4a9ba3818ede7cdc9e71afee8532252d08e2531ca52538655b7e8d912f7ec6d37bbcce8d7ec690709dbf9321e92c565b78e7fe2c22edf23e0902153d1ca15a112ad32fb19695ec65ce11ddf670da7915f05ad4b86c154fb908cb567315d1124f303f75fa075ebde8ef7bb12e27737ad9e4924439097338ea6d7a6fc3721b88c9b830a34e8d55f4c582b74a3895cc848fe57f4fe29f115dabeb6b3175be15d94408ed6771109cfaf57067ae658201082eae7605d26b1449af4425ae8e8f58cdda5c6265f1fd7a386fc6cea3074e4f25b909b96175883676f7610a00fdf34df9eb6c7b9a4ae89b839c69fd1f285e38cdceb634d782cc6d81179759bc9fd47d7fd060470d0b048287764c6837963274e708314f017ac7dc26d0554d59bfcfd3136225798f65f0b0fea337c6b256ebbb63a90b994c0ab93fd8b1d6bd4c74aebe535d6110014cd3d525394027dfe8faa98b4e9b2bee7949eb1961f1b026791092f84deea63afab66603dbe9b6365a102a1fef2f6b9744bc1bb091a8da9130d34d4d39f25dbad191649cfb67e10246364b7ce0c6ec072f9690cabb459d9fda0c849e17535de4357e9907270c75953fca3c845bb613926ecf73205219c7057a4b6bb244c184362bb4e2f24279dc4e60b94a5b1ec11c34081a628428ba5646c995b9558821053ba9c84a05afbf00dabd60223723096516d2f5668f3ec7e11612b01eb7a3a0506189a2272b88e89807943adb34291a17f6cb5516ffd6f945a1c42a524b21f096d66f350b1dad4db455741ae3d0e023309fbda5ef55fb0dc74f3297041448b2be76c525141963934c6afc53d263fb7836626df502d7c2ee9e79cbbd87afd84bbb8dfbf45248af3cd61ad5fac827e7683ca4f91dfad507a8eb9c17b2c9ac5ec051fe645a4a6cb37136f6f19b611e0ea8da7960af2d779507e55f57305bc74b7568928c5dd5132990fe54c22117df91c257d8c7b61935a018a28c1c3b17bab8e4294fa699161ec21123c9fc4e71079df31f300c2822e1246561e04765d3aab333eafd026c7431ac7616debb0e022746f4538e1c6348b600c988eeb2d051fc60c468dca260a84c79ab3ab8342dc345a764672848ea234e17332bc124799daf7c5fcb2e2358514a7461357e1c19c802c5ee32deccf1776885dd825bedd5f781d459984370a6b7ae885d4483a76ddb19b30f47ed47cd56aa5a079a89793dbcad461c59f2e002067ac98dd5a534e525c9c46c2af730741bf1f8629357ec0bfc0bc9ecb31af96777e507648ff4260dc3673716e098d9111dfd245f1d7c55a6de340deb8bd7a053e5d62d760f184dc70ca8fa255b9023b9b9aedfb6e419a5b5951ba0f83b603793830ee68d442d7b88ee1bbf6bbd1bcd6f68cc1af"); @@ -1952,7 +1959,7 @@ fn test_trampoline_inbound_payment_decoding() { short_channel_id: (572330 << 40) + (42 << 16) + 2821, channel_features: ChannelFeatures::empty(), fee_msat: 150_153_000, - cltv_expiry_delta: 0, + cltv_expiry_delta: 24 + 36, maybe_announced_channel: false, }, ], @@ -1995,8 +2002,8 @@ fn test_trampoline_inbound_payment_decoding() { let amt_msat = 150_000_001; let cur_height = 800_001; - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, amt_msat, &recipient_onion_fields, cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); + let (bob_onion, _, _) = onion_utils::create_payment_onion(&secp_ctx, &path, &session_priv, &recipient_onion_fields, cur_height, &PaymentHash([0; 32]), &None, None, [0; 32]).unwrap(); struct TestEcdhSigner { node_secret: SecretKey, @@ -2011,7 +2018,7 @@ fn test_trampoline_inbound_payment_decoding() { } Ok(SharedSecret::new(other_key, &node_secret)) } - fn get_expanded_key(&self) -> ExpandedKey { unreachable!() } + fn get_expanded_key(&self) -> ExpandedKey { ExpandedKey::new([42; 32]) } fn get_node_id(&self, _recipient: Recipient) -> Result<PublicKey, ()> { unreachable!() } fn sign_invoice( &self, _invoice: &RawBolt11Invoice, _recipient: Recipient, @@ -2088,6 +2095,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); let amt_msat = 1000; + let carol_cltv_expiry_delta = 24 + 39; let (payment_preimage, payment_hash, _) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); // We need the session priv to construct an invalid onion packet later. @@ -2120,7 +2128,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { blinded_path::utils::construct_blinded_hops( &secp_ctx, path.into_iter(), &trampoline_session_priv, ) - }; + }; let route = Route { paths: vec![Path { @@ -2143,7 +2151,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: 48, + cltv_expiry_delta: carol_cltv_expiry_delta, maybe_announced_channel: false, } ], @@ -2154,7 +2162,7 @@ fn test_trampoline_forward_payload_encoded_as_receive() { pubkey: carol_node_id, node_features: Features::empty(), fee_msat: amt_msat, - cltv_expiry_delta: 24, + cltv_expiry_delta: carol_cltv_expiry_delta, }, ], hops: carol_blinded_hops, @@ -2163,15 +2171,17 @@ fn test_trampoline_forward_payload_encoded_as_receive() { final_value_msat: amt_msat, }) }], - route_params: None, + route_params: RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(carol_node_id, carol_cltv_expiry_delta), + amt_msat, + ), }; - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let replacement_onion = { // create a substitute onion where the last Trampoline hop is a forward - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); let mut blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); @@ -2181,7 +2191,8 @@ fn test_trampoline_forward_payload_encoded_as_receive() { encrypted_payload: vec![], }); - let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, amt_msat, &recipient_onion_fields, 32, &None).unwrap(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(amt_msat); + let (mut trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads(&blinded_tail, &recipient_onion_fields, 32, &None).unwrap(); // pop the last dummy hop trampoline_payloads.pop(); @@ -2195,7 +2206,8 @@ fn test_trampoline_forward_payload_encoded_as_receive() { None, ).unwrap(); - let (outer_payloads, _, _) = onion_utils::build_onion_payloads(&route.paths[0], outer_total_msat, &recipient_onion_fields, outer_starting_htlc_offset, &None, None, Some(trampoline_packet)).unwrap(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); + let (outer_payloads, _, _) = onion_utils::test_build_onion_payloads(&route.paths[0], &recipient_onion_fields, 32, &None, None, Some(trampoline_packet)).unwrap(); let outer_onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.clone().paths[0], &outer_session_priv); let outer_packet = onion_utils::construct_onion_packet( outer_payloads, @@ -2273,6 +2285,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); let amt_msat = 1000; + let carol_cltv_expiry_delta = 104 + 39; let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); // Create a 1-hop blinded path for Carol. @@ -2282,7 +2295,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: amt_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }), }; let receive_auth_key = nodes[2].keys_manager.get_receive_auth_key(); let blinded_path = BlindedPaymentPath::new(&[], carol_node_id, receive_auth_key, payee_tlvs, u64::MAX, 0, nodes[2].keys_manager, &secp_ctx).unwrap(); @@ -2308,7 +2321,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: 48, + cltv_expiry_delta: carol_cltv_expiry_delta, maybe_announced_channel: false, } ], @@ -2319,7 +2332,7 @@ fn do_test_trampoline_single_hop_receive(success: bool) { pubkey: carol_node_id, node_features: Features::empty(), fee_msat: amt_msat, - cltv_expiry_delta: 104, + cltv_expiry_delta: carol_cltv_expiry_delta, }, ], hops: blinded_path.blinded_hops().to_vec(), @@ -2328,10 +2341,13 @@ fn do_test_trampoline_single_hop_receive(success: bool) { final_value_msat: amt_msat, }) }], - route_params: None, + route_params: RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(carol_node_id, carol_cltv_expiry_delta), + amt_msat, + ), }; - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt_msat, payment_hash, payment_secret); @@ -2424,71 +2440,27 @@ fn test_trampoline_blinded_receive() { do_test_trampoline_relay(true, TrampolineTestCase::OuterCLTVLessThanTrampoline); } -/// Creates a blinded tail where Carol receives via a blinded path. -fn create_blinded_tail( - secp_ctx: &Secp256k1<All>, override_random_bytes: [u8; 32], carol_node_id: PublicKey, - carol_auth_key: ReceiveAuthKey, trampoline_cltv_expiry_delta: u32, final_value_msat: u64, - payment_secret: PaymentSecret, -) -> BlindedTail { - let outer_session_priv = SecretKey::from_slice(&override_random_bytes).unwrap(); - let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); - - let carol_blinding_point = PublicKey::from_secret_key(&secp_ctx, &trampoline_session_priv); - let carol_blinded_hops = { - let payee_tlvs = ReceiveTlvs { - payment_secret, - payment_constraints: PaymentConstraints { - max_cltv_expiry: u32::max_value(), - htlc_minimum_msat: final_value_msat, - }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), - } - .encode(); - - let path = [((carol_node_id, Some(carol_auth_key)), WithoutLength(&payee_tlvs))]; - - blinded_path::utils::construct_blinded_hops( - &secp_ctx, - path.into_iter(), - &trampoline_session_priv, - ) - }; - - BlindedTail { - trampoline_hops: vec![TrampolineHop { - pubkey: carol_node_id, - node_features: Features::empty(), - fee_msat: final_value_msat, - cltv_expiry_delta: trampoline_cltv_expiry_delta, - }], - hops: carol_blinded_hops, - blinding_point: carol_blinding_point, - excess_final_cltv_expiry_delta: 39, - final_value_msat, - } -} - // Creates a replacement onion that is used to produce scenarios that we don't support, specifically // payloads that send to unblinded receives and invalid payloads. fn replacement_onion( test_case: TrampolineTestCase, secp_ctx: &Secp256k1<All>, override_random_bytes: [u8; 32], - route: Route, original_amt_msat: u64, starting_htlc_offset: u32, original_trampoline_cltv: u32, - payment_hash: PaymentHash, payment_secret: PaymentSecret, blinded: bool, + route: Route, original_amt_msat: u64, starting_htlc_offset: u32, excess_final_cltv: u32, + original_trampoline_cltv: u32, payment_hash: PaymentHash, payment_secret: PaymentSecret, + blinded: bool, ) -> msgs::OnionPacket { let outer_session_priv = SecretKey::from_slice(&override_random_bytes[..]).unwrap(); let trampoline_session_priv = onion_utils::compute_trampoline_session_priv(&outer_session_priv); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(original_amt_msat); let blinded_tail = route.paths[0].blinded_tail.clone().unwrap(); // Rebuild our trampoline packet from the original route. If we want to test Carol receiving // as an unblinded trampoline hop, we switch out her inner trampoline onion with a direct // receive payload because LDK doesn't support unblinded trampoline receives. - let (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) = { - let (mut trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = + let (trampoline_packet, outer_total_msat) = { + let (mut trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads( &blinded_tail, - original_amt_msat, &recipient_onion_fields, starting_htlc_offset, &None, @@ -2502,7 +2474,9 @@ fn replacement_onion( total_msat: original_amt_msat, }), sender_intended_htlc_amt_msat: original_amt_msat, - cltv_expiry_height: original_trampoline_cltv + starting_htlc_offset, + cltv_expiry_height: original_trampoline_cltv + + starting_htlc_offset + + excess_final_cltv, }]; } @@ -2520,16 +2494,16 @@ fn replacement_onion( ) .unwrap(); - (trampoline_packet, outer_total_msat, outer_starting_htlc_offset) + (trampoline_packet, outer_total_msat) }; // Use a different session key to construct the replacement onion packet. Note that the // sender isn't aware of this and won't be able to decode the fulfill hold times. - let (mut outer_payloads, _, _) = onion_utils::build_onion_payloads( + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(outer_total_msat); + let (mut outer_payloads, _, _) = onion_utils::test_build_onion_payloads( &route.paths[0], - outer_total_msat, &recipient_onion_fields, - outer_starting_htlc_offset, + starting_htlc_offset, &None, None, Some(trampoline_packet), @@ -2547,7 +2521,7 @@ fn replacement_onion( .. } => { *amt_to_forward = test_case.outer_onion_amt(original_amt_msat); - let outer_cltv = original_trampoline_cltv + starting_htlc_offset; + let outer_cltv = original_trampoline_cltv + starting_htlc_offset + excess_final_cltv; *outgoing_cltv_value = test_case.outer_onion_cltv(outer_cltv); }, _ => panic!("final payload is not trampoline entrypoint"), @@ -2582,11 +2556,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let alice_bob_chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); let bob_carol_chan = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let starting_htlc_offset = (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1; for i in 0..TOTAL_NODE_COUNT { - connect_blocks( - &nodes[i], - (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1, - ); + connect_blocks(&nodes[i], starting_htlc_offset - nodes[i].best_block_info().1); } let alice_node_id = nodes[0].node.get_our_node_id(); @@ -2597,8 +2569,11 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan.2); let original_amt_msat = 1000; - let original_trampoline_cltv = 72; - let starting_htlc_offset = 32; + // Note that for TrampolineTestCase::OuterCLTVLessThanTrampoline to work properly, + // (starting_htlc_offset + excess_final_cltv) / 2 < (starting_htlc_offset + excess_final_cltv + original_trampoline_cltv) + // otherwise dividing the CLTV value by 2 won't kick us under the outer trampoline CLTV. + let original_trampoline_cltv = 42; + let excess_final_cltv = 70; let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(original_amt_msat), None); @@ -2607,6 +2582,39 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { let override_random_bytes = [42; 32]; *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(override_random_bytes); + // Create a blinded tail where Carol is receiving. In our unblinded test cases, we'll + // override this anyway (with a tail sending to an unblinded receive, which LDK doesn't + // allow). + let (blinded_tail, blinded_path) = create_trampoline_forward_blinded_tail( + &secp_ctx, + &nodes[2].keys_manager, + &[], + carol_node_id, + nodes[2].keys_manager.get_receive_auth_key(), + ReceiveTlvs { + payment_secret, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: original_amt_msat, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), + }, + original_trampoline_cltv, + excess_final_cltv, + original_amt_msat, + ); + + // When Carol receives over the blinded path, register it in the payment parameters as we + // would for a real blinded payment. In the unblinded test cases the blinded tail is overridden, + // so the payee is just Carol's unblinded node id. + let payment_params = if blinded { + PaymentParameters::blinded(vec![blinded_path]) + } else { + PaymentParameters::from_node_id(carol_node_id, original_trampoline_cltv + excess_final_cltv) + }; + let route = Route { paths: vec![Path { hops: vec![ @@ -2625,24 +2633,16 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { short_channel_id: bob_carol_scid, channel_features: ChannelFeatures::empty(), fee_msat: 0, - cltv_expiry_delta: 48, + cltv_expiry_delta: original_trampoline_cltv + excess_final_cltv, maybe_announced_channel: false, }, ], - // Create a blinded tail where Carol is receiving. In our unblinded test cases, we'll - // override this anyway (with a tail sending to an unblinded receive, which LDK doesn't - // allow). - blinded_tail: Some(create_blinded_tail( - &secp_ctx, - override_random_bytes, - carol_node_id, - nodes[2].keys_manager.get_receive_auth_key(), - original_trampoline_cltv, - original_amt_msat, - payment_secret, - )), + blinded_tail: Some(blinded_tail), }], - route_params: None, + route_params: RouteParameters::from_payment_params_and_value( + payment_params, + original_amt_msat, + ), }; nodes[0] @@ -2650,7 +2650,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { .send_payment_with_route( route.clone(), payment_hash, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(original_amt_msat), PaymentId(payment_hash.0), ) .unwrap(); @@ -2680,6 +2680,7 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { original_amt_msat, starting_htlc_offset, original_trampoline_cltv, + excess_final_cltv, payment_hash, payment_secret, blinded, @@ -2696,8 +2697,9 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { ); let amt_bytes = test_case.outer_onion_amt(original_amt_msat).to_be_bytes(); - let cltv_bytes = - test_case.outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset).to_be_bytes(); + let cltv_bytes = test_case + .outer_onion_cltv(original_trampoline_cltv + starting_htlc_offset + excess_final_cltv) + .to_be_bytes(); let payment_failure = test_case.payment_failed_conditions(&amt_bytes, &cltv_bytes).map(|p| { if blinded { PaymentFailedConditions::new() @@ -2711,7 +2713,8 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { .without_claimable_event() .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }) } else { - args.with_payment_secret(payment_secret) + let htlc_cltv = starting_htlc_offset + original_trampoline_cltv + excess_final_cltv; + args.with_payment_secret(payment_secret).with_payment_claimable_cltv(htlc_cltv) }; do_pass_along_path(args); @@ -2749,122 +2752,273 @@ fn do_test_trampoline_relay(blinded: bool, test_case: TrampolineTestCase) { } } -#[test] -#[rustfmt::skip] -fn test_trampoline_forward_rejection() { - const TOTAL_NODE_COUNT: usize = 3; +/// Sets up channels and sends a trampoline MPP payment across two paths. +/// +/// Topology: +/// Alice (0) --> Bob (1) --> Carol (2, trampoline node) +/// Alice (0) --> Barry (3) --> Carol (2, trampoline node) +/// +/// Carol's inner trampoline onion is a forward to an unknown next node. We don't need the +/// next hop as a real node since forwarding isn't implemented yet -- we just need the onion to +/// contain a valid forward payload. +/// +/// Returns (payment_hash, per_path_amount, last_hop_cltv_delta, ev_to_bob, ev_to_barry). +fn send_trampoline_mpp_payment<'a, 'b, 'c>( + nodes: &'a Vec<Node<'a, 'b, 'c>>, +) -> (PaymentHash, u64, u32, MessageSendEvent, MessageSendEvent) { + let secp_ctx = Secp256k1::new(); - let chanmon_cfgs = create_chanmon_cfgs(TOTAL_NODE_COUNT); - let node_cfgs = create_node_cfgs(TOTAL_NODE_COUNT, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(TOTAL_NODE_COUNT, &node_cfgs, &vec![None; TOTAL_NODE_COUNT]); - let mut nodes = create_network(TOTAL_NODE_COUNT, &node_cfgs, &node_chanmgrs); + let alice_bob_chan = + create_announced_chan_between_nodes_with_value(nodes, 0, 1, 1_000_000, 0).2; + let bob_carol_chan = + create_announced_chan_between_nodes_with_value(nodes, 1, 2, 1_000_000, 0).2; + let alice_barry_chan = + create_announced_chan_between_nodes_with_value(nodes, 0, 3, 1_000_000, 0).2; + let barry_carol_chan = + create_announced_chan_between_nodes_with_value(nodes, 3, 2, 1_000_000, 0).2; + + let per_path_amt = 500_000; + let total_amt = per_path_amt * 2; + let (_, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[2], Some(total_amt), None); + + let bob_node_id = nodes[1].node.get_our_node_id(); + let carol_node_id = nodes[2].node.get_our_node_id(); + let barry_node_id = nodes[3].node.get_our_node_id(); + + let alice_bob_scid = get_scid_from_channel_id(&nodes[0], alice_bob_chan); + let bob_carol_scid = get_scid_from_channel_id(&nodes[1], bob_carol_chan); + let alice_barry_scid = get_scid_from_channel_id(&nodes[0], alice_barry_chan); + let barry_carol_scid = get_scid_from_channel_id(&nodes[3], barry_carol_chan); + + let trampoline_cltv = 42; + let excess_final_cltv = 70; + + // Note we don't actually have an outgoing channel for Carol, we just use our default fee + // policy. + let carol_relay = ChannelConfig::default(); + + let next_trampoline = PublicKey::from_slice(&[2; 33]).unwrap(); + let fwd_tail = || { + let intermediate_nodes = [ForwardNode { + tlvs: blinded_path::payment::TrampolineForwardTlvs { + next_trampoline, + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: 1, + }, + features: BlindedHopFeatures::empty(), + payment_relay: PaymentRelay { + cltv_expiry_delta: carol_relay.cltv_expiry_delta, + fee_proportional_millionths: carol_relay.forwarding_fee_proportional_millionths, + fee_base_msat: carol_relay.forwarding_fee_base_msat, + }, + next_blinding_override: None, + }, + node_id: carol_node_id, + htlc_maximum_msat: u64::max_value(), + }]; + let payee_tlvs = ReceiveTlvs { + payment_secret: PaymentSecret([0; 32]), + payment_constraints: PaymentConstraints { + max_cltv_expiry: u32::max_value(), + htlc_minimum_msat: 1, + }, + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), + }; + create_trampoline_forward_blinded_tail( + &secp_ctx, + &nodes[2].keys_manager, + &intermediate_nodes, + next_trampoline, + ReceiveAuthKey([0; 32]), + payee_tlvs, + trampoline_cltv, + excess_final_cltv, + per_path_amt, + ) + }; - let (_, _, chan_id_alice_bob, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 0); - let (_, _, chan_id_bob_carol, _) = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let hop = |pubkey, short_channel_id, fee_msat, cltv_expiry_delta| RouteHop { + pubkey, + node_features: NodeFeatures::empty(), + short_channel_id, + channel_features: ChannelFeatures::empty(), + fee_msat, + cltv_expiry_delta, + maybe_announced_channel: true, + }; + let last_hop_cltv_delta = + carol_relay.cltv_expiry_delta as u32 + trampoline_cltv + excess_final_cltv; + let build_path_hops = |first_hop_node_id, first_hop_scid, second_hop_scid| { + vec![ + hop(first_hop_node_id, first_hop_scid, 1000, 48), + hop(carol_node_id, second_hop_scid, 0, last_hop_cltv_delta), + ] + }; - for i in 0..TOTAL_NODE_COUNT { // connect all nodes' blocks - connect_blocks(&nodes[i], (TOTAL_NODE_COUNT as u32) * CHAN_CONFIRM_DEPTH + 1 - nodes[i].best_block_info().1); - } + let (tail_bob, blinded_path_bob) = fwd_tail(); + let (tail_barry, blinded_path_barry) = fwd_tail(); + let payment_params = PaymentParameters::blinded(vec![blinded_path_bob, blinded_path_barry]); + let route_params = RouteParameters { + payment_params, + final_value_msat: total_amt, + max_total_routing_fee_msat: None, + }; + let route = Route { + paths: vec![ + Path { + hops: build_path_hops(bob_node_id, alice_bob_scid, bob_carol_scid), + blinded_tail: Some(tail_bob), + }, + Path { + hops: build_path_hops(barry_node_id, alice_barry_scid, barry_carol_scid), + blinded_tail: Some(tail_barry), + }, + ], + route_params, + }; - let alice_node_id = nodes[0].node().get_our_node_id(); - let bob_node_id = nodes[1].node().get_our_node_id(); - let carol_node_id = nodes[2].node().get_our_node_id(); + let payment_id = PaymentId(payment_hash.0); + let onion = RecipientOnionFields::secret_only(payment_secret, total_amt); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + check_added_monitors(&nodes[0], 2); - let alice_bob_scid = nodes[0].node().list_channels().iter().find(|c| c.channel_id == chan_id_alice_bob).unwrap().short_channel_id.unwrap(); - let bob_carol_scid = nodes[1].node().list_channels().iter().find(|c| c.channel_id == chan_id_bob_carol).unwrap().short_channel_id.unwrap(); + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + let ev_bob = remove_first_msg_event_to_node(&bob_node_id, &mut events); + let ev_barry = remove_first_msg_event_to_node(&barry_node_id, &mut events); + (payment_hash, per_path_amt, last_hop_cltv_delta, ev_bob, ev_barry) +} - let amt_msat = 1000; - let (payment_preimage, payment_hash, _) = get_payment_preimage_hash(&nodes[2], Some(amt_msat), None); +/// How an incomplete trampoline MPP times out (if at all). +enum TrampolineTimeout { + /// Tick timers until MPP timeout fires. + Ticks, + /// Mine blocks until on-chain CLTV timeout fires. + OnChain, +} - let route = Route { - paths: vec![Path { - hops: vec![ - // Bob - RouteHop { - pubkey: bob_node_id, - node_features: NodeFeatures::empty(), - short_channel_id: alice_bob_scid, - channel_features: ChannelFeatures::empty(), - fee_msat: 1000, - cltv_expiry_delta: 48, - maybe_announced_channel: false, - }, +fn do_trampoline_mpp_test(timeout: Option<TrampolineTimeout>) { + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &vec![None; 4]); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); - // Carol - RouteHop { - pubkey: carol_node_id, - node_features: NodeFeatures::empty(), - short_channel_id: bob_carol_scid, - channel_features: ChannelFeatures::empty(), - fee_msat: 0, - cltv_expiry_delta: 48, - maybe_announced_channel: false, - } - ], - blinded_tail: Some(BlindedTail { - trampoline_hops: vec![ - // Carol - TrampolineHop { - pubkey: carol_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, - }, + let (payment_hash, per_path_amt, last_hop_cltv_delta, ev_bob, ev_barry) = + send_trampoline_mpp_payment(&nodes); + let send_both = timeout.is_none(); - // Alice (unreachable) - TrampolineHop { - pubkey: alice_node_id, - node_features: Features::empty(), - fee_msat: amt_msat, - cltv_expiry_delta: 24, - }, - ], - hops: vec![BlindedHop{ - // Fake public key - blinded_node_id: alice_node_id, - encrypted_payload: vec![], - }], - blinding_point: alice_node_id, - excess_final_cltv_expiry_delta: 39, - final_value_msat: amt_msat, - }) - }], - route_params: None, - }; + let bob_path: &[&Node] = &[&nodes[1], &nodes[2]]; + let barry_path: &[&Node] = &[&nodes[3], &nodes[2]]; - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + // Pass first part along Alice -> Bob -> Carol. + let args = PassAlongPathArgs::new(&nodes[0], bob_path, per_path_amt, payment_hash, ev_bob) + .without_claimable_event(); + do_pass_along_path(args); - check_added_monitors(&nodes[0], 1); + // Either complete the MPP (triggering trampoline rejection) or trigger a timeout. + let expected_reason = match timeout { + None => { + let args = + PassAlongPathArgs::new(&nodes[0], barry_path, per_path_amt, payment_hash, ev_barry) + .without_clearing_recipient_events(); + do_pass_along_path(args); + LocalHTLCFailureReason::TemporaryTrampolineFailure + }, + Some(TrampolineTimeout::Ticks) => { + for _ in 0..MPP_TIMEOUT_TICKS { + nodes[2].node.timer_tick_occurred(); + } + LocalHTLCFailureReason::MPPTimeout + }, + Some(TrampolineTimeout::OnChain) => { + let current_height = nodes[2].best_block_info().1; + let send_height = nodes[0].best_block_info().1; + let htlc_cltv = send_height + 1 + last_hop_cltv_delta; + connect_blocks(&nodes[2], htlc_cltv - HTLC_FAIL_BACK_BUFFER - current_height); + LocalHTLCFailureReason::CLTVExpiryTooSoon + }, + }; - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + // Carol rejects the trampoline forward (either after MPP completion or timeout). + let events = nodes[2].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); - let first_message_event = remove_first_msg_event_to_node(&nodes[1].node.get_our_node_id(), &mut events); - - let route: &[&Node] = &[&nodes[1], &nodes[2]]; - let args = PassAlongPathArgs::new(&nodes[0], route, amt_msat, payment_hash, first_message_event) - .with_payment_preimage(payment_preimage) - .without_claimable_event() - .expect_failure(HTLCHandlingFailureType::Receive { payment_hash }); - do_pass_along_path(args); + match events[0] { + crate::events::Event::HTLCHandlingFailed { + ref failure_type, ref failure_reason, .. + } => { + assert_eq!(failure_type, &HTLCHandlingFailureType::TrampolineForward {}); + match failure_reason { + Some(crate::events::HTLCHandlingFailureReason::Local { reason }) => { + assert_eq!(*reason, expected_reason) + }, + Some(_) | None => panic!("expected failure_reason for failed trampoline"), + } + }, + _ => panic!("Unexpected destination"), + } + expect_and_process_pending_htlcs(&nodes[2], false); + assert!(nodes[2].node.get_and_clear_pending_events().is_empty()); + + // Propagate failures back through each forwarded path to Alice. + let both: [&[&Node]; 2] = [bob_path, barry_path]; + let one: [&[&Node]; 1] = [bob_path]; + let forwarded: &[&[&Node]] = if send_both { &both } else { &one }; + let carol_id = nodes[2].node.get_our_node_id(); + check_added_monitors(&nodes[2], forwarded.len()); + let mut carol_msgs = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(carol_msgs.len(), forwarded.len()); + for path in forwarded { + let hop = path[0]; + let hop_id = hop.node.get_our_node_id(); + let ev = remove_first_msg_event_to_node(&hop_id, &mut carol_msgs); + let updates = match ev { + MessageSendEvent::UpdateHTLCs { updates, .. } => updates, + _ => panic!("Expected UpdateHTLCs"), + }; + hop.node.handle_update_fail_htlc(carol_id, &updates.update_fail_htlcs[0]); + do_commitment_signed_dance(hop, &nodes[2], &updates.commitment_signed, true, false); - { - let unblinded_node_updates = get_htlc_update_msgs(&nodes[2], &nodes[1].node.get_our_node_id()); - nodes[1].node.handle_update_fail_htlc( - nodes[2].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] - ); - do_commitment_signed_dance(&nodes[1], &nodes[2], &unblinded_node_updates.commitment_signed, true, false); + let fwd = get_htlc_update_msgs(hop, &nodes[0].node.get_our_node_id()); + nodes[0].node.handle_update_fail_htlc(hop_id, &fwd.update_fail_htlcs[0]); + do_commitment_signed_dance(&nodes[0], hop, &fwd.commitment_signed, false, false); } - { - let unblinded_node_updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); - nodes[0].node.handle_update_fail_htlc( - nodes[1].node.get_our_node_id(), &unblinded_node_updates.update_fail_htlcs[0] - ); - do_commitment_signed_dance(&nodes[0], &nodes[1], &unblinded_node_updates.commitment_signed, false, false); + + // Check Alice's failure events. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), if send_both { 3 } else { 1 }); + for ev in &events[..forwarded.len()] { + match ev { + Event::PaymentPathFailed { payment_hash: h, payment_failed_permanently, .. } => { + assert_eq!(*h, payment_hash); + assert!(!payment_failed_permanently); + }, + _ => panic!("Expected PaymentPathFailed, got {:?}", ev), + } } - { - // Expect UnknownNextPeer error while we are unable to route forwarding Trampoline payments. - let payment_failed_conditions = PaymentFailedConditions::new() - .expected_htlc_error_data(LocalHTLCFailureReason::UnknownNextPeer, &[0; 0]); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, payment_failed_conditions); + if send_both { + match &events[2] { + Event::PaymentFailed { payment_hash: h, reason, .. } => { + assert_eq!(*h, Some(payment_hash)); + assert_eq!(*reason, Some(PaymentFailureReason::RetriesExhausted)); + }, + _ => panic!("Expected PaymentFailed, got {:?}", events[2]), + } + + // Verify no spurious timeout fires after the MPP set was dispatched. + for _ in 0..(MPP_TIMEOUT_TICKS * 3) { + nodes[2].node.timer_tick_occurred(); + } + assert!(nodes[2].node.get_and_clear_pending_events().is_empty()); } } + +#[test] +fn test_trampoline_mpp_accumulation() { + do_trampoline_mpp_test(None); + do_trampoline_mpp_test(Some(TrampolineTimeout::Ticks)); + do_trampoline_mpp_test(Some(TrampolineTimeout::OnChain)); +} diff --git a/lightning/src/ln/bolt11_payment_tests.rs b/lightning/src/ln/bolt11_payment_tests.rs index 8c2ac155ce7..3e0ebbbefc2 100644 --- a/lightning/src/ln/bolt11_payment_tests.rs +++ b/lightning/src/ln/bolt11_payment_tests.rs @@ -30,8 +30,10 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42]; - let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(None, 7200, None).unwrap(); + let (payment_hash, payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment(None, 7200, None, Some(payment_metadata.clone())) + .unwrap(); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let invoice = InvoiceBuilder::new(Currency::Bitcoin) @@ -41,7 +43,7 @@ fn payment_metadata_end_to_end_for_invoice_with_amount() { .duration_since_epoch(timestamp) .min_final_cltv_expiry_delta(144) .amount_milli_satoshis(50_000) - .payment_metadata(payment_metadata.clone()) + .payment_metadata(encrypted_metadata.unwrap()) .build_raw() .unwrap(); let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap(); @@ -97,8 +99,10 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { let payment_metadata = vec![42, 43, 44, 45, 46, 47, 48, 49, 42]; - let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(None, 7200, None).unwrap(); + let (payment_hash, payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment(None, 7200, None, Some(payment_metadata.clone())) + .unwrap(); let timestamp = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap(); let invoice = InvoiceBuilder::new(Currency::Bitcoin) @@ -107,7 +111,7 @@ fn payment_metadata_end_to_end_for_invoice_with_no_amount() { .payment_secret(payment_secret) .duration_since_epoch(timestamp) .min_final_cltv_expiry_delta(144) - .payment_metadata(payment_metadata.clone()) + .payment_metadata(encrypted_metadata.unwrap()) .build_raw() .unwrap(); let sig = nodes[1].keys_manager.backing.sign_invoice(&invoice, Recipient::Node).unwrap(); diff --git a/lightning/src/ln/chan_utils.rs b/lightning/src/ln/chan_utils.rs index 4bb8ffac9ef..781baecd356 100644 --- a/lightning/src/ln/chan_utils.rs +++ b/lightning/src/ln/chan_utils.rs @@ -590,7 +590,7 @@ pub struct TxCreationKeys { pub broadcaster_delayed_payment_key: DelayedPaymentKey, } -impl_writeable_tlv_based!(TxCreationKeys, { +impl_ser_tlv_based!(TxCreationKeys, { (0, per_commitment_point, required), (2, revocation_key, required), (4, broadcaster_htlc_key, required), @@ -622,7 +622,7 @@ pub struct ChannelPublicKeys { pub htlc_basepoint: HtlcBasepoint, } -impl_writeable_tlv_based!(ChannelPublicKeys, { +impl_ser_tlv_based!(ChannelPublicKeys, { (0, funding_pubkey, required), (2, revocation_basepoint, required), (4, payment_point, required), @@ -668,6 +668,18 @@ impl TxCreationKeys { // on-chain funds. pub const REVOKEABLE_REDEEMSCRIPT_MAX_LENGTH: usize = 6 + 4 + 34 * 2; +/// The exact length of the script returned by [`get_revokeable_redeemscript`] for a given +/// `contest_delay`. +/// +/// This is always at most [`REVOKEABLE_REDEEMSCRIPT_MAX_LENGTH`], and shorter when `contest_delay` +/// encodes to fewer than the maximum 4 bytes. +pub fn revokeable_redeemscript_len(contest_delay: u16) -> usize { + // 6 bytes of opcodes + the `OP_CSV` value push + two 33-byte public keys (each with a 1-byte + // push). + let contest_delay_push_len = Builder::new().push_int(contest_delay as i64).into_script().len(); + 6 + contest_delay_push_len + 34 * 2 +} + /// A script either spendable by the revocation /// key or the broadcaster_delayed_payment_key and satisfying the relative-locktime OP_CSV constrain. /// Encumbering a `to_holder` output on a commitment transaction or 2nd-stage HTLC transactions. @@ -683,7 +695,7 @@ pub fn get_revokeable_redeemscript(revocation_key: &RevocationKey, contest_delay .push_opcode(opcodes::all::OP_ENDIF) .push_opcode(opcodes::all::OP_CHECKSIG) .into_script(); - debug_assert!(res.len() <= REVOKEABLE_REDEEMSCRIPT_MAX_LENGTH); + debug_assert_eq!(res.len(), revokeable_redeemscript_len(contest_delay)); res } @@ -738,7 +750,7 @@ impl HTLCOutputInCommitment { } } -impl_writeable_tlv_based!(HTLCOutputInCommitment, { +impl_ser_tlv_based!(HTLCOutputInCommitment, { (0, offered, required), (2, amount_msat, required), (4, cltv_expiry, required), @@ -1164,29 +1176,26 @@ impl ChannelTransactionParameters { } } -impl_writeable_tlv_based!(CounterpartyChannelTransactionParameters, { +impl_ser_tlv_based!(CounterpartyChannelTransactionParameters, { (0, pubkeys, required), (2, selected_contest_delay, required), }); -impl Writeable for ChannelTransactionParameters { - #[rustfmt::skip] - fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { - let legacy_deserialization_prevention_marker = legacy_deserialization_prevention_marker_for_channel_type_features(&self.channel_type_features); - write_tlv_fields!(writer, { - (0, self.holder_pubkeys, required), - (2, self.holder_selected_contest_delay, required), - (4, self.is_outbound_from_holder, required), - (6, self.counterparty_parameters, option), - (8, self.funding_outpoint, option), - (10, legacy_deserialization_prevention_marker, option), - (11, self.channel_type_features, required), - (12, self.splice_parent_funding_txid, option), - (13, self.channel_value_satoshis, required), - }); - Ok(()) - } -} +impl_writeable_tlv_based!(ChannelTransactionParameters, self, { + (0, self.holder_pubkeys, required), + (2, self.holder_selected_contest_delay, required), + (4, self.is_outbound_from_holder, required), + (6, self.counterparty_parameters, option), + (8, self.funding_outpoint, option), + ( + 10, + legacy_deserialization_prevention_marker_for_channel_type_features(&self.channel_type_features), + option + ), + (11, self.channel_type_features, required), + (12, self.splice_parent_funding_txid, option), + (13, self.channel_value_satoshis, required), +}); impl ReadableArgs<Option<u64>> for ChannelTransactionParameters { #[rustfmt::skip] @@ -1336,7 +1345,7 @@ impl PartialEq for HolderCommitmentTransaction { } } -impl_writeable_tlv_based!(HolderCommitmentTransaction, { +impl_ser_tlv_based!(HolderCommitmentTransaction, { (0, inner, required), (2, counterparty_sig, required), (4, holder_sig_first, required), @@ -1424,7 +1433,7 @@ pub struct BuiltCommitmentTransaction { pub txid: Txid, } -impl_writeable_tlv_based!(BuiltCommitmentTransaction, { +impl_ser_tlv_based!(BuiltCommitmentTransaction, { (0, transaction, required), (2, txid, required), }); @@ -1634,25 +1643,22 @@ impl PartialEq for CommitmentTransaction { } } -impl Writeable for CommitmentTransaction { - #[rustfmt::skip] - fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { - let legacy_deserialization_prevention_marker = legacy_deserialization_prevention_marker_for_channel_type_features(&self.channel_type_features); - write_tlv_fields!(writer, { - (0, self.commitment_number, required), - (1, self.to_broadcaster_delay, option), - (2, self.to_broadcaster_value_sat, required), - (4, self.to_countersignatory_value_sat, required), - (6, self.feerate_per_kw, required), - (8, self.keys, required), - (10, self.built, required), - (12, self.nondust_htlcs, required_vec), - (14, legacy_deserialization_prevention_marker, option), - (15, self.channel_type_features, required), - }); - Ok(()) - } -} +impl_writeable_tlv_based!(CommitmentTransaction, self, { + (0, self.commitment_number, required), + (1, self.to_broadcaster_delay, option), + (2, self.to_broadcaster_value_sat, required), + (4, self.to_countersignatory_value_sat, required), + (6, self.feerate_per_kw, required), + (8, self.keys, required), + (10, self.built, required), + (12, self.nondust_htlcs, required_vec), + ( + 14, + legacy_deserialization_prevention_marker_for_channel_type_features(&self.channel_type_features), + option + ), + (15, self.channel_type_features, required), +}); impl Readable for CommitmentTransaction { #[rustfmt::skip] diff --git a/lightning/src/ln/chanmon_update_fail_tests.rs b/lightning/src/ln/chanmon_update_fail_tests.rs index 5a0c37bd61d..81062ea7cc3 100644 --- a/lightning/src/ln/chanmon_update_fail_tests.rs +++ b/lightning/src/ln/chanmon_update_fail_tests.rs @@ -16,10 +16,10 @@ use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::chainmonitor::ChainMonitor; use crate::chain::channelmonitor::{ChannelMonitor, MonitorEvent, ANTI_REORG_DELAY}; use crate::chain::transaction::OutPoint; -use crate::chain::{ChannelMonitorUpdateStatus, Listen, Watch}; +use crate::chain::{BlockLocator, ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaymentPurpose}; use crate::ln::channel::AnnouncementSigsState; -use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder}; +use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder, TrustedChannelFeatures}; use crate::ln::msgs; use crate::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, MessageSendEvent, RoutingMessageHandler, @@ -48,6 +48,7 @@ use crate::util::test_utils; use crate::prelude::*; use crate::sync::{Arc, Mutex}; use bitcoin::hashes::Hash; +use core::sync::atomic::Ordering; #[test] fn test_monitor_and_persister_update_fail() { @@ -89,7 +90,7 @@ fn test_monitor_and_persister_update_fail() { let chain_mon = { let new_monitor = { let monitor = nodes[0].chain_monitor.chain_monitor.get_monitor(chan.2).unwrap(); - let (_, new_monitor) = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read( + let (_, new_monitor) = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read( &mut &monitor.encode()[..], (nodes[0].keys_manager, nodes[0].keys_manager), ) @@ -175,6 +176,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + nodes[0].disable_monitor_completeness_assertion(); let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); @@ -187,7 +189,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -254,7 +256,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { get_route_and_payment_hash!(&nodes[0], nodes[1], 1000000); chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -277,7 +279,7 @@ fn do_test_simple_monitor_temporary_update_fail(disconnect: bool) { }; nodes[0].node.force_close_broadcasting_latest_txn(&channel_id, &node_b_id, message).unwrap(); check_added_monitors(&nodes[0], 1); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); // TODO: Once we hit the chain with the failure transaction we should check that we get a // PaymentPathFailed event @@ -316,6 +318,7 @@ fn do_test_monitor_temporary_update_fail(disconnect_count: usize) { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + nodes[0].disable_monitor_completeness_assertion(); let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); @@ -330,7 +333,7 @@ fn do_test_monitor_temporary_update_fail(disconnect_count: usize) { let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -735,7 +738,7 @@ fn test_monitor_update_fail_cs() { let (route, our_payment_hash, payment_preimage, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -843,7 +846,7 @@ fn test_monitor_update_fail_no_rebroadcast() { let (route, our_payment_hash, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -897,7 +900,7 @@ fn test_monitor_update_raa_while_paused() { send_payment(&nodes[0], &[&nodes[1]], 5000000); let (route, our_payment_hash_1, payment_preimage_1, our_payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret_1); + let onion = RecipientOnionFields::secret_only(our_payment_secret_1, 1000000); let id = PaymentId(our_payment_hash_1.0); nodes[0].node.send_payment_with_route(route, our_payment_hash_1, onion, id).unwrap(); @@ -907,7 +910,7 @@ fn test_monitor_update_raa_while_paused() { let (route, our_payment_hash_2, payment_preimage_2, our_payment_secret_2) = get_route_and_payment_hash!(nodes[1], nodes[0], 1000000); - let onion_2 = RecipientOnionFields::secret_only(our_payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(our_payment_secret_2, 1000000); let id_2 = PaymentId(our_payment_hash_2.0); nodes[1].node.send_payment_with_route(route, our_payment_hash_2, onion_2, id_2).unwrap(); @@ -969,6 +972,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + nodes[1].disable_monitor_completeness_assertion(); let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); @@ -1008,7 +1012,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // holding cell. let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[2], 1000000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1034,7 +1038,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // being paused waiting a monitor update. let (route, payment_hash_3, _, payment_secret_3) = get_route_and_payment_hash!(nodes[0], nodes[2], 1000000); - let onion_3 = RecipientOnionFields::secret_only(payment_secret_3); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3, 1000000); let id_3 = PaymentId(payment_hash_3.0); nodes[0].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1055,7 +1059,7 @@ fn do_test_monitor_update_fail_raa(test_ignore_second_cs: bool) { // Try to route another payment backwards from 2 to make sure 1 holds off on responding let (route, payment_hash_4, payment_preimage_4, payment_secret_4) = get_route_and_payment_hash!(nodes[2], nodes[0], 1000000); - let onion_4 = RecipientOnionFields::secret_only(payment_secret_4); + let onion_4 = RecipientOnionFields::secret_only(payment_secret_4, 1000000); let id_4 = PaymentId(payment_hash_4.0); nodes[2].node.send_payment_with_route(route, payment_hash_4, onion_4, id_4).unwrap(); check_added_monitors(&nodes[2], 1); @@ -1382,20 +1386,20 @@ fn raa_no_response_awaiting_raa_state() { let (route, payment_hash_1, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); let (payment_preimage_2, payment_hash_2, payment_secret_2) = - get_payment_preimage_hash!(nodes[1]); + get_payment_preimage_hash(&nodes[1], None, None); let (payment_preimage_3, payment_hash_3, payment_secret_3) = - get_payment_preimage_hash!(nodes[1]); + get_payment_preimage_hash(&nodes[1], None, None); // Queue up two payments - one will be delivered right away, one immediately goes into the // holding cell as nodes[0] is AwaitingRAA. Ultimately this allows us to deliver an RAA // immediately after a CS. By setting failing the monitor update failure from the CS (which // requires only an RAA response due to AwaitingRAA) we can deliver the RAA and require the CS // generation during RAA while in monitor-update-failed state. - let onion_1 = RecipientOnionFields::secret_only(payment_secret_1); + let onion_1 = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_1, onion_1, id_1).unwrap(); check_added_monitors(&nodes[0], 1); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 0); @@ -1444,7 +1448,7 @@ fn raa_no_response_awaiting_raa_state() { // We send a third payment here, which is somewhat of a redundant test, but the // chanmon_fail_consistency test required it to actually find the bug (by seeing out-of-sync // commitment transaction states) whereas here we can explicitly check for it. - let onion_3 = RecipientOnionFields::secret_only(payment_secret_3); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3, 1000000); let id_3 = PaymentId(payment_hash_3.0); nodes[0].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); check_added_monitors(&nodes[0], 0); @@ -1500,6 +1504,7 @@ fn claim_while_disconnected_monitor_update_fail() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + nodes[1].disable_monitor_completeness_assertion(); let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); @@ -1546,7 +1551,7 @@ fn claim_while_disconnected_monitor_update_fail() { // the monitor still failed let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1653,7 +1658,7 @@ fn monitor_failed_no_reestablish_response() { // on receipt). let (route, payment_hash_1, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1727,6 +1732,7 @@ fn first_message_on_recv_ordering() { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + nodes[1].disable_monitor_completeness_assertion(); let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); @@ -1737,7 +1743,7 @@ fn first_message_on_recv_ordering() { // can deliver it and fail the monitor update. let (route, payment_hash_1, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion_1 = RecipientOnionFields::secret_only(payment_secret_1); + let onion_1 = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion_1, id_1).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1761,7 +1767,7 @@ fn first_message_on_recv_ordering() { // Route the second payment, generating an update_add_htlc/commitment_signed let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); @@ -1854,7 +1860,7 @@ fn test_monitor_update_fail_claim() { let (route, payment_hash_2, _, payment_secret_2) = get_route_and_payment_hash!(nodes[2], nodes[0], 1_000_000); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1_000_000); let id_2 = PaymentId(payment_hash_2.0); nodes[2].node.send_payment_with_route(route.clone(), payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[2], 1); @@ -1872,9 +1878,9 @@ fn test_monitor_update_fail_claim() { do_commitment_signed_dance(&nodes[1], &nodes[2], &payment_event.commitment_msg, false, true); expect_htlc_failure_conditions(nodes[1].node.get_and_clear_pending_events(), &[]); - let (_, payment_hash_3, payment_secret_3) = get_payment_preimage_hash!(nodes[0]); + let (_, payment_hash_3, payment_secret_3) = get_payment_preimage_hash(&nodes[0], None, None); let id_3 = PaymentId(payment_hash_3.0); - let onion_3 = RecipientOnionFields::secret_only(payment_secret_3); + let onion_3 = RecipientOnionFields::secret_only(payment_secret_3, 1_000_000); nodes[2].node.send_payment_with_route(route, payment_hash_3, onion_3, id_3).unwrap(); check_added_monitors(&nodes[2], 1); @@ -1998,7 +2004,7 @@ fn test_monitor_update_on_pending_forwards() { let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[2], nodes[0], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[2].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[2], 1); @@ -2069,7 +2075,7 @@ fn monitor_update_claim_fail_no_response() { // Now start forwarding a second payment, skipping the last RAA so B is in AwaitingRAA let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2309,6 +2315,7 @@ fn test_path_paused_mpp() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_ann.contents.short_channel_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.final_value_msat *= 2; // Set it so that the first monitor update (for the path 0 -> 1 -> 3) succeeds, but the second // (for the path 0 -> 2 -> 3) fails. @@ -2316,7 +2323,7 @@ fn test_path_paused_mpp() { chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); // The first path should have succeeded with the second getting a MonitorUpdateInProgress err. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 200000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 2); @@ -2372,7 +2379,7 @@ fn test_pending_update_fee_ack_on_reconnect() { let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(&nodes[1], nodes[0], 1_000_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -2509,7 +2516,7 @@ fn test_fail_htlc_on_broadcast_after_claim() { mine_transaction(&nodes[1], &bs_txn[0]); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_c_id], 100000); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); check_added_monitors(&nodes[1], 1); expect_and_process_pending_htlcs_and_htlc_handling_failed( @@ -2663,7 +2670,7 @@ fn do_channel_holding_cell_serialize(disconnect: bool, reload_a: bool) { let (route, payment_hash_1, payment_preimage_1, payment_secret_1) = get_route_and_payment_hash!(&nodes[0], nodes[1], 100000); let (payment_preimage_2, payment_hash_2, payment_secret_2) = - get_payment_preimage_hash!(&nodes[1]); + get_payment_preimage_hash(&nodes[1], None, None); // Do a really complicated dance to get an HTLC into the holding cell, with // MonitorUpdateInProgress set but AwaitingRemoteRevoke unset. When this test was written, any @@ -2687,14 +2694,14 @@ fn do_channel_holding_cell_serialize(disconnect: bool, reload_a: bool) { // (c) will not be freed from the holding cell. let (payment_preimage_0, payment_hash_0, ..) = route_payment(&nodes[1], &[&nodes[0]], 100_000); - let onion_1 = RecipientOnionFields::secret_only(payment_secret_1); + let onion_1 = RecipientOnionFields::secret_only(payment_secret_1, 100000); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_1, onion_1, id_1).unwrap(); check_added_monitors(&nodes[0], 1); let send = SendEvent::from_node(&nodes[0]); assert_eq!(send.msgs.len(), 1); - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 100000); let id_2 = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 0); @@ -2871,7 +2878,7 @@ fn do_test_reconnect_dup_htlc_claims(htlc_status: HTLCStatusAtDupClaim, second_f // awaiting a remote revoke_and_ack from nodes[0]. let (route, second_payment_hash, _, second_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); - let onion_2 = RecipientOnionFields::secret_only(second_payment_secret); + let onion_2 = RecipientOnionFields::secret_only(second_payment_secret, 100_000); let id_2 = PaymentId(second_payment_hash.0); nodes[0].node.send_payment_with_route(route, second_payment_hash, onion_2, id_2).unwrap(); check_added_monitors(&nodes[0], 1); @@ -3048,11 +3055,11 @@ fn test_temporary_error_during_shutdown() { node_b_id, &get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id), ); - let (_, closing_signed_a) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, closing_signed_a) = get_closing_signed_broadcast(&nodes[0], node_b_id); let txn_a = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); nodes[1].node.handle_closing_signed(node_a_id, &closing_signed_a.unwrap()); - let (_, none_b) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, none_b) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(none_b.is_none()); let txn_b = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); @@ -3234,7 +3241,13 @@ fn do_test_outbound_reload_without_init_mon(use_0conf: bool) { if use_0conf { nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf(&chan_id, &node_a_id, 0, None) + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroConf, + None, + ) .unwrap(); } else { nodes[1].node.accept_inbound_channel(&chan_id, &node_a_id, 0, None).unwrap(); @@ -3343,7 +3356,13 @@ fn do_test_inbound_reload_without_init_mon(use_0conf: bool, lock_commitment: boo if use_0conf { nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf(&chan_id, &node_a_id, 0, None) + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroConf, + None, + ) .unwrap(); } else { nodes[1].node.accept_inbound_channel(&chan_id, &node_a_id, 0, None).unwrap(); @@ -3519,8 +3538,9 @@ fn do_test_blocked_chan_preimage_release(completion_mode: BlockedUpdateComplMode .node .handle_commitment_signed_batch_test(node_a_id, &as_htlc_fulfill.commitment_signed); check_added_monitors(&nodes[1], 1); - let (a, raa) = do_main_commitment_signed_dance(&nodes[1], &nodes[0], false); + let (a, raa, holding_cell) = do_main_commitment_signed_dance(&nodes[1], &nodes[0], false); assert!(a.is_none()); + assert!(holding_cell.is_empty()); nodes[1].node.handle_revoke_and_ack(node_a_id, &raa); check_added_monitors(&nodes[1], 1); @@ -3847,6 +3867,7 @@ fn do_test_durable_preimages_on_closed_channel( // Now reload node B let manager_b = nodes[1].node.encode(); reload_node!(nodes[1], &manager_b, &[&mon_ab, &mon_bc], persister, chain_mon, node_b_reload); + nodes[1].disable_monitor_completeness_assertion(); nodes[0].node.peer_disconnected(node_b_id); nodes[2].node.peer_disconnected(node_b_id); @@ -3896,11 +3917,28 @@ fn do_test_durable_preimages_on_closed_channel( } if !close_chans_before_reload { check_closed_broadcast(&nodes[1], 1, false); - let reason = ClosureReason::CommitmentTxConfirmed; - check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); + // When hold=false, get_and_clear_pending_events also triggers + // process_background_events (replaying the preimage and force-close updates) + // and resolves the deferred completions, firing PaymentForwarded alongside + // ChannelClosed. When hold=true, only ChannelClosed fires. + let evs = nodes[1].node.get_and_clear_pending_events(); + let expected = if hold_post_reload_mon_update { 1 } else { 2 }; + assert_eq!(evs.len(), expected, "{:?}", evs); + assert!(evs.iter().any(|e| matches!( + e, + Event::ChannelClosed { reason: ClosureReason::CommitmentTxConfirmed, .. } + ))); + if !hold_post_reload_mon_update { + assert!(evs.iter().any(|e| matches!(e, Event::PaymentForwarded { .. }))); + check_added_monitors(&nodes[1], mons_added); + } } nodes[1].node.timer_tick_occurred(); - check_added_monitors(&nodes[1], mons_added); + // For !close_chans_before_reload && !hold, background events were already replayed + // during get_and_clear_pending_events above, so timer_tick adds no monitors. + let expected_mons = + if !close_chans_before_reload && !hold_post_reload_mon_update { 0 } else { mons_added }; + check_added_monitors(&nodes[1], expected_mons); // Finally, check that B created a payment preimage transaction and close out the payment. let bs_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); @@ -3915,39 +3953,61 @@ fn do_test_durable_preimages_on_closed_channel( check_closed_broadcast(&nodes[0], 1, false); expect_payment_sent(&nodes[0], payment_preimage, None, true, true); + if close_chans_before_reload && !hold_post_reload_mon_update { + // For close_chans_before_reload with hold=false, the deferred completions + // haven't been processed yet. Trigger process_pending_monitor_events now. + let _ = nodes[1].node.get_and_clear_pending_msg_events(); + check_added_monitors(&nodes[1], 0); + } + if !close_chans_before_reload || close_only_a { // Make sure the B<->C channel is still alive and well by sending a payment over it. let mut reconnect_args = ReconnectArgs::new(&nodes[1], &nodes[2]); reconnect_args.pending_responding_commitment_signed.1 = true; - // The B<->C `ChannelMonitorUpdate` shouldn't be allowed to complete, which is the - // equivalent to the responding `commitment_signed` being a duplicate for node B, thus we - // need to set the `pending_responding_commitment_signed_dup` flag. - reconnect_args.pending_responding_commitment_signed_dup_monitor.1 = true; + if hold_post_reload_mon_update { + // When the A-B update is still InProgress, B-C monitor updates are blocked, + // so the responding commitment_signed is a duplicate that generates no update. + reconnect_args.pending_responding_commitment_signed_dup_monitor.1 = true; + } reconnect_args.pending_raa.1 = true; reconnect_nodes(reconnect_args); } - // Once the blocked `ChannelMonitorUpdate` *finally* completes, the pending - // `PaymentForwarded` event will finally be released. - let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); - nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id_ab, ab_update_id); + if hold_post_reload_mon_update { + // When the persister returned InProgress, we need to manually complete the + // A-B monitor update to unblock the PaymentForwarded completion action. + let (_, ab_update_id) = nodes[1].chain_monitor.get_latest_mon_update_id(chan_id_ab); + nodes[1] + .chain_monitor + .chain_monitor + .force_channel_monitor_updated(chan_id_ab, ab_update_id); + } // If the A<->B channel was closed before we reload, we'll replay the claim against it on // reload, causing the `PaymentForwarded` event to get replayed. let evs = nodes[1].node.get_and_clear_pending_events(); - assert_eq!(evs.len(), if close_chans_before_reload { 2 } else { 1 }); - for ev in evs { - if let Event::PaymentForwarded { .. } = ev { - } else { - panic!(); + if !close_chans_before_reload && !hold_post_reload_mon_update { + // PaymentForwarded already fired during get_and_clear_pending_events above. + assert!(evs.is_empty(), "{:?}", evs); + } else { + assert_eq!(evs.len(), if close_chans_before_reload { 2 } else { 1 }, "{:?}", evs); + for ev in evs { + if let Event::PaymentForwarded { claim_from_onchain_tx, next_htlcs, .. } = ev { + if !claim_from_onchain_tx { + assert!(next_htlcs[0].user_channel_id.is_some()) + } + } else { + panic!("Unexpected event: {:?}", ev); + } } } if !close_chans_before_reload || close_only_a { - // Once we call `process_pending_events` the final `ChannelMonitor` for the B<->C channel - // will fly, removing the payment preimage from it. - check_added_monitors(&nodes[1], 1); + if hold_post_reload_mon_update { + // The B-C monitor update from the completion action fires now. + check_added_monitors(&nodes[1], 1); + } assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); send_payment(&nodes[1], &[&nodes[2]], 100_000); } @@ -4037,7 +4097,7 @@ fn do_test_reload_mon_update_completion_actions(close_during_reload: bool) { }; nodes[0].node.force_close_broadcasting_latest_txn(&chan_id_ab, &node_b_id, msg).unwrap(); check_added_monitors(&nodes[0], 1); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100_000); let as_closing_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); mine_transaction_without_consistency_checks(&nodes[1], &as_closing_tx[0]); @@ -4148,7 +4208,7 @@ fn do_test_glacial_peer_cant_hang(hold_chan_a: bool) { // With the A<->B preimage persistence not yet complete, the B<->C channel is stuck // waiting. - let onion_2 = RecipientOnionFields::secret_only(payment_secret_2); + let onion_2 = RecipientOnionFields::secret_only(payment_secret_2, 1_000_000); let id_2 = PaymentId(payment_hash_2.0); nodes[1].node.send_payment_with_route(route, payment_hash_2, onion_2, id_2).unwrap(); check_added_monitors(&nodes[1], 0); @@ -4246,7 +4306,7 @@ fn do_test_partial_claim_mon_update_compl_actions(reload_a: bool, reload_b: bool let chan_4_scid = chan_4_update.contents.short_channel_id; let (mut route, payment_hash, preimage, payment_secret) = - get_route_and_payment_hash!(&nodes[0], nodes[3], 100000); + get_route_and_payment_hash!(&nodes[0], nodes[3], 100_000); let path = route.paths[0].clone(); route.paths.push(path); route.paths[0].hops[0].pubkey = node_b_id; @@ -4255,6 +4315,8 @@ fn do_test_partial_claim_mon_update_compl_actions(reload_a: bool, reload_b: bool route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_scid; route.paths[1].hops[1].short_channel_id = chan_4_scid; + route.route_params.final_value_msat *= 2; + let paths = &[&[&nodes[1], &nodes[3]][..], &[&nodes[2], &nodes[3]][..]]; send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret); @@ -4488,13 +4550,13 @@ fn test_claim_to_closed_channel_blocks_forwarded_preimage_removal() { check_added_monitors(&nodes[0], 1); let a_reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, a_reason, &[node_b_id], 1000000); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); let as_commit_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); assert_eq!(as_commit_tx.len(), 1); mine_transaction(&nodes[1], &as_commit_tx[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let b_reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, b_reason, &[node_a_id], 1000000); @@ -4566,13 +4628,13 @@ fn test_claim_to_closed_channel_blocks_claimed_event() { check_added_monitors(&nodes[0], 1); let a_reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, a_reason, &[node_b_id], 1000000); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); let as_commit_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); assert_eq!(as_commit_tx.len(), 1); mine_transaction(&nodes[1], &as_commit_tx[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let b_reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, b_reason, &[node_a_id], 1000000); @@ -4594,6 +4656,7 @@ fn test_claim_to_closed_channel_blocks_claimed_event() { #[test] #[cfg(all(feature = "std", not(target_os = "windows")))] fn test_single_channel_multiple_mpp() { + use crate::util::config::UserConfig; use std::sync::atomic::{AtomicBool, Ordering}; // Test what happens when we attempt to claim an MPP with many parts that came to us through @@ -4605,7 +4668,11 @@ fn test_single_channel_multiple_mpp() { // for more info. let chanmon_cfgs = create_chanmon_cfgs(9); let node_cfgs = create_node_cfgs(9, &chanmon_cfgs); - let configs = [None, None, None, None, None, None, None, None, None]; + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; + let configs: [Option<UserConfig>; 9] = core::array::from_fn(|_| Some(config.clone())); let node_chanmgrs = create_node_chanmgrs(9, &node_cfgs, &configs); let mut nodes = create_network(9, &node_cfgs, &node_chanmgrs); @@ -4918,6 +4985,7 @@ fn native_async_persist() { native_async_persister, Arc::clone(&keys_manager), keys_manager.get_peer_storage_key(), + false, ); // Write the initial ChannelMonitor async, testing primarily that the `MonitorEvent::Completed` @@ -5093,8 +5161,9 @@ fn test_mpp_claim_to_holding_cell() { send_along_route_with_secret(&nodes[0], route, paths, 500_000, paymnt_hash_1, payment_secret); // Put the C <-> D channel into AwaitingRaa - let (preimage_2, paymnt_hash_2, payment_secret_2) = get_payment_preimage_hash!(nodes[3]); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let (preimage_2, paymnt_hash_2, payment_secret_2) = + get_payment_preimage_hash(&nodes[3], None, None); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 400_000); let id = PaymentId([42; 32]); let pay_params = PaymentParameters::from_node_id(node_d_id, TEST_FINAL_CLTV); let route_params = RouteParameters::from_payment_params_and_value(pay_params, 400_000); @@ -5160,3 +5229,341 @@ fn test_mpp_claim_to_holding_cell() { expect_payment_claimable!(nodes[3], paymnt_hash_2, payment_secret_2, 400_000); claim_payment(&nodes[2], &[&nodes[3]], preimage_2); } + +fn do_test_late_counterparty_commitment_update_after_funding_spend(fully_confirmed: bool) { + // Tests that when a ChannelMonitorUpdate containing a new counterparty commitment (with an + // outbound HTLC) is applied to a monitor that has already seen the funding output spent + // on-chain, the HTLC is properly failed back. + // + // This exercises the race condition where: + // 1. A sends an HTLC to B, creating a monitor update with LatestCounterpartyCommitmentTXInfo + // 2. In deferred-write mode, this update is queued but not applied to the in-memory monitor + // 3. B's commitment transaction (without the HTLC) is broadcast and confirmed + // 4. The queued update is flushed, applying the counterparty commitment to the monitor + // 5. The monitor detects the funding spend and fails the HTLC + // + // When `fully_confirmed` is true, ANTI_REORG_DELAY has fully passed before the flush, so + // funding_spend_confirmed is set. Otherwise, the FundingSpendConfirmation entry is still + // pending in onchain_events_awaiting_threshold_conf. + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs_deferred(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_b_id = nodes[1].node.get_our_node_id(); + + let chan_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + + // Get B's commitment transaction before any HTLCs are added. This is the transaction that + // will be mined on-chain, simulating B broadcasting while A's monitor update is pending. + let bs_commitment_tx = get_local_commitment_txn!(nodes[1], chan_id); + assert_eq!(bs_commitment_tx.len(), 1); + + // Pause auto-flush on A so that the monitor update from send_payment is queued but NOT + // applied to the in-memory monitor. + nodes[0].chain_monitor.pause_flush.store(true, Ordering::Release); + + // Send a payment from A to B. The ChannelManager creates a LatestCounterpartyCommitmentTXInfo + // monitor update, but in deferred mode with pause_flush it remains queued. + let (route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); + let payment_id = PaymentId(payment_hash.0); + nodes[0] + .node + .send_payment_with_route( + route, + payment_hash, + RecipientOnionFields::secret_only(payment_secret, 1_000_000), + payment_id, + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + // Mine B's (old) commitment transaction on A and advance blocks. When fully_confirmed, + // advance past ANTI_REORG_DELAY so FundingSpendConfirmation is consumed and + // funding_spend_confirmed is set. Otherwise, stop one block short so the entry remains + // in onchain_events_awaiting_threshold_conf. + mine_transaction(&nodes[0], &bs_commitment_tx[0]); + let extra_blocks = if fully_confirmed { ANTI_REORG_DELAY - 1 } else { ANTI_REORG_DELAY - 2 }; + connect_blocks(&nodes[0], extra_blocks); + + if fully_confirmed { + // The channel close event, error message, and ChannelForceClosed monitor update were + // generated during block connection. Consume them before flushing. + check_closed_event( + &nodes[0], + 1, + ClosureReason::CommitmentTxConfirmed, + &[node_b_id], + 100000, + ); + check_closed_broadcast(&nodes[0], 1, true); + check_added_monitors(&nodes[0], 1); + } + + // Flush the queued monitor updates. This applies the LatestCounterpartyCommitmentTXInfo + // (and ChannelForceClosed) to the monitor, which triggers fail_htlcs_from_update_after_ + // funding_spend to create OnchainEvent::HTLCUpdate entries for the HTLC. + nodes[0].chain_monitor.pause_flush.store(false, Ordering::Release); + let pending_count = nodes[0].chain_monitor.chain_monitor.pending_operation_count(); + nodes[0].chain_monitor.chain_monitor.flush(pending_count, &nodes[0].logger); + + if !fully_confirmed { + // The channel close event, error message, and ChannelForceClosed monitor update were + // generated during block connection. + check_closed_event( + &nodes[0], + 1, + ClosureReason::CommitmentTxConfirmed, + &[node_b_id], + 100000, + ); + check_closed_broadcast(&nodes[0], 1, true); + check_added_monitors(&nodes[0], 1); + } + + // Advance ANTI_REORG_DELAY blocks so the OnchainEvent::HTLCUpdate entries (created at + // best_block.height during the flush) mature into MonitorEvent::HTLCEvent. + connect_blocks(&nodes[0], ANTI_REORG_DELAY); + + // The ChannelManager processes the MonitorEvent::HTLCEvent and fails the payment. + expect_payment_failed_conditions( + &nodes[0], + payment_hash, + false, + PaymentFailedConditions::new(), + ); + // The payment failure generates a ReleasePaymentComplete monitor update. + check_added_monitors(&nodes[0], 1); +} + +#[test] +fn test_late_counterparty_commitment_update_after_funding_spend() { + do_test_late_counterparty_commitment_update_after_funding_spend(false); +} + +#[test] +fn test_late_counterparty_commitment_update_after_funding_spend_fully_confirmed() { + do_test_late_counterparty_commitment_update_after_funding_spend(true); +} + +fn do_test_late_counterparty_commitment_update_after_holder_commitment_spend(dust: bool) { + // Tests that when the confirmed spending transaction is a holder commitment, HTLCs that + // have non-dust outputs in the holder commitment are NOT failed by + // fail_htlcs_from_update_after_funding_spend (they'll be resolved on-chain via + // HTLC-timeout), while HTLCs only present in the late counterparty commitment update ARE + // failed. + // + // When `dust` is true, HTLC Y is a dust amount, verifying that dust HTLCs in late + // counterparty commitment updates are also correctly failed. + // + // Setup: + // 1. Route HTLC X from A to B (fully committed in both holder and counterparty commitments) + // 2. Grab A's holder commitment (which contains HTLC X) + // 3. Pause flush, then send HTLC Y from A to B (counterparty commitment update is queued) + // 4. Mine A's holder commitment (contains X but not Y) + // 5. Flush the queued update (contains both X and Y) + // 6. Verify: X is not failed by our code (on-chain output), Y is failed + // 7. Drive HTLC X to resolution via the on-chain HTLC-timeout path + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs_deferred(2, &chanmon_cfgs); + // Use legacy (non-anchor) channels so that the HTLC-timeout transaction is broadcast + // directly by the monitor rather than going through the BumpTransaction event path. + let legacy_cfg = test_legacy_channel_config(); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg.clone()), Some(legacy_cfg)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_b_id = nodes[1].node.get_our_node_id(); + + let chan_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + + // Route HTLC X fully (committed in both commitments). + let (_, payment_hash_x, ..) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + // Get A's holder commitment which now contains HTLC X. For legacy (non-anchor) channels, + // the HTLC-timeout transaction is also returned. + let as_txn = get_local_commitment_txn!(nodes[0], chan_id); + let as_commitment_tx = &as_txn[0]; + // Verify HTLC X is present as a non-dust output (commitment has HTLC-timeout tx too). + assert!(as_txn.len() >= 2, "Expected commitment + HTLC-timeout tx, got {}", as_txn.len()); + + // Pause flush so the next monitor update is queued. + nodes[0].chain_monitor.pause_flush.store(true, Ordering::Release); + + // Send HTLC Y. When `dust` is true, 1000 msat (1 sat) is well below the dust limit and + // will not appear as an output in any commitment transaction. When false, 2_000_000 msat + // is non-dust. Either way, the LatestCounterpartyCommitmentTXInfo update (containing both + // X and Y) is queued in deferred mode. + let htlc_y_amount = if dust { 1_000 } else { 2_000_000 }; + let (route_y, payment_hash_y, _, payment_secret_y) = + get_route_and_payment_hash!(nodes[0], nodes[1], htlc_y_amount); + let payment_id_y = PaymentId(payment_hash_y.0); + nodes[0] + .node + .send_payment_with_route( + route_y, + payment_hash_y, + RecipientOnionFields::secret_only(payment_secret_y, htlc_y_amount), + payment_id_y, + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + // Mine A's holder commitment (contains X but not Y). + mine_transaction(&nodes[0], as_commitment_tx); + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 2); + + // Flush the queued monitor updates. + nodes[0].chain_monitor.pause_flush.store(false, Ordering::Release); + let pending_count = nodes[0].chain_monitor.chain_monitor.pending_operation_count(); + nodes[0].chain_monitor.chain_monitor.flush(pending_count, &nodes[0].logger); + + check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[node_b_id], 100000); + check_closed_broadcast(&nodes[0], 1, true); + check_added_monitors(&nodes[0], 1); + + // Advance ANTI_REORG_DELAY blocks so OnchainEvent::HTLCUpdate entries mature. + connect_blocks(&nodes[0], ANTI_REORG_DELAY); + + // HTLC Y should be failed by our code. HTLC X has an on-chain output in the holder + // commitment and will be resolved via the HTLC-timeout path. + expect_payment_failed_conditions( + &nodes[0], + payment_hash_y, + false, + PaymentFailedConditions::new(), + ); + check_added_monitors(&nodes[0], 1); + + // Verify HTLC X was NOT failed (no payment failure event for it at this point). + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + + // Drive HTLC X to resolution via the on-chain HTLC-timeout path. Connect blocks until we + // pass the CLTV expiry so the monitor broadcasts the HTLC-timeout transaction. + connect_blocks(&nodes[0], TEST_FINAL_CLTV); + let as_htlc_timeout_claim = + nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); + assert_eq!(as_htlc_timeout_claim.len(), 1); + check_spends!(as_htlc_timeout_claim[0], as_commitment_tx); + + // Mine the HTLC-timeout transaction and wait for ANTI_REORG_DELAY. + mine_transaction(&nodes[0], &as_htlc_timeout_claim[0]); + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + + // HTLC X should now be resolved on-chain. + expect_payment_failed_conditions( + &nodes[0], + payment_hash_x, + false, + PaymentFailedConditions::new(), + ); + check_added_monitors(&nodes[0], 1); +} + +#[test] +fn test_late_counterparty_commitment_update_after_holder_commitment_spend() { + do_test_late_counterparty_commitment_update_after_holder_commitment_spend(false); +} + +#[test] +fn test_late_counterparty_commitment_update_after_holder_commitment_spend_dust() { + do_test_late_counterparty_commitment_update_after_holder_commitment_spend(true); +} + +#[test] +fn test_monitor_update_after_funding_spend() { + // Test that monitor updates still work after a funding spend is detected by the + // ChainMonitor but before ChannelManager has processed the corresponding block. + // + // When the counterparty commitment transaction confirms (funding spend), the + // ChannelMonitor sets funding_spend_seen and no_further_updates_allowed() returns + // true. ChainMonitor overrides all subsequent update_channel results to InProgress + // to freeze the channel. These overridden updates complete via deferred completions + // in release_pending_monitor_events, so that MonitorUpdateCompletionActions (like + // PaymentClaimed) can still fire. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + + let (_, _, chan_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1); + + // Route payment 1 fully so B can claim it later. + let (payment_preimage_1, payment_hash_1, ..) = + route_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + // Get A's commitment tx (this is the "counterparty" commitment from B's perspective). + let as_commitment_tx = get_local_commitment_txn!(nodes[0], chan_id); + assert_eq!(as_commitment_tx.len(), 1); + + // Confirm A's commitment tx on B's chain_monitor ONLY (not on B's ChannelManager). + // This sets funding_spend_seen in the monitor, making no_further_updates_allowed() true. + // We also update the best block on the chain_monitor so the broadcaster height is + // consistent when claiming HTLCs. + let (block_hash, height) = nodes[1].best_block_info(); + let block = create_dummy_block(block_hash, height + 1, vec![as_commitment_tx[0].clone()]); + let txdata: Vec<_> = block.txdata.iter().enumerate().collect(); + nodes[1].chain_monitor.chain_monitor.transactions_confirmed(&block.header, &txdata, height + 1); + nodes[1].chain_monitor.chain_monitor.best_block_updated(&block.header, height + 1); + nodes[1].blocks.lock().unwrap().push((block, height + 1)); + + // Send payment 2 from A to B. + let (route, payment_hash_2, _, payment_secret_2) = + get_route_and_payment_hash!(&nodes[0], nodes[1], 1_000_000); + nodes[0] + .node + .send_payment_with_route( + route, + payment_hash_2, + RecipientOnionFields::secret_only(payment_secret_2, 1_000_000), + PaymentId(payment_hash_2.0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let payment_event = SendEvent::from_event(events.remove(0)); + + nodes[1].node.handle_update_add_htlc(node_a_id, &payment_event.msgs[0]); + + // B processes commitment_signed. The monitor applies the update but returns Err + // because no_further_updates_allowed() is true. ChainMonitor overrides to InProgress, + // freezing the channel. + nodes[1].node.handle_commitment_signed(node_a_id, &payment_event.commitment_msg[0]); + check_added_monitors(&nodes[1], 1); + + // B claims payment 1. The preimage monitor update also returns InProgress (deferred), + // so no Completed-while-InProgress assertion fires. + nodes[1].node.claim_funds(payment_preimage_1); + check_added_monitors(&nodes[1], 1); + + // First event cycle: the force-close MonitorEvent (CommitmentTxConfirmed) fires first, + // then the deferred completions resolve. The force-close generates a ChannelForceClosed + // update (also deferred), which blocks completion actions. So we only get ChannelClosed. + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::ChannelClosed { reason: ClosureReason::CommitmentTxConfirmed, .. } => {}, + _ => panic!("Unexpected event: {:?}", events[0]), + } + check_added_monitors(&nodes[1], 1); + nodes[1].node.get_and_clear_pending_msg_events(); + + // Second event cycle: the ChannelForceClosed deferred completion resolves, unblocking + // the PaymentClaimed completion action. + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::PaymentClaimed { payment_hash, amount_msat, .. } => { + assert_eq!(payment_hash_1, *payment_hash); + assert_eq!(1_000_000, *amount_msat); + }, + _ => panic!("Unexpected event: {:?}", events[0]), + } +} diff --git a/lightning/src/ln/channel.rs b/lightning/src/ln/channel.rs index 3236ebdefed..79def619a17 100644 --- a/lightning/src/ln/channel.rs +++ b/lightning/src/ln/channel.rs @@ -11,7 +11,7 @@ use bitcoin::absolute::LockTime; use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::encode; use bitcoin::constants::ChainHash; -use bitcoin::script::{Builder, Script, ScriptBuf, WScriptHash}; +use bitcoin::script::{Builder, Script, ScriptBuf}; use bitcoin::sighash::EcdsaSighashType; use bitcoin::transaction::{Transaction, TxOut}; use bitcoin::Witness; @@ -28,39 +28,39 @@ use bitcoin::{secp256k1, sighash, FeeRate, Sequence, TxIn}; use crate::blinded_path::message::BlindedMessagePath; use crate::chain::chaininterface::{ - fee_for_weight, ConfirmationTarget, FeeEstimator, LowerBoundedFeeEstimator, TransactionType, + ChannelFunding, ConfirmationTarget, FeeEstimator, FundingCandidate, FundingPurpose, + LowerBoundedFeeEstimator, TransactionType, }; use crate::chain::channelmonitor::{ ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, CommitmentHTLCData, LATENCY_GRACE_PERIOD_BLOCKS, }; use crate::chain::transaction::{OutPoint, TransactionData}; -use crate::chain::BestBlock; -use crate::events::{ClosureReason, FundingInfo}; +use crate::chain::BlockLocator; +use crate::events::{ClosureReason, FundingInfo, NegotiationFailureReason}; use crate::ln::chan_utils; use crate::ln::chan_utils::{ get_commitment_transaction_number_obscure_factor, max_htlcs, second_stage_tx_fees_sat, selected_commitment_sat_per_1000_weight, ChannelPublicKeys, ChannelTransactionParameters, ClosingTransaction, CommitmentTransaction, CounterpartyChannelTransactionParameters, CounterpartyCommitmentSecrets, HTLCOutputInCommitment, HolderCommitmentTransaction, - BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, + EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, }; use crate::ln::channel_state::{ - ChannelShutdownState, CounterpartyForwardingInfo, InboundHTLCDetails, InboundHTLCStateDetails, - OutboundHTLCDetails, OutboundHTLCStateDetails, + ChannelShutdownState, ConfirmedSpliceCandidate, CounterpartyForwardingInfo, InboundHTLCDetails, + InboundHTLCStateDetails, OutboundHTLCDetails, OutboundHTLCStateDetails, SpliceCandidateDetails, + SpliceCandidateStatus, SpliceDetails, }; use crate::ln::channelmanager::{ - self, ChannelReadyOrder, FundingConfirmedMessage, HTLCFailureMsg, HTLCPreviousHopData, - HTLCSource, OpenChannelMessage, PaymentClaimDetails, PendingHTLCInfo, PendingHTLCStatus, - RAACommitmentOrder, SentHTLCId, BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, - MIN_CLTV_EXPIRY_DELTA, + self, BlindedFailure, ChannelReadyOrder, FundingConfirmedMessage, HTLCFailureMsg, + HTLCPreviousHopData, HTLCSource, OpenChannelMessage, PaymentClaimDetails, PendingHTLCInfo, + PendingHTLCStatus, RAACommitmentOrder, SentHTLCId, TrustedChannelFeatures, TxSignaturesOrder, + BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::funding::{FeeRateAdjustmentError, FundingContribution, FundingTemplate}; use crate::ln::interactivetxs::{ - calculate_change_output_value, get_output_weight, AbortReason, HandleTxCompleteValue, - InteractiveTxConstructor, InteractiveTxConstructorArgs, InteractiveTxMessageSend, - InteractiveTxSigningSession, NegotiationError, SharedOwnedInput, SharedOwnedOutput, - TX_COMMON_FIELDS_WEIGHT, + AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, + InteractiveTxMessageSend, InteractiveTxSigningSession, SharedOwnedInput, SharedOwnedOutput, }; use crate::ln::msgs; use crate::ln::msgs::{ClosingSigned, ClosingSignedFeeRange, DecodeError, OnionErrorPacket}; @@ -69,11 +69,12 @@ use crate::ln::onion_utils::{ }; use crate::ln::script::{self, ShutdownScript}; use crate::ln::types::ChannelId; -use crate::ln::LN_MAX_MSG_LEN; use crate::offers::static_invoice::StaticInvoice; use crate::routing::gossip::NodeId; use crate::sign::ecdsa::EcdsaChannelSigner; -use crate::sign::tx_builder::{HTLCAmountDirection, NextCommitmentStats, SpecTxBuilder, TxBuilder}; +use crate::sign::tx_builder::{ + ChannelConstraints, ChannelStats, HTLCAmountDirection, SpecTxBuilder, TxBuilder, +}; use crate::sign::{ChannelSigner, EntropySource, NodeSigner, Recipient, SignerProvider}; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; @@ -82,16 +83,16 @@ use crate::util::config::{ MaxDustHTLCExposure, UserConfig, }; use crate::util::errors::APIError; -use crate::util::logger::{Logger, Record, WithContext}; +use crate::util::logger::{Level as LoggerLevel, Logger, Record, WithContext}; use crate::util::scid_utils::{block_from_scid, scid_from_parts}; -use crate::util::ser::{Readable, ReadableArgs, RequiredWrapper, Writeable, Writer}; +use crate::util::ser::{Iterable, Readable, ReadableArgs, RequiredWrapper, Writeable, Writer}; +use crate::util::wallet_utils::{ConfirmedUtxo, Input}; use crate::{impl_readable_for_vec, impl_writeable_for_vec}; use alloc::collections::{btree_map, BTreeMap}; use crate::io; use crate::prelude::*; -use crate::sign::type_resolver::ChannelSignerType; #[cfg(any(test, fuzzing, debug_assertions))] use crate::sync::Mutex; use core::time::Duration; @@ -121,6 +122,17 @@ pub struct AvailableBalances { pub next_outbound_htlc_limit_msat: u64, /// The minimum value we can assign to the next outbound HTLC pub next_outbound_htlc_minimum_msat: u64, + /// The current total dust exposure on this channel, in millisatoshis. + /// + /// This is the maximum of the dust exposure on the holder and counterparty commitment + /// transactions, and includes both the value of all pending HTLCs that are below the dust + /// threshold as well as any excess commitment transaction fees that contribute to dust + /// exposure. + /// + /// See [`ChannelConfig::max_dust_htlc_exposure`] for more information on the dust calculation and to configure a limit. + pub dust_exposure_msat: u64, + /// The maximum value of the next splice-out + pub next_splice_out_maximum_sat: u64, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -161,7 +173,7 @@ enum InboundHTLCResolution { Pending { update_add_htlc: msgs::UpdateAddHTLC }, } -impl_writeable_tlv_based_enum!(InboundHTLCResolution, +impl_ser_tlv_based_enum!(InboundHTLCResolution, (0, Resolved) => { (0, pending_htlc_status, required), }, @@ -308,6 +320,32 @@ impl InboundHTLCState { } } +/// Information about the outbound hop for a forwarded HTLC. Useful for generating an accurate +/// [`Event::PaymentForwarded`] if we need to claim this HTLC post-restart. +/// +/// [`Event::PaymentForwarded`]: crate::events::Event::PaymentForwarded +#[derive(Debug, Copy, Clone)] +pub(super) struct OutboundHop { + /// The amount forwarded outbound. + pub(super) amt_msat: u64, + /// The outbound channel this HTLC was forwarded over. + pub(super) channel_id: ChannelId, + /// The next-hop recipient of this HTLC. + pub(super) node_id: PublicKey, + /// The outbound channel's funding outpoint. + pub(super) funding_txo: OutPoint, + /// The outbound channel's user channel ID. + pub(super) user_channel_id: u128, +} + +impl_ser_tlv_based!(OutboundHop, { + (0, amt_msat, required), + (2, channel_id, required), + (4, node_id, required), + (6, funding_txo, required), + (8, user_channel_id, required), +}); + /// A field of `InboundHTLCState::Committed` containing the HTLC's `update_add_htlc` message. If /// the HTLC is a forward and gets irrevocably committed to the outbound edge, we convert to /// `InboundUpdateAdd::Forwarded`, thus pruning the onion and not persisting it on every @@ -315,23 +353,22 @@ impl InboundHTLCState { /// /// Useful for reconstructing the pending HTLC set on startup. #[derive(Debug, Clone)] -pub(super) enum InboundUpdateAdd { +enum InboundUpdateAdd { /// The inbound committed HTLC's update_add_htlc message. WithOnion { update_add_htlc: msgs::UpdateAddHTLC }, /// This inbound HTLC is a forward that was irrevocably committed to the outbound edge, allowing /// its onion to be pruned and no longer persisted. + /// + /// Contains data that is useful if we need to fail or claim this HTLC backwards after a restart + /// and it's missing in the outbound edge. Forwarded { - /// Useful if we need to fail or claim this HTLC backwards after restart, if it's missing in the - /// outbound edge. - hop_data: HTLCPreviousHopData, - /// Useful if we need to claim this HTLC backwards after a restart and it's missing in the - /// outbound edge, to generate an accurate [`Event::PaymentForwarded`]. - /// - /// [`Event::PaymentForwarded`]: crate::events::Event::PaymentForwarded - outbound_amt_msat: u64, + incoming_packet_shared_secret: [u8; 32], + phantom_shared_secret: Option<[u8; 32]>, + trampoline_shared_secret: Option<[u8; 32]>, + blinded_failure: Option<BlindedFailure>, + outbound_hop: OutboundHop, }, - /// This HTLC was received pre-LDK 0.3, before we started persisting the onion for inbound - /// committed HTLCs. + /// This HTLC was received before we started persisting the onion for inbound committed HTLCs. Legacy, } @@ -341,8 +378,11 @@ impl_writeable_tlv_based_enum_upgradable!(InboundUpdateAdd, }, (2, Legacy) => {}, (4, Forwarded) => { - (0, hop_data, required), - (2, outbound_amt_msat, required), + (0, incoming_packet_shared_secret, required), + (2, outbound_hop, required), + (4, phantom_shared_secret, option), + (6, trampoline_shared_secret, option), + (8, blinded_failure, option), }, ); @@ -651,10 +691,9 @@ mod state_flags { pub const LOCAL_SHUTDOWN_SENT: u32 = 1 << 11; pub const SHUTDOWN_COMPLETE: u32 = 1 << 12; pub const WAITING_FOR_BATCH: u32 = 1 << 13; - pub const AWAITING_QUIESCENCE: u32 = 1 << 14; - pub const LOCAL_STFU_SENT: u32 = 1 << 15; - pub const REMOTE_STFU_SENT: u32 = 1 << 16; - pub const QUIESCENT: u32 = 1 << 17; + pub const LOCAL_STFU_SENT: u32 = 1 << 14; + pub const REMOTE_STFU_SENT: u32 = 1 << 15; + pub const QUIESCENT: u32 = 1 << 16; } define_state_flags!( @@ -721,13 +760,8 @@ define_state_flags!( implicit ACK, so instead we have to hold them away temporarily to be sent later.", AWAITING_REMOTE_REVOKE, state_flags::AWAITING_REMOTE_REVOKE, is_awaiting_remote_revoke, set_awaiting_remote_revoke, clear_awaiting_remote_revoke), - ("Indicates a local request has been made for the channel to become quiescent. Both nodes \ - must send `stfu` for the channel to become quiescent. This flag will be cleared and we \ - will no longer attempt quiescence if either node requests a shutdown.", - AWAITING_QUIESCENCE, state_flags::AWAITING_QUIESCENCE, - is_awaiting_quiescence, set_awaiting_quiescence, clear_awaiting_quiescence), ("Indicates we have sent a `stfu` message to the counterparty. This message can only be sent \ - if either `AWAITING_QUIESCENCE` or `REMOTE_STFU_SENT` is set. Shutdown requests are \ + if `REMOTE_STFU_SENT` is set, or a `QuiescentAction` is pending. Shutdown requests are \ rejected if this flag is set.", LOCAL_STFU_SENT, state_flags::LOCAL_STFU_SENT, is_local_stfu_sent, set_local_stfu_sent, clear_local_stfu_sent), @@ -922,12 +956,6 @@ impl ChannelState { clear_awaiting_remote_revoke, ChannelReady ); - impl_state_flag!( - is_awaiting_quiescence, - set_awaiting_quiescence, - clear_awaiting_quiescence, - ChannelReady - ); impl_state_flag!(is_local_stfu_sent, set_local_stfu_sent, clear_local_stfu_sent, ChannelReady); impl_state_flag!( is_remote_stfu_sent, @@ -965,8 +993,11 @@ pub const TOTAL_BITCOIN_SUPPLY_SATOSHIS: u64 = 21_000_000 * 1_0000_0000; /// implementations use this value for their dust limit today. pub const MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS: u64 = 546; +/// The maximum channel dust limit we will accept from our counterparty for non-anchor channels. +pub const MAX_LEGACY_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS; + /// The maximum channel dust limit we will accept from our counterparty. -pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = MAX_STD_OUTPUT_DUST_LIMIT_SATOSHIS; +pub const MAX_CHAN_DUST_LIMIT_SATOSHIS: u64 = 10_000; /// The dust limit is used for both the commitment transaction outputs as well as the closing /// transactions. For cooperative closing transactions, we require segwit outputs, though accept @@ -980,6 +1011,9 @@ pub const MIN_CHAN_DUST_LIMIT_SATOSHIS: u64 = 354; // Just a reasonable implementation-specific safe lower bound, higher than the dust limit. pub const MIN_THEIR_CHAN_RESERVE_SATOSHIS: u64 = 1000; +// Just a reasonable implementation-specific safe lower bound. +pub const MIN_CHANNEL_VALUE_SATOSHIS: u64 = 1000; + /// Used to return a simple Error back to ChannelManager. Will get converted to a /// msgs::ErrorAction::SendErrorMessage or msgs::ErrorAction::IgnoreError as appropriate with our /// channel_id in ChannelManager. @@ -1102,26 +1136,6 @@ pub enum AnnouncementSigsState { PeerReceived, } -/// An enum indicating whether the local or remote side offered a given HTLC. -enum HTLCInitiator { - LocalOffered, - #[allow(dead_code)] - RemoteOffered, -} - -/// Current counts of various HTLCs, useful for calculating current balances available exactly. -struct HTLCStats { - pending_outbound_htlcs: usize, - pending_inbound_htlcs_value_msat: u64, - pending_outbound_htlcs_value_msat: u64, - on_counterparty_tx_dust_exposure_msat: u64, - // If the counterparty sets a feerate on the channel in excess of our dust_exposure_limiting_feerate, - // this will be set to the dust exposure that would result from us adding an additional nondust outbound - // htlc on the counterparty's commitment transaction. - extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat: Option<u64>, - on_holder_tx_dust_exposure_msat: u64, -} - /// A struct gathering data on a commitment, either local or remote. struct CommitmentData<'a> { tx: CommitmentTransaction, @@ -1141,18 +1155,6 @@ pub(crate) struct CommitmentStats { pub remote_balance_before_fee_msat: u64, } -/// Used when calculating whether we or the remote can afford an additional HTLC. -struct HTLCCandidate { - amount_msat: u64, - origin: HTLCInitiator, -} - -impl HTLCCandidate { - fn new(amount_msat: u64, origin: HTLCInitiator) -> Self { - Self { amount_msat, origin } - } -} - /// A return value enum for get_update_fulfill_htlc. See UpdateFulfillCommitFetch variants for /// description enum UpdateFulfillFetch { @@ -1176,6 +1178,44 @@ pub enum UpdateFulfillCommitFetch { DuplicateClaim {}, } +/// Error returned when processing an invalid interactive-tx message from our counterparty. +pub(super) struct InteractiveTxMsgError { + /// The underlying error. + pub(super) err: ChannelError, + /// If a splice was in progress when processing the message, this contains the splice funding + /// information for emitting a `SpliceNegotiationFailed` event. + pub(super) splice_funding_failed: Option<SpliceFundingFailed>, + /// The event reason to use if this error causes a `SpliceNegotiationFailed` event. + pub(super) negotiation_failure_reason: Option<NegotiationFailureReason>, +} + +impl InteractiveTxMsgError { + fn new(err: ChannelError, splice_funding_failed: Option<SpliceFundingFailed>) -> Self { + Self { err, splice_funding_failed, negotiation_failure_reason: None } + } + + fn with_negotiation_failure_reason(mut self, reason: NegotiationFailureReason) -> Self { + self.negotiation_failure_reason = Some(reason); + self + } + + pub(super) fn into_parts( + self, + ) -> (ChannelError, Option<(SpliceFundingFailed, NegotiationFailureReason)>) { + let Self { err, splice_funding_failed, negotiation_failure_reason } = self; + let splice_failure = splice_funding_failed.map(|splice_funding_failed| { + let reason = + negotiation_failure_reason.unwrap_or_else(|| Self::reason_from_channel_error(&err)); + (splice_funding_failed, reason) + }); + (err, splice_failure) + } + + fn reason_from_channel_error(err: &ChannelError) -> NegotiationFailureReason { + NegotiationFailureReason::NegotiationError { msg: format!("{:?}", err) } + } +} + /// The return value of `monitor_updating_restored` pub(super) struct MonitorRestoreUpdates { pub raa: Option<msgs::RevokeAndACK>, @@ -1192,11 +1232,14 @@ pub(super) struct MonitorRestoreUpdates { pub channel_ready: Option<msgs::ChannelReady>, pub channel_ready_order: ChannelReadyOrder, pub announcement_sigs: Option<msgs::AnnouncementSignatures>, - pub tx_signatures: Option<msgs::TxSignatures>, + pub funding_tx_signed: Option<FundingTxSigned>, /// The sources of outbound HTLCs that were forwarded and irrevocably committed on this channel /// (the outbound edge), along with their outbound amounts. Useful to store in the inbound HTLC /// to ensure it gets resolved. pub committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, + /// Whether the restoration changed serialized channel state that needs ChannelManager + /// persistence. + pub requires_channel_manager_persistence: bool, } /// The return value of `signer_maybe_unblocked` @@ -1207,8 +1250,7 @@ pub(super) struct SignerResumeUpdates { pub accept_channel: Option<msgs::AcceptChannel>, pub funding_created: Option<msgs::FundingCreated>, pub funding_signed: Option<msgs::FundingSigned>, - pub funding_commit_sig: Option<msgs::CommitmentSigned>, - pub tx_signatures: Option<msgs::TxSignatures>, + pub funding_tx_signed: Option<FundingTxSigned>, pub channel_ready: Option<msgs::ChannelReady>, pub order: RAACommitmentOrder, pub closing_signed: Option<msgs::ClosingSigned>, @@ -1225,8 +1267,9 @@ pub(super) struct ReestablishResponses { pub commitment_order: RAACommitmentOrder, pub announcement_sigs: Option<msgs::AnnouncementSignatures>, pub shutdown_msg: Option<msgs::Shutdown>, - pub tx_signatures: Option<msgs::TxSignatures>, + pub tx_signatures: Option<(TxSignaturesOrder, msgs::TxSignatures)>, pub tx_abort: Option<msgs::TxAbort>, + pub splice_locked: Option<msgs::SpliceLocked>, pub inferred_splice_locked: Option<msgs::SpliceLocked>, } @@ -1257,7 +1300,7 @@ pub(crate) struct ShutdownResult { pub(crate) channel_funding_txo: Option<OutPoint>, pub(crate) last_local_balance_msat: u64, /// If a splice was in progress when the channel was shut down, this contains - /// the splice funding information for emitting a SpliceFailed event. + /// the splice funding information for emitting a SpliceNegotiationFailed event. pub(crate) splice_funding_failed: Option<SpliceFundingFailed>, } @@ -1265,7 +1308,7 @@ pub(crate) struct ShutdownResult { pub(crate) struct DisconnectResult { pub(crate) is_resumable: bool, /// If a splice was in progress when the channel was shut down, this contains - /// the splice funding information for emitting a SpliceFailed event. + /// the splice funding information for emitting a SpliceNegotiationFailed event. pub(crate) splice_funding_failed: Option<SpliceFundingFailed>, } @@ -1289,14 +1332,14 @@ struct HolderCommitmentPoint { impl HolderCommitmentPoint { #[rustfmt::skip] - pub fn new<SP: SignerProvider>(signer: &ChannelSignerType<SP>, secp_ctx: &Secp256k1<secp256k1::All>) -> Option<Self> { + pub fn new<S: ChannelSigner>(signer: &S, secp_ctx: &Secp256k1<secp256k1::All>) -> Option<Self> { Some(HolderCommitmentPoint { next_transaction_number: INITIAL_COMMITMENT_NUMBER, previous_revoked_point: None, last_revoked_point: None, current_point: None, - next_point: signer.as_ref().get_per_commitment_point(INITIAL_COMMITMENT_NUMBER, secp_ctx).ok()?, - pending_next_point: signer.as_ref().get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 1, secp_ctx).ok(), + next_point: signer.get_per_commitment_point(INITIAL_COMMITMENT_NUMBER, secp_ctx).ok()?, + pending_next_point: signer.get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 1, secp_ctx).ok(), }) } @@ -1330,13 +1373,12 @@ impl HolderCommitmentPoint { /// If we are pending advancing the next commitment point, this method tries asking the signer /// again. - pub fn try_resolve_pending<SP: SignerProvider, L: Logger>( - &mut self, signer: &ChannelSignerType<SP>, secp_ctx: &Secp256k1<secp256k1::All>, logger: &L, + pub fn try_resolve_pending<S: ChannelSigner, L: Logger>( + &mut self, signer: &S, secp_ctx: &Secp256k1<secp256k1::All>, logger: &L, ) { if !self.can_advance() { - let pending_next_point = signer - .as_ref() - .get_per_commitment_point(self.next_transaction_number - 1, secp_ctx); + let pending_next_point = + signer.get_per_commitment_point(self.next_transaction_number - 1, secp_ctx); if let Ok(point) = pending_next_point { log_trace!( logger, @@ -1364,8 +1406,8 @@ impl HolderCommitmentPoint { /// /// If our signer is ready to provide the next commitment point, the next call to `advance` will /// succeed. - pub fn advance<SP: SignerProvider, L: Logger>( - &mut self, signer: &ChannelSignerType<SP>, secp_ctx: &Secp256k1<secp256k1::All>, logger: &L, + pub fn advance<S: ChannelSigner, L: Logger>( + &mut self, signer: &S, secp_ctx: &Secp256k1<secp256k1::All>, logger: &L, ) -> Result<(), ()> { if let Some(next_point) = self.pending_next_point { *self = Self { @@ -1398,7 +1440,7 @@ impl HolderCommitmentPoint { #[cfg(any(fuzzing, test, feature = "_test_utils"))] pub const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2; #[cfg(not(any(fuzzing, test, feature = "_test_utils")))] -const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2; +pub(crate) const FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE: u64 = 2; /// If we fail to see a funding transaction confirmed on-chain within this many blocks after the /// channel creation on an inbound channel, we simply force-close and move on. @@ -1476,7 +1518,7 @@ struct PendingChannelMonitorUpdate { update: ChannelMonitorUpdate, } -impl_writeable_tlv_based!(PendingChannelMonitorUpdate, { +impl_ser_tlv_based!(PendingChannelMonitorUpdate, { (0, update, required), }); @@ -1642,11 +1684,11 @@ where #[rustfmt::skip] pub fn signer_maybe_unblocked<L: Logger, CBP>( - &mut self, chain_hash: ChainHash, logger: &L, path_for_release_htlc: CBP + &mut self, chain_hash: ChainHash, best_block_height: u32, logger: &L, path_for_release_htlc: CBP ) -> Result<Option<SignerResumeUpdates>, ChannelError> where CBP: Fn(u64) -> BlindedMessagePath { match &mut self.phase { ChannelPhase::Undefined => unreachable!(), - ChannelPhase::Funded(chan) => chan.signer_maybe_unblocked(logger, path_for_release_htlc).map(|r| Some(r)), + ChannelPhase::Funded(chan) => chan.signer_maybe_unblocked(best_block_height, logger, path_for_release_htlc).map(|r| Some(r)), ChannelPhase::UnfundedOutboundV1(chan) => { let (open_channel, funding_created) = chan.signer_maybe_unblocked(chain_hash, logger); Ok(Some(SignerResumeUpdates { @@ -1656,8 +1698,7 @@ where accept_channel: None, funding_created, funding_signed: None, - funding_commit_sig: None, - tx_signatures: None, + funding_tx_signed: None, channel_ready: None, order: chan.context.resend_order.clone(), closing_signed: None, @@ -1674,8 +1715,7 @@ where accept_channel, funding_created: None, funding_signed: None, - funding_commit_sig: None, - tx_signatures: None, + funding_tx_signed: None, channel_ready: None, order: chan.context.resend_order.clone(), closing_signed: None, @@ -1710,13 +1750,9 @@ where let splice_funding_failed = if let ChannelPhase::Funded(chan) = &mut self.phase { // Reset any quiescence-related state as it is implicitly terminated once disconnected. if matches!(chan.context.channel_state, ChannelState::ChannelReady(_)) { - if chan.quiescent_action.is_some() { - // If we were trying to get quiescent, try again after reconnection. - chan.context.channel_state.set_awaiting_quiescence(); - } chan.context.channel_state.clear_local_stfu_sent(); chan.context.channel_state.clear_remote_stfu_sent(); - if chan.should_reset_pending_splice_state(false) { + if chan.should_reset_pending_splice_state(true) { // If there was a pending splice negotiation that failed due to disconnecting, we // also take the opportunity to clean up our state. let splice_funding_failed = chan.reset_pending_splice_state(); @@ -1726,7 +1762,10 @@ where // We shouldn't be quiescent anymore upon reconnecting if: // - We were in quiescence but a splice/RBF was never negotiated or // - We were in quiescence but the splice negotiation failed due to disconnecting - chan.context.channel_state.clear_quiescent(); + // + // NOTE: While `exit_quiescence` clears the disconnect timer, it should already + // have been cleared by `remove_uncommitted_htlcs_and_mark_paused`. + chan.exit_quiescence(); None } else { None @@ -1818,7 +1857,7 @@ where fn fail_interactive_tx_negotiation<L: Logger>( &mut self, reason: AbortReason, logger: &L, - ) -> (ChannelError, Option<SpliceFundingFailed>) { + ) -> InteractiveTxMsgError { let logger = WithChannelContext::from(logger, &self.context(), None); log_info!(logger, "Failed interactive transaction negotiation: {reason}"); @@ -1830,7 +1869,7 @@ where None }, ChannelPhase::Funded(funded_channel) => { - if funded_channel.should_reset_pending_splice_state(false) { + if funded_channel.should_reset_pending_splice_state(true) { funded_channel.reset_pending_splice_state() } else { debug_assert!(false, "We should never fail an interactive funding negotiation once we're exchanging tx_signatures"); @@ -1839,17 +1878,17 @@ where }, }; - (ChannelError::Abort(reason), splice_funding_failed) + InteractiveTxMsgError::new(ChannelError::Abort(reason), splice_funding_failed) } pub fn tx_add_input<L: Logger>( &mut self, msg: &msgs::TxAddInput, logger: &L, - ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)> { + ) -> Result<InteractiveTxMessageSend, InteractiveTxMsgError> { match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_add_input(msg) .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)), - None => Err(( + None => Err(InteractiveTxMsgError::new( ChannelError::WarnAndDisconnect( "Received unexpected interactive transaction negotiation message".to_owned(), ), @@ -1860,12 +1899,12 @@ where pub fn tx_add_output<L: Logger>( &mut self, msg: &msgs::TxAddOutput, logger: &L, - ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)> { + ) -> Result<InteractiveTxMessageSend, InteractiveTxMsgError> { match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_add_output(msg) .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)), - None => Err(( + None => Err(InteractiveTxMsgError::new( ChannelError::WarnAndDisconnect( "Received unexpected interactive transaction negotiation message".to_owned(), ), @@ -1876,12 +1915,12 @@ where pub fn tx_remove_input<L: Logger>( &mut self, msg: &msgs::TxRemoveInput, logger: &L, - ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)> { + ) -> Result<InteractiveTxMessageSend, InteractiveTxMsgError> { match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_remove_input(msg) .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)), - None => Err(( + None => Err(InteractiveTxMsgError::new( ChannelError::WarnAndDisconnect( "Received unexpected interactive transaction negotiation message".to_owned(), ), @@ -1892,12 +1931,12 @@ where pub fn tx_remove_output<L: Logger>( &mut self, msg: &msgs::TxRemoveOutput, logger: &L, - ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)> { + ) -> Result<InteractiveTxMessageSend, InteractiveTxMsgError> { match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_remove_output(msg) .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger)), - None => Err(( + None => Err(InteractiveTxMsgError::new( ChannelError::WarnAndDisconnect( "Received unexpected interactive transaction negotiation message".to_owned(), ), @@ -1908,14 +1947,17 @@ where pub fn tx_complete<F: FeeEstimator, L: Logger>( &mut self, msg: &msgs::TxComplete, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L, - ) -> Result<TxCompleteResult, (ChannelError, Option<SpliceFundingFailed>)> { + ) -> Result<TxCompleteResult, InteractiveTxMsgError> { let tx_complete_action = match self.interactive_tx_constructor_mut() { Some(interactive_tx_constructor) => interactive_tx_constructor .handle_tx_complete(msg) .map_err(|reason| self.fail_interactive_tx_negotiation(reason, logger))?, None => { let err = "Received unexpected interactive transaction negotiation message"; - return Err((ChannelError::WarnAndDisconnect(err.to_owned()), None)); + return Err(InteractiveTxMsgError::new( + ChannelError::WarnAndDisconnect(err.to_owned()), + None, + )); }, }; @@ -2010,7 +2052,7 @@ where "Received tx_abort while awaiting tx_signatures exchange".to_owned(), )); } - if funded_channel.should_reset_pending_splice_state(true) { + if funded_channel.should_reset_pending_splice_state(false) { let has_funding_negotiation = funded_channel .pending_splice .as_ref() @@ -2029,9 +2071,12 @@ where let tx_abort = should_ack.then(|| { let logger = WithChannelContext::from(logger, &self.context(), None); - let reason = - types::string::UntrustedString(String::from_utf8_lossy(&msg.data).to_string()); - log_info!(logger, "Counterparty failed interactive transaction negotiation: {reason}"); + let reason = String::from_utf8_lossy(&msg.data); + log_info!( + logger, + "Counterparty failed interactive transaction negotiation: {}", + log_msg!(reason) + ); msgs::TxAbort { channel_id: msg.channel_id, data: "Acknowledged tx_abort".to_string().into_bytes(), @@ -2043,7 +2088,7 @@ where #[rustfmt::skip] pub fn funding_signed<L: Logger>( - &mut self, msg: &msgs::FundingSigned, best_block: BestBlock, signer_provider: &SP, logger: &L + &mut self, msg: &msgs::FundingSigned, best_block: BlockLocator, signer_provider: &SP, logger: &L ) -> Result<(&mut FundedChannel<SP>, ChannelMonitor<SP::EcdsaSigner>), ChannelError> { let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); let result = if let ChannelPhase::UnfundedOutboundV1(chan) = phase { @@ -2099,6 +2144,7 @@ where let funding_negotiation = pending_splice.funding_negotiation.take(); if let Some(FundingNegotiation::ConstructingTransaction { mut funding, + funding_feerate_sat_per_1000_weight, interactive_tx_constructor, }) = funding_negotiation { @@ -2109,6 +2155,7 @@ where Some(FundingNegotiation::AwaitingSignatures { is_initiator, funding, + funding_feerate_sat_per_1000_weight, initial_commitment_signed_from_counterparty: None, }); interactive_tx_constructor @@ -2158,9 +2205,6 @@ where }, }; - let channel_id = context.channel_id; - let counterparty_node_id = context.counterparty_node_id; - let signing_session = if let Some(signing_session) = context.interactive_tx_signing_session.as_mut() { @@ -2175,9 +2219,7 @@ where .unwrap_or(false)); } - if signing_session.holder_tx_signatures().is_some() { - // Our `tx_signatures` either should've been the first time we processed them, - // or we're waiting for our counterparty to send theirs first. + if signing_session.has_holder_witnesses() { return Ok(FundingTxSigned { commitment_signed: None, counterparty_initial_commitment_signed_result: None, @@ -2206,40 +2248,42 @@ where return Err(APIError::APIMisuseError { err }); }; - let tx = signing_session.unsigned_tx().tx(); - if funding_txid_signed != tx.compute_txid() { - return Err(APIError::APIMisuseError { - err: "Transaction was malleated prior to signing".to_owned(), - }); - } + let (mut tx_signatures, mut funding_tx) = signing_session + .provide_holder_witnesses( + context.channel_id, + funding_txid_signed, + witnesses, + &context.secp_ctx, + ) + .map_err(|err| APIError::APIMisuseError { err })?; - let shared_input_signature = - if let Some(splice_input_index) = signing_session.unsigned_tx().shared_input_index() { - let sig = match &context.holder_signer { - ChannelSignerType::Ecdsa(signer) => signer.sign_splice_shared_input( - &funding.channel_transaction_parameters, - tx, - splice_input_index as usize, - &context.secp_ctx, - ), - #[cfg(taproot)] - ChannelSignerType::Taproot(_) => todo!(), - }; - Some(sig) + debug_assert_eq!( + pending_splice.is_some(), + signing_session.unsigned_tx().shared_input_index().is_some() + ); + if let Some(splice_input_index) = signing_session.unsigned_tx().shared_input_index() { + let sig = context + .holder_signer + .sign_splice_shared_input( + &funding.channel_transaction_parameters, + signing_session.unsigned_tx().tx(), + splice_input_index as usize, + &context.secp_ctx, + ) + .ok(); + if let Some(sig) = sig { + (tx_signatures, funding_tx) = signing_session + .provide_holder_shared_input_signature(sig) + .map_err(|err| APIError::APIMisuseError { err })?; } else { - None - }; - debug_assert_eq!(pending_splice.is_some(), shared_input_signature.is_some()); - - let tx_signatures = msgs::TxSignatures { - channel_id: context.channel_id, - tx_hash: funding_txid_signed, - witnesses, - shared_input_signature, - }; - let (tx_signatures, funding_tx) = signing_session - .provide_holder_witnesses(tx_signatures, &context.secp_ctx) - .map_err(|err| APIError::APIMisuseError { err })?; + log_debug!( + logger, + "Splice shared input signature not available, waiting on async signer" + ); + debug_assert!(tx_signatures.is_none()); + debug_assert!(funding_tx.is_none()); + } + } let logger = WithChannelContext::from(logger, &context, None); if tx_signatures.is_some() { @@ -2256,33 +2300,18 @@ where .unwrap_or(funding); let commitment_signed = context.get_initial_commitment_signed_v2(funding, &&logger); - // For zero conf channels, we don't expect the funding transaction to be ready for broadcast - // yet as, according to the spec, our counterparty shouldn't have sent their `tx_signatures` - // without us having sent our initial commitment signed to them first. However, in the event - // they do, we choose to handle it anyway. Note that because of this behavior not being - // spec-compliant, we're not able to test this without custom logic. - let (splice_negotiated, splice_locked) = if let Some(funding_tx) = funding_tx.clone() { - debug_assert!(tx_signatures.is_some()); - let funded_channel = self.as_funded_mut().expect( - "Funding transactions ready for broadcast can only exist for funded channels", - ); - funded_channel.on_tx_signatures_exchange(funding_tx, best_block_height, &logger) - } else { - (None, None) + let mut funding_tx_signed = FundingTxSigned { + commitment_signed, + counterparty_initial_commitment_signed_result: None, + tx_signatures, + funding_tx: None, + splice_negotiated: None, + splice_locked: None, }; - let funding_tx = funding_tx.map(|tx| { - let tx_type = if splice_negotiated.is_some() { - TransactionType::Splice { counterparty_node_id, channel_id } - } else { - TransactionType::Funding { channels: vec![(counterparty_node_id, channel_id)] } - }; - (tx, tx_type) - }); - // If we have a pending splice with a buffered initial commitment signed from our // counterparty, process it now that we have provided our signatures. - let counterparty_initial_commitment_signed_result = + funding_tx_signed.counterparty_initial_commitment_signed_result = self.as_funded_mut().and_then(|funded_channel| { funded_channel .pending_splice @@ -2308,14 +2337,25 @@ where }) }); - Ok(FundingTxSigned { - commitment_signed, - counterparty_initial_commitment_signed_result, - tx_signatures, - funding_tx, - splice_negotiated, - splice_locked, - }) + // For zero conf channels, we don't expect the funding transaction to be ready for broadcast + // yet as, according to the spec, our counterparty shouldn't have sent their `tx_signatures` + // without us having sent our initial commitment signed to them first. However, in the event + // they do, we choose to handle it anyway. Note that because of this behavior not being + // spec-compliant, we're not able to test this without custom logic. + if let Some(funding_tx) = funding_tx { + debug_assert!(funding_tx_signed.tx_signatures.is_some()); + let funded_channel = self.as_funded_mut().expect( + "Funding transactions ready for broadcast can only exist for funded channels", + ); + funded_channel.on_tx_signatures_exchange( + &mut funding_tx_signed, + funding_tx, + best_block_height, + &logger, + ) + }; + + Ok(funding_tx_signed) } pub fn force_shutdown(&mut self, closure_reason: ClosureReason) -> ShutdownResult { @@ -2325,7 +2365,7 @@ where #[rustfmt::skip] pub fn commitment_signed<F: FeeEstimator, L: Logger>( - &mut self, msg: &msgs::CommitmentSigned, best_block: BestBlock, signer_provider: &SP, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L + &mut self, msg: &msgs::CommitmentSigned, best_block: BlockLocator, signer_provider: &SP, fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L ) -> Result<(Option<ChannelMonitor<SP::EcdsaSigner>>, Option<ChannelMonitorUpdate>), ChannelError> { let phase = core::mem::replace(&mut self.phase, ChannelPhase::Undefined); match phase { @@ -2375,18 +2415,17 @@ where // which must always come after the initial commitment signed is sent. .unwrap_or(true); let res = if has_negotiated_pending_splice && !session_received_commitment_signed { - let has_holder_tx_signatures = funded_channel + let has_holder_witnesses = funded_channel .context .interactive_tx_signing_session .as_ref() - .map(|session| session.holder_tx_signatures().is_some()) + .map(|session| session.has_holder_witnesses()) .unwrap_or(false); // We delay processing this until the user manually approves the splice via - // [`Channel::funding_transaction_signed`], as otherwise, there would be a - // [`ChannelMonitorUpdateStep::RenegotiatedFunding`] committed that we would - // need to undo if they no longer wish to proceed. - if has_holder_tx_signatures { + // [`Channel::funding_transaction_signed`], as otherwise, it would prevent the + // user from canceling their contribution if they no longer wish to proceed. + if has_holder_witnesses { funded_channel .splice_initial_commitment_signed(msg, fee_estimator, logger) .map(|monitor_update_opt| (None, monitor_update_opt)) @@ -2395,6 +2434,7 @@ where .expect("We have a pending splice negotiated"); let funding_negotiation = pending_splice.funding_negotiation.as_mut() .expect("We have a pending splice negotiated"); + log_debug!(logger, "Stashing counterparty initial commitment_signed to process after funding_transaction_signed"); if let FundingNegotiation::AwaitingSignatures { ref mut initial_commitment_signed_from_counterparty, .. } = funding_negotiation { @@ -2418,13 +2458,13 @@ where } } - /// Get the available balances, see [`AvailableBalances`]'s fields for more info. - /// Doesn't bother handling the - /// if-we-removed-it-already-but-haven't-fully-resolved-they-can-still-send-an-inbound-HTLC - /// corner case properly. + /// Gets the available balances, see [`AvailableBalances`]'s fields for more info. + /// + /// Returns `Err` if some party cannot currently pay for the HTLCs outbound from said party, and the anchors and + /// transaction fee if they are the funder. pub fn get_available_balances<F: FeeEstimator>( &self, fee_estimator: &LowerBoundedFeeEstimator<F>, - ) -> AvailableBalances { + ) -> Result<AvailableBalances, ()> { match &self.phase { ChannelPhase::Undefined => unreachable!(), ChannelPhase::Funded(chan) => chan.get_available_balances(fee_estimator), @@ -2520,6 +2560,9 @@ pub(super) struct FundingScope { value_to_self_msat: u64, // Excluding all pending_htlcs, fees, and anchor outputs /// minimum channel reserve for self to maintain - set by them. + #[cfg(any(test, feature = "_externalize_tests"))] + pub(super) counterparty_selected_channel_reserve_satoshis: Option<u64>, + #[cfg(not(any(test, feature = "_externalize_tests")))] counterparty_selected_channel_reserve_satoshis: Option<u64>, #[cfg(any(test, feature = "_externalize_tests"))] @@ -2529,10 +2572,10 @@ pub(super) struct FundingScope { #[cfg(debug_assertions)] /// Max to_local and to_remote outputs in a locally-generated commitment transaction - holder_max_commitment_tx_output: Mutex<(u64, u64)>, + holder_prev_commitment_tx_balance: Mutex<(u64, u64)>, #[cfg(debug_assertions)] /// Max to_local and to_remote outputs in a remote-generated commitment transaction - counterparty_max_commitment_tx_output: Mutex<(u64, u64)>, + counterparty_prev_commitment_tx_balance: Mutex<(u64, u64)>, // We save these values so we can make sure validation of channel updates properly predicts // what the next commitment transaction fee will be, by comparing the cached values to the @@ -2557,22 +2600,17 @@ pub(super) struct FundingScope { minimum_depth_override: Option<u32>, } -impl Writeable for FundingScope { - fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { - write_tlv_fields!(writer, { - (1, self.value_to_self_msat, required), - (3, self.counterparty_selected_channel_reserve_satoshis, option), - (5, self.holder_selected_channel_reserve_satoshis, required), - (7, self.channel_transaction_parameters, (required: ReadableArgs, None)), - (9, self.funding_transaction, option), - (11, self.funding_tx_confirmed_in, option), - (13, self.funding_tx_confirmation_height, required), - (15, self.short_channel_id, option), - (17, self.minimum_depth_override, option), - }); - Ok(()) - } -} +impl_writeable_tlv_based!(FundingScope, self, { + (1, self.value_to_self_msat, required), + (3, self.counterparty_selected_channel_reserve_satoshis, option), + (5, self.holder_selected_channel_reserve_satoshis, required), + (7, self.channel_transaction_parameters, (required: ReadableArgs, None)), + (9, self.funding_transaction, option), + (11, self.funding_tx_confirmed_in, option), + (13, self.funding_tx_confirmation_height, required), + (15, self.short_channel_id, option), + (17, self.minimum_depth_override, option), +}); impl Readable for FundingScope { #[rustfmt::skip] @@ -2604,9 +2642,9 @@ impl Readable for FundingScope { counterparty_selected_channel_reserve_satoshis, holder_selected_channel_reserve_satoshis: holder_selected_channel_reserve_satoshis.0.unwrap(), #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((0, 0)), + holder_prev_commitment_tx_balance: Mutex::new((0, 0)), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((0, 0)), + counterparty_prev_commitment_tx_balance: Mutex::new((0, 0)), channel_transaction_parameters: channel_transaction_parameters.0.unwrap(), funding_transaction, funding_tx_confirmed_in, @@ -2657,6 +2695,20 @@ impl FundingScope { self.channel_transaction_parameters.funding_outpoint } + /// Gets the funding output for this channel, if available. + /// + /// When a channel is spliced, this continues to refer to the original funding output (which + /// was spent by the splice transaction) until the splice transaction reaches sufficient + /// confirmations to be locked (and we exchange `splice_locked` messages with our peer). + pub fn get_funding_output(&self) -> Option<TxOut> { + self.channel_transaction_parameters.make_funding_redeemscript_opt().map(|redeem_script| { + TxOut { + value: Amount::from_sat(self.get_value_satoshis()), + script_pubkey: redeem_script.to_p2wsh(), + } + }) + } + fn get_funding_txid(&self) -> Option<Txid> { self.channel_transaction_parameters.funding_outpoint.map(|txo| txo.txid) } @@ -2729,21 +2781,68 @@ impl FundingScope { fn for_splice<SP: SignerProvider>( prev_funding: &Self, context: &ChannelContext<SP>, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, counterparty_funding_pubkey: PublicKey, - our_new_holder_keys: ChannelPublicKeys, - ) -> Self { - debug_assert!(our_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); - debug_assert!(their_funding_contribution.unsigned_abs() <= Amount::MAX_MONEY); + our_new_holder_keys: ChannelPublicKeys, min_funding_satoshis: u64, + ) -> Result<Self, String> { + if our_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { + return Err(format!( + "Our {} contribution exceeds the total bitcoin supply", + our_funding_contribution, + )); + } - let post_channel_value = prev_funding.compute_post_splice_value( - our_funding_contribution.to_sat(), - their_funding_contribution.to_sat(), - ); + if their_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { + return Err(format!( + "Their {} contribution exceeds the total bitcoin supply", + their_funding_contribution, + )); + } + + let channel_value_satoshis = prev_funding.get_value_satoshis(); + let value_to_self_satoshis = prev_funding.get_value_to_self_msat() / 1000; + let value_to_counterparty_satoshis = channel_value_satoshis + .checked_sub(value_to_self_satoshis) + .expect("value_to_self is greater than channel value"); + let our_funding_contribution_sat = our_funding_contribution.to_sat(); + let their_funding_contribution_sat = their_funding_contribution.to_sat(); let post_value_to_self_msat = prev_funding - .value_to_self_msat - .checked_add_signed(our_funding_contribution.to_sat() * 1000); - debug_assert!(post_value_to_self_msat.is_some()); - let post_value_to_self_msat = post_value_to_self_msat.unwrap(); + .get_value_to_self_msat() + .checked_add_signed(our_funding_contribution_sat * 1000) + .ok_or(format!( + "Our contribution candidate {our_funding_contribution_sat}sat is \ + greater than our total balance in the channel {value_to_self_satoshis}sat" + ))?; + + value_to_counterparty_satoshis.checked_add_signed(their_funding_contribution_sat).ok_or( + format!( + "Their contribution candidate {their_funding_contribution_sat}sat is \ + greater than their total balance in the channel {value_to_counterparty_satoshis}sat" + ), + )?; + + let post_channel_value_sat = prev_funding + .get_value_satoshis() + .checked_add_signed(our_funding_contribution.to_sat()) + .and_then(|v| v.checked_add_signed(their_funding_contribution.to_sat())) + .ok_or(format!( + "The sum of contributions {our_funding_contribution} and \ + {their_funding_contribution} is greater than the channel's value" + ))?; + if post_channel_value_sat < MIN_CHANNEL_VALUE_SATOSHIS { + return Err(format!( + "Spliced channel value must be at least 1000 satoshis. It would be \ + {post_channel_value_sat}" + )); + } + if post_channel_value_sat < min_funding_satoshis + && their_funding_contribution.is_negative() + && !prev_funding.is_outbound() + { + return Err(format!( + "Spliced channel value {post_channel_value_sat} would be smaller \ + than the configured min_funding_satoshis {min_funding_satoshis}" + )); + } let channel_parameters = &prev_funding.channel_transaction_parameters; let mut post_channel_transaction_parameters = ChannelTransactionParameters { @@ -2755,7 +2854,7 @@ impl FundingScope { funding_outpoint: None, // filled later splice_parent_funding_txid: prev_funding.get_funding_txid(), channel_type_features: channel_parameters.channel_type_features.clone(), - channel_value_satoshis: post_channel_value, + channel_value_satoshis: post_channel_value_sat, }; post_channel_transaction_parameters .counterparty_parameters @@ -2765,29 +2864,60 @@ impl FundingScope { .funding_pubkey = counterparty_funding_pubkey; // New reserve values are based on the new channel value and are v2-specific - let counterparty_selected_channel_reserve_satoshis = - Some(get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS)); + let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( + post_channel_value_sat, + context.holder_dust_limit_satoshis, + prev_funding + .counterparty_selected_channel_reserve_satoshis + .expect("counterparty reserve is set") + == 0, + ) + .map_err(|()| { + format!( + "The post-splice channel value {post_channel_value_sat} is smaller \ + than our dust limit {}", + context.holder_dust_limit_satoshis + ) + })?; let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - post_channel_value, + post_channel_value_sat, context.counterparty_dust_limit_satoshis, - ); + prev_funding.holder_selected_channel_reserve_satoshis == 0, + ) + .map_err(|()| { + format!( + "The post-splice channel value {post_channel_value_sat} is smaller \ + than their dust limit {}", + context.counterparty_dust_limit_satoshis, + ) + })?; - Self { + Ok(Self { channel_transaction_parameters: post_channel_transaction_parameters, value_to_self_msat: post_value_to_self_msat, funding_transaction: None, - counterparty_selected_channel_reserve_satoshis, + counterparty_selected_channel_reserve_satoshis: Some( + counterparty_selected_channel_reserve_satoshis, + ), holder_selected_channel_reserve_satoshis, #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new(( - post_value_to_self_msat, - (post_channel_value * 1000).saturating_sub(post_value_to_self_msat), - )), + holder_prev_commitment_tx_balance: { + let prev = *prev_funding.holder_prev_commitment_tx_balance.lock().unwrap(); + let new_holder_balance_msat = + prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000); + let new_counterparty_balance_msat = + prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000); + Mutex::new((new_holder_balance_msat, new_counterparty_balance_msat)) + }, #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new(( - post_value_to_self_msat, - (post_channel_value * 1000).saturating_sub(post_value_to_self_msat), - )), + counterparty_prev_commitment_tx_balance: { + let prev = *prev_funding.counterparty_prev_commitment_tx_balance.lock().unwrap(); + let new_holder_balance_msat = + prev.0.saturating_add_signed(our_funding_contribution.to_sat() * 1000); + let new_counterparty_balance_msat = + prev.1.saturating_add_signed(their_funding_contribution.to_sat() * 1000); + Mutex::new((new_holder_balance_msat, new_counterparty_balance_msat)) + }, #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), #[cfg(any(test, fuzzing))] @@ -2796,16 +2926,7 @@ impl FundingScope { funding_tx_confirmed_in: None, minimum_depth_override: None, short_channel_id: None, - } - } - - /// Compute the post-splice channel value from each counterparty's contributions. - pub(super) fn compute_post_splice_value( - &self, our_funding_contribution: i64, their_funding_contribution: i64, - ) -> u64 { - self.get_value_satoshis().saturating_add_signed( - our_funding_contribution.saturating_add(their_funding_contribution), - ) + }) } /// Returns a `SharedOwnedInput` for using this `FundingScope` as the input to a new splice. @@ -2844,22 +2965,48 @@ impl FundingScope { struct PendingFunding { funding_negotiation: Option<FundingNegotiation>, + /// Our contribution to the funding negotiation round currently in progress, if we are + /// contributing to it. Set when the round starts, moved into the [`NegotiatedCandidate`] + /// when negotiation completes, and dropped in + /// [`FundedChannel::reset_pending_splice_state`] if the round is abandoned. + /// + /// When the counterparty initiates an RBF and a prior round included our contribution, this + /// is set to that contribution adjusted to the new feerate (or the RBF is rejected if the + /// adjustment fails, in which case no round starts). This ensures a splice we contributed to + /// never loses our contribution in subsequent rounds. + negotiation_contribution: Option<FundingContribution>, + /// Funding candidates that have been negotiated but have not reached enough confirmations /// by both counterparties to have exchanged `splice_locked` and be promoted. - negotiated_candidates: Vec<FundingScope>, + negotiated_candidates: Vec<NegotiatedCandidate>, /// The funding txid used in the `splice_locked` sent to the counterparty. sent_funding_txid: Option<Txid>, /// The funding txid used in the `splice_locked` received from the counterparty. received_funding_txid: Option<Txid>, + + /// The feerate used in the last successfully negotiated funding transaction. + /// Used for validating the minimum feerate increase rule on RBF attempts. + last_funding_feerate_sat_per_1000_weight: Option<u32>, +} + +/// A funding candidate that has been negotiated, together with our contribution, if any, to the +/// negotiation round that produced it. +#[derive(Debug)] +struct NegotiatedCandidate { + funding: FundingScope, + + /// Our contribution to the negotiation round that produced this candidate, or `None` if only + /// the counterparty contributed. Once a candidate includes our contribution, every later + /// candidate does as well: RBF rounds carry the contribution forward (possibly adjusted to a + /// new feerate) rather than dropping it, preserving the splice intention. + contribution: Option<FundingContribution>, } -impl_writeable_tlv_based!(PendingFunding, { - (1, funding_negotiation, upgradable_option), - (3, negotiated_candidates, required_vec), - (5, sent_funding_txid, option), - (7, received_funding_txid, option), +impl_ser_tlv_based!(NegotiatedCandidate, { + (1, funding, required), + (3, contribution, option), }); #[derive(Debug)] @@ -2870,10 +3017,12 @@ enum FundingNegotiation { }, ConstructingTransaction { funding: FundingScope, + funding_feerate_sat_per_1000_weight: u32, interactive_tx_constructor: InteractiveTxConstructor, }, AwaitingSignatures { funding: FundingScope, + funding_feerate_sat_per_1000_weight: u32, is_initiator: bool, /// The initial [`msgs::CommitmentSigned`] message received for the [`FundingScope`] above. /// We delay processing this until the user manually approves the splice via @@ -2893,11 +3042,131 @@ impl_writeable_tlv_based_enum_upgradable!(FundingNegotiation, (0, AwaitingSignatures) => { (1, funding, required), (3, is_initiator, required), + (5, funding_feerate_sat_per_1000_weight, (default_value, 0)), (_unused, initial_commitment_signed_from_counterparty, (static_value, None)), }, unread_variants: AwaitingAck, ConstructingTransaction ); +struct PendingFundingWriteable<'a> { + pending_funding: &'a PendingFunding, + reset_funding_negotiation: bool, +} + +impl Writeable for PendingFundingWriteable<'_> { + fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { + let funding_negotiation = if self.reset_funding_negotiation { + None + } else { + self.pending_funding.funding_negotiation.as_ref() + }; + debug_assert!( + funding_negotiation.is_none() + || matches!( + funding_negotiation, + Some(FundingNegotiation::AwaitingSignatures { .. }) + ) + ); + // The in-flight round's contribution is only written if its negotiation survives + // serialization round trips. It goes in an odd TLV that LDK 0.2 skips (0.2 never tracked + // contributions), so a single in-flight splice we contributed to stays loadable there. + let negotiation_contribution = funding_negotiation + .is_some() + .then(|| self.pending_funding.negotiation_contribution.as_ref()) + .flatten(); + let candidates = &self.pending_funding.negotiated_candidates; + debug_assert!( + self.pending_funding.contributions_form_suffix(), + "contributions must form a suffix of the negotiated candidates", + ); + // TLV 3 exposes only the first candidate's funding: the single-splice view LDK 0.2 + // understands. The authoritative candidate list -- each funding bundled with its + // contribution -- goes in the odd TLV 11, which current reads and 0.2 skips. A single + // non-contributory splice is fully captured by TLV 3 alone, so the bundle is then omitted. + // When a single splice does carry a contribution, 0.2 skips it (and operates the splice + // without it), so it need not block 0.2 from loading. + // + // The even TLV 14 is the only thing that makes 0.2 refuse, and it's written exactly when + // there is more than one negotiation round (RBF) -- the one thing 0.2 cannot operate. The + // odd contribution fields are safe despite being load-bearing for RBF: this gate makes 0.2 + // refuse the whole channel in that case, so no reader ever skips them when they matter. + let first_funding = Iterable(candidates.iter().take(1).map(|candidate| &candidate.funding)); + let any_contribution = candidates.iter().any(|candidate| candidate.contribution.is_some()); + let negotiated_candidates = + (candidates.len() > 1 || any_contribution).then(|| Iterable(candidates.iter())); + let is_rbf = candidates.len() + usize::from(funding_negotiation.is_some()) > 1; + let rbf_gate = is_rbf.then_some(()); + write_tlv_fields!(writer, { + (1, funding_negotiation, upgradable_option), + (3, first_funding, required), + (5, self.pending_funding.sent_funding_txid, option), + (7, self.pending_funding.received_funding_txid, option), + (9, self.pending_funding.last_funding_feerate_sat_per_1000_weight, option), + (11, negotiated_candidates, option), + (13, negotiation_contribution, option), + (14, rbf_gate, option), + }); + Ok(()) + } +} + +impl Readable for PendingFunding { + fn read<R: io::Read>(reader: &mut R) -> Result<Self, DecodeError> { + let mut funding_negotiation = None; + let mut legacy_negotiated_candidates: Option<Vec<FundingScope>> = None; + let mut sent_funding_txid = None; + let mut received_funding_txid = None; + let mut last_funding_feerate_sat_per_1000_weight = None; + let mut negotiated_candidates: Option<Vec<NegotiatedCandidate>> = None; + let mut negotiation_contribution: Option<FundingContribution> = None; + let mut rbf_gate: Option<()> = None; + + read_tlv_fields!(reader, { + (1, funding_negotiation, upgradable_option), + (3, legacy_negotiated_candidates, optional_vec), + (5, sent_funding_txid, option), + (7, received_funding_txid, option), + (9, last_funding_feerate_sat_per_1000_weight, option), + (11, negotiated_candidates, optional_vec), + (13, negotiation_contribution, option), + (14, rbf_gate, option), + }); + + // TLV 11 (the candidate list, each funding bundled with its contribution) is authoritative + // when present. It is omitted for a single non-contributory splice (TLV 3 holds its + // funding) and for data written by LDK 0.2 (which only ever wrote TLV 3 and tracked no + // contributions); in both cases the candidates carry no contribution. + let negotiated_candidates = negotiated_candidates.unwrap_or_else(|| { + legacy_negotiated_candidates + .unwrap_or_default() + .into_iter() + .map(|funding| NegotiatedCandidate { funding, contribution: None }) + .collect() + }); + // An in-flight contribution is only written alongside a surviving negotiation round, so a + // contribution without one is invalid. + if funding_negotiation.is_none() && negotiation_contribution.is_some() { + return Err(DecodeError::InvalidValue); + } + // TLV 14 (the RBF gate) is written exactly when there is more than one negotiation round, so + // pre-RBF readers (LDK 0.2) refuse an RBF they cannot operate. Current reconstructs RBF state + // from the candidate list, but a gate inconsistent with that state is invalid. + let is_rbf = negotiated_candidates.len() + usize::from(funding_negotiation.is_some()) > 1; + if rbf_gate.is_some() != is_rbf { + return Err(DecodeError::InvalidValue); + } + + Ok(PendingFunding { + funding_negotiation, + negotiation_contribution, + negotiated_candidates, + sent_funding_txid, + received_funding_txid, + last_funding_feerate_sat_per_1000_weight, + }) + } +} + impl FundingNegotiation { fn as_funding(&self) -> Option<&FundingScope> { match self { @@ -2907,6 +3176,21 @@ impl FundingNegotiation { } } + fn funding_feerate_sat_per_1000_weight(&self) -> u32 { + match self { + FundingNegotiation::AwaitingAck { context, .. } => { + context.funding_feerate_sat_per_1000_weight + }, + FundingNegotiation::ConstructingTransaction { + funding_feerate_sat_per_1000_weight, + .. + } => *funding_feerate_sat_per_1000_weight, + FundingNegotiation::AwaitingSignatures { + funding_feerate_sat_per_1000_weight, .. + } => *funding_feerate_sat_per_1000_weight, + } + } + fn is_initiator(&self) -> bool { match self { FundingNegotiation::AwaitingAck { context, .. } => context.is_initiator, @@ -2916,30 +3200,298 @@ impl FundingNegotiation { FundingNegotiation::AwaitingSignatures { is_initiator, .. } => *is_initiator, } } -} - -impl PendingFunding { - fn check_get_splice_locked<SP: SignerProvider>( - &mut self, context: &ChannelContext<SP>, confirmed_funding_index: usize, height: u32, - ) -> Option<msgs::SpliceLocked> { - debug_assert!(confirmed_funding_index < self.negotiated_candidates.len()); - - let funding = &self.negotiated_candidates[confirmed_funding_index]; - if !context.check_funding_meets_minimum_depth(funding, height) { - return None; - } + fn for_initiator<SP: SignerProvider, ES: EntropySource>( + funding: FundingScope, context: &ChannelContext<SP>, + funding_negotiation_context: FundingNegotiationContext, entropy_source: &ES, + holder_node_id: &PublicKey, + ) -> (FundingNegotiation, Option<InteractiveTxMessageSend>) { + let funding_feerate_sat_per_1000_weight = + funding_negotiation_context.funding_feerate_sat_per_1000_weight; + let (interactive_tx_constructor, tx_msg_opt) = funding_negotiation_context + .into_interactive_tx_constructor( + context, + &funding, + entropy_source, + holder_node_id.clone(), + ); + debug_assert!(tx_msg_opt.is_some()); - let confirmed_funding_txid = match funding.get_funding_txid() { - Some(funding_txid) => funding_txid, - None => { - debug_assert!(false); - return None; + ( + FundingNegotiation::ConstructingTransaction { + funding, + funding_feerate_sat_per_1000_weight, + interactive_tx_constructor, }, + tx_msg_opt, + ) + } + + fn for_acceptor<SP: SignerProvider, ES: EntropySource>( + funding: FundingScope, context: &ChannelContext<SP>, entropy_source: &ES, + holder_node_id: &PublicKey, our_funding_contribution: SignedAmount, + prev_funding_input: SharedOwnedInput, locktime: u32, feerate_sat_per_1000_weight: u32, + our_funding_inputs: Vec<ConfirmedUtxo>, our_funding_outputs: Vec<TxOut>, + ) -> FundingNegotiation { + let funding_negotiation_context = FundingNegotiationContext { + is_initiator: false, + our_funding_contribution, + funding_tx_locktime: LockTime::from_consensus(locktime), + funding_feerate_sat_per_1000_weight: feerate_sat_per_1000_weight, + shared_funding_input: Some(prev_funding_input), + our_funding_inputs, + our_funding_outputs, }; - match self.sent_funding_txid { - Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => None, - _ => { + let (interactive_tx_constructor, first_message) = funding_negotiation_context + .into_interactive_tx_constructor( + context, + &funding, + entropy_source, + holder_node_id.clone(), + ); + debug_assert!(first_message.is_none()); + + FundingNegotiation::ConstructingTransaction { + funding, + funding_feerate_sat_per_1000_weight: feerate_sat_per_1000_weight, + interactive_tx_constructor, + } + } +} + +impl PendingFunding { + /// Whether our contributions form a suffix of the negotiated candidates: once a round includes + /// our contribution, every later round carries it forward (so the splice intention is never + /// lost). + fn contributions_form_suffix(&self) -> bool { + self.negotiated_candidates + .iter() + .skip_while(|candidate| candidate.contribution.is_none()) + .all(|candidate| candidate.contribution.is_some()) + } + + fn awaiting_ack_context( + &self, msg_name: &str, + ) -> Result<(&FundingNegotiationContext, &PublicKey), ChannelError> { + match &self.funding_negotiation { + Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key }) => { + Ok((context, new_holder_funding_key)) + }, + Some(FundingNegotiation::ConstructingTransaction { .. }) + | Some(FundingNegotiation::AwaitingSignatures { .. }) => Err(ChannelError::WarnAndDisconnect( + format!("Got unexpected {}; funding negotiation already in progress", msg_name,), + )), + None => Err(ChannelError::Ignore(format!( + "Got unexpected {}; no funding negotiation in progress", + msg_name, + ))), + } + } + + fn take_awaiting_ack_context( + &mut self, msg_name: &str, + ) -> Result<FundingNegotiationContext, ChannelError> { + match self.funding_negotiation.take() { + Some(FundingNegotiation::AwaitingAck { context, .. }) => Ok(context), + Some(other) => { + self.funding_negotiation = Some(other); + Err(ChannelError::WarnAndDisconnect(format!( + "Got unexpected {}; funding negotiation already in progress", + msg_name, + ))) + }, + None => Err(ChannelError::Ignore(format!( + "Got unexpected {}; no funding negotiation in progress", + msg_name, + ))), + } + } + + /// Returns the minimum feerate for RBF attempts given a previous feerate. + /// + /// The spec (tx_init_rbf) requires the new feerate to be >= the maximum of 25/24 of the + /// previous feerate and the previous feerate + 25 sat/kwu. The flat +25 sat/kwu increment + /// ensures BIP125's relay requirement of an absolute fee increase is satisfied at low feerates + /// where the multiplicative 25/24 rule alone would be insufficient. + fn min_rbf_feerate_above(prev_feerate: u32) -> FeeRate { + let flat_increment = (prev_feerate as u64).saturating_add(25); + let spec_increment = (prev_feerate as u64) * 25 / 24; + FeeRate::from_sat_per_kwu(cmp::max(flat_increment, spec_increment)) + } + + /// The minimum feerate a new contribution must pay to replace the pending splice via RBF, + /// derived from the most recent round's feerate: + /// - `last_funding_feerate_sat_per_1000_weight`: from a completed but unlocked negotiation + /// - the `funding_negotiation` feerate: from an in-progress negotiation + /// + /// Returns `None` when neither feerate is known. The feerate is only persisted by LDK 0.3+, + /// so its absence means the splice was last written by an older version (negotiated there, or + /// round-tripped 0.3 -> 0.2 -> 0.3), in which case the pending splice cannot be RBF'd. + fn min_rbf_feerate(&self) -> Option<FeeRate> { + self.last_funding_feerate_sat_per_1000_weight + .or_else(|| { + self.funding_negotiation.as_ref().map(|n| n.funding_feerate_sat_per_1000_weight()) + }) + .map(Self::min_rbf_feerate_above) + } + + /// After several RBF attempts, checks that the feerate is high enough to confirm. Returns + /// `true` if the feerate is sufficient or the threshold hasn't been reached. + /// + /// The spec requires: "MUST set a high enough feerate to ensure quick confirmation." + fn is_rbf_feerate_sufficient<F: FeeEstimator>( + &self, feerate_sat_per_kw: u32, fee_estimator: &LowerBoundedFeeEstimator<F>, + ) -> bool { + const MAX_LOW_FEERATE_RBF_ATTEMPTS: usize = 10; + if self.negotiated_candidates.len() <= MAX_LOW_FEERATE_RBF_ATTEMPTS { + return true; + } + let min_feerate = + fee_estimator.bounded_sat_per_1000_weight(ConfirmationTarget::NonAnchorChannelFee); + feerate_sat_per_kw >= min_feerate + } + + /// All stored contributions: those of the negotiated candidates followed by the in-flight + /// negotiation round's, if any. + fn contributions(&self) -> impl Iterator<Item = &FundingContribution> + '_ { + self.negotiated_candidates + .iter() + .filter_map(|candidate| candidate.contribution.as_ref()) + .chain(self.negotiation_contribution.as_ref()) + } + + fn contributed_inputs(&self) -> impl Iterator<Item = bitcoin::OutPoint> + '_ { + self.contributions().flat_map(|c| c.contributed_inputs()) + } + + fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.contributions().flat_map(|c| c.contributed_outputs()) + } + + fn prior_contributed_inputs(&self) -> impl Iterator<Item = bitcoin::OutPoint> + '_ { + self.negotiated_candidates + .iter() + .filter_map(|candidate| candidate.contribution.as_ref()) + .flat_map(|c| c.contributed_inputs()) + } + + fn prior_contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.negotiated_candidates + .iter() + .filter_map(|candidate| candidate.contribution.as_ref()) + .flat_map(|c| c.contributed_outputs()) + } + + /// Our most recent contribution across rounds, including any round still under negotiation. + fn latest_contribution(&self) -> Option<&FundingContribution> { + self.negotiation_contribution.as_ref().or_else(|| { + self.negotiated_candidates.last().and_then(|candidate| candidate.contribution.as_ref()) + }) + } + + fn to_details<SP: SignerProvider>( + &self, context: &ChannelContext<SP>, best_block_height: u32, + ) -> SpliceDetails { + let mut candidates: Vec<SpliceCandidateDetails> = self + .negotiated_candidates + .iter() + .map(|candidate| SpliceCandidateDetails { + contribution: candidate.contribution.clone(), + status: SpliceCandidateStatus::Negotiated { + txid: candidate + .funding + .get_funding_txid() + .expect("negotiated candidates should have a funding txid"), + new_channel_value_satoshis: candidate.funding.get_value_satoshis(), + }, + }) + .collect(); + + // The round currently under negotiation, if any, follows the negotiated candidates. + if let Some(funding_negotiation) = self.funding_negotiation.as_ref() { + let is_initiator = funding_negotiation.is_initiator(); + let funding_feerate_sat_per_1000_weight = + funding_negotiation.funding_feerate_sat_per_1000_weight(); + let status = match funding_negotiation { + FundingNegotiation::AwaitingAck { .. } => SpliceCandidateStatus::AwaitingAck { + is_initiator, + funding_feerate_sat_per_1000_weight, + }, + FundingNegotiation::ConstructingTransaction { funding, .. } => { + SpliceCandidateStatus::ConstructingTransaction { + is_initiator, + funding_feerate_sat_per_1000_weight, + new_channel_value_satoshis: funding.get_value_satoshis(), + } + }, + FundingNegotiation::AwaitingSignatures { funding, .. } => { + SpliceCandidateStatus::AwaitingSignatures { + is_initiator, + funding_feerate_sat_per_1000_weight, + new_channel_value_satoshis: funding.get_value_satoshis(), + txid: funding + .get_funding_txid() + .expect("a splice awaiting signatures should have a funding txid"), + } + }, + }; + candidates.push(SpliceCandidateDetails { + contribution: self.negotiation_contribution.clone(), + status, + }); + } + // At most one candidate can confirm, as they all double-spend the same input. A zero-conf + // splice is locked (we send `splice_locked`) before it has any confirmations, so also report + // a candidate we have locked even at zero confirmations. + let confirmed_candidate = self.negotiated_candidates.iter().find_map(|candidate| { + let confirmations = candidate.funding.get_funding_tx_confirmations(best_block_height); + let txid = candidate + .funding + .get_funding_txid() + .expect("negotiated candidates should have a funding txid"); + // The `splice_locked` we sent always refers to the confirmed candidate, as it is + // cleared if that candidate is ever unconfirmed by a reorg. + let splice_locked_sent = self.sent_funding_txid == Some(txid); + if confirmations == 0 && !splice_locked_sent { + return None; + } + Some(ConfirmedSpliceCandidate { + txid, + confirmations, + confirmations_required: context + .minimum_depth(&candidate.funding) + .expect("set for a ready channel"), + splice_locked_sent, + }) + }); + SpliceDetails { + candidates, + confirmed_candidate, + received_splice_locked_txid: self.received_funding_txid, + } + } + + fn check_get_splice_locked<SP: SignerProvider>( + &mut self, context: &ChannelContext<SP>, confirmed_funding_index: usize, height: u32, + ) -> Option<msgs::SpliceLocked> { + debug_assert!(confirmed_funding_index < self.negotiated_candidates.len()); + + let funding = &self.negotiated_candidates[confirmed_funding_index].funding; + if !context.check_funding_meets_minimum_depth(funding, height) { + return None; + } + + let confirmed_funding_txid = match funding.get_funding_txid() { + Some(funding_txid) => funding_txid, + None => { + debug_assert!(false); + return None; + }, + }; + + match self.sent_funding_txid { + Some(sent_funding_txid) if confirmed_funding_txid == sent_funding_txid => None, + _ => { let splice_locked = msgs::SpliceLocked { channel_id: context.channel_id(), splice_txid: confirmed_funding_txid, @@ -2952,55 +3504,27 @@ impl PendingFunding { } #[derive(Debug)] -pub(crate) struct SpliceInstructions { - adjusted_funding_contribution: SignedAmount, - our_funding_inputs: Vec<FundingTxInput>, - our_funding_outputs: Vec<TxOut>, - change_script: Option<ScriptBuf>, - funding_feerate_per_kw: u32, - locktime: u32, -} - -impl SpliceInstructions { - fn into_contributed_inputs_and_outputs(self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) { - ( - self.our_funding_inputs.into_iter().map(|input| input.utxo.outpoint).collect(), - self.our_funding_outputs, - ) - } +pub(crate) enum QuiescentAction { + Splice { + contribution: FundingContribution, + locktime: LockTime, + }, + #[cfg(any(test, fuzzing, feature = "_test_utils"))] + DoNothing, } -impl_writeable_tlv_based!(SpliceInstructions, { - (1, adjusted_funding_contribution, required), - (3, our_funding_inputs, required_vec), - (5, our_funding_outputs, required_vec), - (7, change_script, option), - (9, funding_feerate_per_kw, required), - (11, locktime, required), -}); - -#[derive(Debug)] -pub(crate) enum QuiescentAction { - Splice(SpliceInstructions), - #[cfg(any(test, fuzzing))] +pub(super) enum QuiescentError { DoNothing, + DiscardFunding { inputs: Vec<bitcoin::OutPoint>, outputs: Vec<bitcoin::ScriptBuf> }, + FailSplice(SpliceFundingFailed, NegotiationFailureReason), } pub(crate) enum StfuResponse { Stfu(msgs::Stfu), SpliceInit(msgs::SpliceInit), + TxInitRbf(msgs::TxInitRbf), } -#[cfg(any(test, fuzzing))] -impl_writeable_tlv_based_enum_upgradable!(QuiescentAction, - (0, DoNothing) => {}, - {1, Splice} => (), -); -#[cfg(not(any(test, fuzzing)))] -impl_writeable_tlv_based_enum_upgradable!(QuiescentAction,, - {1, Splice} => (), -); - /// Wrapper around a [`Transaction`] useful for caching the result of [`Transaction::compute_txid`]. struct ConfirmedTransaction<'a> { tx: &'a Transaction, @@ -3026,7 +3550,6 @@ impl<'a> From<&'a Transaction> for ConfirmedTransaction<'a> { } /// Contains everything about the channel including state, and various flags. -#[cfg_attr(test, derive(Debug))] pub(super) struct ChannelContext<SP: SignerProvider> { config: LegacyChannelConfig, @@ -3062,7 +3585,7 @@ pub(super) struct ChannelContext<SP: SignerProvider> { latest_monitor_update_id: u64, - holder_signer: ChannelSignerType<SP>, + holder_signer: SP::EcdsaSigner, shutdown_scriptpubkey: Option<ShutdownScript>, destination_script: ScriptBuf, @@ -3186,6 +3709,9 @@ pub(super) struct ChannelContext<SP: SignerProvider> { /// We use this to close if funding is never broadcasted. pub(super) channel_creation_height: u32, + #[cfg(any(test, feature = "_test_utils"))] + pub(crate) counterparty_dust_limit_satoshis: u64, + #[cfg(not(any(test, feature = "_test_utils")))] counterparty_dust_limit_satoshis: u64, #[cfg(any(test, feature = "_test_utils"))] @@ -3249,6 +3775,12 @@ pub(super) struct ChannelContext<SP: SignerProvider> { /// See-also <https://github.com/lightningnetwork/lnd/issues/4006> pub workaround_lnd_bug_4006: Option<msgs::ChannelReady>, + /// The `my_current_funding_locked` txid included in our `channel_reestablish` for the current + /// reconnect, if any. We track this as we cannot tell what was included after we've already + /// sent it, as it's possible it was unconfirmed at the time we sent it, but confirmed shortly + /// after. + funding_locked_txid_sent_in_reestablish: Option<Txid>, + /// An option set when we wish to track how many ticks have elapsed while waiting for a response /// from our counterparty after entering specific states. If the peer has yet to respond after /// reaching `DISCONNECT_PEER_AWAITING_RESPONSE_TICKS`, a reconnection should be attempted to @@ -3315,6 +3847,13 @@ pub(super) struct ChannelContext<SP: SignerProvider> { pub interactive_tx_signing_session: Option<InteractiveTxSigningSession>, } +#[cfg(test)] +impl<SP: SignerProvider> fmt::Debug for ChannelContext<SP> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ChannelContext").finish() + } +} + /// A channel struct implementing this trait can receive an initial counterparty commitment /// transaction signature. trait InitialRemoteCommitmentReceiver<SP: SignerProvider> { @@ -3354,7 +3893,7 @@ trait InitialRemoteCommitmentReceiver<SP: SignerProvider> { #[rustfmt::skip] fn initial_commitment_signed<L: Logger>( &mut self, channel_id: ChannelId, counterparty_signature: Signature, holder_commitment_point: &mut HolderCommitmentPoint, - best_block: BestBlock, signer_provider: &SP, logger: &L, + best_block: BlockLocator, signer_provider: &SP, logger: &L, ) -> Result<(ChannelMonitor<SP::EcdsaSigner>, CommitmentTransaction), ChannelError> { let initial_commitment_tx = match self.check_counterparty_commitment_signature(&counterparty_signature, holder_commitment_point, logger) { Ok(res) => res, @@ -3390,7 +3929,7 @@ trait InitialRemoteCommitmentReceiver<SP: SignerProvider> { &self.funding().counterparty_funding_pubkey() ); - if context.holder_signer.as_ref().validate_holder_commitment(&holder_commitment_tx, Vec::new()).is_err() { + if context.holder_signer.validate_holder_commitment(&holder_commitment_tx, Vec::new()).is_err() { return Err(ChannelError::close("Failed to validate our commitment".to_owned())); } @@ -3531,184 +4070,277 @@ impl<SP: SignerProvider> InitialRemoteCommitmentReceiver<SP> for FundedChannel<S } impl<SP: SignerProvider> ChannelContext<SP> { - #[rustfmt::skip] fn new_for_inbound_channel<'a, ES: EntropySource, F: FeeEstimator, L: Logger>( - fee_estimator: &'a LowerBoundedFeeEstimator<F>, - entropy_source: &'a ES, - signer_provider: &'a SP, - counterparty_node_id: PublicKey, - their_features: &'a InitFeatures, - user_id: u128, - config: &'a UserConfig, - current_chain_height: u32, - logger: &'a L, - is_0conf: bool, - our_funding_satoshis: u64, - counterparty_pubkeys: ChannelPublicKeys, - channel_type: ChannelTypeFeatures, - holder_selected_channel_reserve_satoshis: u64, - msg_channel_reserve_satoshis: u64, - msg_push_msat: u64, - open_channel_fields: msgs::CommonOpenChannelFields, + fee_estimator: &'a LowerBoundedFeeEstimator<F>, entropy_source: &'a ES, + signer_provider: &'a SP, counterparty_node_id: PublicKey, their_features: &'a InitFeatures, + user_id: u128, config: &'a UserConfig, current_chain_height: u32, logger: &'a L, + trusted_channel_features: Option<TrustedChannelFeatures>, our_funding_satoshis: u64, + counterparty_pubkeys: ChannelPublicKeys, channel_type: ChannelTypeFeatures, + holder_selected_channel_reserve_satoshis: u64, msg_channel_reserve_satoshis: u64, + msg_push_msat: u64, open_channel_fields: msgs::CommonOpenChannelFields, ) -> Result<(FundingScope, ChannelContext<SP>), ChannelError> { - let logger = WithContext::from(logger, Some(counterparty_node_id), Some(open_channel_fields.temporary_channel_id), None); - let announce_for_forwarding = if (open_channel_fields.channel_flags & 1) == 1 { true } else { false }; + let logger = WithContext::from( + logger, + Some(counterparty_node_id), + Some(open_channel_fields.temporary_channel_id), + None, + ); + let announce_for_forwarding = + if (open_channel_fields.channel_flags & 1) == 1 { true } else { false }; - let channel_value_satoshis = our_funding_satoshis.saturating_add(open_channel_fields.funding_satoshis); + let channel_value_satoshis = + our_funding_satoshis.saturating_add(open_channel_fields.funding_satoshis); + if channel_value_satoshis < MIN_CHANNEL_VALUE_SATOSHIS { + return Err(ChannelError::close(format!( + "Channel value must be at least 1000 satoshis. It was {channel_value_satoshis}", + ))); + } let channel_keys_id = signer_provider.generate_channel_keys_id(true, user_id); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); if config.channel_handshake_config.our_to_self_delay < BREAKDOWN_TIMEOUT { - return Err(ChannelError::close(format!("Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks. It must be greater than {}", config.channel_handshake_config.our_to_self_delay, BREAKDOWN_TIMEOUT))); + return Err(ChannelError::close(format!( + "Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks. It must be greater than {BREAKDOWN_TIMEOUT}", + config.channel_handshake_config.our_to_self_delay + ))); } - // Check sanity of message fields: - if channel_value_satoshis > config.channel_handshake_limits.max_funding_satoshis { + if channel_value_satoshis >= TOTAL_BITCOIN_SUPPLY_SATOSHIS { return Err(ChannelError::close(format!( - "Per our config, funding must be at most {}. It was {}. Peer contribution: {}. Our contribution: {}", - config.channel_handshake_limits.max_funding_satoshis, channel_value_satoshis, - open_channel_fields.funding_satoshis, our_funding_satoshis))); + "Funding must be smaller than the total bitcoin supply. It was {channel_value_satoshis}" + ))); } - if channel_value_satoshis >= TOTAL_BITCOIN_SUPPLY_SATOSHIS { - return Err(ChannelError::close(format!("Funding must be smaller than the total bitcoin supply. It was {}", channel_value_satoshis))); + if !channel_type.supports_anchors_zero_fee_htlc_tx() + && !channel_type.supports_anchor_zero_fee_commitments() + && holder_selected_channel_reserve_satoshis == 0 + { + return Err(ChannelError::close( + "0-reserve is not allowed on legacy channels".to_owned(), + )); } if msg_channel_reserve_satoshis > channel_value_satoshis { - return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must be no greater than channel_value_satoshis: {}", msg_channel_reserve_satoshis, channel_value_satoshis))); + return Err(ChannelError::close(format!( + "Bogus channel_reserve_satoshis ({msg_channel_reserve_satoshis}). Must be no greater than channel_value_satoshis: {channel_value_satoshis}" + ))); } - let full_channel_value_msat = (channel_value_satoshis - msg_channel_reserve_satoshis) * 1000; + let full_channel_value_msat = + (channel_value_satoshis - msg_channel_reserve_satoshis) * 1000; if msg_push_msat > full_channel_value_msat { - return Err(ChannelError::close(format!("push_msat {} was larger than channel amount minus reserve ({})", msg_push_msat, full_channel_value_msat))); + return Err(ChannelError::close(format!( + "push_msat {msg_push_msat} was larger than channel amount minus reserve ({full_channel_value_msat})" + ))); } if open_channel_fields.dust_limit_satoshis > channel_value_satoshis { - return Err(ChannelError::close(format!("dust_limit_satoshis {} was larger than channel_value_satoshis {}. Peer never wants payout outputs?", open_channel_fields.dust_limit_satoshis, channel_value_satoshis))); + return Err(ChannelError::close(format!( + "dust_limit_satoshis {} was larger than channel_value_satoshis {channel_value_satoshis}. Peer never wants payout outputs?", + open_channel_fields.dust_limit_satoshis + ))); } if open_channel_fields.htlc_minimum_msat >= full_channel_value_msat { - return Err(ChannelError::close(format!("Minimum htlc value ({}) was larger than full channel value ({})", open_channel_fields.htlc_minimum_msat, full_channel_value_msat))); + return Err(ChannelError::close(format!( + "Minimum htlc value ({}) was larger than full channel value ({full_channel_value_msat})", + open_channel_fields.htlc_minimum_msat + ))); } - FundedChannel::<SP>::check_remote_fee(&channel_type, fee_estimator, open_channel_fields.commitment_feerate_sat_per_1000_weight, None, &&logger)?; + FundedChannel::<SP>::check_remote_fee( + &channel_type, + fee_estimator, + open_channel_fields.commitment_feerate_sat_per_1000_weight, + None, + &&logger, + )?; - let max_counterparty_selected_contest_delay = u16::min(config.channel_handshake_limits.their_to_self_delay, MAX_LOCAL_BREAKDOWN_TIMEOUT); + let max_counterparty_selected_contest_delay = u16::min( + config.channel_handshake_limits.their_to_self_delay, + MAX_LOCAL_BREAKDOWN_TIMEOUT, + ); if open_channel_fields.to_self_delay > max_counterparty_selected_contest_delay { - return Err(ChannelError::close(format!("They wanted our payments to be delayed by a needlessly long period. Upper limit: {}. Actual: {}", max_counterparty_selected_contest_delay, open_channel_fields.to_self_delay))); + return Err(ChannelError::close(format!( + "They wanted our payments to be delayed by a needlessly long period. Upper limit: {max_counterparty_selected_contest_delay}. Actual: {}", + open_channel_fields.to_self_delay + ))); } if open_channel_fields.max_accepted_htlcs < 1 { - return Err(ChannelError::close("0 max_accepted_htlcs makes for a useless channel".to_owned())); + return Err(ChannelError::close( + "0 max_accepted_htlcs makes for a useless channel".to_owned(), + )); } if open_channel_fields.max_accepted_htlcs > max_htlcs(&channel_type) { - return Err(ChannelError::close(format!("max_accepted_htlcs was {}. It must not be larger than {}", open_channel_fields.max_accepted_htlcs, max_htlcs(&channel_type)))); + return Err(ChannelError::close(format!( + "max_accepted_htlcs was {}. It must not be larger than {}", + open_channel_fields.max_accepted_htlcs, + max_htlcs(&channel_type) + ))); } // Now check against optional parameters as set by config... if channel_value_satoshis < config.channel_handshake_limits.min_funding_satoshis { - return Err(ChannelError::close(format!("Funding satoshis ({}) is less than the user specified limit ({})", channel_value_satoshis, config.channel_handshake_limits.min_funding_satoshis))); + return Err(ChannelError::close(format!( + "Funding satoshis ({channel_value_satoshis}) is less than the user specified limit ({})", + config.channel_handshake_limits.min_funding_satoshis + ))); } - if open_channel_fields.htlc_minimum_msat > config.channel_handshake_limits.max_htlc_minimum_msat { - return Err(ChannelError::close(format!("htlc_minimum_msat ({}) is higher than the user specified limit ({})", open_channel_fields.htlc_minimum_msat, config.channel_handshake_limits.max_htlc_minimum_msat))); + if open_channel_fields.htlc_minimum_msat + > config.channel_handshake_limits.max_htlc_minimum_msat + { + return Err(ChannelError::close(format!( + "htlc_minimum_msat ({}) is higher than the user specified limit ({})", + open_channel_fields.htlc_minimum_msat, + config.channel_handshake_limits.max_htlc_minimum_msat + ))); } - if open_channel_fields.max_htlc_value_in_flight_msat < config.channel_handshake_limits.min_max_htlc_value_in_flight_msat { - return Err(ChannelError::close(format!("max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", open_channel_fields.max_htlc_value_in_flight_msat, config.channel_handshake_limits.min_max_htlc_value_in_flight_msat))); + if open_channel_fields.max_htlc_value_in_flight_msat + < config.channel_handshake_limits.min_max_htlc_value_in_flight_msat + { + return Err(ChannelError::close(format!( + "max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", + open_channel_fields.max_htlc_value_in_flight_msat, + config.channel_handshake_limits.min_max_htlc_value_in_flight_msat + ))); } - if msg_channel_reserve_satoshis > config.channel_handshake_limits.max_channel_reserve_satoshis { - return Err(ChannelError::close(format!("channel_reserve_satoshis ({}) is higher than the user specified limit ({})", msg_channel_reserve_satoshis, config.channel_handshake_limits.max_channel_reserve_satoshis))); + if msg_channel_reserve_satoshis + > config.channel_handshake_limits.max_channel_reserve_satoshis + { + return Err(ChannelError::close(format!( + "channel_reserve_satoshis ({msg_channel_reserve_satoshis}) is higher than the user specified limit ({})", + config.channel_handshake_limits.max_channel_reserve_satoshis + ))); } - if open_channel_fields.max_accepted_htlcs < config.channel_handshake_limits.min_max_accepted_htlcs { - return Err(ChannelError::close(format!("max_accepted_htlcs ({}) is less than the user specified limit ({})", open_channel_fields.max_accepted_htlcs, config.channel_handshake_limits.min_max_accepted_htlcs))); + if open_channel_fields.max_accepted_htlcs + < config.channel_handshake_limits.min_max_accepted_htlcs + { + return Err(ChannelError::close(format!( + "max_accepted_htlcs ({}) is less than the user specified limit ({})", + open_channel_fields.max_accepted_htlcs, + config.channel_handshake_limits.min_max_accepted_htlcs + ))); } if open_channel_fields.dust_limit_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is less than the implementation limit ({})", open_channel_fields.dust_limit_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS))); + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is less than the implementation limit ({MIN_CHAN_DUST_LIMIT_SATOSHIS})", + open_channel_fields.dust_limit_satoshis + ))); } - if open_channel_fields.dust_limit_satoshis > MAX_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is greater than the implementation limit ({})", open_channel_fields.dust_limit_satoshis, MAX_CHAN_DUST_LIMIT_SATOSHIS))); + + let max_chan_dust_limit_satoshis = if channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_anchor_zero_fee_commitments() + { + MAX_CHAN_DUST_LIMIT_SATOSHIS + } else { + MAX_LEGACY_CHAN_DUST_LIMIT_SATOSHIS + }; + if open_channel_fields.dust_limit_satoshis > max_chan_dust_limit_satoshis { + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is greater than the implementation limit ({max_chan_dust_limit_satoshis})", + open_channel_fields.dust_limit_satoshis + ))); } // Convert things into internal flags and prep our state: if config.channel_handshake_limits.force_announced_channel_preference { if config.channel_handshake_config.announce_for_forwarding != announce_for_forwarding { - return Err(ChannelError::close("Peer tried to open channel but their announcement preference is different from ours".to_owned())); + return Err(ChannelError::close(String::from( + "Peer tried to open channel but their announcement preference is different from ours" + ))); } } - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { + if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS + && holder_selected_channel_reserve_satoshis != 0 + { // Protocol level safety check in place, although it should never happen because - // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` - return Err(ChannelError::close(format!("Suitable channel reserve not found. remote_channel_reserve was ({}). dust_limit_satoshis is ({}).", holder_selected_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS))); + // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` and `MIN_CHANNEL_VALUE_SATOSHIS` + return Err(ChannelError::close(format!( + "Suitable channel reserve not found. remote_channel_reserve was ({holder_selected_channel_reserve_satoshis}). dust_limit_satoshis is ({MIN_CHAN_DUST_LIMIT_SATOSHIS})." + ))); } if holder_selected_channel_reserve_satoshis * 1000 >= full_channel_value_msat { - return Err(ChannelError::close(format!("Suitable channel reserve not found. remote_channel_reserve was ({})msats. Channel value is ({} - {})msats.", holder_selected_channel_reserve_satoshis * 1000, full_channel_value_msat, msg_push_msat))); + return Err(ChannelError::close(format!( + "Suitable channel reserve not found. remote_channel_reserve was ({})msats. Channel value is ({full_channel_value_msat} - {msg_push_msat})msats.", + holder_selected_channel_reserve_satoshis * 1000 + ))); } if msg_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - log_debug!(logger, "channel_reserve_satoshis ({}) is smaller than our dust limit ({}). We can broadcast stale states without any risk, implying this channel is very insecure for our counterparty.", - msg_channel_reserve_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + log_debug!( + logger, + "channel_reserve_satoshis ({msg_channel_reserve_satoshis}) is smaller than our dust limit ({MIN_CHAN_DUST_LIMIT_SATOSHIS}). We can broadcast \ + stale states without any risk, implying this channel is very insecure for our counterparty."); } - if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis { - return Err(ChannelError::close(format!("Dust limit ({}) too high for the channel reserve we require the remote to keep ({})", open_channel_fields.dust_limit_satoshis, holder_selected_channel_reserve_satoshis))); + if holder_selected_channel_reserve_satoshis < open_channel_fields.dust_limit_satoshis + && holder_selected_channel_reserve_satoshis != 0 + { + return Err(ChannelError::close(format!( + "Dust limit ({}) too high for the channel reserve we require the remote to keep ({holder_selected_channel_reserve_satoshis})", + open_channel_fields.dust_limit_satoshis + ))); } // v1 channel opens set `our_funding_satoshis` to 0, and v2 channel opens set `msg_push_msat` to 0. debug_assert!(our_funding_satoshis == 0 || msg_push_msat == 0); let value_to_self_msat = our_funding_satoshis * 1000 + msg_push_msat; - // check if the funder's amount for the initial commitment tx is sufficient - // for full fee payment plus a few HTLCs to ensure the channel will be useful. - let funders_amount_msat = open_channel_fields.funding_satoshis * 1000 - msg_push_msat; - let commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(open_channel_fields.commitment_feerate_sat_per_1000_weight, MIN_AFFORDABLE_HTLC_COUNT, &channel_type); - // Subtract any non-HTLC outputs from the remote balance - let (_, remote_balance_before_fee_msat) = SpecTxBuilder {}.subtract_non_htlc_outputs(false, value_to_self_msat, funders_amount_msat, &channel_type); - if remote_balance_before_fee_msat / 1000 < commit_tx_fee_sat { - return Err(ChannelError::close(format!("Funding amount ({} sats) can't even pay fee for initial commitment transaction fee of {} sats.", funders_amount_msat / 1000, commit_tx_fee_sat))); - } - - let to_remote_satoshis = remote_balance_before_fee_msat / 1000 - commit_tx_fee_sat; - // While it's reasonable for us to not meet the channel reserve initially (if they don't - // want to push much to us), our counterparty should always have more than our reserve. - if to_remote_satoshis < holder_selected_channel_reserve_satoshis { - return Err(ChannelError::close("Insufficient funding amount for initial reserve".to_owned())); - } - - let counterparty_shutdown_scriptpubkey = if their_features.supports_upfront_shutdown_script() { - match &open_channel_fields.shutdown_scriptpubkey { - &Some(ref script) => { - // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything - if script.len() == 0 { - None - } else { - if !script::is_bolt2_compliant(&script, their_features) { - return Err(ChannelError::close(format!("Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {}", script))) + let counterparty_shutdown_scriptpubkey = + if their_features.supports_upfront_shutdown_script() { + match &open_channel_fields.shutdown_scriptpubkey { + &Some(ref script) => { + // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything + if script.len() == 0 { + None + } else { + if !script::is_bolt2_compliant(&script, their_features) { + return Err(ChannelError::close(format!( + "Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {script}" + ))); + } + Some(script.clone()) } - Some(script.clone()) - } - }, - // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel - &None => { - return Err(ChannelError::close("Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out".to_owned())); + }, + // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel + &None => { + return Err(ChannelError::close(String::from( + "Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out" + ))); + }, } - } - } else { None }; + } else { + None + }; - let shutdown_scriptpubkey = if config.channel_handshake_config.commit_upfront_shutdown_pubkey { - match signer_provider.get_shutdown_scriptpubkey() { - Ok(scriptpubkey) => Some(scriptpubkey), - Err(_) => return Err(ChannelError::close("Failed to get upfront shutdown scriptpubkey".to_owned())), - } - } else { None }; + let shutdown_scriptpubkey = + if config.channel_handshake_config.commit_upfront_shutdown_pubkey { + match signer_provider.get_shutdown_scriptpubkey() { + Ok(scriptpubkey) => Some(scriptpubkey), + Err(_) => { + return Err(ChannelError::close( + "Failed to get upfront shutdown scriptpubkey".to_owned(), + )) + }, + } + } else { + None + }; if let Some(shutdown_scriptpubkey) = &shutdown_scriptpubkey { if !shutdown_scriptpubkey.is_compatible(&their_features) { - return Err(ChannelError::close(format!("Provided a scriptpubkey format not accepted by peer: {}", shutdown_scriptpubkey))); + return Err(ChannelError::close(format!( + "Provided a scriptpubkey format not accepted by peer: {shutdown_scriptpubkey}" + ))); } } let destination_script = match signer_provider.get_destination_script(channel_keys_id) { Ok(script) => script, - Err(_) => return Err(ChannelError::close("Failed to get destination script".to_owned())), + Err(_) => { + return Err(ChannelError::close("Failed to get destination script".to_owned())) + }, }; let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); - let minimum_depth = if is_0conf { + let minimum_depth = if trusted_channel_features.is_some_and(|f| f.is_0conf()) { Some(0) } else { Some(cmp::max(config.channel_handshake_config.minimum_depth, 1)) @@ -3724,9 +4356,15 @@ impl<SP: SignerProvider> ChannelContext<SP> { holder_selected_channel_reserve_satoshis, #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat))), + holder_prev_commitment_tx_balance: Mutex::new(( + value_to_self_msat, + (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat), + )), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((value_to_self_msat, (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat))), + counterparty_prev_commitment_tx_balance: Mutex::new(( + value_to_self_msat, + (channel_value_satoshis * 1000 - msg_push_msat).saturating_sub(value_to_self_msat), + )), #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), @@ -3758,7 +4396,9 @@ impl<SP: SignerProvider> ChannelContext<SP> { config: LegacyChannelConfig { options: config.channel_config.clone(), announce_for_forwarding, - commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey, + commit_upfront_shutdown_pubkey: config + .channel_handshake_config + .commit_upfront_shutdown_pubkey, }, prev_config: None, @@ -3768,14 +4408,14 @@ impl<SP: SignerProvider> ChannelContext<SP> { temporary_channel_id: Some(open_channel_fields.temporary_channel_id), channel_id: open_channel_fields.temporary_channel_id, channel_state: ChannelState::NegotiatingFunding( - NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT + NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT, ), announcement_sigs_state: AnnouncementSigsState::NotSent, secp_ctx, latest_monitor_update_id: 0, - holder_signer: ChannelSignerType::Ecdsa(holder_signer), + holder_signer, shutdown_scriptpubkey, destination_script, @@ -3820,19 +4460,36 @@ impl<SP: SignerProvider> ChannelContext<SP> { feerate_per_kw: open_channel_fields.commitment_feerate_sat_per_1000_weight, counterparty_dust_limit_satoshis: open_channel_fields.dust_limit_satoshis, holder_dust_limit_satoshis: MIN_CHAN_DUST_LIMIT_SATOSHIS, - counterparty_max_htlc_value_in_flight_msat: cmp::min(open_channel_fields.max_htlc_value_in_flight_msat, channel_value_satoshis * 1000), - holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat(channel_value_satoshis, &config.channel_handshake_config), + counterparty_max_htlc_value_in_flight_msat: cmp::min( + open_channel_fields.max_htlc_value_in_flight_msat, + channel_value_satoshis * 1000, + ), + holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat( + channel_value_satoshis, + announce_for_forwarding, + &config.channel_handshake_config, + ), counterparty_htlc_minimum_msat: open_channel_fields.htlc_minimum_msat, - holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 { 1 } else { config.channel_handshake_config.our_htlc_minimum_msat }, + holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 + { + 1 + } else { + config.channel_handshake_config.our_htlc_minimum_msat + }, counterparty_max_accepted_htlcs: open_channel_fields.max_accepted_htlcs, - holder_max_accepted_htlcs: cmp::min(config.channel_handshake_config.our_max_accepted_htlcs, max_htlcs(&channel_type)), + holder_max_accepted_htlcs: cmp::min( + config.channel_handshake_config.our_max_accepted_htlcs, + max_htlcs(&channel_type), + ), minimum_depth, counterparty_forwarding_info: None, is_batch_funding: None, - counterparty_next_commitment_point: Some(open_channel_fields.first_per_commitment_point), + counterparty_next_commitment_point: Some( + open_channel_fields.first_per_commitment_point, + ), counterparty_current_commitment_point: None, counterparty_node_id, @@ -3846,6 +4503,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { announcement_sigs: None, workaround_lnd_bug_4006: None, + funding_locked_txid_sent_in_reestablish: None, sent_message_awaiting_response: None, latest_inbound_scid_alias: None, @@ -3867,91 +4525,146 @@ impl<SP: SignerProvider> ChannelContext<SP> { interactive_tx_signing_session: None, }; + // check if the funder's amount for the initial commitment tx is sufficient + // for full fee payment plus a few HTLCs to ensure the channel will be useful. + let funders_amount_msat = + funding.get_value_satoshis() * 1000 - funding.get_value_to_self_msat(); + let htlc_candidate = None; + let include_counterparty_unknown_htlcs = false; + let addl_nondust_htlc_count = MIN_AFFORDABLE_HTLC_COUNT; + let dust_exposure_limiting_feerate = channel_context + .get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type()); + let (remote_stats, _remote_htlcs) = channel_context + .get_next_remote_commitment_stats( + &funding, + htlc_candidate, + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + channel_context.feerate_per_kw, + false, + dust_exposure_limiting_feerate, + ) + .map_err(|()| { + ChannelError::close(format!( + "Funding amount ({} sats) can't even pay fee for initial commitment transaction.", + funders_amount_msat / 1000 + )) + })?; + + // While it's reasonable for us to not meet the channel reserve initially (if they don't + // want to push much to us), our counterparty should always have more than our reserve. + if remote_stats.commitment_stats.counterparty_balance_msat / 1000 + < funding.holder_selected_channel_reserve_satoshis + { + return Err(ChannelError::close( + "Insufficient funding amount for initial reserve".to_owned(), + )); + } + Ok((funding, channel_context)) } - #[rustfmt::skip] fn new_for_outbound_channel<'a, ES: EntropySource, F: FeeEstimator, L: Logger>( - fee_estimator: &'a LowerBoundedFeeEstimator<F>, - entropy_source: &'a ES, - signer_provider: &'a SP, - counterparty_node_id: PublicKey, - their_features: &'a InitFeatures, - funding_satoshis: u64, - push_msat: u64, - user_id: u128, - config: &'a UserConfig, - current_chain_height: u32, - outbound_scid_alias: u64, + fee_estimator: &'a LowerBoundedFeeEstimator<F>, entropy_source: &'a ES, + signer_provider: &'a SP, counterparty_node_id: PublicKey, their_features: &'a InitFeatures, + funding_satoshis: u64, push_msat: u64, user_id: u128, config: &'a UserConfig, + current_chain_height: u32, outbound_scid_alias: u64, temporary_channel_id_fn: Option<impl Fn(&ChannelPublicKeys) -> ChannelId>, - holder_selected_channel_reserve_satoshis: u64, - channel_keys_id: [u8; 32], - holder_signer: SP::EcdsaSigner, - _logger: L, + holder_selected_channel_reserve_satoshis: u64, channel_keys_id: [u8; 32], + holder_signer: SP::EcdsaSigner, _logger: L, ) -> Result<(FundingScope, ChannelContext<SP>), APIError> { // This will be updated with the counterparty contribution if this is a dual-funded channel let channel_value_satoshis = funding_satoshis; let holder_selected_contest_delay = config.channel_handshake_config.our_to_self_delay; - if !their_features.supports_wumbo() && channel_value_satoshis > MAX_FUNDING_SATOSHIS_NO_WUMBO { - return Err(APIError::APIMisuseError{err: format!("funding_value must not exceed {}, it was {}", MAX_FUNDING_SATOSHIS_NO_WUMBO, channel_value_satoshis)}); + if !their_features.supports_wumbo() + && channel_value_satoshis > MAX_FUNDING_SATOSHIS_NO_WUMBO + { + return Err(APIError::APIMisuseError { + err: format!( + "funding_value must not exceed {MAX_FUNDING_SATOSHIS_NO_WUMBO}, it was {channel_value_satoshis}" + ), + }); } if channel_value_satoshis >= TOTAL_BITCOIN_SUPPLY_SATOSHIS { - return Err(APIError::APIMisuseError{err: format!("funding_value must be smaller than the total bitcoin supply, it was {}", channel_value_satoshis)}); + return Err(APIError::APIMisuseError { + err: format!( + "funding_value must be smaller than the total bitcoin supply, it was {channel_value_satoshis}" + ), + }); } let channel_value_msat = channel_value_satoshis * 1000; if push_msat > channel_value_msat { - return Err(APIError::APIMisuseError { err: format!("Push value ({}) was larger than channel_value ({})", push_msat, channel_value_msat) }); + return Err(APIError::APIMisuseError { + err: format!( + "Push value ({push_msat}) was larger than channel_value ({channel_value_msat})" + ), + }); } if holder_selected_contest_delay < BREAKDOWN_TIMEOUT { - return Err(APIError::APIMisuseError {err: format!("Configured with an unreasonable our_to_self_delay ({}) putting user funds at risks", holder_selected_contest_delay)}); + return Err(APIError::APIMisuseError { + err: format!( + "Configured with an unreasonable our_to_self_delay ({holder_selected_contest_delay}) putting user funds at risks" + ), + }); } let channel_type = get_initial_channel_type(&config, their_features); + if !channel_type.supports_anchors_zero_fee_htlc_tx() + && !channel_type.supports_anchor_zero_fee_commitments() + && holder_selected_channel_reserve_satoshis == 0 + { + return Err(APIError::APIMisuseError { + err: "0-reserve is not allowed on legacy channels".to_owned(), + }); + } debug_assert!(!channel_type.supports_any_optional_bits()); - debug_assert!(!channel_type.requires_unknown_bits_from(&channelmanager::provided_channel_type_features(&config))); + debug_assert!(!channel_type + .requires_unknown_bits_from(&channelmanager::provided_channel_type_features(&config))); - let commitment_feerate = selected_commitment_sat_per_1000_weight( - &fee_estimator, &channel_type, - ); + let commitment_feerate = + selected_commitment_sat_per_1000_weight(&fee_estimator, &channel_type); let value_to_self_msat = channel_value_satoshis * 1000 - push_msat; - let commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(commitment_feerate, MIN_AFFORDABLE_HTLC_COUNT, &channel_type); - // Subtract any non-HTLC outputs from the local balance - let (local_balance_before_fee_msat, _) = SpecTxBuilder {}.subtract_non_htlc_outputs( - true, - value_to_self_msat, - push_msat, - &channel_type, - ); - if local_balance_before_fee_msat / 1000 < commit_tx_fee_sat { - return Err(APIError::APIMisuseError{ err: format!("Funding amount ({}) can't even pay fee for initial commitment transaction fee of {}.", value_to_self_msat / 1000, commit_tx_fee_sat) }); - } let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); - let shutdown_scriptpubkey = if config.channel_handshake_config.commit_upfront_shutdown_pubkey { - match signer_provider.get_shutdown_scriptpubkey() { - Ok(scriptpubkey) => Some(scriptpubkey), - Err(_) => return Err(APIError::ChannelUnavailable { err: "Failed to get shutdown scriptpubkey".to_owned()}), - } - } else { None }; + let shutdown_scriptpubkey = + if config.channel_handshake_config.commit_upfront_shutdown_pubkey { + match signer_provider.get_shutdown_scriptpubkey() { + Ok(scriptpubkey) => Some(scriptpubkey), + Err(_) => { + return Err(APIError::ChannelUnavailable { + err: "Failed to get shutdown scriptpubkey".to_owned(), + }) + }, + } + } else { + None + }; if let Some(shutdown_scriptpubkey) = &shutdown_scriptpubkey { if !shutdown_scriptpubkey.is_compatible(&their_features) { - return Err(APIError::IncompatibleShutdownScript { script: shutdown_scriptpubkey.clone() }); + return Err(APIError::IncompatibleShutdownScript { + script: shutdown_scriptpubkey.clone(), + }); } } let destination_script = match signer_provider.get_destination_script(channel_keys_id) { Ok(script) => script, - Err(_) => return Err(APIError::ChannelUnavailable { err: "Failed to get destination script".to_owned()}), + Err(_) => { + return Err(APIError::ChannelUnavailable { + err: "Failed to get destination script".to_owned(), + }) + }, }; let pubkeys = holder_signer.pubkeys(&secp_ctx); - let temporary_channel_id = temporary_channel_id_fn.map(|f| f(&pubkeys)) + let temporary_channel_id = temporary_channel_id_fn + .map(|f| f(&pubkeys)) .unwrap_or_else(|| ChannelId::temporary_from_entropy_source(entropy_source)); let funding = FundingScope { @@ -3962,9 +4675,15 @@ impl<SP: SignerProvider> ChannelContext<SP> { // We'll add our counterparty's `funding_satoshis` to these max commitment output assertions // when we receive `accept_channel2`. #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat)), + holder_prev_commitment_tx_balance: Mutex::new(( + channel_value_satoshis * 1000 - push_msat, + push_msat, + )), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((channel_value_satoshis * 1000 - push_msat, push_msat)), + counterparty_prev_commitment_tx_balance: Mutex::new(( + channel_value_satoshis * 1000 - push_msat, + push_msat, + )), #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), @@ -3994,7 +4713,9 @@ impl<SP: SignerProvider> ChannelContext<SP> { config: LegacyChannelConfig { options: config.channel_config.clone(), announce_for_forwarding: config.channel_handshake_config.announce_for_forwarding, - commit_upfront_shutdown_pubkey: config.channel_handshake_config.commit_upfront_shutdown_pubkey, + commit_upfront_shutdown_pubkey: config + .channel_handshake_config + .commit_upfront_shutdown_pubkey, }, prev_config: None, @@ -4009,7 +4730,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { latest_monitor_update_id: 0, - holder_signer: ChannelSignerType::Ecdsa(holder_signer), + holder_signer, shutdown_scriptpubkey, destination_script, @@ -4057,11 +4778,23 @@ impl<SP: SignerProvider> ChannelContext<SP> { counterparty_max_htlc_value_in_flight_msat: 0, // We'll adjust this to include our counterparty's `funding_satoshis` when we // receive `accept_channel2`. - holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat(channel_value_satoshis, &config.channel_handshake_config), + holder_max_htlc_value_in_flight_msat: get_holder_max_htlc_value_in_flight_msat( + channel_value_satoshis, + config.channel_handshake_config.announce_for_forwarding, + &config.channel_handshake_config, + ), counterparty_htlc_minimum_msat: 0, - holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 { 1 } else { config.channel_handshake_config.our_htlc_minimum_msat }, + holder_htlc_minimum_msat: if config.channel_handshake_config.our_htlc_minimum_msat == 0 + { + 1 + } else { + config.channel_handshake_config.our_htlc_minimum_msat + }, counterparty_max_accepted_htlcs: 0, - holder_max_accepted_htlcs: cmp::min(config.channel_handshake_config.our_max_accepted_htlcs, max_htlcs(&channel_type)), + holder_max_accepted_htlcs: cmp::min( + config.channel_handshake_config.our_max_accepted_htlcs, + max_htlcs(&channel_type), + ), minimum_depth: None, // Filled in in accept_channel counterparty_forwarding_info: None, @@ -4082,6 +4815,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { announcement_sigs: None, workaround_lnd_bug_4006: None, + funding_locked_txid_sent_in_reestablish: None, sent_message_awaiting_response: None, latest_inbound_scid_alias: None, @@ -4101,6 +4835,28 @@ impl<SP: SignerProvider> ChannelContext<SP> { interactive_tx_signing_session: None, }; + let htlc_candidate = None; + let include_counterparty_unknown_htlcs = false; + let addl_nondust_htlc_count = MIN_AFFORDABLE_HTLC_COUNT; + let dust_exposure_limiting_feerate = channel_context + .get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type()); + let _local_stats = channel_context + .get_next_local_commitment_stats( + &funding, + htlc_candidate, + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + channel_context.feerate_per_kw, + false, + dust_exposure_limiting_feerate, + ) + .map_err(|()| APIError::APIMisuseError { + err: format!( + "Funding amount ({}) can't even pay fee for initial commitment transaction.", + funding.get_value_to_self_msat() / 1000 + ), + })?; + Ok((funding, channel_context)) } @@ -4311,7 +5067,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { /// Returns the holder signer for this channel. #[cfg(any(test, feature = "_test_utils"))] - pub fn get_mut_signer(&mut self) -> &mut ChannelSignerType<SP> { + pub fn get_mut_signer(&mut self) -> &mut SP::EcdsaSigner { return &mut self.holder_signer; } @@ -4325,103 +5081,194 @@ impl<SP: SignerProvider> ChannelContext<SP> { /// Performs checks against necessary constraints after receiving either an `accept_channel` or /// `accept_channel2` message. - #[rustfmt::skip] pub fn do_accept_channel_checks( &mut self, funding: &mut FundingScope, default_limits: &ChannelHandshakeLimits, their_features: &InitFeatures, common_fields: &msgs::CommonAcceptChannelFields, channel_reserve_satoshis: u64, ) -> Result<(), ChannelError> { - let peer_limits = if let Some(ref limits) = self.inbound_handshake_limits_override { limits } else { default_limits }; + let peer_limits = if let Some(ref limits) = self.inbound_handshake_limits_override { + limits + } else { + default_limits + }; // Check sanity of message fields: if !funding.is_outbound() { - return Err(ChannelError::close("Got an accept_channel message from an inbound peer".to_owned())); + return Err(ChannelError::close( + "Got an accept_channel message from an inbound peer".to_owned(), + )); } - if !matches!(self.channel_state, ChannelState::NegotiatingFunding(flags) if flags == NegotiatingFundingFlags::OUR_INIT_SENT) { - return Err(ChannelError::close("Got an accept_channel message at a strange time".to_owned())); + if !matches!(self.channel_state, ChannelState::NegotiatingFunding(flags) + if flags == NegotiatingFundingFlags::OUR_INIT_SENT) + { + return Err(ChannelError::close( + "Got an accept_channel message at a strange time".to_owned(), + )); } - let channel_type = common_fields.channel_type.as_ref() - .ok_or_else(|| ChannelError::close("option_channel_type assumed to be supported".to_owned()))?; + let channel_type = common_fields.channel_type.as_ref().ok_or_else(|| { + ChannelError::close("option_channel_type assumed to be supported".to_owned()) + })?; if channel_type != funding.get_channel_type() { - return Err(ChannelError::close("Channel Type in accept_channel didn't match the one sent in open_channel.".to_owned())); + return Err(ChannelError::close(String::from( + "Channel Type in accept_channel didn't match the one sent in open_channel.", + ))); } if common_fields.dust_limit_satoshis > 21000000 * 100000000 { - return Err(ChannelError::close(format!("Peer never wants payout outputs? dust_limit_satoshis was {}", common_fields.dust_limit_satoshis))); + return Err(ChannelError::close(format!( + "Peer never wants payout outputs? dust_limit_satoshis was {}", + common_fields.dust_limit_satoshis + ))); } if channel_reserve_satoshis > funding.get_value_satoshis() { - return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than ({})", channel_reserve_satoshis, funding.get_value_satoshis()))); + return Err(ChannelError::close(format!( + "Bogus channel_reserve_satoshis ({channel_reserve_satoshis}). Must not be greater than ({})", + funding.get_value_satoshis() + ))); } - if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis { - return Err(ChannelError::close(format!("Dust limit ({}) is bigger than our channel reserve ({})", common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis))); + if common_fields.dust_limit_satoshis > funding.holder_selected_channel_reserve_satoshis + && funding.holder_selected_channel_reserve_satoshis != 0 + { + return Err(ChannelError::close(format!( + "Dust limit ({}) is bigger than our channel reserve ({})", + common_fields.dust_limit_satoshis, funding.holder_selected_channel_reserve_satoshis + ))); } - if channel_reserve_satoshis > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis { - return Err(ChannelError::close(format!("Bogus channel_reserve_satoshis ({}). Must not be greater than channel value minus our reserve ({})", - channel_reserve_satoshis, funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis))); + if channel_reserve_satoshis + > funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis + { + return Err(ChannelError::close(format!( + "Bogus channel_reserve_satoshis ({channel_reserve_satoshis}). Must not be greater than channel value minus our reserve ({})", + funding.get_value_satoshis() - funding.holder_selected_channel_reserve_satoshis + ))); } - let full_channel_value_msat = (funding.get_value_satoshis() - channel_reserve_satoshis) * 1000; + let full_channel_value_msat = + (funding.get_value_satoshis() - channel_reserve_satoshis) * 1000; if common_fields.htlc_minimum_msat >= full_channel_value_msat { - return Err(ChannelError::close(format!("Minimum htlc value ({}) is full channel value ({})", common_fields.htlc_minimum_msat, full_channel_value_msat))); + return Err(ChannelError::close(format!( + "Minimum htlc value ({}) is full channel value ({full_channel_value_msat})", + common_fields.htlc_minimum_msat + ))); } - let max_delay_acceptable = u16::min(peer_limits.their_to_self_delay, MAX_LOCAL_BREAKDOWN_TIMEOUT); + let max_delay_acceptable = + u16::min(peer_limits.their_to_self_delay, MAX_LOCAL_BREAKDOWN_TIMEOUT); if common_fields.to_self_delay > max_delay_acceptable { - return Err(ChannelError::close(format!("They wanted our payments to be delayed by a needlessly long period. Upper limit: {}. Actual: {}", max_delay_acceptable, common_fields.to_self_delay))); + return Err(ChannelError::close(format!( + "They wanted our payments to be delayed by a needlessly long period. Upper limit: {max_delay_acceptable}. Actual: {}", + common_fields.to_self_delay + ))); } if common_fields.max_accepted_htlcs < 1 { - return Err(ChannelError::close("0 max_accepted_htlcs makes for a useless channel".to_owned())); + return Err(ChannelError::close( + "0 max_accepted_htlcs makes for a useless channel".to_owned(), + )); } let channel_type = funding.get_channel_type(); + if !channel_type.supports_anchors_zero_fee_htlc_tx() + && !channel_type.supports_anchor_zero_fee_commitments() + && funding.holder_selected_channel_reserve_satoshis == 0 + { + return Err(ChannelError::close( + "0-reserve is not allowed on legacy channels".to_owned(), + )); + } if common_fields.max_accepted_htlcs > max_htlcs(channel_type) { - return Err(ChannelError::close(format!("max_accepted_htlcs was {}. It must not be larger than {}", common_fields.max_accepted_htlcs, max_htlcs(channel_type)))); + return Err(ChannelError::close(format!( + "max_accepted_htlcs was {}. It must not be larger than {}", + common_fields.max_accepted_htlcs, + max_htlcs(channel_type) + ))); } // Now check against optional parameters as set by config... if common_fields.htlc_minimum_msat > peer_limits.max_htlc_minimum_msat { - return Err(ChannelError::close(format!("htlc_minimum_msat ({}) is higher than the user specified limit ({})", common_fields.htlc_minimum_msat, peer_limits.max_htlc_minimum_msat))); + return Err(ChannelError::close(format!( + "htlc_minimum_msat ({}) is higher than the user specified limit ({})", + common_fields.htlc_minimum_msat, peer_limits.max_htlc_minimum_msat + ))); } - if common_fields.max_htlc_value_in_flight_msat < peer_limits.min_max_htlc_value_in_flight_msat { - return Err(ChannelError::close(format!("max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", common_fields.max_htlc_value_in_flight_msat, peer_limits.min_max_htlc_value_in_flight_msat))); + if common_fields.max_htlc_value_in_flight_msat + < peer_limits.min_max_htlc_value_in_flight_msat + { + return Err(ChannelError::close(format!( + "max_htlc_value_in_flight_msat ({}) is less than the user specified limit ({})", + common_fields.max_htlc_value_in_flight_msat, + peer_limits.min_max_htlc_value_in_flight_msat + ))); } if channel_reserve_satoshis > peer_limits.max_channel_reserve_satoshis { - return Err(ChannelError::close(format!("channel_reserve_satoshis ({}) is higher than the user specified limit ({})", channel_reserve_satoshis, peer_limits.max_channel_reserve_satoshis))); + return Err(ChannelError::close(format!( + "channel_reserve_satoshis ({channel_reserve_satoshis}) is higher than the user specified limit ({})", + peer_limits.max_channel_reserve_satoshis + ))); } if common_fields.max_accepted_htlcs < peer_limits.min_max_accepted_htlcs { - return Err(ChannelError::close(format!("max_accepted_htlcs ({}) is less than the user specified limit ({})", common_fields.max_accepted_htlcs, peer_limits.min_max_accepted_htlcs))); + return Err(ChannelError::close(format!( + "max_accepted_htlcs ({}) is less than the user specified limit ({})", + common_fields.max_accepted_htlcs, peer_limits.min_max_accepted_htlcs + ))); } if common_fields.dust_limit_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is less than the implementation limit ({})", common_fields.dust_limit_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS))); + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is less than the implementation limit ({MIN_CHAN_DUST_LIMIT_SATOSHIS})", + common_fields.dust_limit_satoshis + ))); } - if common_fields.dust_limit_satoshis > MAX_CHAN_DUST_LIMIT_SATOSHIS { - return Err(ChannelError::close(format!("dust_limit_satoshis ({}) is greater than the implementation limit ({})", common_fields.dust_limit_satoshis, MAX_CHAN_DUST_LIMIT_SATOSHIS))); + + let max_chan_dust_limit_satoshis = if channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_anchor_zero_fee_commitments() + { + MAX_CHAN_DUST_LIMIT_SATOSHIS + } else { + MAX_LEGACY_CHAN_DUST_LIMIT_SATOSHIS + }; + if common_fields.dust_limit_satoshis > max_chan_dust_limit_satoshis { + return Err(ChannelError::close(format!( + "dust_limit_satoshis ({}) is greater than the implementation limit ({max_chan_dust_limit_satoshis})", + common_fields.dust_limit_satoshis + ))); } if common_fields.minimum_depth > peer_limits.max_minimum_depth { - return Err(ChannelError::close(format!("We consider the minimum depth to be unreasonably large. Expected minimum: ({}). Actual: ({})", peer_limits.max_minimum_depth, common_fields.minimum_depth))); + return Err(ChannelError::close(format!( + "We consider the minimum depth to be unreasonably large. Expected minimum: ({}). Actual: ({})", + peer_limits.max_minimum_depth, common_fields.minimum_depth + ))); } - let counterparty_shutdown_scriptpubkey = if their_features.supports_upfront_shutdown_script() { - match &common_fields.shutdown_scriptpubkey { - &Some(ref script) => { - // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything - if script.len() == 0 { - None - } else { - if !script::is_bolt2_compliant(&script, their_features) { - return Err(ChannelError::close(format!("Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {}", script))); + let counterparty_shutdown_scriptpubkey = + if their_features.supports_upfront_shutdown_script() { + match &common_fields.shutdown_scriptpubkey { + &Some(ref script) => { + // Peer is signaling upfront_shutdown and has opt-out with a 0-length script. We don't enforce anything + if script.len() == 0 { + None + } else { + if !script::is_bolt2_compliant(&script, their_features) { + return Err(ChannelError::close(format!( + "Peer is signaling upfront_shutdown but has provided an unacceptable scriptpubkey format: {script}" + ))); + } + Some(script.clone()) } - Some(script.clone()) - } - }, - // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel - &None => { - return Err(ChannelError::close("Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out".to_owned())); + }, + // Peer is signaling upfront shutdown but don't opt-out with correct mechanism (a.k.a 0-length script). Peer looks buggy, we fail the channel + &None => { + return Err(ChannelError::close(String::from( + "Peer is signaling upfront_shutdown but we don't get any script. Use 0-length script to opt-out" + ))); + }, } - } - } else { None }; + } else { + None + }; self.counterparty_dust_limit_satoshis = common_fields.dust_limit_satoshis; - self.counterparty_max_htlc_value_in_flight_msat = cmp::min(common_fields.max_htlc_value_in_flight_msat, funding.get_value_satoshis() * 1000); + self.counterparty_max_htlc_value_in_flight_msat = cmp::min( + common_fields.max_htlc_value_in_flight_msat, + funding.get_value_satoshis() * 1000, + ); funding.counterparty_selected_channel_reserve_satoshis = Some(channel_reserve_satoshis); self.counterparty_htlc_minimum_msat = common_fields.htlc_minimum_msat; self.counterparty_max_accepted_htlcs = common_fields.max_accepted_htlcs; @@ -4436,20 +5283,23 @@ impl<SP: SignerProvider> ChannelContext<SP> { funding_pubkey: common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint::from(common_fields.revocation_basepoint), payment_point: common_fields.payment_basepoint, - delayed_payment_basepoint: DelayedPaymentBasepoint::from(common_fields.delayed_payment_basepoint), - htlc_basepoint: HtlcBasepoint::from(common_fields.htlc_basepoint) + delayed_payment_basepoint: DelayedPaymentBasepoint::from( + common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint::from(common_fields.htlc_basepoint), }; - funding.channel_transaction_parameters.counterparty_parameters = Some(CounterpartyChannelTransactionParameters { - selected_contest_delay: common_fields.to_self_delay, - pubkeys: counterparty_pubkeys, - }); + funding.channel_transaction_parameters.counterparty_parameters = + Some(CounterpartyChannelTransactionParameters { + selected_contest_delay: common_fields.to_self_delay, + pubkeys: counterparty_pubkeys, + }); self.counterparty_next_commitment_point = Some(common_fields.first_per_commitment_point); self.counterparty_shutdown_scriptpubkey = counterparty_shutdown_scriptpubkey; self.channel_state = ChannelState::NegotiatingFunding( - NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT + NegotiatingFundingFlags::OUR_INIT_SENT | NegotiatingFundingFlags::THEIR_INIT_SENT, ); self.inbound_handshake_limits_override = None; // We're done enforcing limits on our peer's handshake now. @@ -4636,7 +5486,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { ChannelState::FundingNegotiated(_) => self .interactive_tx_signing_session .as_ref() - .map(|signing_session| signing_session.holder_tx_signatures().is_some()) + .map(|signing_session| signing_session.has_holder_witnesses()) .unwrap_or(false), ChannelState::AwaitingChannelReady(flags) => !flags.is_waiting_for_batch(), _ => true, @@ -4777,11 +5627,27 @@ impl<SP: SignerProvider> ChannelContext<SP> { .saturating_add(inbound_claimed_htlc_msat) } + fn get_channel_constraints(&self, funding: &FundingScope) -> ChannelConstraints { + ChannelConstraints { + holder_dust_limit_satoshis: self.holder_dust_limit_satoshis, + counterparty_selected_channel_reserve_satoshis: funding + .counterparty_selected_channel_reserve_satoshis + .unwrap_or(0), + counterparty_dust_limit_satoshis: self.counterparty_dust_limit_satoshis, + holder_selected_channel_reserve_satoshis: funding + .holder_selected_channel_reserve_satoshis, + counterparty_htlc_minimum_msat: self.counterparty_htlc_minimum_msat, + counterparty_max_accepted_htlcs: self.counterparty_max_accepted_htlcs as u64, + counterparty_max_htlc_value_in_flight_msat: self + .counterparty_max_htlc_value_in_flight_msat, + } + } + fn get_next_local_commitment_stats( &self, funding: &FundingScope, htlc_candidate: Option<HTLCAmountDirection>, include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize, - feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>, - ) -> Result<NextCommitmentStats, ()> { + feerate_per_kw: u32, assume_fee_spike: bool, dust_exposure_limiting_feerate: Option<u32>, + ) -> Result<(ChannelStats, Vec<HTLCAmountDirection>), ()> { let next_commitment_htlcs = self.get_next_commitment_htlcs( true, htlc_candidate, @@ -4789,7 +5655,12 @@ impl<SP: SignerProvider> ChannelContext<SP> { ); let next_value_to_self_msat = self.get_next_commitment_value_to_self_msat(true, funding); - let ret = SpecTxBuilder {}.get_next_commitment_stats( + let max_dust_htlc_exposure_msat = + self.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); + + let channel_constraints = self.get_channel_constraints(funding); + + let local_stats = SpecTxBuilder {}.get_channel_stats( true, funding.is_outbound(), funding.get_value_satoshis(), @@ -4797,8 +5668,10 @@ impl<SP: SignerProvider> ChannelContext<SP> { &next_commitment_htlcs, addl_nondust_htlc_count, feerate_per_kw, + assume_fee_spike, dust_exposure_limiting_feerate, - self.holder_dust_limit_satoshis, + max_dust_htlc_exposure_msat, + channel_constraints, funding.get_channel_type(), )?; @@ -4807,12 +5680,12 @@ impl<SP: SignerProvider> ChannelContext<SP> { if addl_nondust_htlc_count == 0 { *funding.next_local_fee.lock().unwrap() = PredictedNextFee { predicted_feerate: feerate_per_kw, - predicted_nondust_htlc_count: ret.nondust_htlc_count, - predicted_fee_sat: ret.commit_tx_fee_sat, + predicted_nondust_htlc_count: local_stats.commitment_stats.nondust_htlc_count, + predicted_fee_sat: local_stats.commitment_stats.commit_tx_fee_sat, }; } else { let predicted_stats = SpecTxBuilder {} - .get_next_commitment_stats( + .get_channel_stats( true, funding.is_outbound(), funding.get_value_satoshis(), @@ -4820,11 +5693,14 @@ impl<SP: SignerProvider> ChannelContext<SP> { &next_commitment_htlcs, 0, feerate_per_kw, + false, dust_exposure_limiting_feerate, - self.holder_dust_limit_satoshis, + max_dust_htlc_exposure_msat, + channel_constraints, funding.get_channel_type(), ) - .expect("Balance after HTLCs and anchors exhausted on local commitment"); + .expect("Balance exhausted on local commitment") + .commitment_stats; *funding.next_local_fee.lock().unwrap() = PredictedNextFee { predicted_feerate: feerate_per_kw, predicted_nondust_htlc_count: predicted_stats.nondust_htlc_count, @@ -4833,14 +5709,14 @@ impl<SP: SignerProvider> ChannelContext<SP> { } } - Ok(ret) + Ok((local_stats, next_commitment_htlcs)) } fn get_next_remote_commitment_stats( &self, funding: &FundingScope, htlc_candidate: Option<HTLCAmountDirection>, include_counterparty_unknown_htlcs: bool, addl_nondust_htlc_count: usize, - feerate_per_kw: u32, dust_exposure_limiting_feerate: Option<u32>, - ) -> Result<NextCommitmentStats, ()> { + feerate_per_kw: u32, assume_fee_spike: bool, dust_exposure_limiting_feerate: Option<u32>, + ) -> Result<(ChannelStats, Vec<HTLCAmountDirection>), ()> { let next_commitment_htlcs = self.get_next_commitment_htlcs( false, htlc_candidate, @@ -4848,7 +5724,12 @@ impl<SP: SignerProvider> ChannelContext<SP> { ); let next_value_to_self_msat = self.get_next_commitment_value_to_self_msat(false, funding); - let ret = SpecTxBuilder {}.get_next_commitment_stats( + let max_dust_htlc_exposure_msat = + self.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); + + let channel_constraints = self.get_channel_constraints(funding); + + let remote_stats = SpecTxBuilder {}.get_channel_stats( false, funding.is_outbound(), funding.get_value_satoshis(), @@ -4856,8 +5737,10 @@ impl<SP: SignerProvider> ChannelContext<SP> { &next_commitment_htlcs, addl_nondust_htlc_count, feerate_per_kw, + assume_fee_spike, dust_exposure_limiting_feerate, - self.counterparty_dust_limit_satoshis, + max_dust_htlc_exposure_msat, + channel_constraints, funding.get_channel_type(), )?; @@ -4866,12 +5749,12 @@ impl<SP: SignerProvider> ChannelContext<SP> { if addl_nondust_htlc_count == 0 { *funding.next_remote_fee.lock().unwrap() = PredictedNextFee { predicted_feerate: feerate_per_kw, - predicted_nondust_htlc_count: ret.nondust_htlc_count, - predicted_fee_sat: ret.commit_tx_fee_sat, + predicted_nondust_htlc_count: remote_stats.commitment_stats.nondust_htlc_count, + predicted_fee_sat: remote_stats.commitment_stats.commit_tx_fee_sat, }; } else { let predicted_stats = SpecTxBuilder {} - .get_next_commitment_stats( + .get_channel_stats( false, funding.is_outbound(), funding.get_value_satoshis(), @@ -4879,11 +5762,14 @@ impl<SP: SignerProvider> ChannelContext<SP> { &next_commitment_htlcs, 0, feerate_per_kw, + false, dust_exposure_limiting_feerate, - self.counterparty_dust_limit_satoshis, + max_dust_htlc_exposure_msat, + channel_constraints, funding.get_channel_type(), ) - .expect("Balance after HTLCs and anchors exhausted on remote commitment"); + .expect("Balance exhausted on remote commitment") + .commitment_stats; *funding.next_remote_fee.lock().unwrap() = PredictedNextFee { predicted_feerate: feerate_per_kw, predicted_nondust_htlc_count: predicted_stats.nondust_htlc_count, @@ -4892,7 +5778,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { } } - Ok(ret) + Ok((remote_stats, next_commitment_htlcs)) } fn validate_update_add_htlc<F: FeeEstimator>( @@ -4912,30 +5798,33 @@ impl<SP: SignerProvider> ChannelContext<SP> { let include_counterparty_unknown_htlcs = false; // Don't include the extra fee spike buffer HTLC in calculations let fee_spike_buffer_htlc = 0; - let next_remote_commitment_stats = self + let (remote_stats, remote_htlcs) = self .get_next_remote_commitment_stats( funding, Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }), include_counterparty_unknown_htlcs, fee_spike_buffer_htlc, self.feerate_per_kw, + false, dust_exposure_limiting_feerate, ) .map_err(|()| { ChannelError::close(String::from("Remote HTLC add would overdraw remaining funds")) })?; - if next_remote_commitment_stats.inbound_htlcs_count - > self.holder_max_accepted_htlcs as usize - { + let inbound_htlcs_count = remote_htlcs.iter().filter(|htlc| !htlc.outbound).count(); + let inbound_htlcs_value_msat: u64 = remote_htlcs + .iter() + .filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat)) + .sum(); + + if inbound_htlcs_count > self.holder_max_accepted_htlcs as usize { return Err(ChannelError::close(format!( "Remote tried to push more than our max accepted HTLCs ({})", self.holder_max_accepted_htlcs, ))); } - if next_remote_commitment_stats.inbound_htlcs_value_msat - > self.holder_max_htlc_value_in_flight_msat - { + if inbound_htlcs_value_msat > self.holder_max_htlc_value_in_flight_msat { return Err(ChannelError::close(format!( "Remote HTLC add would put them over our max HTLC value ({})", self.holder_max_htlc_value_in_flight_msat, @@ -4957,55 +5846,30 @@ impl<SP: SignerProvider> ChannelContext<SP> { // violate the reserve value if we do not do this (as we forget inbound HTLCs from the // Channel state once they will not be present in the next received commitment // transaction). + if remote_stats.commitment_stats.counterparty_balance_msat + < funding.holder_selected_channel_reserve_satoshis * 1000 { - let remote_commit_tx_fee_msat = if funding.is_outbound() { - 0 - } else { - next_remote_commitment_stats.commit_tx_fee_sat * 1000 - }; - if next_remote_commitment_stats.counterparty_balance_before_fee_msat - < remote_commit_tx_fee_msat - { - return Err(ChannelError::close( - "Remote HTLC add would not leave enough to pay for fees".to_owned(), - )); - }; - if next_remote_commitment_stats - .counterparty_balance_before_fee_msat - .saturating_sub(remote_commit_tx_fee_msat) - < funding.holder_selected_channel_reserve_satoshis * 1000 - { - return Err(ChannelError::close( - "Remote HTLC add would put them under remote reserve value".to_owned(), - )); - } + return Err(ChannelError::close( + "Remote HTLC add would put them under remote reserve value".to_owned(), + )); } - if funding.is_outbound() { - let next_local_commitment_stats = self - .get_next_local_commitment_stats( - funding, - Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }), - include_counterparty_unknown_htlcs, - fee_spike_buffer_htlc, - self.feerate_per_kw, - dust_exposure_limiting_feerate, - ) - .map_err(|()| { - ChannelError::close(String::from( - "Balance after HTLCs and anchors exhausted on local commitment", - )) - })?; - // Check that they won't violate our local required channel reserve by adding this HTLC. - if next_local_commitment_stats.holder_balance_before_fee_msat - < funding.counterparty_selected_channel_reserve_satoshis.unwrap() * 1000 - + next_local_commitment_stats.commit_tx_fee_sat * 1000 - { - return Err(ChannelError::close( - "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value".to_owned() - )); - } - } + // Here we check two things 1) that our local commitment still has at least 1 output + // (particularly relevant in 0-reserve channels), and 2) that the counterparty can + // still afford the fee on our commitment if they are the funder. + let (_local_stats, _local_htlcs) = self + .get_next_local_commitment_stats( + funding, + Some(HTLCAmountDirection { outbound: false, amount_msat: msg.amount_msat }), + include_counterparty_unknown_htlcs, + fee_spike_buffer_htlc, + self.feerate_per_kw, + false, + dust_exposure_limiting_feerate, + ) + .map_err(|()| { + ChannelError::close(String::from("Balance exhausted on local commitment")) + })?; Ok(()) } @@ -5020,64 +5884,59 @@ impl<SP: SignerProvider> ChannelContext<SP> { // Do not include outbound update_add_htlc's in the holding cell, or those which haven't yet been ACK'ed // by the counterparty (ie. LocalAnnounced HTLCs) let include_counterparty_unknown_htlcs = false; - let next_local_commitment_stats = self + let (local_stats, _local_htlcs) = self .get_next_local_commitment_stats( funding, None, include_counterparty_unknown_htlcs, 0, new_feerate_per_kw, + false, dust_exposure_limiting_feerate, ) .map_err(|()| { - ChannelError::close(String::from( - "Balance after HTLCs and anchors exhausted on local commitment", - )) + ChannelError::close(String::from("Funding remote cannot afford proposed new fee")) })?; - next_local_commitment_stats - .get_holder_counterparty_balances_incl_fee_msat() - .and_then(|(_, counterparty_balance_incl_fee_msat)| { - counterparty_balance_incl_fee_msat - .checked_sub(funding.holder_selected_channel_reserve_satoshis * 1000) - .ok_or(()) - }) - .map_err(|()| { - ChannelError::close("Funding remote cannot afford proposed new fee".to_owned()) - })?; + local_stats + .commitment_stats + .counterparty_balance_msat + .checked_sub(funding.holder_selected_channel_reserve_satoshis * 1000) + .ok_or(ChannelError::close( + "Funding remote cannot afford proposed new fee".to_owned(), + ))?; - let next_remote_commitment_stats = self + let (remote_stats, _remote_htlcs) = self .get_next_remote_commitment_stats( funding, None, include_counterparty_unknown_htlcs, 0, new_feerate_per_kw, + false, dust_exposure_limiting_feerate, ) .map_err(|()| { - ChannelError::close(String::from( - "Balance after HTLCs and anchors exhausted on remote commitment", - )) + ChannelError::close(String::from("Balance exhausted on remote commitment")) })?; let max_dust_htlc_exposure_msat = self.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); - if next_local_commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { + if local_stats.commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { return Err(ChannelError::close( format!( "Peer sent update_fee with a feerate ({}) which may over-expose us to dust-in-flight on our own transactions (totaling {} msat)", new_feerate_per_kw, - next_local_commitment_stats.dust_exposure_msat, + local_stats.commitment_stats.dust_exposure_msat, ) )); } - if next_remote_commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { + if remote_stats.commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { return Err(ChannelError::close( format!( "Peer sent update_fee with a feerate ({}) which may over-expose us to dust-in-flight on our counterparty's transactions (totaling {} msat)", new_feerate_per_kw, - next_remote_commitment_stats.dust_exposure_msat, + remote_stats.commitment_stats.dust_exposure_msat, ) )); } @@ -5105,6 +5964,12 @@ impl<SP: SignerProvider> ChannelContext<SP> { let commitment_txid = { let trusted_tx = commitment_data.tx.trust(); let bitcoin_tx = trusted_tx.built_transaction(); + if bitcoin_tx.transaction.output.is_empty() { + return Err(ChannelError::close( + "Commitment tx from peer has 0 outputs".to_owned(), + )); + } + let sighash = bitcoin_tx.get_sighash_all(&funding_script, funding.get_value_satoshis()); log_trace!(logger, "Checking commitment tx signature {} by key {} against tx {} (sighash {}) with redeemscript {} in channel {}", @@ -5203,7 +6068,6 @@ impl<SP: SignerProvider> ChannelContext<SP> { ); self.holder_signer - .as_ref() .validate_holder_commitment( &holder_commitment_tx, commitment_data.outbound_htlc_preimages, @@ -5223,27 +6087,28 @@ impl<SP: SignerProvider> ChannelContext<SP> { // Include outbound update_add_htlc's in the holding cell, and those which haven't yet been ACK'ed by // the counterparty (ie. LocalAnnounced HTLCs) let include_counterparty_unknown_htlcs = true; - let next_remote_commitment_stats = if let Ok(stats) = self.get_next_remote_commitment_stats( - funding, - None, - include_counterparty_unknown_htlcs, - CONCURRENT_INBOUND_HTLC_FEE_BUFFER as usize, - feerate_per_kw, - dust_exposure_limiting_feerate, - ) { + let (remote_stats, _remote_htlcs) = if let Ok(stats) = self + .get_next_remote_commitment_stats( + funding, + None, + include_counterparty_unknown_htlcs, + CONCURRENT_INBOUND_HTLC_FEE_BUFFER as usize, + feerate_per_kw, + false, + dust_exposure_limiting_feerate, + ) { stats } else { log_debug!( logger, - "Cannot afford to send new feerate due to balance after HTLCs and anchors exhausted on remote commitment", + "Cannot afford to send new feerate due to balance exhausted on remote commitment", ); return false; }; // Note that `stats.commit_tx_fee_sat` accounts for any HTLCs that transition from non-dust to dust // under a higher feerate (in the case where HTLC-transactions pay endogenous fees). - if next_remote_commitment_stats.holder_balance_before_fee_msat - < next_remote_commitment_stats.commit_tx_fee_sat * 1000 - + funding.counterparty_selected_channel_reserve_satoshis.unwrap() * 1000 + if remote_stats.commitment_stats.holder_balance_msat + < funding.counterparty_selected_channel_reserve_satoshis.unwrap() * 1000 { //TODO: auto-close after a number of failures? log_debug!(logger, "Cannot afford to send new feerate at {}", feerate_per_kw); @@ -5254,7 +6119,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { // `feerate_per_kw`. let max_dust_htlc_exposure_msat = self.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); - if next_remote_commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { + if remote_stats.commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { log_debug!( logger, "Cannot afford to send new feerate at {} without infringing max dust htlc exposure", @@ -5263,23 +6128,24 @@ impl<SP: SignerProvider> ChannelContext<SP> { return false; } - let next_local_commitment_stats = if let Ok(stats) = self.get_next_local_commitment_stats( + let (local_stats, _local_htlcs) = if let Ok(stats) = self.get_next_local_commitment_stats( funding, None, include_counterparty_unknown_htlcs, CONCURRENT_INBOUND_HTLC_FEE_BUFFER as usize, feerate_per_kw, + false, dust_exposure_limiting_feerate, ) { stats } else { log_debug!( logger, - "Cannot afford to send new feerate due to balance after HTLCs and anchors exhausted on local commitment", + "Cannot afford to send new feerate due to balance exhausted on local commitment", ); return false; }; - if next_local_commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { + if local_stats.commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { log_debug!( logger, "Cannot afford to send new feerate at {} without infringing max dust htlc exposure", @@ -5309,69 +6175,86 @@ impl<SP: SignerProvider> ChannelContext<SP> { cmp::max(self.feerate_per_kw, self.pending_update_fee.map(|(fee, _)| fee).unwrap_or(0)); // A `None` `HTLCCandidate` is used as in this case because we're already accounting for // the incoming HTLC as it has been fully committed by both sides. - let next_local_commitment_stats = self + let (local_stats, _local_htlcs) = self .get_next_local_commitment_stats( funding, None, include_counterparty_unknown_htlcs, fee_spike_buffer_htlc, feerate, + false, dust_exposure_limiting_feerate, ) .map_err(|()| { - log_trace!(logger, "Attempting to fail HTLC due to balance after HTLCs and anchors exhausted on local commitment"); + log_trace!( + logger, + "Attempting to fail HTLC due to balance exhausted on local commitment" + ); LocalHTLCFailureReason::ChannelBalanceOverdrawn })?; - let next_remote_commitment_stats = self + let (remote_stats, _remote_htlcs) = self .get_next_remote_commitment_stats( funding, None, include_counterparty_unknown_htlcs, fee_spike_buffer_htlc, feerate, + false, dust_exposure_limiting_feerate, ) .map_err(|()| { - log_trace!(logger, "Attempting to fail HTLC due to balance after HTLCs and anchors exhausted on remote commitment"); + log_trace!( + logger, + "Attempting to fail HTLC due to balance exhausted on remote commitment" + ); LocalHTLCFailureReason::ChannelBalanceOverdrawn })?; let max_dust_htlc_exposure_msat = self.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); - if next_remote_commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { + if remote_stats.commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { // Note that the total dust exposure includes both the dust HTLCs and the excess mining fees of // the counterparty commitment transaction log_info!( logger, "Cannot accept value that would put our total dust exposure at {} over the limit {} on counterparty commitment tx", - next_remote_commitment_stats.dust_exposure_msat, + remote_stats.commitment_stats.dust_exposure_msat, max_dust_htlc_exposure_msat, ); return Err(LocalHTLCFailureReason::DustLimitCounterparty); } - if next_local_commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { + if local_stats.commitment_stats.dust_exposure_msat > max_dust_htlc_exposure_msat { log_info!( logger, "Cannot accept value that would put our exposure to dust HTLCs at {} over the limit {} on holder commitment tx", - next_local_commitment_stats.dust_exposure_msat, + local_stats.commitment_stats.dust_exposure_msat, max_dust_htlc_exposure_msat, ); return Err(LocalHTLCFailureReason::DustLimitHolder); } if !funding.is_outbound() { - let mut remote_fee_incl_fee_spike_buffer_htlc_msat = - next_remote_commitment_stats.commit_tx_fee_sat * 1000; // Note that with anchor outputs we are no longer as sensitive to fee spikes, so we don't need // to account for them. - if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() { - remote_fee_incl_fee_spike_buffer_htlc_msat *= - FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; - } - if next_remote_commitment_stats - .counterparty_balance_before_fee_msat - .saturating_sub(funding.holder_selected_channel_reserve_satoshis * 1000) - < remote_fee_incl_fee_spike_buffer_htlc_msat + let (remote_stats, _remote_htlcs) = self + .get_next_remote_commitment_stats( + funding, + None, + include_counterparty_unknown_htlcs, + fee_spike_buffer_htlc, + feerate, + true, + dust_exposure_limiting_feerate, + ) + .map_err(|()| { + log_trace!( + logger, + "Attempting to fail HTLC due to balance exhausted on remote commitment" + ); + LocalHTLCFailureReason::FeeSpikeBuffer + })?; + if remote_stats.commitment_stats.counterparty_balance_msat + < funding.holder_selected_channel_reserve_satoshis * 1000 { log_info!( logger, @@ -5490,7 +6373,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { let value_to_self_msat = (funding.value_to_self_msat + value_to_self_claimed_msat).checked_sub(value_to_remote_claimed_msat).unwrap(); - let (tx, stats) = SpecTxBuilder {}.build_commitment_transaction( + let (tx, _stats) = SpecTxBuilder {}.build_commitment_transaction( local, commitment_number, per_commitment_point, @@ -5506,24 +6389,38 @@ impl<SP: SignerProvider> ChannelContext<SP> { { let PredictedNextFee { predicted_feerate, predicted_nondust_htlc_count, predicted_fee_sat } = if local { *funding.next_local_fee.lock().unwrap() } else { *funding.next_remote_fee.lock().unwrap() }; if predicted_feerate == tx.negotiated_feerate_per_kw() && predicted_nondust_htlc_count == tx.nondust_htlcs().len() { - assert_eq!(predicted_fee_sat, stats.commit_tx_fee_sat); + assert_eq!(predicted_fee_sat, _stats.commit_tx_fee_sat); } } #[cfg(debug_assertions)] { // Make sure that the to_self/to_remote is always either past the appropriate // channel_reserve *or* it is making progress towards it. - let mut broadcaster_max_commitment_tx_output = if generated_by_local { - funding.holder_max_commitment_tx_output.lock().unwrap() + let mut broadcaster_prev_commitment_balance = if generated_by_local { + funding.holder_prev_commitment_tx_balance.lock().unwrap() } else { - funding.counterparty_max_commitment_tx_output.lock().unwrap() + funding.counterparty_prev_commitment_tx_balance.lock().unwrap() }; - debug_assert!(broadcaster_max_commitment_tx_output.0 <= stats.local_balance_before_fee_msat || stats.local_balance_before_fee_msat / 1000 >= funding.counterparty_selected_channel_reserve_satoshis.unwrap()); - broadcaster_max_commitment_tx_output.0 = cmp::max(broadcaster_max_commitment_tx_output.0, stats.local_balance_before_fee_msat); - debug_assert!(broadcaster_max_commitment_tx_output.1 <= stats.remote_balance_before_fee_msat || stats.remote_balance_before_fee_msat / 1000 >= funding.holder_selected_channel_reserve_satoshis); - broadcaster_max_commitment_tx_output.1 = cmp::max(broadcaster_max_commitment_tx_output.1, stats.remote_balance_before_fee_msat); - } + // This assumes that once our balance rises above the counterparty selected + // reserve, it never drops below again. But we allow our counterparty to + // push us under our reserve when we are the funder and they add a HTLC, as + // this is really their problem. Hence, we only run this assert in tests. + #[cfg(test)] + if _stats.local_balance_before_fee_msat / 1000 < funding.counterparty_selected_channel_reserve_satoshis.unwrap() { + // If the local balance is below the reserve on this new commitment, it MUST be + // greater than or equal to the one on the previous commitment. + debug_assert!(broadcaster_prev_commitment_balance.0 <= _stats.local_balance_before_fee_msat); + } + broadcaster_prev_commitment_balance.0 = _stats.local_balance_before_fee_msat; + + if _stats.remote_balance_before_fee_msat / 1000 < funding.holder_selected_channel_reserve_satoshis { + // If the remote balance is below the reserve on this new commitment, it MUST be + // greater than or equal to the one on the previous commitment. + debug_assert!(broadcaster_prev_commitment_balance.1 <= _stats.remote_balance_before_fee_msat); + } + broadcaster_prev_commitment_balance.1 = _stats.remote_balance_before_fee_msat; + } // This populates the HTLC-source table with the indices from the HTLCs in the commitment // transaction. @@ -5592,112 +6489,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { self.counterparty_forwarding_info.clone() } - /// Returns a HTLCStats about pending htlcs - #[rustfmt::skip] - fn get_pending_htlc_stats( - &self, funding: &FundingScope, outbound_feerate_update: Option<u32>, - dust_exposure_limiting_feerate: Option<u32>, - ) -> HTLCStats { - let context = self; - - let dust_buffer_feerate = self.get_dust_buffer_feerate(outbound_feerate_update); - let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), dust_buffer_feerate, - ); - - let mut on_holder_tx_dust_exposure_msat = 0; - let mut on_counterparty_tx_dust_exposure_msat = 0; - - let mut on_counterparty_tx_offered_nondust_htlcs = 0; - let mut on_counterparty_tx_accepted_nondust_htlcs = 0; - - let mut pending_inbound_htlcs_value_msat = 0; - - { - let counterparty_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let holder_dust_limit_success_sat = htlc_success_tx_fee_sat + context.holder_dust_limit_satoshis; - for htlc in context.pending_inbound_htlcs.iter() { - pending_inbound_htlcs_value_msat += htlc.amount_msat; - if htlc.amount_msat / 1000 < counterparty_dust_limit_timeout_sat { - on_counterparty_tx_dust_exposure_msat += htlc.amount_msat; - } else { - on_counterparty_tx_offered_nondust_htlcs += 1; - } - if htlc.amount_msat / 1000 < holder_dust_limit_success_sat { - on_holder_tx_dust_exposure_msat += htlc.amount_msat; - } - } - } - - let mut pending_outbound_htlcs_value_msat = 0; - let mut pending_outbound_htlcs = self.pending_outbound_htlcs.len(); - { - let counterparty_dust_limit_success_sat = htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let holder_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; - for htlc in context.pending_outbound_htlcs.iter() { - pending_outbound_htlcs_value_msat += htlc.amount_msat; - if htlc.amount_msat / 1000 < counterparty_dust_limit_success_sat { - on_counterparty_tx_dust_exposure_msat += htlc.amount_msat; - } else { - on_counterparty_tx_accepted_nondust_htlcs += 1; - } - if htlc.amount_msat / 1000 < holder_dust_limit_timeout_sat { - on_holder_tx_dust_exposure_msat += htlc.amount_msat; - } - } - - for update in context.holding_cell_htlc_updates.iter() { - if let &HTLCUpdateAwaitingACK::AddHTLC { ref amount_msat, .. } = update { - pending_outbound_htlcs += 1; - pending_outbound_htlcs_value_msat += amount_msat; - if *amount_msat / 1000 < counterparty_dust_limit_success_sat { - on_counterparty_tx_dust_exposure_msat += amount_msat; - } else { - on_counterparty_tx_accepted_nondust_htlcs += 1; - } - if *amount_msat / 1000 < holder_dust_limit_timeout_sat { - on_holder_tx_dust_exposure_msat += amount_msat; - } - } - } - } - - // Include any mining "excess" fees in the dust calculation - let excess_feerate_opt = outbound_feerate_update - .or(self.pending_update_fee.map(|(fee, _)| fee)) - .unwrap_or(self.feerate_per_kw) - .checked_sub(dust_exposure_limiting_feerate.unwrap_or(0)); - - // Dust exposure is only decoupled from feerate for zero fee commitment channels. - let is_zero_fee_comm = funding.get_channel_type().supports_anchor_zero_fee_commitments(); - debug_assert_eq!(is_zero_fee_comm, dust_exposure_limiting_feerate.is_none()); - if is_zero_fee_comm { - debug_assert_eq!(excess_feerate_opt, Some(0)); - } - - let extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat = excess_feerate_opt.map(|excess_feerate| { - let extra_htlc_commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + 1 + on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - let extra_htlc_htlc_tx_fees_sat = chan_utils::htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + 1, on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - - let commit_tx_fee_sat = SpecTxBuilder {}.commit_tx_fee_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs + on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - let htlc_tx_fees_sat = chan_utils::htlc_tx_fees_sat(excess_feerate, on_counterparty_tx_accepted_nondust_htlcs, on_counterparty_tx_offered_nondust_htlcs, funding.get_channel_type()); - - let extra_htlc_dust_exposure = on_counterparty_tx_dust_exposure_msat + (extra_htlc_commit_tx_fee_sat + extra_htlc_htlc_tx_fees_sat) * 1000; - on_counterparty_tx_dust_exposure_msat += (commit_tx_fee_sat + htlc_tx_fees_sat) * 1000; - extra_htlc_dust_exposure - }); - - HTLCStats { - pending_outbound_htlcs, - pending_inbound_htlcs_value_msat, - pending_outbound_htlcs_value_msat, - on_counterparty_tx_dust_exposure_msat, - extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat, - on_holder_tx_dust_exposure_msat, - } - } - - /// Returns information on all pending inbound HTLCs. + /// Returns information on all pending inbound HTLCs. #[rustfmt::skip] pub fn get_pending_inbound_htlc_details(&self, funding: &FundingScope) -> Vec<InboundHTLCDetails> { let mut holding_cell_states = new_hash_map(); @@ -5751,7 +6543,6 @@ impl<SP: SignerProvider> ChannelContext<SP> { #[rustfmt::skip] pub fn get_pending_outbound_htlc_details(&self, funding: &FundingScope) -> Vec<OutboundHTLCDetails> { let mut outbound_details = Vec::new(); - let dust_buffer_feerate = self.get_dust_buffer_feerate(None); let (_, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( funding.get_channel_type(), dust_buffer_feerate, @@ -5766,6 +6557,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { skimmed_fee_msat: htlc.skimmed_fee_msat, state: Some((&htlc.state).into()), is_dust: htlc.amount_msat / 1000 < holder_dust_limit_timeout_sat, + source: Some(htlc.source.to_outbound()), }); } for holding_cell_update in self.holding_cell_htlc_updates.iter() { @@ -5774,6 +6566,7 @@ impl<SP: SignerProvider> ChannelContext<SP> { cltv_expiry, payment_hash, skimmed_fee_msat, + ref source, .. } = *holding_cell_update { outbound_details.push(OutboundHTLCDetails{ @@ -5784,321 +6577,62 @@ impl<SP: SignerProvider> ChannelContext<SP> { skimmed_fee_msat: skimmed_fee_msat, state: Some(OutboundHTLCStateDetails::AwaitingRemoteRevokeToAdd), is_dust: amount_msat / 1000 < holder_dust_limit_timeout_sat, + source: Some(source.to_outbound()), }); } } outbound_details } - #[rustfmt::skip] fn get_available_balances_for_scope<F: FeeEstimator>( &self, funding: &FundingScope, fee_estimator: &LowerBoundedFeeEstimator<F>, - ) -> AvailableBalances { - let context = &self; - // Note that we have to handle overflow due to the case mentioned in the docs in general - // here. - - let dust_exposure_limiting_feerate = self.get_dust_exposure_limiting_feerate( - &fee_estimator, funding.get_channel_type(), - ); - let htlc_stats = context.get_pending_htlc_stats(funding, None, dust_exposure_limiting_feerate); - - // Subtract any non-HTLC outputs from the local and remote balances - let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = SpecTxBuilder {}.subtract_non_htlc_outputs( - funding.is_outbound(), - funding.value_to_self_msat.saturating_sub(htlc_stats.pending_outbound_htlcs_value_msat), - (funding.get_value_satoshis() * 1000).checked_sub(funding.value_to_self_msat).unwrap().saturating_sub(htlc_stats.pending_inbound_htlcs_value_msat), - funding.get_channel_type(), - ); - - let outbound_capacity_msat = local_balance_before_fee_msat - .saturating_sub( - funding.counterparty_selected_channel_reserve_satoshis.unwrap_or(0) * 1000); - - let mut available_capacity_msat = outbound_capacity_msat; - let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), context.feerate_per_kw, - ); - - if funding.is_outbound() { - // We should mind channel commit tx fee when computing how much of the available capacity - // can be used in the next htlc. Mirrors the logic in send_htlc. - // - // The fee depends on whether the amount we will be sending is above dust or not, - // and the answer will in turn change the amount itself — making it a circular - // dependency. - // This complicates the computation around dust-values, up to the one-htlc-value. - let fee_spike_buffer_htlc = if funding.get_channel_type().supports_anchor_zero_fee_commitments() { - None - } else { - Some(()) - }; - - let real_dust_limit_timeout_sat = real_htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; - let htlc_above_dust = HTLCCandidate::new(real_dust_limit_timeout_sat * 1000, HTLCInitiator::LocalOffered); - let mut max_reserved_commit_tx_fee_msat = context.next_local_commit_tx_fee_msat(&funding, htlc_above_dust, fee_spike_buffer_htlc); - let htlc_dust = HTLCCandidate::new(real_dust_limit_timeout_sat * 1000 - 1, HTLCInitiator::LocalOffered); - let mut min_reserved_commit_tx_fee_msat = context.next_local_commit_tx_fee_msat(&funding, htlc_dust, fee_spike_buffer_htlc); - - if !funding.get_channel_type().supports_anchors_zero_fee_htlc_tx() { - max_reserved_commit_tx_fee_msat *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; - min_reserved_commit_tx_fee_msat *= FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE; - } - - // We will first subtract the fee as if we were above-dust. Then, if the resulting - // value ends up being below dust, we have this fee available again. In that case, - // match the value to right-below-dust. - let mut capacity_minus_commitment_fee_msat: i64 = available_capacity_msat as i64 - - max_reserved_commit_tx_fee_msat as i64; - if capacity_minus_commitment_fee_msat < (real_dust_limit_timeout_sat as i64) * 1000 { - let one_htlc_difference_msat = max_reserved_commit_tx_fee_msat - min_reserved_commit_tx_fee_msat; - debug_assert!(one_htlc_difference_msat != 0); - capacity_minus_commitment_fee_msat += one_htlc_difference_msat as i64; - capacity_minus_commitment_fee_msat = cmp::min(real_dust_limit_timeout_sat as i64 * 1000 - 1, capacity_minus_commitment_fee_msat); - available_capacity_msat = cmp::max(0, cmp::min(capacity_minus_commitment_fee_msat, available_capacity_msat as i64)) as u64; - } else { - available_capacity_msat = capacity_minus_commitment_fee_msat as u64; - } - } else { - // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure - // sending a new HTLC won't reduce their balance below our reserve threshold. - let real_dust_limit_success_sat = real_htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let htlc_above_dust = HTLCCandidate::new(real_dust_limit_success_sat * 1000, HTLCInitiator::LocalOffered); - let max_reserved_commit_tx_fee_msat = context.next_remote_commit_tx_fee_msat(funding, Some(htlc_above_dust), None); - - let holder_selected_chan_reserve_msat = funding.holder_selected_channel_reserve_satoshis * 1000; - if remote_balance_before_fee_msat < max_reserved_commit_tx_fee_msat + holder_selected_chan_reserve_msat { - // If another HTLC's fee would reduce the remote's balance below the reserve limit - // we've selected for them, we can only send dust HTLCs. - available_capacity_msat = cmp::min(available_capacity_msat, real_dust_limit_success_sat * 1000 - 1); - } - } - - let mut next_outbound_htlc_minimum_msat = context.counterparty_htlc_minimum_msat; - - // If we get close to our maximum dust exposure, we end up in a situation where we can send - // between zero and the remaining dust exposure limit remaining OR above the dust limit. - // Because we cannot express this as a simple min/max, we prefer to tell the user they can - // send above the dust limit (as the router can always overpay to meet the dust limit). - let mut remaining_msat_below_dust_exposure_limit = None; - let mut dust_exposure_dust_limit_msat = 0; - let max_dust_htlc_exposure_msat = context.get_max_dust_htlc_exposure_msat(dust_exposure_limiting_feerate); - - let dust_buffer_feerate = self.get_dust_buffer_feerate(None); - let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), dust_buffer_feerate, - ); - let buffer_dust_limit_success_sat = buffer_htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let buffer_dust_limit_timeout_sat = buffer_htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; - - if let Some(extra_htlc_dust_exposure) = htlc_stats.extra_nondust_htlc_on_counterparty_tx_dust_exposure_msat { - if extra_htlc_dust_exposure > max_dust_htlc_exposure_msat { - // If adding an extra HTLC would put us over the dust limit in total fees, we cannot - // send any non-dust HTLCs. - available_capacity_msat = cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); - } - } - - if htlc_stats.on_counterparty_tx_dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) > max_dust_htlc_exposure_msat.saturating_add(1) { - // Note that we don't use the `counterparty_tx_dust_exposure` (with - // `htlc_dust_exposure_msat`) here as it only applies to non-dust HTLCs. - remaining_msat_below_dust_exposure_limit = - Some(max_dust_htlc_exposure_msat.saturating_sub(htlc_stats.on_counterparty_tx_dust_exposure_msat)); - dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_success_sat * 1000); - } - - if htlc_stats.on_holder_tx_dust_exposure_msat as i64 + buffer_dust_limit_timeout_sat as i64 * 1000 - 1 > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) { - remaining_msat_below_dust_exposure_limit = Some(cmp::min( - remaining_msat_below_dust_exposure_limit.unwrap_or(u64::max_value()), - max_dust_htlc_exposure_msat.saturating_sub(htlc_stats.on_holder_tx_dust_exposure_msat))); - dust_exposure_dust_limit_msat = cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_timeout_sat * 1000); - } - - if let Some(remaining_limit_msat) = remaining_msat_below_dust_exposure_limit { - if available_capacity_msat < dust_exposure_dust_limit_msat { - available_capacity_msat = cmp::min(available_capacity_msat, remaining_limit_msat); - } else { - next_outbound_htlc_minimum_msat = cmp::max(next_outbound_htlc_minimum_msat, dust_exposure_dust_limit_msat); - } - } - - available_capacity_msat = cmp::min(available_capacity_msat, - context.counterparty_max_htlc_value_in_flight_msat - htlc_stats.pending_outbound_htlcs_value_msat); - - if htlc_stats.pending_outbound_htlcs + 1 > context.counterparty_max_accepted_htlcs as usize { - available_capacity_msat = 0; - } - - #[allow(deprecated)] // TODO: Remove once balance_msat is removed. - AvailableBalances { - inbound_capacity_msat: remote_balance_before_fee_msat.saturating_sub(funding.holder_selected_channel_reserve_satoshis * 1000), - outbound_capacity_msat, - next_outbound_htlc_limit_msat: available_capacity_msat, - next_outbound_htlc_minimum_msat, - } - } - - /// Get the commitment tx fee for the local's (i.e. our) next commitment transaction based on the - /// number of pending HTLCs that are on track to be in our next commitment tx. - /// - /// Includes the `HTLCCandidate` given by `htlc` and an additional non-dust HTLC if - /// `fee_spike_buffer_htlc` is `Some`. - /// - /// The first extra HTLC is useful for determining whether we can accept a further HTLC, the - /// second allows for creating a buffer to ensure a further HTLC can always be accepted/added. - /// - /// Dust HTLCs are excluded. - #[rustfmt::skip] - fn next_local_commit_tx_fee_msat( - &self, funding: &FundingScope, htlc: HTLCCandidate, fee_spike_buffer_htlc: Option<()>, - ) -> u64 { - let context = self; - assert!(funding.is_outbound()); - - if funding.get_channel_type().supports_anchor_zero_fee_commitments() { - debug_assert_eq!(context.feerate_per_kw, 0); - debug_assert!(fee_spike_buffer_htlc.is_none()); - return 0; - } - - let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), context.feerate_per_kw, - ); - let real_dust_limit_success_sat = htlc_success_tx_fee_sat + context.holder_dust_limit_satoshis; - let real_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.holder_dust_limit_satoshis; - - let mut addl_htlcs = 0; - if fee_spike_buffer_htlc.is_some() { addl_htlcs += 1; } - match htlc.origin { - HTLCInitiator::LocalOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_timeout_sat { - addl_htlcs += 1; - } - }, - HTLCInitiator::RemoteOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_success_sat { - addl_htlcs += 1; - } - } - } - - let mut included_htlcs = 0; - for ref htlc in context.pending_inbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_success_sat { - continue - } - // We include LocalRemoved HTLCs here because we may still need to broadcast a commitment - // transaction including this HTLC if it times out before they RAA. - included_htlcs += 1; - } - - for ref htlc in context.pending_outbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_timeout_sat { - continue - } - match htlc.state { - OutboundHTLCState::LocalAnnounced {..} => included_htlcs += 1, - OutboundHTLCState::Committed => included_htlcs += 1, - OutboundHTLCState::RemoteRemoved {..} => included_htlcs += 1, - // We don't include AwaitingRemoteRevokeToRemove HTLCs because our next commitment - // transaction won't be generated until they send us their next RAA, which will mean - // dropping any HTLCs in this state. - _ => {}, - } - } - - for htlc in context.holding_cell_htlc_updates.iter() { - match htlc { - &HTLCUpdateAwaitingACK::AddHTLC { amount_msat, .. } => { - if amount_msat / 1000 < real_dust_limit_timeout_sat { - continue - } - included_htlcs += 1 - }, - _ => {}, // Don't include claims/fails that are awaiting ack, because once we get the - // ack we're guaranteed to never include them in commitment txs anymore. - } - } - - let num_htlcs = included_htlcs + addl_htlcs; - SpecTxBuilder {}.commit_tx_fee_sat(context.feerate_per_kw, num_htlcs, funding.get_channel_type()) * 1000 - } - - /// Get the commitment tx fee for the remote's next commitment transaction based on the number of - /// pending HTLCs that are on track to be in their next commitment tx - /// - /// Optionally includes the `HTLCCandidate` given by `htlc` and an additional non-dust HTLC if - /// `fee_spike_buffer_htlc` is `Some`. - /// - /// The first extra HTLC is useful for determining whether we can accept a further HTLC, the - /// second allows for creating a buffer to ensure a further HTLC can always be accepted/added. - /// - /// Dust HTLCs are excluded. - #[rustfmt::skip] - fn next_remote_commit_tx_fee_msat( - &self, funding: &FundingScope, htlc: Option<HTLCCandidate>, fee_spike_buffer_htlc: Option<()>, - ) -> u64 { - let context = self; - assert!(!funding.is_outbound()); - - if funding.get_channel_type().supports_anchor_zero_fee_commitments() { - debug_assert_eq!(context.feerate_per_kw, 0); - debug_assert!(fee_spike_buffer_htlc.is_none()); - return 0 - } - - debug_assert!(htlc.is_some() || fee_spike_buffer_htlc.is_some(), "At least one of the options must be set"); - - let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat( - funding.get_channel_type(), context.feerate_per_kw, - ); - let real_dust_limit_success_sat = htlc_success_tx_fee_sat + context.counterparty_dust_limit_satoshis; - let real_dust_limit_timeout_sat = htlc_timeout_tx_fee_sat + context.counterparty_dust_limit_satoshis; - - let mut addl_htlcs = 0; - if fee_spike_buffer_htlc.is_some() { addl_htlcs += 1; } - if let Some(htlc) = &htlc { - match htlc.origin { - HTLCInitiator::LocalOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_success_sat { - addl_htlcs += 1; - } - }, - HTLCInitiator::RemoteOffered => { - if htlc.amount_msat / 1000 >= real_dust_limit_timeout_sat { - addl_htlcs += 1; - } - } - } - } + ) -> Result<AvailableBalances, ()> { + let htlc_candidate = None; + let include_counterparty_unknown_htlcs = true; + let addl_nondust_htlc_count = 0; + let dust_exposure_limiting_feerate = + self.get_dust_exposure_limiting_feerate(&fee_estimator, funding.get_channel_type()); - // When calculating the set of HTLCs which will be included in their next commitment_signed, all - // non-dust inbound HTLCs are included (as all states imply it will be included) and only - // committed outbound HTLCs, see below. - let mut included_htlcs = 0; - for ref htlc in context.pending_inbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_timeout_sat { - continue - } - included_htlcs += 1; - } + let balances = self + .get_next_remote_commitment_stats( + funding, + htlc_candidate, + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + self.feerate_per_kw, + false, + dust_exposure_limiting_feerate, + ) + .map(|(remote_stats, _)| remote_stats.available_balances)?; - for ref htlc in context.pending_outbound_htlcs.iter() { - if htlc.amount_msat / 1000 < real_dust_limit_success_sat { - continue - } - // We only include outbound HTLCs if it will not be included in their next commitment_signed, - // i.e. if they've responded to us with an RAA after announcement. - match htlc.state { - OutboundHTLCState::Committed => included_htlcs += 1, - OutboundHTLCState::RemoteRemoved {..} => included_htlcs += 1, - OutboundHTLCState::LocalAnnounced { .. } => included_htlcs += 1, - _ => {}, - } + #[cfg(debug_assertions)] + if balances.next_outbound_htlc_limit_msat >= balances.next_outbound_htlc_minimum_msat + && balances.next_outbound_htlc_limit_msat != 0 + { + let (remote_stats, _remote_htlcs) = self + .get_next_remote_commitment_stats( + funding, + Some(HTLCAmountDirection { + outbound: true, + // Note that this likely creates a non-dust HTLC, we could add a check for the + // biggest dust HTLC to make sure we still have a broadcastable commitment in + // that case. + amount_msat: balances.next_outbound_htlc_limit_msat, + }), + include_counterparty_unknown_htlcs, + addl_nondust_htlc_count, + self.feerate_per_kw, + false, + dust_exposure_limiting_feerate, + ) + .unwrap(); + assert!( + remote_stats.commitment_stats.holder_balance_msat + >= funding.counterparty_selected_channel_reserve_satoshis.unwrap_or(0) * 1000 + ); } - let num_htlcs = included_htlcs + addl_htlcs; - SpecTxBuilder {}.commit_tx_fee_sat(context.feerate_per_kw, num_htlcs, funding.get_channel_type()) * 1000 + Ok(balances) } #[rustfmt::skip] @@ -6282,15 +6816,16 @@ impl<SP: SignerProvider> ChannelContext<SP> { &self.channel_id(), counterparty_initial_bitcoin_tx.txid, encode::serialize_hex(&counterparty_initial_bitcoin_tx.transaction)); // We sign "counterparty" commitment transaction, allowing them to broadcast the tx if they wish. - let signature = match &self.holder_signer { - // TODO (arik): move match into calling method for Taproot - ChannelSignerType::Ecdsa(ecdsa) => ecdsa.sign_counterparty_commitment( - channel_parameters, &counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &self.secp_ctx - ).ok(), - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!() - }; + let signature = self + .holder_signer + .sign_counterparty_commitment( + channel_parameters, + &counterparty_initial_commitment_tx, + Vec::new(), + Vec::new(), + &self.secp_ctx, + ) + .ok(); if signature.is_some() && self.signer_pending_funding { log_trace!(logger, "Counterparty commitment signature available for funding_signed message; clearing signer_pending_funding"); @@ -6303,25 +6838,21 @@ impl<SP: SignerProvider> ChannelContext<SP> { signature.map(|(signature, _)| msgs::FundingSigned { channel_id: self.channel_id(), signature, - #[cfg(taproot)] - partial_signature_with_nonce: None, }) } /// If we receive an error message when attempting to open a channel, it may only be a rejection /// of the channel type we tried, not of our ability to open any channel at all. We can see if a /// downgrade of channel features would be possible so that we can still open the channel. - #[rustfmt::skip] pub(crate) fn maybe_downgrade_channel_features<F: FeeEstimator>( &mut self, funding: &mut FundingScope, fee_estimator: &LowerBoundedFeeEstimator<F>, user_config: &UserConfig, their_features: &InitFeatures, ) -> Result<(), ()> { - if !funding.is_outbound() || - !matches!( + if !funding.is_outbound() + || !matches!( self.channel_state, ChannelState::NegotiatingFunding(flags) if flags == NegotiatingFundingFlags::OUR_INIT_SENT - ) - { + ) { return Err(()); } if funding.get_channel_type() == &ChannelTypeFeatures::only_static_remote_key() { @@ -6352,11 +6883,17 @@ impl<SP: SignerProvider> ChannelContext<SP> { } let next_channel_type = get_initial_channel_type(user_config, &eligible_features); + if !next_channel_type.supports_anchors_zero_fee_htlc_tx() + && !next_channel_type.supports_anchor_zero_fee_commitments() + && funding.holder_selected_channel_reserve_satoshis == 0 + { + // 0-reserve is not allowed on legacy channels + return Err(()); + } - self.feerate_per_kw = selected_commitment_sat_per_1000_weight( - &fee_estimator, &next_channel_type, - ); - funding.channel_transaction_parameters.channel_type_features = next_channel_type; + self.feerate_per_kw = + selected_commitment_sat_per_1000_weight(&fee_estimator, &next_channel_type); + funding.channel_transaction_parameters.channel_type_features = next_channel_type; Ok(()) } @@ -6398,24 +6935,16 @@ impl<SP: SignerProvider> ChannelContext<SP> { logger, ); let counterparty_initial_commitment_tx = commitment_data.tx; - match self.holder_signer { - // TODO (taproot|arik): move match into calling method for Taproot - ChannelSignerType::Ecdsa(ref ecdsa) => { - let channel_parameters = &funding.channel_transaction_parameters; - ecdsa - .sign_counterparty_commitment( - channel_parameters, - &counterparty_initial_commitment_tx, - Vec::new(), - Vec::new(), - &self.secp_ctx, - ) - .ok() - }, - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!(), - } + let channel_parameters = &funding.channel_transaction_parameters; + self.holder_signer + .sign_counterparty_commitment( + channel_parameters, + &counterparty_initial_commitment_tx, + Vec::new(), + Vec::new(), + &self.secp_ctx, + ) + .ok() } fn get_initial_commitment_signed_v2<L: Logger>( @@ -6434,8 +6963,6 @@ impl<SP: SignerProvider> ChannelContext<SP> { htlc_signatures, signature, funding_txid: funding.get_funding_txo().map(|funding_txo| funding_txo.txid), - #[cfg(taproot)] - partial_signature_with_nonce: None, }) } else { log_debug!( @@ -6550,24 +7077,41 @@ impl<SP: SignerProvider> ChannelContext<SP> { /// Returns the value to use for `holder_max_htlc_value_in_flight_msat` as a percentage of the /// `channel_value_satoshis` in msat, set through -/// [`ChannelHandshakeConfig::max_inbound_htlc_value_in_flight_percent_of_channel`] +/// [`ChannelHandshakeConfig::announced_channel_max_inbound_htlc_value_in_flight_percentage`] +/// or [`ChannelHandshakeConfig::unannounced_channel_max_inbound_htlc_value_in_flight_percentage`] +/// depending on the value of [`ChannelHandshakeConfig::announce_for_forwarding`]. /// /// The effective percentage is lower bounded by 1% and upper bounded by 100%. /// -/// [`ChannelHandshakeConfig::max_inbound_htlc_value_in_flight_percent_of_channel`]: crate::util::config::ChannelHandshakeConfig::max_inbound_htlc_value_in_flight_percent_of_channel +/// [`ChannelHandshakeConfig::announced_channel_max_inbound_htlc_value_in_flight_percentage`]: crate::util::config::ChannelHandshakeConfig::announced_channel_max_inbound_htlc_value_in_flight_percentage +/// [`ChannelHandshakeConfig::unannounced_channel_max_inbound_htlc_value_in_flight_percentage`]: crate::util::config::ChannelHandshakeConfig::unannounced_channel_max_inbound_htlc_value_in_flight_percentage +/// [`ChannelHandshakeConfig::announce_for_forwarding`]: crate::util::config::ChannelHandshakeConfig::announce_for_forwarding fn get_holder_max_htlc_value_in_flight_msat( - channel_value_satoshis: u64, config: &ChannelHandshakeConfig, + channel_value_satoshis: u64, is_announced_channel: bool, config: &ChannelHandshakeConfig, ) -> u64 { - let configured_percent = if config.max_inbound_htlc_value_in_flight_percent_of_channel < 1 { + let config_setting = if is_announced_channel { + config.announced_channel_max_inbound_htlc_value_in_flight_percentage + } else { + config.unannounced_channel_max_inbound_htlc_value_in_flight_percentage + }; + let configured_percent = if config_setting < 1 { 1 - } else if config.max_inbound_htlc_value_in_flight_percent_of_channel > 100 { + } else if config_setting > 100 { 100 } else { - config.max_inbound_htlc_value_in_flight_percent_of_channel as u64 + config_setting as u64 }; channel_value_satoshis * 10 * configured_percent } +/// This is for legacy reasons, present for forward-compatibility. +/// LDK versions older than 0.0.104 don't know how read/handle values other than the legacy +/// percentage from storage. Hence, we use this function to not persist legacy values of +/// `holder_max_htlc_value_in_flight_msat` for channels into storage. +fn get_legacy_default_holder_max_htlc_value_in_flight_msat(channel_value_satoshis: u64) -> u64 { + channel_value_satoshis * 10 * MAX_IN_FLIGHT_PERCENT_LEGACY as u64 +} + /// Returns a minimum channel reserve value the remote needs to maintain, /// required by us according to the configured or default /// [`ChannelHandshakeConfig::their_channel_reserve_proportional_millionths`] @@ -6575,15 +7119,34 @@ fn get_holder_max_htlc_value_in_flight_msat( /// Guaranteed to return a value no larger than channel_value_satoshis /// /// This is used both for outbound and inbound channels and has lower bound -/// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`. +/// of `MIN_THEIR_CHAN_RESERVE_SATOSHIS`, and the `dust_limit_satoshis` of +/// the counterparty. +/// +/// Returns `Err` if `channel_value_satoshis` is smaller than +/// `MIN_THEIR_CHAN_RESERVE_SATOSHIS` or the `dust_limit_satoshis` of the +/// counterparty. pub(crate) fn get_holder_selected_channel_reserve_satoshis( - channel_value_satoshis: u64, config: &UserConfig, -) -> u64 { - let counterparty_chan_reserve_prop_mil = - config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64; + channel_value_satoshis: u64, their_dust_limit_satoshis: u64, config: &UserConfig, + is_0reserve: bool, +) -> Result<u64, ()> { + if channel_value_satoshis < MIN_THEIR_CHAN_RESERVE_SATOSHIS + || channel_value_satoshis < their_dust_limit_satoshis + { + return Err(()); + } + if is_0reserve { + return Ok(0); + } + // As described in the `ChannelHandshakeConfig` docs, we cap this value at 1_000_000. + let counterparty_chan_reserve_prop_mil = cmp::min( + config.channel_handshake_config.their_channel_reserve_proportional_millionths as u64, + 1_000_000, + ); let calculated_reserve = channel_value_satoshis.saturating_mul(counterparty_chan_reserve_prop_mil) / 1_000_000; - cmp::min(channel_value_satoshis, cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS)) + let channel_reserve_satoshis = cmp::max(calculated_reserve, MIN_THEIR_CHAN_RESERVE_SATOSHIS); + let channel_reserve_satoshis = cmp::max(channel_reserve_satoshis, their_dust_limit_satoshis); + Ok(channel_reserve_satoshis) } /// This is for legacy reasons, present for forward-compatibility. @@ -6600,138 +7163,24 @@ pub(crate) fn get_legacy_default_holder_selected_channel_reserve_satoshis( /// Returns a minimum channel reserve value each party needs to maintain, fixed in the spec to a /// default of 1% of the total channel value. /// -/// Guaranteed to return a value no larger than channel_value_satoshis +/// Guaranteed to return a value no larger than `channel_value_satoshis` /// /// This is used both for outbound and inbound channels and has lower bound /// of `dust_limit_satoshis`. -fn get_v2_channel_reserve_satoshis(channel_value_satoshis: u64, dust_limit_satoshis: u64) -> u64 { - // Fixed at 1% of channel value by spec. - let (q, _) = channel_value_satoshis.overflowing_div(100); - cmp::min(channel_value_satoshis, cmp::max(q, dust_limit_satoshis)) -} - -fn check_splice_contribution_sufficient( - contribution: &SpliceContribution, is_initiator: bool, funding_feerate: FeeRate, -) -> Result<SignedAmount, String> { - if contribution.inputs().is_empty() { - let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( - contribution.inputs(), - contribution.outputs(), - is_initiator, - true, // is_splice - funding_feerate.to_sat_per_kwu() as u32, - )); - - let contribution_amount = contribution.net_value(); - contribution_amount - .checked_sub( - estimated_fee.to_signed().expect("fees should never exceed Amount::MAX_MONEY"), - ) - .ok_or(format!( - "{estimated_fee} splice-out amount plus {} fee estimate exceeds the total bitcoin supply", - contribution_amount.unsigned_abs(), - )) - } else { - check_v2_funding_inputs_sufficient( - contribution.value_added(), - contribution.inputs(), - contribution.outputs(), - is_initiator, - true, - funding_feerate.to_sat_per_kwu() as u32, - ) - .map(|_| contribution.net_value()) - } -} - -/// Estimate our part of the fee of the new funding transaction. -#[allow(dead_code)] // TODO(dual_funding): TODO(splicing): Remove allow once used. -#[rustfmt::skip] -fn estimate_v2_funding_transaction_fee( - funding_inputs: &[FundingTxInput], outputs: &[TxOut], is_initiator: bool, is_splice: bool, - funding_feerate_sat_per_1000_weight: u32, -) -> u64 { - let input_weight: u64 = funding_inputs - .iter() - .map(|input| BASE_INPUT_WEIGHT.saturating_add(input.utxo.satisfaction_weight)) - .fold(0, |total_weight, input_weight| total_weight.saturating_add(input_weight)); - - let output_weight: u64 = outputs - .iter() - .map(|txout| txout.weight().to_wu()) - .fold(0, |total_weight, output_weight| total_weight.saturating_add(output_weight)); - - let mut weight = input_weight.saturating_add(output_weight); - - // The initiator pays for all common fields and the shared output in the funding transaction. - if is_initiator { - weight = weight - .saturating_add(TX_COMMON_FIELDS_WEIGHT) - // The weight of the funding output, a P2WSH output - // NOTE: The witness script hash given here is irrelevant as it's a fixed size and we just want - // to calculate the contributed weight, so we use an all-zero hash. - .saturating_add(get_output_weight(&ScriptBuf::new_p2wsh( - &WScriptHash::from_raw_hash(Hash::all_zeros()) - )).to_wu()); - - // The splice initiator pays for the input spending the previous funding output. - if is_splice { - weight = weight - .saturating_add(BASE_INPUT_WEIGHT) - .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT) - .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); - #[cfg(feature = "grind_signatures")] - { - // Guarantees a low R signature - weight -= 1; - } - } +/// +/// Returns `Err` if `channel_value_satoshis` is smaller than `dust_limit_satoshis`. +pub(crate) fn get_v2_channel_reserve_satoshis( + channel_value_satoshis: u64, dust_limit_satoshis: u64, is_0reserve: bool, +) -> Result<u64, ()> { + if channel_value_satoshis < dust_limit_satoshis { + return Err(()); } - - fee_for_weight(funding_feerate_sat_per_1000_weight, weight) -} - -/// Verify that the provided inputs to the funding transaction are enough -/// to cover the intended contribution amount *plus* the proportional fees. -/// Fees are computed using `estimate_v2_funding_transaction_fee`, and contain -/// the fees of the inputs, fees of the inputs weight, and for the initiator, -/// the fees of the common fields as well as the output and extra input weights. -/// Returns estimated (partial) fees as additional information -#[rustfmt::skip] -fn check_v2_funding_inputs_sufficient( - contributed_input_value: Amount, funding_inputs: &[FundingTxInput], outputs: &[TxOut], - is_initiator: bool, is_splice: bool, funding_feerate_sat_per_1000_weight: u32, -) -> Result<Amount, String> { - let estimated_fee = Amount::from_sat(estimate_v2_funding_transaction_fee( - funding_inputs, outputs, is_initiator, is_splice, funding_feerate_sat_per_1000_weight, - )); - - let mut total_input_value = Amount::ZERO; - for FundingTxInput { utxo, .. } in funding_inputs.iter() { - total_input_value = total_input_value.checked_add(utxo.output.value) - .ok_or("Sum of input values is greater than the total bitcoin supply")?; - } - - // If the inputs are enough to cover intended contribution amount, with fees even when - // there is a change output, we are fine. - // If the inputs are less, but enough to cover intended contribution amount, with - // (lower) fees with no change, we are also fine (change will not be generated). - // So it's enough to check considering the lower, no-change fees. - // - // Note: dust limit is not relevant in this check. - // - // TODO(splicing): refine check including the fact wether a change will be added or not. - // Can be done once dual funding preparation is included. - - let minimal_input_amount_needed = contributed_input_value.checked_add(estimated_fee) - .ok_or(format!("{contributed_input_value} contribution plus {estimated_fee} fee estimate exceeds the total bitcoin supply"))?; - if total_input_value < minimal_input_amount_needed { - Err(format!( - "Total input amount {total_input_value} is lower than needed for splice-in contribution {contributed_input_value}, considering fees of {estimated_fee}. Need more inputs.", - )) - } else { - Ok(estimated_fee) + if is_0reserve { + return Ok(0); } + // Fixed at 1% of channel value by spec. + let (q, _) = channel_value_satoshis.overflowing_div(100); + Ok(cmp::max(q, dust_limit_satoshis)) } /// Context for negotiating channels (dual-funded V2 open, splicing) @@ -6752,23 +7201,19 @@ pub(super) struct FundingNegotiationContext { pub shared_funding_input: Option<SharedOwnedInput>, /// The funding inputs we will be contributing to the channel. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. - pub our_funding_inputs: Vec<FundingTxInput>, + pub our_funding_inputs: Vec<ConfirmedUtxo>, /// The funding outputs we will be contributing to the channel. #[allow(dead_code)] // TODO(dual_funding): Remove once contribution to V2 channels is enabled. pub our_funding_outputs: Vec<TxOut>, - /// The change output script. This will be used if needed or -- if not set -- generated using - /// `SignerProvider::get_destination_script`. - #[allow(dead_code)] // TODO(splicing): Remove once splicing is enabled. - pub change_script: Option<ScriptBuf>, } impl FundingNegotiationContext { /// Prepare and start interactive transaction negotiation. /// If error occurs, it is caused by our side, not the counterparty. fn into_interactive_tx_constructor<SP: SignerProvider, ES: EntropySource>( - mut self, context: &ChannelContext<SP>, funding: &FundingScope, signer_provider: &SP, - entropy_source: &ES, holder_node_id: PublicKey, - ) -> Result<InteractiveTxConstructor, NegotiationError> { + self, context: &ChannelContext<SP>, funding: &FundingScope, entropy_source: &ES, + holder_node_id: PublicKey, + ) -> (InteractiveTxConstructor, Option<InteractiveTxMessageSend>) { debug_assert_eq!( self.shared_funding_input.is_some(), funding.channel_transaction_parameters.splice_parent_funding_txid.is_some(), @@ -6780,63 +7225,17 @@ impl FundingNegotiationContext { debug_assert!(matches!(context.channel_state, ChannelState::NegotiatingFunding(_))); } - // Note: For the error case when the inputs are insufficient, it will be handled after - // the `calculate_change_output_value` call below - let shared_funding_output = TxOut { value: Amount::from_sat(funding.get_value_satoshis()), script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), }; - // Optionally add change output - let change_value_opt = if !self.our_funding_inputs.is_empty() { - match calculate_change_output_value( - &self, - self.shared_funding_input.is_some(), - &shared_funding_output.script_pubkey, - context.holder_dust_limit_satoshis, - ) { - Ok(change_value_opt) => change_value_opt, - Err(reason) => { - return Err(self.into_negotiation_error(reason)); - }, - } - } else { - None - }; - - if let Some(change_value) = change_value_opt { - let change_script = if let Some(script) = self.change_script { - script - } else { - match signer_provider.get_destination_script(context.channel_keys_id) { - Ok(script) => script, - Err(_) => { - let reason = AbortReason::InternalError("Error getting change script"); - return Err(self.into_negotiation_error(reason)); - }, - } - }; - let mut change_output = TxOut { value: change_value, script_pubkey: change_script }; - let change_output_weight = get_output_weight(&change_output.script_pubkey).to_wu(); - let change_output_fee = - fee_for_weight(self.funding_feerate_sat_per_1000_weight, change_output_weight); - let change_value_decreased_with_fee = - change_value.to_sat().saturating_sub(change_output_fee); - // Check dust limit again - if change_value_decreased_with_fee > context.holder_dust_limit_satoshis { - change_output.value = Amount::from_sat(change_value_decreased_with_fee); - self.our_funding_outputs.push(change_output); - } - } - let constructor_args = InteractiveTxConstructorArgs { entropy_source, holder_node_id, counterparty_node_id: context.counterparty_node_id, channel_id: context.channel_id(), feerate_sat_per_kw: self.funding_feerate_sat_per_1000_weight, - is_initiator: self.is_initiator, funding_tx_locktime: self.funding_tx_locktime, inputs_to_contribute: self.our_funding_inputs, shared_funding_input: self.shared_funding_input, @@ -6846,26 +7245,19 @@ impl FundingNegotiationContext { ), outputs_to_contribute: self.our_funding_outputs, }; - InteractiveTxConstructor::new(constructor_args) - } - - fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { - let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs(); - NegotiationError { reason, contributed_inputs, contributed_outputs } + if self.is_initiator { + InteractiveTxConstructor::new_for_outbound(constructor_args) + } else { + (InteractiveTxConstructor::new_for_inbound(constructor_args), None) + } } - fn into_contributed_inputs_and_outputs(self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) { - let contributed_inputs = - self.our_funding_inputs.into_iter().map(|input| input.utxo.outpoint).collect(); - let contributed_outputs = self.our_funding_outputs; - (contributed_inputs, contributed_outputs) + fn contributed_inputs(&self) -> impl Iterator<Item = bitcoin::OutPoint> + '_ { + self.our_funding_inputs.iter().map(|input| input.utxo.outpoint) } - fn to_contributed_inputs_and_outputs(&self) -> (Vec<bitcoin::OutPoint>, Vec<TxOut>) { - let contributed_inputs = - self.our_funding_inputs.iter().map(|input| input.utxo.outpoint).collect(); - let contributed_outputs = self.our_funding_outputs.clone(); - (contributed_inputs, contributed_outputs) + fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.our_funding_outputs.iter().map(|output| output.script_pubkey.as_script()) } } @@ -6980,6 +7372,7 @@ pub(super) struct TxCompleteResult { } /// The result of signing a funding transaction negotiated using the interactive-tx protocol. +#[derive(Default)] pub(super) struct FundingTxSigned { /// The initial `commitment_signed` message to send to the counterparty, if necessary. pub commitment_signed: Option<msgs::CommitmentSigned>, @@ -7007,6 +7400,9 @@ pub struct SpliceFundingNegotiated { /// The outpoint of the channel's splice funding transaction. pub funding_txo: bitcoin::OutPoint, + /// Whether the holder contributed local inputs or outputs to the negotiated splice. + pub has_local_contribution: bool, + /// The features that this channel will operate with. pub channel_type: ChannelTypeFeatures, @@ -7016,57 +7412,54 @@ pub struct SpliceFundingNegotiated { /// Information about a splice funding negotiation that has failed. pub struct SpliceFundingFailed { - /// The outpoint of the channel's splice funding transaction, if one was created. - pub funding_txo: Option<bitcoin::OutPoint>, - - /// The features that this channel will operate with, if available. - pub channel_type: Option<ChannelTypeFeatures>, + /// UTXOs spent as inputs contributed to the splice transaction. Excludes inputs already + /// contributed in prior rounds, which may be included in `contribution`. + contributed_inputs: Vec<bitcoin::OutPoint>, - /// UTXOs spent as inputs contributed to the splice transaction. - pub contributed_inputs: Vec<bitcoin::OutPoint>, + /// Outputs contributed to the splice transaction. Excludes outputs already contributed + /// in prior rounds, which may be included in `contribution`. + contributed_outputs: Vec<ScriptBuf>, - /// Outputs contributed to the splice transaction. - pub contributed_outputs: Vec<bitcoin::TxOut>, + /// The funding contribution from the failed round. + contribution: FundingContribution, } -macro_rules! maybe_create_splice_funding_failed { - ($funded_channel: expr, $pending_splice: expr, $get: ident, $contributed_inputs_and_outputs: ident) => {{ - $pending_splice - .and_then(|pending_splice| pending_splice.funding_negotiation.$get()) - .filter(|funding_negotiation| funding_negotiation.is_initiator()) - .map(|funding_negotiation| { - let funding_txo = funding_negotiation - .as_funding() - .and_then(|funding| funding.get_funding_txo()) - .map(|txo| txo.into_bitcoin_outpoint()); - - let channel_type = funding_negotiation - .as_funding() - .map(|funding| funding.get_channel_type().clone()); - - let (contributed_inputs, contributed_outputs) = match funding_negotiation { - FundingNegotiation::AwaitingAck { context, .. } => { - context.$contributed_inputs_and_outputs() - }, - FundingNegotiation::ConstructingTransaction { - interactive_tx_constructor, - .. - } => interactive_tx_constructor.$contributed_inputs_and_outputs(), - FundingNegotiation::AwaitingSignatures { .. } => $funded_channel - .context - .interactive_tx_signing_session - .$get() - .expect("We have a pending splice awaiting signatures") - .$contributed_inputs_and_outputs(), - }; +impl SpliceFundingFailed { + /// Splits into the funding info for `DiscardFunding` (if there are inputs or outputs to + /// discard) and the contribution for `SpliceNegotiationFailed`. + pub(super) fn into_parts(self) -> (Option<FundingInfo>, FundingContribution) { + let funding_info = + if !self.contributed_inputs.is_empty() || !self.contributed_outputs.is_empty() { + Some(FundingInfo::Contribution { + inputs: self.contributed_inputs, + outputs: self.contributed_outputs, + }) + } else { + None + }; + (funding_info, self.contribution) + } +} - SpliceFundingFailed { - funding_txo, - channel_type, - contributed_inputs, - contributed_outputs, - } - }) +macro_rules! splice_funding_failed_for { + ($self: expr, $contribution: expr, $contributed_inputs: ident, $contributed_outputs: ident) => {{ + let contribution = $contribution; + let existing_inputs = + $self.pending_splice.as_ref().into_iter().flat_map(|ps| ps.$contributed_inputs()); + let existing_outputs = + $self.pending_splice.as_ref().into_iter().flat_map(|ps| ps.$contributed_outputs()); + let filtered = + contribution.clone().into_unique_contributions(existing_inputs, existing_outputs); + match filtered { + None => SpliceFundingFailed { + contributed_inputs: vec![], + contributed_outputs: vec![], + contribution, + }, + Some((contributed_inputs, contributed_outputs)) => { + SpliceFundingFailed { contributed_inputs, contributed_outputs, contribution } + }, + } }}; } @@ -7093,29 +7486,31 @@ where shutdown_result } + /// Builds a [`SpliceFundingFailed`] from a contribution, filtering out inputs/outputs + /// that are still committed to a prior splice round. + fn splice_funding_failed_for(&self, contribution: FundingContribution) -> SpliceFundingFailed { + // The contribution was never stored in the pending splice state, so + // `contributed_inputs()` and `contributed_outputs()` return only prior rounds' entries + // for filtering. + splice_funding_failed_for!(self, contribution, contributed_inputs, contributed_outputs) + } + + fn abandon_quiescent_action(&mut self) -> Option<SpliceFundingFailed> { + match self.quiescent_action.take()? { + QuiescentAction::Splice { contribution, .. } => { + Some(self.splice_funding_failed_for(contribution)) + }, + #[cfg(any(test, fuzzing, feature = "_test_utils"))] + QuiescentAction::DoNothing => None, + } + } + fn maybe_fail_splice_negotiation(&mut self) -> Option<SpliceFundingFailed> { if matches!(self.context.channel_state, ChannelState::ChannelReady(_)) { - if self.should_reset_pending_splice_state(false) { + if self.should_reset_pending_splice_state(true) { self.reset_pending_splice_state() } else { - match self.quiescent_action.take() { - Some(QuiescentAction::Splice(instructions)) => { - self.context.channel_state.clear_awaiting_quiescence(); - let (inputs, outputs) = instructions.into_contributed_inputs_and_outputs(); - Some(SpliceFundingFailed { - funding_txo: None, - channel_type: None, - contributed_inputs: inputs, - contributed_outputs: outputs, - }) - }, - #[cfg(any(test, fuzzing))] - Some(quiescent_action) => { - self.quiescent_action = Some(quiescent_action); - None - }, - None => None, - } + self.abandon_quiescent_action() } } else { None @@ -7139,12 +7534,15 @@ where }) } - fn pending_funding(&self) -> &[FundingScope] { - if let Some(pending_splice) = &self.pending_splice { - pending_splice.negotiated_candidates.as_slice() - } else { - &[] - } + fn negotiated_candidates(&self) -> &[NegotiatedCandidate] { + self.pending_splice + .as_ref() + .map(|pending_splice| pending_splice.negotiated_candidates.as_slice()) + .unwrap_or(&[]) + } + + fn pending_funding(&self) -> impl ExactSizeIterator<Item = &FundingScope> + '_ { + self.negotiated_candidates().iter().map(|candidate| &candidate.funding) } fn funding_and_pending_funding_iter_mut(&mut self) -> impl Iterator<Item = &mut FundingScope> { @@ -7153,10 +7551,51 @@ where .as_mut() .map(|pending_splice| pending_splice.negotiated_candidates.as_mut_slice()) .unwrap_or(&mut []) - .iter_mut(), + .iter_mut() + .map(|candidate| &mut candidate.funding), ) } + /// Returns details about any pending splice attempts for inclusion in + /// [`crate::ln::channel_state::ChannelDetails`]. + pub fn pending_splice_details(&self, best_block_height: u32) -> Option<SpliceDetails> { + let mut details = self + .pending_splice + .as_ref() + .map(|pending_splice| pending_splice.to_details(&self.context, best_block_height)); + + // A contribution committed via `funding_contributed` sits in `quiescent_action` until + // quiescence is reached and it begins negotiating; surface it as the last candidate, in a + // `WaitingOn*` status describing what it is waiting on. + if let Some(contribution) = self.queued_funding_contribution() { + // It begins negotiating at the next quiescence if there is no pending candidate or it can + // replace one via RBF; otherwise it must wait for the pending candidate to lock. + let status = if self.pending_splice.is_none() + || self.queued_contribution_can_rbf(contribution) + { + SpliceCandidateStatus::WaitingOnQuiescence + } else { + SpliceCandidateStatus::WaitingOnLock + }; + let candidate = + SpliceCandidateDetails { contribution: Some(contribution.clone()), status }; + match &mut details { + Some(details) => details.candidates.push(candidate), + // No `PendingFunding` yet (a first splice still awaiting quiescence), but the queued + // contribution is still worth surfacing. + None => { + details = Some(SpliceDetails { + candidates: vec![candidate], + confirmed_candidate: None, + received_splice_locked_txid: None, + }); + }, + } + } + + details + } + fn has_pending_splice_awaiting_signatures(&self) -> bool { self.pending_splice .as_ref() @@ -7169,7 +7608,7 @@ where /// Returns a boolean indicating whether we should reset the splice's /// [`PendingFunding::funding_negotiation`]. - fn should_reset_pending_splice_state(&self, counterparty_aborted: bool) -> bool { + fn should_reset_pending_splice_state(&self, allow_resumption: bool) -> bool { self.pending_splice .as_ref() .map(|pending_splice| { @@ -7181,7 +7620,11 @@ where funding_negotiation, FundingNegotiation::AwaitingSignatures { .. } ); - if counterparty_aborted { + if allow_resumption { + // If we want to resume the negotiation after reconnecting, we must be + // in [`FundingNegotiation::AwaitingSignatures`] to not reset our state. + !is_awaiting_signatures + } else { !is_awaiting_signatures || !self .context() @@ -7189,8 +7632,6 @@ where .as_ref() .expect("We have a pending splice awaiting signatures") .has_received_commitment_signed() - } else { - !is_awaiting_signatures } }) .unwrap_or_else(|| { @@ -7204,45 +7645,86 @@ where } fn reset_pending_splice_state(&mut self) -> Option<SpliceFundingFailed> { - debug_assert!(self.should_reset_pending_splice_state(true)); + debug_assert!(self.should_reset_pending_splice_state(false)); + + // Only clear the signing session if the current round is mid-signing. When an earlier + // round completed signing and a later RBF round is in AwaitingAck or + // ConstructingTransaction, the session belongs to the prior round and must be preserved. + let current_is_awaiting_signatures = self + .pending_splice + .as_ref() + .and_then(|ps| ps.funding_negotiation.as_ref()) + .map(|fn_| matches!(fn_, FundingNegotiation::AwaitingSignatures { .. })) + .unwrap_or(false); + if current_is_awaiting_signatures { + debug_assert!( + self.context.interactive_tx_signing_session.is_none() + || !self + .context + .interactive_tx_signing_session + .as_ref() + .expect("We have a pending splice awaiting signatures") + .has_received_commitment_signed() + ); + } + + // Take the funding negotiation and pop the current round's contribution, if any + // (acceptors may not have one). + let pending_splice = self + .pending_splice + .as_mut() + .expect("reset_pending_splice_state requires pending_splice"); debug_assert!( - self.context.interactive_tx_signing_session.is_none() - || !self - .context - .interactive_tx_signing_session - .as_ref() - .expect("We have a pending splice awaiting signatures") - .has_received_commitment_signed() + pending_splice.funding_negotiation.is_some(), + "reset_pending_splice_state requires an active funding negotiation" ); + pending_splice.funding_negotiation.take(); + let contribution = pending_splice.negotiation_contribution.take(); + if let Some(ref contribution) = contribution { + debug_assert!( + pending_splice + .last_funding_feerate_sat_per_1000_weight + .map(|f| contribution.feerate() > FeeRate::from_sat_per_kwu(f as u64)) + .unwrap_or(true), + "current round's feerate should be greater than the last negotiated feerate", + ); + } - let splice_funding_failed = maybe_create_splice_funding_failed!( - self, - self.pending_splice.as_mut(), - take, - into_contributed_inputs_and_outputs - ); + // With the in-flight contribution taken, `contributed_inputs()` / + // `contributed_outputs()` return only prior rounds' entries for filtering. + let splice_funding_failed = contribution.map(|contribution| { + splice_funding_failed_for!(self, contribution, contributed_inputs, contributed_outputs) + }); - if self.pending_funding().is_empty() { + if self.negotiated_candidates().is_empty() { self.pending_splice.take(); } - self.context.channel_state.clear_quiescent(); - self.context.interactive_tx_signing_session.take(); + self.exit_quiescence(); + if current_is_awaiting_signatures { + self.context.interactive_tx_signing_session.take(); + } splice_funding_failed } pub(super) fn maybe_splice_funding_failed(&self) -> Option<SpliceFundingFailed> { - if !self.should_reset_pending_splice_state(false) { + if !self.should_reset_pending_splice_state(true) { return None; } - maybe_create_splice_funding_failed!( + let pending_splice = self.pending_splice.as_ref()?; + debug_assert!( + pending_splice.funding_negotiation.is_some(), + "maybe_splice_funding_failed requires an active funding negotiation" + ); + let contribution = pending_splice.negotiation_contribution.clone()?; + Some(splice_funding_failed_for!( self, - self.pending_splice.as_ref(), - as_ref, - to_contributed_inputs_and_outputs - ) + contribution, + prior_contributed_inputs, + prior_contributed_outputs + )) } #[rustfmt::skip] @@ -7743,7 +8225,7 @@ where .interactive_tx_signing_session .as_ref() .map(|signing_session| { - signing_session.holder_tx_signatures().is_some() + signing_session.has_holder_witnesses() || signing_session.has_received_tx_signatures() }) .unwrap_or(false); @@ -7762,7 +8244,7 @@ where #[rustfmt::skip] pub fn channel_ready<NS: NodeSigner, L: Logger>( &mut self, msg: &msgs::ChannelReady, node_signer: &NS, chain_hash: ChainHash, - user_config: &UserConfig, best_block: &BestBlock, logger: &L + user_config: &UserConfig, best_block: &BlockLocator, logger: &L ) -> Result<Option<msgs::AnnouncementSignatures>, ChannelError> { if self.context.channel_state.is_peer_disconnected() { self.context.workaround_lnd_bug_4006 = Some(msg.clone()); @@ -7868,7 +8350,7 @@ where } core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) + .chain(self.pending_funding()) .try_for_each(|funding| self.context.validate_update_add_htlc(funding, msg, fee_estimator))?; // Now update local state: @@ -7885,10 +8367,36 @@ where Ok(()) } - /// Useful for reconstructing the set of pending HTLCs when deserializing the `ChannelManager`. - pub(super) fn inbound_committed_unresolved_htlcs( + /// Returns true if any committed inbound HTLCs were received before we started serializing + /// inbound committed payment onions in `Channel` and cannot be used during `ChannelManager` + /// deserialization to reconstruct the set of pending HTLCs. + pub(super) fn has_legacy_inbound_htlcs(&self) -> bool { + self.context.pending_inbound_htlcs.iter().any(|htlc| { + matches!( + &htlc.state, + InboundHTLCState::Committed { update_add_htlc: InboundUpdateAdd::Legacy } + ) + }) + } + + /// Returns committed inbound HTLCs whose onion has not yet been decoded and processed. Useful + /// for reconstructing the set of pending HTLCs when deserializing the `ChannelManager`. + pub(super) fn inbound_htlcs_pending_decode( + &self, + ) -> impl Iterator<Item = msgs::UpdateAddHTLC> + '_ { + self.context.pending_inbound_htlcs.iter().filter_map(|htlc| match &htlc.state { + InboundHTLCState::Committed { + update_add_htlc: InboundUpdateAdd::WithOnion { update_add_htlc }, + } => Some(update_add_htlc.clone()), + _ => None, + }) + } + + /// Returns committed inbound HTLCs that have been forwarded but not yet fully resolved. Useful + /// when reconstructing the set of pending HTLCs when deserializing the `ChannelManager`. + pub(super) fn inbound_forwarded_htlcs( &self, - ) -> Vec<(PaymentHash, InboundUpdateAdd)> { + ) -> impl Iterator<Item = (PaymentHash, HTLCPreviousHopData, OutboundHop)> + '_ { // We don't want to return an HTLC as needing processing if it already has a resolution that's // pending in the holding cell. let htlc_resolution_in_holding_cell = |id: u64| -> bool { @@ -7902,19 +8410,46 @@ where }) }; - self.context - .pending_inbound_htlcs - .iter() - .filter_map(|htlc| match &htlc.state { - InboundHTLCState::Committed { update_add_htlc } => { - if htlc_resolution_in_holding_cell(htlc.htlc_id) { - return None; - } - Some((htlc.payment_hash, update_add_htlc.clone())) - }, - _ => None, - }) - .collect() + let prev_outbound_scid_alias = self.context.outbound_scid_alias(); + let user_channel_id = self.context.get_user_id(); + let channel_id = self.context.channel_id(); + let outpoint = self.funding_outpoint(); + let counterparty_node_id = self.context.get_counterparty_node_id(); + + self.context.pending_inbound_htlcs.iter().filter_map(move |htlc| match &htlc.state { + InboundHTLCState::Committed { + update_add_htlc: + InboundUpdateAdd::Forwarded { + incoming_packet_shared_secret, + phantom_shared_secret, + trampoline_shared_secret, + blinded_failure, + outbound_hop, + }, + } => { + if htlc_resolution_in_holding_cell(htlc.htlc_id) { + return None; + } + // The reconstructed `HTLCPreviousHopData` is used to fail or claim the HTLC backwards + // post-restart, if it is missing in the outbound edge. + let prev_hop_data = HTLCPreviousHopData { + prev_outbound_scid_alias, + user_channel_id: Some(user_channel_id), + amount_msat: Some(htlc.amount_msat), + htlc_id: htlc.htlc_id, + incoming_packet_shared_secret: *incoming_packet_shared_secret, + phantom_shared_secret: *phantom_shared_secret, + trampoline_shared_secret: *trampoline_shared_secret, + blinded_failure: *blinded_failure, + channel_id, + outpoint, + counterparty_node_id: Some(counterparty_node_id), + cltv_expiry: Some(htlc.cltv_expiry), + }; + Some((htlc.payment_hash, prev_hop_data, *outbound_hop)) + }, + _ => None, + }) } /// Useful when reconstructing the set of pending HTLC forwards when deserializing the @@ -7961,12 +8496,19 @@ where /// This inbound HTLC was irrevocably forwarded to the outbound edge, so we no longer need to /// persist its onion. pub(super) fn prune_inbound_htlc_onion( - &mut self, htlc_id: u64, hop_data: HTLCPreviousHopData, outbound_amt_msat: u64, + &mut self, htlc_id: u64, prev_hop_data: &HTLCPreviousHopData, + outbound_hop_data: OutboundHop, ) { for htlc in self.context.pending_inbound_htlcs.iter_mut() { if htlc.htlc_id == htlc_id { if let InboundHTLCState::Committed { ref mut update_add_htlc } = htlc.state { - *update_add_htlc = InboundUpdateAdd::Forwarded { hop_data, outbound_amt_msat }; + *update_add_htlc = InboundUpdateAdd::Forwarded { + incoming_packet_shared_secret: prev_hop_data.incoming_packet_shared_secret, + phantom_shared_secret: prev_hop_data.phantom_shared_secret, + trampoline_shared_secret: prev_hop_data.trampoline_shared_secret, + blinded_failure: prev_hop_data.blinded_failure, + outbound_hop: outbound_hop_data, + }; return; } } @@ -7974,6 +8516,43 @@ where debug_assert!(false, "If we go to prune an inbound HTLC it should be present") } + /// Clears the `hold_htlc` flag for a pending inbound HTLC, returning `true` if the HTLC was + /// successfully released. Useful when a [`ReleaseHeldHtlc`] onion message arrives before the + /// HTLC has been fully committed. + /// + /// [`ReleaseHeldHtlc`]: crate::onion_message::async_payments::ReleaseHeldHtlc + pub(super) fn release_pending_inbound_held_htlc(&mut self, htlc_id: u64) -> bool { + for update_add in self.context.monitor_pending_update_adds.iter_mut() { + if update_add.htlc_id == htlc_id { + update_add.hold_htlc.take(); + return true; + } + } + for htlc in self.context.pending_inbound_htlcs.iter_mut() { + if htlc.htlc_id != htlc_id { + continue; + } + match &mut htlc.state { + // Clearing `hold_htlc` here directly affects the copy that will be cloned into the decode + // pipeline when RAA promotes the HTLC. + InboundHTLCState::RemoteAnnounced(InboundHTLCResolution::Pending { + update_add_htlc, + }) + | InboundHTLCState::AwaitingRemoteRevokeToAnnounce( + InboundHTLCResolution::Pending { update_add_htlc }, + ) + | InboundHTLCState::AwaitingAnnouncedRemoteRevoke( + InboundHTLCResolution::Pending { update_add_htlc }, + ) => { + update_add_htlc.hold_htlc.take(); + return true; + }, + _ => return false, + } + } + false + } + /// Useful for testing crash scenarios where the holding cell is not persisted. #[cfg(test)] pub(super) fn test_clear_holding_cell(&mut self) { @@ -8074,7 +8653,7 @@ where } pub fn initial_commitment_signed_v2<L: Logger>( - &mut self, msg: &msgs::CommitmentSigned, best_block: BestBlock, signer_provider: &SP, + &mut self, msg: &msgs::CommitmentSigned, best_block: BlockLocator, signer_provider: &SP, logger: &L, ) -> Result<ChannelMonitor<SP::EcdsaSigner>, ChannelError> { if let Some(signing_session) = self.context.interactive_tx_signing_session.as_ref() { @@ -8201,6 +8780,12 @@ where ); } + let funding_contribution = self + .pending_splice + .as_ref() + .and_then(|pending_splice| pending_splice.negotiation_contribution.as_ref()) + .cloned(); + log_info!( logger, "Received splice initial commitment_signed from peer with funding txid {}", @@ -8214,6 +8799,7 @@ where channel_parameters: pending_splice_funding.channel_transaction_parameters.clone(), holder_commitment_tx, counterparty_commitment_tx, + funding_contribution, }], channel_id: Some(self.context.channel_id()), }; @@ -8260,11 +8846,20 @@ where ) -> Result<Option<ChannelMonitorUpdate>, ChannelError> { self.commitment_signed_check_state()?; - if !self.pending_funding().is_empty() { + if !self.negotiated_candidates().is_empty() { return Err(ChannelError::close( "Got a single commitment_signed message when expecting a batch".to_owned(), )); } + if let Some(funding_txid) = msg.funding_txid { + let locked_funding_txid = + self.funding.get_funding_txid().expect("funded channel must have known txid"); + if funding_txid != locked_funding_txid { + return Err(ChannelError::Ignore(format!( + "Ignoring commitment_signed for stale funding txid {funding_txid}" + ))); + } + } let transaction_number = self.holder_commitment_point.next_transaction_number(); let commitment_point = self.holder_commitment_point.next_point(); @@ -8328,7 +8923,7 @@ where // pending splice transaction has confirmed since receiving the batch. let mut commitment_txs = Vec::with_capacity(self.pending_funding().len() + 1); let mut htlc_data = None; - for funding in core::iter::once(&self.funding).chain(self.pending_funding().iter()) { + for funding in core::iter::once(&self.funding).chain(self.pending_funding()) { let funding_txid = funding.get_funding_txid().expect("Funding txid must be known for pending scope"); let msg = messages.get(&funding_txid).ok_or_else(|| { @@ -8820,21 +9415,15 @@ where return Err(ChannelError::close("Received an unexpected revoke_and_ack".to_owned())); } - match &self.context.holder_signer { - ChannelSignerType::Ecdsa(ecdsa) => { - ecdsa - .validate_counterparty_revocation( - self.context.counterparty_next_commitment_transaction_number + 1, - &secret, - ) - .map_err(|_| { - ChannelError::close("Failed to validate revocation from peer".to_owned()) - })?; - }, - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!(), - }; + self.context + .holder_signer + .validate_counterparty_revocation( + self.context.counterparty_next_commitment_transaction_number + 1, + &secret, + ) + .map_err(|_| { + ChannelError::close("Failed to validate revocation from peer".to_owned()) + })?; self.context .commitment_secrets @@ -9184,28 +9773,48 @@ where } fn on_tx_signatures_exchange<'a, L: Logger>( - &mut self, funding_tx: Transaction, best_block_height: u32, - logger: &WithChannelContext<'a, L>, - ) -> (Option<SpliceFundingNegotiated>, Option<msgs::SpliceLocked>) { - debug_assert!(!self.context.channel_state.is_monitor_update_in_progress()); - debug_assert!(!self.context.channel_state.is_awaiting_remote_revoke()); + &mut self, funding_tx_signed: &mut FundingTxSigned, funding_tx: Transaction, + best_block_height: u32, logger: &WithChannelContext<'a, L>, + ) { + debug_assert!( + !self.is_awaiting_monitor_update() || !self.context.monitor_pending_tx_signatures + ); + debug_assert!(!self.context.is_waiting_on_peer_pending_channel_update()); if let Some(pending_splice) = self.pending_splice.as_mut() { - self.context.channel_state.clear_quiescent(); - if let Some(FundingNegotiation::AwaitingSignatures { mut funding, .. }) = - pending_splice.funding_negotiation.take() + if let Some(FundingNegotiation::AwaitingSignatures { + mut funding, + funding_feerate_sat_per_1000_weight, + .. + }) = pending_splice.funding_negotiation.take() { - funding.funding_transaction = Some(funding_tx); + funding.funding_transaction = Some(funding_tx.clone()); + pending_splice.last_funding_feerate_sat_per_1000_weight = + Some(funding_feerate_sat_per_1000_weight); let funding_txo = funding.get_funding_txo().expect("funding outpoint should be set"); let channel_type = funding.get_channel_type().clone(); let funding_redeem_script = funding.get_funding_redeemscript(); + let has_local_contribution = self + .context + .interactive_tx_signing_session + .as_ref() + .map(|signing_session| signing_session.has_local_contribution()) + .unwrap_or(false); - pending_splice.negotiated_candidates.push(funding); + let contribution = pending_splice.negotiation_contribution.take(); + pending_splice + .negotiated_candidates + .push(NegotiatedCandidate { funding, contribution }); + debug_assert!( + pending_splice.contributions_form_suffix(), + "a round following one we contributed to must carry our contribution", + ); let splice_negotiated = SpliceFundingNegotiated { funding_txo: funding_txo.into_bitcoin_outpoint(), + has_local_contribution, channel_type, funding_redeem_script, }; @@ -9225,16 +9834,42 @@ where ); } - (Some(splice_negotiated), splice_locked) + let candidates = pending_splice + .negotiated_candidates + .iter() + .map(|candidate| { + let txid = candidate + .funding + .get_funding_txid() + .expect("negotiated candidates should have a funding txid"); + FundingCandidate { + txid, + channels: vec![ChannelFunding { + counterparty_node_id: self.context.counterparty_node_id, + channel_id: self.context.channel_id, + purpose: FundingPurpose::Splice, + contribution: candidate.contribution.clone(), + }], + } + }) + .collect(); + let tx_type = TransactionType::InteractiveFunding { candidates }; + funding_tx_signed.funding_tx = Some((funding_tx, tx_type)); + funding_tx_signed.splice_negotiated = Some(splice_negotiated); + funding_tx_signed.splice_locked = splice_locked; } else { debug_assert!(false); - (None, None) } + + self.exit_quiescence(); } else { - self.funding.funding_transaction = Some(funding_tx); + self.funding.funding_transaction = Some(funding_tx.clone()); self.context.channel_state = ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::new()); - (None, None) + let tx_type = TransactionType::Funding { + channels: vec![(self.context.counterparty_node_id, self.context.channel_id)], + }; + funding_tx_signed.funding_tx = Some((funding_tx, tx_type)); } } @@ -9283,6 +9918,8 @@ where } } + let awaiting_holder_shared_input_signature = + signing_session.awaiting_holder_shared_input_signature(); let (holder_tx_signatures, funding_tx) = signing_session.received_tx_signatures(msg).map_err(|msg| ChannelError::Warn(msg))?; @@ -9293,34 +9930,46 @@ where msg.tx_hash ); - let (splice_negotiated, splice_locked) = if let Some(funding_tx) = funding_tx.clone() { - self.on_tx_signatures_exchange(funding_tx, best_block_height, &logger) - } else { - (None, None) - }; - - let funding_tx = funding_tx.map(|tx| { - let tx_type = if splice_negotiated.is_some() { - TransactionType::Splice { - counterparty_node_id: self.context.counterparty_node_id, - channel_id: self.context.channel_id, - } - } else { - TransactionType::Funding { - channels: vec![(self.context.counterparty_node_id, self.context.channel_id)], - } - }; - (tx, tx_type) - }); - - Ok(FundingTxSigned { + let mut funding_tx_signed = FundingTxSigned { commitment_signed: None, counterparty_initial_commitment_signed_result: None, - tx_signatures: holder_tx_signatures, - funding_tx, - splice_negotiated, - splice_locked, - }) + tx_signatures: None, + funding_tx: None, + splice_negotiated: None, + splice_locked: None, + }; + if self.is_awaiting_monitor_update() && self.context.monitor_pending_tx_signatures { + // Although the user may have already provided our `tx_signatures`, we must not send + // them if we're waiting for the monitor to durably persist the counterparty's signature + // for our initial commitment post-splice. + debug_assert!(holder_tx_signatures.is_some()); + log_debug!( + logger, + "Waiting for async monitor update to complete prior to releasing our tx_signatures" + ); + return Ok(funding_tx_signed); + } + + funding_tx_signed.tx_signatures = holder_tx_signatures; + if let Some(funding_tx) = funding_tx { + self.on_tx_signatures_exchange( + &mut funding_tx_signed, + funding_tx, + best_block_height, + &logger, + ); + } else if awaiting_holder_shared_input_signature { + log_debug!( + logger, + "Waiting for funding transaction shared input signature before finalizing negotiation" + ); + } else { + debug_assert!( + false, + "Signed funding transaction should be available upon tx_signatures exchange" + ); + } + Ok(funding_tx_signed) } /// Queues up an outbound update fee by placing it in the holding cell. You should call @@ -9360,7 +10009,7 @@ where debug_assert!(!self.funding.get_channel_type().supports_anchor_zero_fee_commitments()); let can_send_update_fee = core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) + .chain(self.pending_funding()) .all(|funding| self.context.can_send_update_fee(funding, feerate_per_kw, fee_estimator, logger)); if !can_send_update_fee { return None; @@ -9501,8 +10150,8 @@ where /// successfully and we should restore normal operation. Returns messages which should be sent /// to the remote side. #[rustfmt::skip] - pub fn monitor_updating_restored<L: Logger, NS: NodeSigner, CBP>( - &mut self, logger: &L, node_signer: &NS, chain_hash: ChainHash, + pub fn monitor_updating_restored<'a, L: Logger, NS: NodeSigner, CBP>( + &mut self, logger: &WithChannelContext<'a, L>, node_signer: &NS, chain_hash: ChainHash, user_config: &UserConfig, best_block_height: u32, path_for_release_htlc: CBP ) -> MonitorRestoreUpdates where @@ -9511,28 +10160,47 @@ where assert!(self.context.channel_state.is_monitor_update_in_progress()); self.context.channel_state.clear_monitor_update_in_progress(); assert_eq!(self.blocked_monitor_updates_pending(), 0); + // Some cases below may not strictly require ChannelManager persistence, but we err on + // the conservative side to avoid missing state changes. + let mut requires_channel_manager_persistence = false; + // We want to clear that the monitor update for our `tx_signatures` has completed, but + // we may still need to hold back the message until it's ready to be sent. let mut tx_signatures = self .context .monitor_pending_tx_signatures .then(|| ()) .and_then(|_| self.context.interactive_tx_signing_session.as_ref()) - .and_then(|signing_session| signing_session.holder_tx_signatures().clone()); - if tx_signatures.is_some() { - // We want to clear that the monitor update for our `tx_signatures` has completed, but - // we may still need to hold back the message until it's ready to be sent. - self.context.monitor_pending_tx_signatures = false; - - if self.context.signer_pending_funding { - tx_signatures.take(); - } + .and_then(|signing_session| signing_session.holder_tx_signatures()); + self.context.monitor_pending_tx_signatures = false; + let mut funding_tx_signed = None; + if tx_signatures.is_some() { let signing_session = self.context.interactive_tx_signing_session.as_ref() .expect("We have a tx_signatures message so we must have a valid signing session"); - if !signing_session.holder_sends_tx_signatures_first() - && !signing_session.has_received_tx_signatures() - { + if self.context.signer_pending_funding { tx_signatures.take(); + } else { + debug_assert!(tx_signatures.is_some()); + funding_tx_signed = Some(FundingTxSigned { + commitment_signed: None, + counterparty_initial_commitment_signed_result: None, + tx_signatures, + funding_tx: None, + splice_negotiated: None, + splice_locked: None, + }); + requires_channel_manager_persistence = true; + if let Some(funding_tx) = signing_session.signed_tx() { + self.on_tx_signatures_exchange( + funding_tx_signed.as_mut().unwrap(), + funding_tx, + best_block_height, + logger, + ); + } else if signing_session.has_received_tx_signatures() { + debug_assert!(false, "Signed funding transaction should be available upon tx_signatures exchange"); + } } } @@ -9547,7 +10215,8 @@ where { // Broadcast only if not yet confirmed if self.funding.get_funding_tx_confirmation_height().is_none() { - funding_broadcastable = Some(funding_transaction.clone()) + funding_broadcastable = Some(funding_transaction.clone()); + requires_channel_manager_persistence = true; } } } @@ -9573,20 +10242,27 @@ where assert!(!self.funding.is_outbound() || self.context.minimum_depth == Some(0), "Funding transaction broadcast by the local client before it should have - LDK didn't do it!"); self.context.monitor_pending_channel_ready = false; - self.get_channel_ready(logger) + let channel_ready = self.get_channel_ready(logger); + requires_channel_manager_persistence |= channel_ready.is_some(); + channel_ready } else { None }; let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, best_block_height, logger); + requires_channel_manager_persistence |= announcement_sigs.is_some(); let mut accepted_htlcs = Vec::new(); mem::swap(&mut accepted_htlcs, &mut self.context.monitor_pending_forwards); + requires_channel_manager_persistence |= !accepted_htlcs.is_empty(); let mut failed_htlcs = Vec::new(); mem::swap(&mut failed_htlcs, &mut self.context.monitor_pending_failures); + requires_channel_manager_persistence |= !failed_htlcs.is_empty(); let mut finalized_claimed_htlcs = Vec::new(); mem::swap(&mut finalized_claimed_htlcs, &mut self.context.monitor_pending_finalized_fulfills); + requires_channel_manager_persistence |= !finalized_claimed_htlcs.is_empty(); let mut pending_update_adds = Vec::new(); mem::swap(&mut pending_update_adds, &mut self.context.monitor_pending_update_adds); - let committed_outbound_htlc_sources = self.context.pending_outbound_htlcs.iter().filter_map(|htlc| { + requires_channel_manager_persistence |= !pending_update_adds.is_empty(); + let committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)> = self.context.pending_outbound_htlcs.iter().filter_map(|htlc| { if let &OutboundHTLCState::LocalAnnounced(_) = &htlc.state { if let HTLCSource::PreviousHopData(prev_hop_data) = &htlc.source { return Some((prev_hop_data.clone(), htlc.amount_msat)) @@ -9594,6 +10270,7 @@ where } None }).collect(); + requires_channel_manager_persistence |= !committed_outbound_htlc_sources.is_empty(); if self.context.channel_state.is_peer_disconnected() { self.context.monitor_pending_revoke_and_ack = false; @@ -9601,8 +10278,9 @@ where return MonitorRestoreUpdates { raa: None, commitment_update: None, commitment_order: RAACommitmentOrder::RevokeAndACKFirst, accepted_htlcs, failed_htlcs, finalized_claimed_htlcs, pending_update_adds, - funding_broadcastable, channel_ready, announcement_sigs, tx_signatures: None, - channel_ready_order, committed_outbound_htlc_sources + funding_broadcastable, channel_ready, channel_ready_order, announcement_sigs, + funding_tx_signed, committed_outbound_htlc_sources, + requires_channel_manager_persistence, }; } @@ -9632,8 +10310,9 @@ where match commitment_order { RAACommitmentOrder::CommitmentFirst => "commitment", RAACommitmentOrder::RevokeAndACKFirst => "RAA"}); MonitorRestoreUpdates { raa, commitment_update, commitment_order, accepted_htlcs, failed_htlcs, finalized_claimed_htlcs, - pending_update_adds, funding_broadcastable, channel_ready, announcement_sigs, tx_signatures, - channel_ready_order, committed_outbound_htlc_sources + pending_update_adds, funding_broadcastable, channel_ready, channel_ready_order, + announcement_sigs, funding_tx_signed, committed_outbound_htlc_sources, + requires_channel_manager_persistence, } } @@ -9681,7 +10360,7 @@ where } core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) + .chain(self.pending_funding()) .try_for_each(|funding| FundedChannel::<SP>::check_remote_fee(funding.get_channel_type(), fee_estimator, msg.feerate_per_kw, Some(self.context.feerate_per_kw), logger))?; self.context.pending_update_fee = Some((msg.feerate_per_kw, FeeUpdateState::RemoteAnnounced)); @@ -9693,10 +10372,12 @@ where /// blocked. #[rustfmt::skip] pub fn signer_maybe_unblocked<L: Logger, CBP>( - &mut self, logger: &L, path_for_release_htlc: CBP + &mut self, best_block_height: u32, logger: &L, path_for_release_htlc: CBP ) -> Result<SignerResumeUpdates, ChannelError> where CBP: Fn(u64) -> BlindedMessagePath { if let Some((commitment_number, commitment_secret)) = self.context.signer_pending_stale_state_verification.clone() { - if let Ok(expected_point) = self.context.holder_signer.as_ref() + if let Ok(expected_point) = self + .context + .holder_signer .get_per_commitment_point(commitment_number, &self.context.secp_ctx) { self.context.signer_pending_stale_state_verification.take(); @@ -9747,20 +10428,65 @@ where None }; - let tx_signatures = if funding_commit_sig.is_some() { + let mut shared_input_signature_unblocked = false; + { + if let Some(signing_session) = self.context.interactive_tx_signing_session.as_mut() { + if signing_session.awaiting_holder_shared_input_signature() { + let splice_input_index = signing_session + .unsigned_tx() + .shared_input_index() + .expect("Missing shared input index while awaiting a splice signature"); + log_trace!(logger, "Attempting to generate pending splice shared input signature..."); + if let Ok(shared_input_signature) = self.context.holder_signer.sign_splice_shared_input( + &self.funding.channel_transaction_parameters, + signing_session.unsigned_tx().tx(), + splice_input_index as usize, + &self.context.secp_ctx, + ) { + shared_input_signature_unblocked = true; + signing_session + .provide_holder_shared_input_signature(shared_input_signature) + .map_err(ChannelError::close)?; + } + } + } + } + + let mut tx_signatures = None; + let mut funding_tx = None; + if funding_commit_sig.is_some() || shared_input_signature_unblocked { if let Some(signing_session) = self.context.interactive_tx_signing_session.as_ref() { - let should_send_tx_signatures = signing_session.holder_sends_tx_signatures_first() - || signing_session.has_received_tx_signatures(); - should_send_tx_signatures - .then(|| ()) - .and_then(|_| signing_session.holder_tx_signatures().clone()) + if !self.is_awaiting_monitor_update() && !self.context.signer_pending_funding { + tx_signatures = signing_session.holder_tx_signatures(); + funding_tx = tx_signatures.as_ref().and_then(|_| signing_session.signed_tx()); + } } else { debug_assert!(false); - None } - } else { - None - }; + } + + let mut funding_tx_signed = None; + if funding_commit_sig.is_some() || tx_signatures.is_some() || funding_tx.is_some() { + let mut resumed = FundingTxSigned { + commitment_signed: funding_commit_sig, + counterparty_initial_commitment_signed_result: None, + tx_signatures, + funding_tx: None, + splice_negotiated: None, + splice_locked: None, + }; + if let Some(funding_tx) = funding_tx { + let funding_logger = WithChannelContext::from(logger, &self.context, None); + debug_assert!(resumed.tx_signatures.is_some()); + self.on_tx_signatures_exchange( + &mut resumed, + funding_tx, + best_block_height, + &funding_logger, + ); + } + funding_tx_signed = Some(resumed); + } // Provide a `channel_ready` message if we need to, but only if we're _not_ still pending // funding. @@ -9790,6 +10516,13 @@ where self.context.signer_pending_commitment_update = true; commitment_update = None; } + if revoke_and_ack.is_some() { + // If signer-pending state regenerated an RAA, the monitor update for that RAA was + // already persisted before we set `signer_pending_revoke_and_ack`. Thus, if reconnect + // also marked the same RAA monitor-pending while another monitor update was in flight, + // the RAA we're returning here satisfies that monitor-pending resend. + self.context.monitor_pending_revoke_and_ack = false; + } let (closing_signed, signed_closing_tx, shutdown_result) = if self.context.signer_pending_closing { debug_assert!(self.context.last_sent_closing_fee.is_some()); @@ -9826,8 +10559,8 @@ where if revoke_and_ack.is_some() { "a" } else { "no" }, self.context.resend_order, if funding_signed.is_some() { "a" } else { "no" }, - if funding_commit_sig.is_some() { "a" } else { "no" }, - if tx_signatures.is_some() { "a" } else { "no" }, + if funding_tx_signed.as_ref().map(|v| v.commitment_signed.is_some()).unwrap_or(false) { "a" } else { "no" }, + if funding_tx_signed.as_ref().map(|v| v.tx_signatures.is_some()).unwrap_or(false) { "a" } else { "no" }, if channel_ready.is_some() { "a" } else { "no" }, if closing_signed.is_some() { "a" } else { "no" }, if signed_closing_tx.is_some() { "a" } else { "no" }, @@ -9840,8 +10573,7 @@ where accept_channel: None, funding_created: None, funding_signed, - funding_commit_sig, - tx_signatures, + funding_tx_signed, channel_ready, order: self.context.resend_order.clone(), closing_signed, @@ -9862,7 +10594,6 @@ where let signer = &self.context.holder_signer; self.holder_commitment_point.try_resolve_pending(signer, &self.context.secp_ctx, logger); let per_commitment_secret = signer - .as_ref() .release_commitment_secret(self.holder_commitment_point.next_transaction_number() + 2) .ok(); if let Some(per_commitment_secret) = per_commitment_secret { @@ -9880,8 +10611,6 @@ where channel_id: self.context.channel_id, per_commitment_secret, next_per_commitment_point: self.holder_commitment_point.next_point(), - #[cfg(taproot)] - next_local_nonce: None, release_htlc_message_paths, }); } @@ -10043,7 +10772,7 @@ where #[rustfmt::skip] pub fn channel_reestablish<L: Logger, NS: NodeSigner, CBP>( &mut self, msg: &msgs::ChannelReestablish, logger: &L, node_signer: &NS, - chain_hash: ChainHash, user_config: &UserConfig, best_block: &BestBlock, + chain_hash: ChainHash, user_config: &UserConfig, best_block: &BlockLocator, path_for_release_htlc: CBP, ) -> Result<ReestablishResponses, ChannelError> where @@ -10073,7 +10802,7 @@ where .map_err(|_| ChannelError::close("Peer sent a garbage channel_reestablish with unparseable secret key".to_owned()))?; if msg.next_remote_commitment_number > our_commitment_transaction { let given_commitment_number = INITIAL_COMMITMENT_NUMBER - msg.next_remote_commitment_number + 1; - let expected_point = self.context.holder_signer.as_ref() + let expected_point = self.context.holder_signer .get_per_commitment_point(given_commitment_number, &self.context.secp_ctx) .ok(); if expected_point.is_none() { @@ -10114,6 +10843,8 @@ where // remaining cases either succeed or ErrorMessage-fail). self.context.channel_state.clear_peer_disconnected(); self.mark_response_received(); + let funding_locked_txid_sent_in_reestablish = + self.context.funding_locked_txid_sent_in_reestablish.take(); let shutdown_msg = self.get_outbound_shutdown(); @@ -10131,7 +10862,23 @@ where } } - let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, best_block.height, logger); + // If the counterparty's `my_current_funding_locked` matches the splice we've already + // confirmed and are about to promote, any `announcement_signatures` we'd generate here + // would be for the soon-to-be-superseded pre-splice funding. Skip them; + // `maybe_promote_splice_funding` will emit correct post-splice sigs once + // `inferred_splice_locked` is processed. + let our_splice_txid = + self.pending_splice.as_ref().and_then(|ps| ps.sent_funding_txid); + let splice_promotion_pending = msg + .my_current_funding_locked + .as_ref() + .map(|funding_locked| Some(funding_locked.txid) == our_splice_txid) + .unwrap_or(false); + let announcement_sigs = if splice_promotion_pending { + None + } else { + self.get_announcement_sigs(node_signer, chain_hash, user_config, best_block.height, logger) + }; let mut commitment_update = None; let mut tx_signatures = None; @@ -10152,36 +10899,44 @@ where ))); } - if !session.has_received_commitment_signed() { - self.context.expecting_peer_commitment_signed = true; - } - - // - if it has not received `tx_signatures` for that funding transaction: - // - if the `commitment_signed` bit is set in `retransmit_flags`: - if !session.has_received_tx_signatures() - && next_funding.should_retransmit(msgs::NextFundingFlag::CommitmentSigned) - { - // - MUST retransmit its `commitment_signed` for that funding transaction. - retransmit_funding_commit_sig = Some(next_funding.txid); - } + if !session.has_holder_witnesses() { + log_debug!(logger, "Waiting for funding transaction signatures to be provided"); + } else { + // - if it has not received `tx_signatures` for that funding transaction: + // - if the `commitment_signed` bit is set in `retransmit_flags`: + if !session.has_received_tx_signatures() + && next_funding.should_retransmit(msgs::NextFundingFlag::CommitmentSigned) + { + // - MUST retransmit its `commitment_signed` for that funding transaction. + retransmit_funding_commit_sig = Some(next_funding.txid); + } - // - if it has already received `commitment_signed` and it should sign first - // - MUST send its `tx_signatures` for that funding transaction. - // - // - if it has already received `tx_signatures` for that funding transaction: - // - MUST send its `tx_signatures` for that funding transaction. - if (session.has_received_commitment_signed() && session.holder_sends_tx_signatures_first()) - || session.has_received_tx_signatures() - { - // If `holder_tx_signatures` is `None` here, the `tx_signatures` message will be sent - // when the holder provides their witnesses as this will queue a `tx_signatures` if the - // holder must send one. - if session.holder_tx_signatures().is_none() { - log_debug!(logger, "Waiting for funding transaction signatures to be provided"); - } else if self.context.channel_state.is_monitor_update_in_progress() { - log_debug!(logger, "Waiting for monitor update before providing funding transaction signatures"); - } else { - tx_signatures = session.holder_tx_signatures().clone(); + // - if it has already received `commitment_signed` and it should sign first + // - MUST send its `tx_signatures` for that funding transaction. + // + // - if it has already received `tx_signatures` for that funding transaction: + // - MUST send its `tx_signatures` for that funding transaction. + if let Some(holder_tx_signatures) = session.holder_tx_signatures() { + // A completed exchange may precede an unrelated monitor update, so + // retransmitting the same signatures does not depend on that update. + let splice_signatures_exchange_complete = self + .pending_splice + .as_ref() + .map(|pending_splice| { + pending_splice.negotiated_candidates.iter().any(|candidate| { + candidate.funding.get_funding_txid() == Some(next_funding.txid) + }) + }) + .unwrap_or(false); + if self.is_awaiting_monitor_update() + && !splice_signatures_exchange_complete + { + log_debug!(logger, "Waiting for monitor update before providing funding transaction signatures"); + } else if self.context.signer_pending_funding { + log_debug!(logger, "Waiting for signer to provide counterparty commitment_signed before releasing funding transaction signatures"); + } else { + tx_signatures = Some(holder_tx_signatures); + } } } } else { @@ -10191,7 +10946,7 @@ where tx_abort = Some(msgs::TxAbort { channel_id: self.context.channel_id(), data: - "No active signing session. The associated funding transaction may have already been broadcast.".as_bytes().to_vec() }); + "Signing was not completed for this funding transaction; it may be forgotten.".as_bytes().to_vec() }); } } if let Some(funding_txid) = retransmit_funding_commit_sig { @@ -10250,8 +11005,10 @@ where raa: None, commitment_update, commitment_order: self.context.resend_order.clone(), shutdown_msg, announcement_sigs, - tx_signatures, + tx_signatures: tx_signatures + .map(|msg| (TxSignaturesOrder::CommitmentFirst, msg)), tx_abort: None, + splice_locked: None, inferred_splice_locked: None, }); } @@ -10263,8 +11020,10 @@ where raa: None, commitment_update, commitment_order: self.context.resend_order.clone(), shutdown_msg, announcement_sigs, - tx_signatures, + tx_signatures: tx_signatures + .map(|msg| (TxSignaturesOrder::CommitmentFirst, msg)), tx_abort, + splice_locked: None, inferred_splice_locked: None, }); } @@ -10272,6 +11031,9 @@ where let required_revoke = if msg.next_remote_commitment_number == our_commitment_transaction { // Remote isn't waiting on any RevokeAndACK from us! // Note that if we need to repeat our ChannelReady we'll do that in the next if block. + // If a stale ChannelManager replayed a completed update, the monitor-pending state may + // still think we owe one; the reestablish proof is authoritative here. + self.context.monitor_pending_revoke_and_ack = false; None } else if msg.next_remote_commitment_number + 1 == our_commitment_transaction { if self.context.channel_state.is_monitor_update_in_progress() { @@ -10321,7 +11083,6 @@ where // for this `txid`. let inferred_splice_locked = msg.my_current_funding_locked.as_ref().and_then(|funding_locked| { self.pending_funding() - .iter() .find(|funding| funding.get_funding_txid() == Some(funding_locked.txid)) .and_then(|_| { self.pending_splice.as_ref().and_then(|pending_splice| { @@ -10334,14 +11095,48 @@ where splice_txid, }) }); + let splice_locked = self.pending_splice.as_ref().and_then(|pending_splice| { + pending_splice + .sent_funding_txid + .filter(|splice_txid| Some(*splice_txid) != funding_locked_txid_sent_in_reestablish) + .map(|splice_txid| msgs::SpliceLocked { + channel_id: self.context.channel_id, + splice_txid, + }) + }).or_else(|| { + // If a splice confirms after we've sent `channel_reestablish` but before we've received + // theirs, we may promote the splice and clear `pending_splice`. We still need to send + // `splice_locked` after reestablishing as it was not included in our + // `channel_reestablish`. + let current_funding_txid = self.funding.get_funding_txid()?; + (self.pending_splice.is_none() + && self.funding.channel_transaction_parameters.splice_parent_funding_txid.is_some() + && Some(current_funding_txid) != funding_locked_txid_sent_in_reestablish) + .then(|| msgs::SpliceLocked { + channel_id: self.context.channel_id, + splice_txid: current_funding_txid, + }) + }); if msg.next_local_commitment_number == next_counterparty_commitment_number { + // If a stale ChannelManager replayed a completed update, the monitor-pending state may + // still think we owe one. + self.context.monitor_pending_commitment_signed = false; if required_revoke.is_some() || self.context.signer_pending_revoke_and_ack { log_debug!(logger, "Reconnected with only lost outbound RAA"); } else { log_debug!(logger, "Reconnected with no loss"); } + // A commitment update generated above retransmits the initial splice + // `commitment_signed` and must precede its funding signatures. Otherwise a completed + // exchange's retransmitted signatures must precede any `splice_locked` below. + let tx_signatures_order = if commitment_update.is_some() { + TxSignaturesOrder::CommitmentFirst + } else { + TxSignaturesOrder::SignaturesFirst + }; + Ok(ReestablishResponses { channel_ready, channel_ready_order: ChannelReadyOrder::SignaturesFirst, @@ -10350,16 +11145,17 @@ where raa: required_revoke, commitment_update, commitment_order: self.context.resend_order.clone(), - tx_signatures, + tx_signatures: tx_signatures.map(|msg| (tx_signatures_order, msg)), tx_abort, + splice_locked, inferred_splice_locked, }) } else if msg.next_local_commitment_number == next_counterparty_commitment_number - 1 { - debug_assert!(commitment_update.is_none()); - - // TODO(splicing): Assert in a test that we don't retransmit tx_signatures instead - #[cfg(test)] - assert!(tx_signatures.is_none()); + if retransmit_funding_commit_sig.is_some() { + return Err(ChannelError::close( + "Peer requested retransmission of an initial commitment_signed while claiming to have lost a later commitment_signed".to_owned(), + )); + } if required_revoke.is_some() || self.context.signer_pending_revoke_and_ack { log_debug!(logger, "Reconnected channel with lost outbound RAA and lost remote commitment tx"); @@ -10375,8 +11171,10 @@ where shutdown_msg, announcement_sigs, commitment_update: None, raa: None, commitment_order: self.context.resend_order.clone(), - tx_signatures: None, + tx_signatures: tx_signatures + .map(|msg| (TxSignaturesOrder::SignaturesFirst, msg)), tx_abort, + splice_locked, inferred_splice_locked, }) } else { @@ -10402,8 +11200,10 @@ where shutdown_msg, announcement_sigs, raa, commitment_update, commitment_order: self.context.resend_order.clone(), - tx_signatures: None, + tx_signatures: tx_signatures + .map(|msg| (TxSignaturesOrder::SignaturesFirst, msg)), tx_abort, + splice_locked, inferred_splice_locked, }) } @@ -10600,7 +11400,12 @@ where &mut self, logger: &L, signer_provider: &SP, their_features: &InitFeatures, msg: &msgs::Shutdown, ) -> Result< - (Option<msgs::Shutdown>, Option<ChannelMonitorUpdate>, Vec<(HTLCSource, PaymentHash)>), + ( + Option<msgs::Shutdown>, + Option<ChannelMonitorUpdate>, + Vec<(HTLCSource, PaymentHash)>, + Option<SpliceFundingFailed>, + ), ChannelError, > { if self.context.channel_state.is_peer_disconnected() { @@ -10612,7 +11417,7 @@ where matches!(self.context.channel_state, ChannelState::NegotiatingFunding(_)); if matches!(self.context.channel_state, ChannelState::FundingNegotiated(_)) { if let Some(signing_session) = self.context.interactive_tx_signing_session.as_ref() { - if signing_session.holder_tx_signatures().is_none() { + if !signing_session.has_holder_witnesses() { // If we're a V1 channel or we haven't yet sent our `tx_signatures` for a dual // funded channel, the funding tx couldn't be broadcasted yet, so just short-circuit // the shutdown logic. @@ -10691,11 +11496,6 @@ where // From here on out, we may not fail! self.context.channel_state.set_remote_shutdown_sent(); - if self.context.channel_state.is_awaiting_quiescence() { - // We haven't been able to send `stfu` yet, and there's no point in attempting - // quiescence anymore since the counterparty wishes to close the channel. - self.context.channel_state.clear_awaiting_quiescence(); - } self.context.update_time_counter += 1; let monitor_update = if update_shutdown_script { @@ -10746,7 +11546,9 @@ where self.context.channel_state.set_local_shutdown_sent(); self.context.update_time_counter += 1; - Ok((shutdown, monitor_update, dropped_outbound_htlcs)) + let splice_funding_failed = self.abandon_quiescent_action(); + + Ok((shutdown, monitor_update, dropped_outbound_htlcs, splice_funding_failed)) } fn build_signed_closing_transaction( @@ -10778,18 +11580,15 @@ where &mut self, closing_tx: &ClosingTransaction, skip_remote_output: bool, fee_satoshis: u64, min_fee_satoshis: u64, max_fee_satoshis: u64, logger: &L, ) -> Option<msgs::ClosingSigned> { - let sig = match &self.context.holder_signer { - ChannelSignerType::Ecdsa(ecdsa) => ecdsa - .sign_closing_transaction( - &self.funding.channel_transaction_parameters, - closing_tx, - &self.context.secp_ctx, - ) - .ok(), - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!(), - }; + let sig = self + .context + .holder_signer + .sign_closing_transaction( + &self.funding.channel_transaction_parameters, + closing_tx, + &self.context.secp_ctx, + ) + .ok(); if sig.is_none() { log_trace!(logger, "Closing transaction signature unavailable, waiting on signer"); self.context.signer_pending_closing = true; @@ -11091,7 +11890,7 @@ where ); core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) + .chain(self.pending_funding()) .try_for_each(|funding| self.context.can_accept_incoming_htlc(funding, dust_exposure_limiting_feerate, &logger)) } @@ -11111,7 +11910,7 @@ where } #[cfg(any(test, feature = "_externalize_tests"))] - pub fn get_signer(&self) -> &ChannelSignerType<SP> { + pub fn get_signer(&self) -> &SP::EcdsaSigner { &self.context.holder_signer } @@ -11409,9 +12208,9 @@ where let funding = pending_splice .negotiated_candidates .iter_mut() + .map(|candidate| &mut candidate.funding) .find(|funding| funding.get_funding_txid() == Some(splice_txid)) .unwrap(); - let prev_funding_txid = self.funding.get_funding_txid(); if let Some(scid) = self.funding.short_channel_id { self.context.historical_scids.push(scid); @@ -11419,22 +12218,24 @@ where core::mem::swap(&mut self.funding, funding); - // The swap above places the previous `FundingScope` into `pending_funding`. - pending_splice - .negotiated_candidates - .drain(..) - .filter(|funding| funding.get_funding_txid() != prev_funding_txid) - .map(|mut funding| { - funding - .funding_transaction - .take() - .map(|tx| FundingInfo::Tx { transaction: tx }) - .unwrap_or_else(|| FundingInfo::OutPoint { - outpoint: funding - .get_funding_txo() - .expect("Negotiated splices must have a known funding outpoint"), - }) + let promoted_tx = self + .funding + .funding_transaction + .as_ref() + .expect("Promoted splice funding should have a funding transaction"); + let candidates = core::mem::take(&mut pending_splice.negotiated_candidates); + let negotiation_contribution = pending_splice.negotiation_contribution.take(); + candidates + .into_iter() + .filter_map(|candidate| candidate.contribution) + .chain(negotiation_contribution) + .filter_map(|contribution| { + contribution.into_unique_contributions( + promoted_tx.input.iter().map(|i| i.previous_output), + promoted_tx.output.iter().map(|o| o.script_pubkey.as_script()), + ) }) + .map(|(inputs, outputs)| FundingInfo::Contribution { inputs, outputs }) .collect::<Vec<_>>() }; @@ -11470,12 +12271,6 @@ where let announcement_sigs = self.get_announcement_sigs(node_signer, chain_hash, user_config, block_height, logger); - if let Some(quiescent_action) = self.quiescent_action.as_ref() { - if matches!(quiescent_action, QuiescentAction::Splice(_)) { - self.context.channel_state.set_awaiting_quiescence(); - } - } - Some(SpliceFundingPromotion { funding_txo, monitor_update, @@ -11522,7 +12317,9 @@ where let mut confirmed_funding_index = None; let mut funding_already_confirmed = false; - for (index, funding) in pending_splice.negotiated_candidates.iter_mut().enumerate() { + let candidates = + pending_splice.negotiated_candidates.iter_mut().map(|candidate| &mut candidate.funding); + for (index, funding) in candidates.enumerate() { if self.context.check_for_funding_tx_confirmed( funding, block_hash, height, index_in_block, &mut confirmed_tx, logger, )? { @@ -11682,7 +12479,8 @@ where if let Some(pending_splice) = &mut self.pending_splice { let mut confirmed_funding_index = None; - for (index, funding) in pending_splice.negotiated_candidates.iter().enumerate() { + let candidates = pending_splice.negotiated_candidates.iter().map(|candidate| &candidate.funding); + for (index, funding) in candidates.enumerate() { if funding.funding_tx_confirmation_height != 0 { if confirmed_funding_index.is_some() { let err_reason = "splice tx of another pending funding already confirmed"; @@ -11694,7 +12492,8 @@ where } if let Some(confirmed_funding_index) = confirmed_funding_index { - let funding = &mut pending_splice.negotiated_candidates[confirmed_funding_index]; + let funding = + &mut pending_splice.negotiated_candidates[confirmed_funding_index].funding; // Check if the splice funding transaction was unconfirmed if funding.get_funding_tx_confirmations(height) == 0 { @@ -11750,7 +12549,7 @@ where pub fn get_relevant_txids(&self) -> impl Iterator<Item = (Txid, u32, Option<BlockHash>)> + '_ { core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) + .chain(self.pending_funding()) .map(|funding| { ( funding.get_funding_txid(), @@ -11889,35 +12688,30 @@ where }, Ok(v) => v }; - match &self.context.holder_signer { - ChannelSignerType::Ecdsa(ecdsa) => { - let our_bitcoin_sig = match ecdsa.sign_channel_announcement_with_funding_key( - &self.funding.channel_transaction_parameters, &announcement, &self.context.secp_ctx, - ) { - Err(_) => { - log_error!(logger, "Signer rejected channel_announcement signing. Channel will not be announced!"); - return None; - }, - Ok(v) => v - }; - let short_channel_id = match self.funding.get_short_channel_id() { - Some(scid) => scid, - None => return None, - }; + let our_bitcoin_sig = match self.context.holder_signer.sign_channel_announcement_with_funding_key( + &self.funding.channel_transaction_parameters, + &announcement, + &self.context.secp_ctx, + ) { + Err(_) => { + log_error!(logger, "Signer rejected channel_announcement signing. Channel will not be announced!"); + return None; + }, + Ok(v) => v + }; + let short_channel_id = match self.funding.get_short_channel_id() { + Some(scid) => scid, + None => return None, + }; - self.context.announcement_sigs_state = AnnouncementSigsState::MessageSent; + self.context.announcement_sigs_state = AnnouncementSigsState::MessageSent; - Some(msgs::AnnouncementSignatures { - channel_id: self.context.channel_id(), - short_channel_id, - node_signature: our_node_sig, - bitcoin_signature: our_bitcoin_sig, - }) - }, - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!() - } + Some(msgs::AnnouncementSignatures { + channel_id: self.context.channel_id(), + short_channel_id, + node_signature: our_node_sig, + bitcoin_signature: our_bitcoin_sig, + }) } /// Signs the given channel announcement, returning a ChannelError::Ignore if no keys are @@ -11933,24 +12727,20 @@ where let our_node_sig = node_signer.sign_gossip_message(msgs::UnsignedGossipMessage::ChannelAnnouncement(&announcement)) .map_err(|_| ChannelError::Ignore("Failed to generate node signature for channel_announcement".to_owned()))?; - match &self.context.holder_signer { - ChannelSignerType::Ecdsa(ecdsa) => { - let our_bitcoin_sig = ecdsa.sign_channel_announcement_with_funding_key( - &self.funding.channel_transaction_parameters, &announcement, &self.context.secp_ctx, - ) - .map_err(|_| ChannelError::Ignore("Signer rejected channel_announcement".to_owned()))?; - Ok(msgs::ChannelAnnouncement { - node_signature_1: if were_node_one { our_node_sig } else { their_node_sig }, - node_signature_2: if were_node_one { their_node_sig } else { our_node_sig }, - bitcoin_signature_1: if were_node_one { our_bitcoin_sig } else { their_bitcoin_sig }, - bitcoin_signature_2: if were_node_one { their_bitcoin_sig } else { our_bitcoin_sig }, - contents: announcement, - }) - }, - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!() - } + let our_bitcoin_sig = self.context.holder_signer + .sign_channel_announcement_with_funding_key( + &self.funding.channel_transaction_parameters, + &announcement, + &self.context.secp_ctx, + ) + .map_err(|_| ChannelError::Ignore("Signer rejected channel_announcement".to_owned()))?; + Ok(msgs::ChannelAnnouncement { + node_signature_1: if were_node_one { our_node_sig } else { their_node_sig }, + node_signature_2: if were_node_one { their_node_sig } else { our_node_sig }, + bitcoin_signature_1: if were_node_one { our_bitcoin_sig } else { their_bitcoin_sig }, + bitcoin_signature_2: if were_node_one { their_bitcoin_sig } else { our_bitcoin_sig }, + contents: announcement, + }) } else { Err(ChannelError::Ignore("Attempted to sign channel announcement before we'd received announcement_signatures".to_string())) } @@ -11964,6 +12754,17 @@ where &mut self, node_signer: &NS, chain_hash: ChainHash, best_block_height: u32, msg: &msgs::AnnouncementSignatures, user_config: &UserConfig ) -> Result<msgs::ChannelAnnouncement, ChannelError> { + // Ignore sigs signed over a `short_channel_id` other than our current one (e.g. stale + // pre-splice sigs arriving after our side has promoted). Verifying them against the + // current `UnsignedChannelAnnouncement` would always fail the hash check, but per BOLT #7 + // that's not a protocol violation warranting a force-close. + if Some(msg.short_channel_id) != self.funding.get_short_channel_id() { + return Err(ChannelError::Ignore(format!( + "Ignoring announcement_signatures for short_channel_id {} which does not match our current short_channel_id {:?}", + msg.short_channel_id, self.funding.get_short_channel_id(), + ))); + } + let announcement = self.get_channel_announcement(node_signer, chain_hash, user_config)?; let msghash = hash_to_message!(&Sha256d::hash(&announcement.encode()[..])[..]); @@ -12088,6 +12889,9 @@ where log_info!(logger, "Sending a data_loss_protect with no previous remote per_commitment_secret for channel {}", &self.context.channel_id()); [0;32] }; + let my_current_funding_locked = self.maybe_get_my_current_funding_locked(); + self.context.funding_locked_txid_sent_in_reestablish = + my_current_funding_locked.as_ref().map(|funding_locked| funding_locked.txid); msgs::ChannelReestablish { channel_id: self.context.channel_id(), // The protocol has two different commitment number concepts - the "commitment @@ -12111,19 +12915,12 @@ where your_last_per_commitment_secret: remote_last_secret, my_current_per_commitment_point: dummy_pubkey, next_funding: self.maybe_get_next_funding(), - my_current_funding_locked: self.maybe_get_my_current_funding_locked(), + my_current_funding_locked, } } - /// Initiate splicing. - /// - `our_funding_inputs`: the inputs we contribute to the new funding transaction. - /// Includes the witness weight for this input (e.g. P2WPKH_WITNESS_WEIGHT=109 for typical P2WPKH inputs). - /// - `change_script`: an option change output script. If `None` and needed, one will be - /// generated by `SignerProvider::get_destination_script`. - pub fn splice_channel<L: Logger>( - &mut self, contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: u32, - logger: &L, - ) -> Result<Option<msgs::Stfu>, APIError> { + /// Builds a [`FundingTemplate`] for splicing or RBF, if the channel state allows it. + pub fn splice_channel(&self) -> Result<FundingTemplate, APIError> { if self.holder_commitment_point.current_point().is_none() { return Err(APIError::APIMisuseError { err: format!( @@ -12133,17 +12930,29 @@ where }); } - // Check if a splice has been initiated already. - // Note: only a single outstanding splice is supported (per spec) - if self.pending_splice.is_some() || self.quiescent_action.is_some() { + if self.quiescent_action.is_some() { return Err(APIError::APIMisuseError { err: format!( - "Channel {} cannot be spliced, as it has already a splice pending", + "Channel {} cannot be spliced as one is waiting to be negotiated", self.context.channel_id(), ), }); } + if let Some(pending_splice) = &self.pending_splice { + if let Some(funding_negotiation) = &pending_splice.funding_negotiation { + debug_assert!(self.context.channel_state.is_quiescent()); + if funding_negotiation.is_initiator() { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced as one is currently being negotiated", + self.context.channel_id(), + ), + }); + } + } + } + if !self.context.is_usable() { return Err(APIError::APIMisuseError { err: format!( @@ -12153,391 +12962,1034 @@ where }); } - let our_funding_contribution = contribution.net_value(); - if our_funding_contribution == SignedAmount::ZERO { - return Err(APIError::APIMisuseError { + let spliceable_balance = self.get_next_splice_out_maximum(&self.funding).map_err(|e| { + APIError::ChannelUnavailable { err: format!( - "Channel {} cannot be spliced; contribution cannot be zero", + "Channel {} cannot be spliced at this time: {}", self.context.channel_id(), + e ), - }); - } - - // Fees for splice-out are paid from the channel balance whereas fees for splice-in - // are paid by the funding inputs. Therefore, in the case of splice-out, we add the - // fees on top of the user-specified contribution. We leave the user-specified - // contribution as-is for splice-ins. - let adjusted_funding_contribution = check_splice_contribution_sufficient( - &contribution, - true, - FeeRate::from_sat_per_kwu(u64::from(funding_feerate_per_kw)), - ) - .map_err(|e| APIError::APIMisuseError { - err: format!( - "Channel {} cannot be {}; {}", - self.context.channel_id(), - if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, - e - ), + } })?; - // Note: post-splice channel value is not yet known at this point, counterparty contribution is not known - // (Cannot test for miminum required post-splice channel value) - let their_funding_contribution = SignedAmount::ZERO; - self.validate_splice_contributions( - adjusted_funding_contribution, - their_funding_contribution, - ) - .map_err(|err| APIError::APIMisuseError { err })?; - - for FundingTxInput { utxo, prevtx, .. } in contribution.inputs().iter() { - const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { - channel_id: ChannelId([0; 32]), - serial_id: 0, - prevtx: None, - prevtx_out: 0, - sequence: 0, - // Mutually exclusive with prevtx, which is accounted for below. - shared_input_txid: None, + let (min_rbf_feerate, prior_contribution) = if self.is_rbf_compatible().is_err() { + // Channel can never RBF (e.g., zero-conf). + (None, None) + } else if let Some(pending_splice) = self.pending_splice.as_ref() { + // A splice is pending — either a completed negotiation that hasn't locked yet + // or an in-progress negotiation. In either case, the user's splice will need + // to satisfy the minimum RBF feerate. When that feerate is unknown (the splice + // was last written by an LDK version prior to 0.3, which persisted neither it nor + // our contribution), the minimum RBF feerate is left unset so the new splice is + // queued and begins as a fresh splice once the pending candidate locks, rather than + // attempting to replace it. + // + // If an in-progress negotiation later fails (e.g., tx_abort), the derived + // min_rbf_feerate becomes stale, causing a slightly higher feerate than + // necessary. Call splice_channel again after receiving SpliceNegotiationFailed to get a + // fresh template without the stale RBF constraint. + let min_rbf_feerate = pending_splice.min_rbf_feerate(); + let prior = if pending_splice.last_funding_feerate_sat_per_1000_weight.is_some() { + pending_splice.latest_contribution().cloned() + } else { + None }; - let message_len = MESSAGE_TEMPLATE.serialized_length() + prevtx.serialized_length(); - if message_len > LN_MAX_MSG_LEN { - return Err(APIError::APIMisuseError { - err: format!( - "Funding input references a prevtx that is too large for tx_add_input: {}", - utxo.outpoint, - ), - }); - } - } - - let (our_funding_inputs, our_funding_outputs, change_script) = contribution.into_tx_parts(); - - let action = QuiescentAction::Splice(SpliceInstructions { - adjusted_funding_contribution, - our_funding_inputs, - our_funding_outputs, - change_script, - funding_feerate_per_kw, - locktime, - }); - self.propose_quiescence(logger, action) - .map_err(|e| APIError::APIMisuseError { err: e.to_owned() }) - } - - fn send_splice_init(&mut self, instructions: SpliceInstructions) -> msgs::SpliceInit { - debug_assert!(self.pending_splice.is_none()); - - let SpliceInstructions { - adjusted_funding_contribution, - our_funding_inputs, - our_funding_outputs, - change_script, - funding_feerate_per_kw, - locktime, - } = instructions; - - let prev_funding_input = self.funding.to_splice_funding_input(); - let context = FundingNegotiationContext { - is_initiator: true, - our_funding_contribution: adjusted_funding_contribution, - funding_tx_locktime: LockTime::from_consensus(locktime), - funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, - shared_funding_input: Some(prev_funding_input), - our_funding_inputs, - our_funding_outputs, - change_script, + (min_rbf_feerate, prior) + } else { + // No pending splice — fresh splice with no RBF constraint. + (None, None) }; - // Rotate the funding pubkey using the prev_funding_txid as a tweak - let prev_funding_txid = self.funding.get_funding_txid(); - let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) { - (None, _) => { - debug_assert!(false); - self.funding.get_holder_pubkeys().funding_pubkey - }, - (Some(prev_funding_txid), ChannelSignerType::Ecdsa(ecdsa)) => { - ecdsa.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx) - }, - #[cfg(taproot)] - _ => todo!(), + let funding_txo = self.funding.get_funding_txo().expect("funding_txo should be set"); + let previous_utxo = + self.funding.get_funding_output().expect("funding_output should be set"); + let shared_input = Input { + outpoint: funding_txo.into_bitcoin_outpoint(), + previous_utxo, + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + FUNDING_TRANSACTION_WITNESS_WEIGHT, }; - let funding_negotiation = - FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey }; - self.pending_splice = Some(PendingFunding { - funding_negotiation: Some(funding_negotiation), - negotiated_candidates: vec![], - sent_funding_txid: None, - received_funding_txid: None, - }); + Ok(FundingTemplate::new( + Some(shared_input), + min_rbf_feerate, + prior_contribution, + spliceable_balance, + )) + } - msgs::SpliceInit { - channel_id: self.context.channel_id, - funding_contribution_satoshis: adjusted_funding_contribution.to_sat(), - funding_feerate_per_kw, - locktime, - funding_pubkey, - require_confirmed_inputs: None, + /// Returns whether this channel can ever RBF, independent of splice state. + fn is_rbf_compatible(&self) -> Result<(), String> { + if self.context.minimum_depth(&self.funding) == Some(0) { + return Err(format!( + "Channel {} has option_zeroconf, cannot RBF", + self.context.channel_id(), + )); } + Ok(()) } - #[cfg(test)] - pub fn abandon_splice( - &mut self, - ) -> Result<(msgs::TxAbort, Option<SpliceFundingFailed>), APIError> { - if self.should_reset_pending_splice_state(false) { - let tx_abort = - msgs::TxAbort { channel_id: self.context.channel_id(), data: Vec::new() }; - let splice_funding_failed = self.reset_pending_splice_state(); - Ok((tx_abort, splice_funding_failed)) - } else if self.has_pending_splice_awaiting_signatures() { - Err(APIError::APIMisuseError { - err: format!( - "Channel {} splice cannot be abandoned; already awaiting signatures", - self.context.channel_id(), - ), - }) - } else { - Err(APIError::APIMisuseError { - err: format!( - "Channel {} splice cannot be abandoned; no pending splice", - self.context.channel_id(), - ), - }) + /// Whether a committed-but-not-yet-negotiating contribution can replace the pending candidate + /// via RBF, rather than having to wait for that candidate to lock. Used to classify a queued + /// contribution's status while it awaits quiescence. + fn queued_contribution_can_rbf(&self, contribution: &FundingContribution) -> bool { + let pending_splice = match &self.pending_splice { + Some(pending_splice) => pending_splice, + None => return false, + }; + // A zero-conf channel can never RBF, and a candidate that is already locking can no longer + // be replaced. + if self.is_rbf_compatible().is_err() { + return false; + } + if pending_splice.sent_funding_txid.is_some() + || pending_splice.received_funding_txid.is_some() + { + return false; } + // The replacement must pay a higher feerate than the most recent round: the one currently + // under negotiation if any (which is the candidate we would replace once it signs), + // otherwise the most recently negotiated candidate. The in-flight feerate is fixed when the + // round starts, so affordability is determinable even before it signs. + let prev_feerate = match pending_splice.funding_negotiation.as_ref() { + Some(funding_negotiation) => funding_negotiation.funding_feerate_sat_per_1000_weight(), + None => match pending_splice.last_funding_feerate_sat_per_1000_weight { + Some(prev_feerate) => prev_feerate, + None => return false, + }, + }; + contribution.feerate() >= PendingFunding::min_rbf_feerate_above(prev_feerate) } - /// Checks during handling splice_init - pub fn validate_splice_init( - &self, msg: &msgs::SpliceInit, our_funding_contribution: SignedAmount, - ) -> Result<FundingScope, ChannelError> { - if self.holder_commitment_point.current_point().is_none() { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} commitment point needs to be advanced once before spliced", - self.context.channel_id(), - ))); - } + fn can_initiate_rbf(&self) -> Result<FeeRate, String> { + self.is_rbf_compatible()?; - if !self.context.channel_state.is_quiescent() { - return Err(ChannelError::WarnAndDisconnect("Quiescence needed to splice".to_owned())); - } + let pending_splice = match &self.pending_splice { + Some(pending_splice) => pending_splice, + None => { + return Err(format!( + "Channel {} has no pending splice to RBF", + self.context.channel_id(), + )); + }, + }; - // Check if a splice has been initiated already. - if self.pending_splice.is_some() { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} already has a splice pending", + if pending_splice.funding_negotiation.is_some() { + return Err(format!( + "Channel {} cannot RBF as a funding negotiation is already in progress", self.context.channel_id(), - ))); - } - - // - If it has received shutdown: - // MUST send a warning and close the connection or send an error - // and fail the channel. - if !self.context.is_live() { - return Err(ChannelError::WarnAndDisconnect( - "Splicing requested on a channel that is not live".to_owned(), )); } - // TODO(splicing): Once splice acceptor can contribute, check that inputs are sufficient, - // similarly to the check in `splice_channel`. - debug_assert_eq!(our_funding_contribution, SignedAmount::ZERO); - - let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); - if their_funding_contribution == SignedAmount::ZERO { - return Err(ChannelError::WarnAndDisconnect(format!( - "Channel {} cannot be spliced; they are the initiator, and their contribution is zero", + if pending_splice.sent_funding_txid.is_some() { + return Err(format!( + "Channel {} already sent splice_locked, cannot RBF", self.context.channel_id(), - ))); + )); } - self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) - .map_err(|e| ChannelError::WarnAndDisconnect(e))?; - - // Rotate the pubkeys using the prev_funding_txid as a tweak - let prev_funding_txid = self.funding.get_funding_txid(); - let funding_pubkey = match (prev_funding_txid, &self.context.holder_signer) { - (None, _) => { - debug_assert!(false); - self.funding.get_holder_pubkeys().funding_pubkey - }, - (Some(prev_funding_txid), ChannelSignerType::Ecdsa(ecdsa)) => { - ecdsa.new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx) - }, - #[cfg(taproot)] - _ => todo!(), - }; - let mut new_keys = self.funding.get_holder_pubkeys().clone(); - new_keys.funding_pubkey = funding_pubkey; - - Ok(FundingScope::for_splice( - &self.funding, - &self.context, - our_funding_contribution, - their_funding_contribution, - msg.funding_pubkey, - new_keys, - )) - } - - fn validate_splice_contributions( - &self, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, - ) -> Result<(), String> { - if our_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { + if pending_splice.received_funding_txid.is_some() { return Err(format!( - "Channel {} cannot be spliced; our {} contribution exceeds the total bitcoin supply", + "Channel {} counterparty already sent splice_locked, cannot RBF", self.context.channel_id(), - our_funding_contribution, )); } - if their_funding_contribution.unsigned_abs() > Amount::MAX_MONEY { + if pending_splice.negotiated_candidates.is_empty() { return Err(format!( - "Channel {} cannot be spliced; their {} contribution exceeds the total bitcoin supply", + "Channel {} has no negotiated splice candidates to RBF", self.context.channel_id(), - their_funding_contribution, )); } - let (holder_balance_remaining, counterparty_balance_remaining) = - self.get_holder_counterparty_balances_floor_incl_fee(&self.funding).map_err(|e| { - format!("Channel {} cannot be spliced; {}", self.context.channel_id(), e) - })?; - - let post_channel_value = self.funding.compute_post_splice_value( - our_funding_contribution.to_sat(), - their_funding_contribution.to_sat(), - ); - let counterparty_selected_channel_reserve = Amount::from_sat( - get_v2_channel_reserve_satoshis(post_channel_value, MIN_CHAN_DUST_LIMIT_SATOSHIS), - ); - let holder_selected_channel_reserve = Amount::from_sat(get_v2_channel_reserve_satoshis( - post_channel_value, - self.context.counterparty_dust_limit_satoshis, - )); - - // We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve - - if our_funding_contribution != SignedAmount::ZERO { - let post_splice_holder_balance = Amount::from_sat( - holder_balance_remaining.to_sat() - .checked_add_signed(our_funding_contribution.to_sat()) - .ok_or(format!( - "Channel {} cannot be spliced out; our remaining balance {} does not cover our negative funding contribution {}", - self.context.channel_id(), - holder_balance_remaining, - our_funding_contribution, - ))?, - ); - - post_splice_holder_balance.checked_sub(counterparty_selected_channel_reserve) - .ok_or(format!( - "Channel {} cannot be {}; our post-splice channel balance {} is smaller than their selected v2 reserve {}", - self.context.channel_id(), - if our_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, - post_splice_holder_balance, - counterparty_selected_channel_reserve, - ))?; + match pending_splice.last_funding_feerate_sat_per_1000_weight { + Some(prev_feerate) => Ok(PendingFunding::min_rbf_feerate_above(prev_feerate)), + None => Err(format!( + "Channel {} has no prior feerate to compute RBF minimum", + self.context.channel_id(), + )), } + } - if their_funding_contribution != SignedAmount::ZERO { - let post_splice_counterparty_balance = Amount::from_sat( - counterparty_balance_remaining.to_sat() - .checked_add_signed(their_funding_contribution.to_sat()) - .ok_or(format!( - "Channel {} cannot be spliced out; their remaining balance {} does not cover their negative funding contribution {}", - self.context.channel_id(), - counterparty_balance_remaining, - their_funding_contribution, - ))?, - ); - - post_splice_counterparty_balance.checked_sub(holder_selected_channel_reserve) - .ok_or(format!( - "Channel {} cannot be {}; their post-splice channel balance {} is smaller than our selected v2 reserve {}", - self.context.channel_id(), - if their_funding_contribution.is_positive() { "spliced in" } else { "spliced out" }, - post_splice_counterparty_balance, - holder_selected_channel_reserve, - ))?; + /// Attempts to adjust the contribution's feerate to the minimum RBF feerate so the splice can + /// proceed as an RBF immediately rather than waiting for the pending splice to lock. + /// Returns the adjusted contribution on success, or the original on failure. + fn maybe_adjust_for_rbf<L: Logger>( + &self, contribution: FundingContribution, min_rbf_feerate: FeeRate, logger: &L, + ) -> FundingContribution { + if contribution.feerate() >= min_rbf_feerate { + return contribution; } - Ok(()) - } + let spliceable_balance = match self.get_next_splice_out_maximum(&self.funding) { + Ok(balance) => balance, + Err(_) => return contribution, + }; - pub(crate) fn splice_init<ES: EntropySource, L: Logger>( - &mut self, msg: &msgs::SpliceInit, our_funding_contribution_satoshis: i64, - signer_provider: &SP, entropy_source: &ES, holder_node_id: &PublicKey, logger: &L, - ) -> Result<msgs::SpliceAck, ChannelError> { - let our_funding_contribution = SignedAmount::from_sat(our_funding_contribution_satoshis); - let splice_funding = self.validate_splice_init(msg, our_funding_contribution)?; + if let Err(e) = + contribution.net_value_for_initiator_at_feerate(min_rbf_feerate, spliceable_balance) + { + log_info!( + logger, + "Cannot adjust to minimum RBF feerate {}: {}; will proceed as fresh splice after lock", + min_rbf_feerate, + e, + ); + // Note: try_send_stfu prevents sending stfu until the contribution's + // feerate meets the minimum RBF feerate, effectively waiting for the + // prior splice to lock before proceeding. + return contribution; + } log_info!( logger, - "Starting splice funding negotiation for channel {} after receiving splice_init; new channel value: {} sats (old: {} sats)", - self.context.channel_id, - splice_funding.get_value_satoshis(), - self.funding.get_value_satoshis(), + "Adjusting contribution feerate from {} to minimum RBF feerate {}", + contribution.feerate(), + min_rbf_feerate, ); + contribution + .for_initiator_at_feerate(min_rbf_feerate, spliceable_balance) + .expect("feerate compatibility already checked") + } - let prev_funding_input = self.funding.to_splice_funding_input(); - let funding_negotiation_context = FundingNegotiationContext { - is_initiator: false, + pub fn funding_contributed<F: FeeEstimator, L: Logger>( + &mut self, contribution: FundingContribution, locktime: LockTime, + fee_estimator: &LowerBoundedFeeEstimator<F>, logger: &L, + ) -> Result<Option<msgs::Stfu>, QuiescentError> { + debug_assert!(contribution.is_splice()); + + match self.quiescent_action.as_ref() { + Some(QuiescentAction::Splice { contribution: existing, .. }) => { + let pending_splice = self.pending_splice.as_ref(); + let prior_inputs = pending_splice + .into_iter() + .flat_map(|pending_splice| pending_splice.contributed_inputs()); + let prior_outputs = pending_splice + .into_iter() + .flat_map(|pending_splice| pending_splice.contributed_outputs()); + return match contribution.into_unique_contributions( + existing.contributed_inputs().chain(prior_inputs), + existing.contributed_outputs().chain(prior_outputs), + ) { + None => Err(QuiescentError::DoNothing), + Some((inputs, outputs)) => { + Err(QuiescentError::DiscardFunding { inputs, outputs }) + }, + }; + }, + #[cfg(any(test, fuzzing, feature = "_test_utils"))] + Some(QuiescentAction::DoNothing) => unreachable!(), + None => {}, + } + + let initiated_funding_negotiation = self + .pending_splice + .as_ref() + .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) + .filter(|funding_negotiation| funding_negotiation.is_initiator()); + + if let Some(funding_negotiation) = initiated_funding_negotiation { + let pending_splice = + self.pending_splice.as_ref().expect("funding negotiation implies pending splice"); + let prior_inputs = pending_splice.contributed_inputs(); + let prior_outputs = pending_splice.contributed_outputs(); + let unique_contributions = match funding_negotiation { + FundingNegotiation::AwaitingAck { context, .. } => contribution + .into_unique_contributions( + context.contributed_inputs().chain(prior_inputs), + context.contributed_outputs().chain(prior_outputs), + ), + FundingNegotiation::ConstructingTransaction { + interactive_tx_constructor, .. + } => contribution.into_unique_contributions( + interactive_tx_constructor.contributed_inputs().chain(prior_inputs), + interactive_tx_constructor.contributed_outputs().chain(prior_outputs), + ), + FundingNegotiation::AwaitingSignatures { .. } => { + let session = self + .context + .interactive_tx_signing_session + .as_ref() + .expect("pending splice awaiting signatures"); + contribution.into_unique_contributions( + session.contributed_inputs().chain(prior_inputs), + session.contributed_outputs().chain(prior_outputs), + ) + }, + }; + + return match unique_contributions { + None => Err(QuiescentError::DoNothing), + Some((inputs, outputs)) => Err(QuiescentError::DiscardFunding { inputs, outputs }), + }; + } + + let our_funding_contribution = contribution.net_value(); + let unsigned_contribution = our_funding_contribution.unsigned_abs(); + if let Err(e) = self.get_next_splice_out_maximum(&self.funding) + .and_then(|splice_max| splice_max + .to_sat() + .checked_add_signed(our_funding_contribution.to_sat()) + .ok_or(format!("Our splice-out value of {unsigned_contribution} is greater than the maximum {splice_max}")) + ) + { + log_error!(logger, "Channel {} cannot be funded: {}", self.context.channel_id(), e); + return Err(QuiescentError::FailSplice( + self.splice_funding_failed_for(contribution), + NegotiationFailureReason::ContributionInvalid, + )); + } + + if let Some(pending_splice) = self.pending_splice.as_ref() { + if !pending_splice.is_rbf_feerate_sufficient( + contribution.feerate().to_sat_per_kwu() as u32, + fee_estimator, + ) { + log_error!( + logger, + "Channel {} RBF feerate {} below fee estimator minimum", + self.context.channel_id(), + contribution.feerate(), + ); + return Err(QuiescentError::FailSplice( + self.splice_funding_failed_for(contribution), + NegotiationFailureReason::FeeRateTooLow, + )); + } + } + + // If a pending splice exists with negotiated candidates, attempt to adjust the + // contribution's feerate to the minimum RBF feerate so it can proceed as an RBF immediately + // rather than waiting for the splice to lock. + let contribution = if let Ok(min_rbf_feerate) = self.can_initiate_rbf() { + self.maybe_adjust_for_rbf(contribution, min_rbf_feerate, logger) + } else { + contribution + }; + + // A queued splice never coexists with a negotiation we initiated: we return early above if + // one is already in flight, and a queued action is cleared the moment it becomes our + // negotiation at quiescence. It may coexist with a counterparty-initiated negotiation (e.g. + // queuing our own contribution while accepting their splice), so we only rule out our own. + debug_assert!( + self.pending_splice + .as_ref() + .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()) + .map_or(true, |funding_negotiation| !funding_negotiation.is_initiator()), + "A queued splice must not coexist with a funding negotiation we initiated", + ); + + self.propose_quiescence(logger, QuiescentAction::Splice { contribution, locktime }) + } + + /// Returns a reference to the funding contribution queued by a pending [`QuiescentAction`], + /// if any. + fn queued_funding_contribution(&self) -> Option<&FundingContribution> { + match &self.quiescent_action { + Some(QuiescentAction::Splice { contribution, .. }) => Some(contribution), + _ => None, + } + } + + /// Consumes and returns the funding contribution from the pending [`QuiescentAction`], if any. + fn take_queued_funding_contribution(&mut self) -> Option<FundingContribution> { + match &self.quiescent_action { + Some(QuiescentAction::Splice { .. }) => match self.quiescent_action.take() { + Some(QuiescentAction::Splice { contribution, .. }) => Some(contribution), + _ => unreachable!(), + }, + _ => None, + } + } + + fn send_splice_init( + &mut self, context: FundingNegotiationContext, contribution: FundingContribution, + ) -> msgs::SpliceInit { + debug_assert!(self.pending_splice.is_none()); + // Rotate the funding pubkey using the prev_funding_txid as a tweak + let prev_funding_txid = self.funding.get_funding_txid(); + let funding_pubkey = match prev_funding_txid { + None => { + debug_assert!(false); + self.funding.get_holder_pubkeys().funding_pubkey + }, + Some(prev_funding_txid) => self + .context + .holder_signer + .new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx), + }; + + let funding_feerate_per_kw = context.funding_feerate_sat_per_1000_weight; + let funding_contribution_satoshis = context.our_funding_contribution.to_sat(); + let locktime = context.funding_tx_locktime.to_consensus_u32(); + + let funding_negotiation = + FundingNegotiation::AwaitingAck { context, new_holder_funding_key: funding_pubkey }; + self.pending_splice = Some(PendingFunding { + funding_negotiation: Some(funding_negotiation), + negotiation_contribution: Some(contribution), + negotiated_candidates: vec![], + sent_funding_txid: None, + received_funding_txid: None, + last_funding_feerate_sat_per_1000_weight: None, + }); + + msgs::SpliceInit { + channel_id: self.context.channel_id, + funding_contribution_satoshis, + funding_feerate_per_kw, + locktime, + funding_pubkey, + require_confirmed_inputs: None, + } + } + + fn send_tx_init_rbf( + &mut self, context: FundingNegotiationContext, contribution: FundingContribution, + ) -> msgs::TxInitRbf { + let pending_splice = + self.pending_splice.as_mut().expect("pending_splice should exist for RBF"); + debug_assert!(!pending_splice.negotiated_candidates.is_empty()); + + let new_holder_funding_key = pending_splice + .negotiated_candidates + .first() + .unwrap() + .funding + .get_holder_pubkeys() + .funding_pubkey; + + let funding_feerate_per_kw = context.funding_feerate_sat_per_1000_weight; + let funding_contribution_satoshis = context.our_funding_contribution.to_sat(); + let locktime = context.funding_tx_locktime.to_consensus_u32(); + + pending_splice.funding_negotiation = + Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key }); + pending_splice.negotiation_contribution = Some(contribution); + + msgs::TxInitRbf { + channel_id: self.context.channel_id, + locktime, + feerate_sat_per_1000_weight: funding_feerate_per_kw, + funding_output_contribution: Some(funding_contribution_satoshis), + } + } + + pub fn cancel_funding_contributed(&mut self) -> Result<InteractiveTxMsgError, APIError> { + if matches!(self.quiescent_action, Some(QuiescentAction::Splice { .. })) { + let splice_funding_failed = self.abandon_quiescent_action(); + debug_assert!(splice_funding_failed.is_some()); + let str = "Manually canceled funding contribution"; + let err = if self.context.channel_state.is_local_stfu_sent() + && !self.context.channel_state.is_remote_stfu_sent() + { + // If we've already sent `stfu` and haven't received the counterparty's yet, we know + // it corresponds to our action. + ChannelError::WarnAndDisconnect(str.into()) + } else { + // We don't need to send `tx_abort` because our action still pending means we're not + // quiescent for it. + ChannelError::Ignore(str.into()) + }; + return Ok(InteractiveTxMsgError::new(err, splice_funding_failed) + .with_negotiation_failure_reason(NegotiationFailureReason::LocallyCanceled)); + } + + let funding_negotiation = self + .pending_splice + .as_ref() + .and_then(|pending_splice| pending_splice.funding_negotiation.as_ref()); + let Some(funding_negotiation) = funding_negotiation else { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} does not have a pending splice negotiation", + self.context.channel_id() + ), + }); + }; + + let made_contribution = match funding_negotiation { + FundingNegotiation::AwaitingAck { context, .. } => { + context.contributed_inputs().next().is_some() + || context.contributed_outputs().next().is_some() + }, + FundingNegotiation::ConstructingTransaction { interactive_tx_constructor, .. } => { + interactive_tx_constructor.contributed_inputs().next().is_some() + || interactive_tx_constructor.contributed_outputs().next().is_some() + }, + FundingNegotiation::AwaitingSignatures { .. } => self + .context + .interactive_tx_signing_session + .as_ref() + .expect("We have a pending splice awaiting signatures") + .has_local_contribution(), + }; + if !made_contribution { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} has a pending splice negotiation with no contribution made", + self.context.channel_id() + ), + }); + } + + // We typically don't reset the pending funding negotiation when we're in + // [`FundingNegotiation::AwaitingSignatures`] since we're able to resume it on + // re-establishment, so we still need to handle this case separately if the user wishes to + // cancel. If they've yet to call [`Channel::funding_transaction_signed`], then we can + // guarantee to never have sent any signatures to the counterparty, or have processed any + // signatures from them. + if matches!(funding_negotiation, FundingNegotiation::AwaitingSignatures { .. }) { + let already_signed = self + .context + .interactive_tx_signing_session + .as_ref() + .expect("We have a pending splice awaiting signatures") + .has_holder_witnesses(); + if already_signed { + return Err(APIError::APIMisuseError { + err: format!( + "Channel {} has pending splice negotiation that was already signed", + self.context.channel_id(), + ), + }); + } + } + + debug_assert!(self.context.channel_state.is_quiescent()); + let splice_funding_failed = self.reset_pending_splice_state(); + debug_assert!(splice_funding_failed.is_some()); + Ok(InteractiveTxMsgError::new( + ChannelError::Abort(AbortReason::ManualIntervention), + splice_funding_failed, + ) + .with_negotiation_failure_reason(NegotiationFailureReason::LocallyCanceled)) + } + + /// Checks during handling splice_init + pub fn validate_splice_init(&self, msg: &msgs::SpliceInit) -> Result<(), ChannelError> { + // - If it has received shutdown: + // MUST send a warning and close the connection or send an error + // and fail the channel. + if !self.context.is_live() { + return Err(ChannelError::WarnAndDisconnect( + "Splicing requested on a channel that is not live".to_owned(), + )); + } + + if !self.context.channel_state.is_quiescent() { + return Err(ChannelError::WarnAndDisconnect("Quiescence needed to splice".to_owned())); + } + + // Check if a splice has been initiated already. + if self.pending_splice.is_some() { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} already has a splice pending", + self.context.channel_id(), + ))); + } + + let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); + if their_funding_contribution == SignedAmount::ZERO { + return Err(ChannelError::WarnAndDisconnect(format!( + "Channel {} cannot be spliced; they are the initiator, and their contribution is zero", + self.context.channel_id(), + ))); + } + + if self.holder_commitment_point.current_point().is_none() { + return Err(ChannelError::Abort(AbortReason::InternalError( + "Commitment point needs to be advanced once before spliced".into(), + ))); + } + + Ok(()) + } + + fn validate_splice_contributions( + &self, our_funding_contribution: SignedAmount, their_funding_contribution: SignedAmount, + counterparty_funding_pubkey: PublicKey, our_new_holder_keys: ChannelPublicKeys, + min_funding_satoshis: u64, + ) -> Result<FundingScope, String> { + let candidate_scope = FundingScope::for_splice( + &self.funding, + self.context(), our_funding_contribution, - funding_tx_locktime: LockTime::from_consensus(msg.locktime), - funding_feerate_sat_per_1000_weight: msg.funding_feerate_per_kw, - shared_funding_input: Some(prev_funding_input), - our_funding_inputs: Vec::new(), - our_funding_outputs: Vec::new(), - change_script: None, + their_funding_contribution, + counterparty_funding_pubkey, + our_new_holder_keys, + min_funding_satoshis, + )?; + + let (post_splice_holder_balance, post_splice_counterparty_balance) = + self.get_holder_counterparty_balances_floor_incl_fee(&candidate_scope)?; + + let holder_selected_channel_reserve = + Amount::from_sat(candidate_scope.holder_selected_channel_reserve_satoshis); + let counterparty_selected_channel_reserve = Amount::from_sat( + candidate_scope.counterparty_selected_channel_reserve_satoshis.expect("Reserve is set"), + ); + + // We allow parties to draw from their previous reserve, as long as they satisfy their v2 reserve + if our_funding_contribution != SignedAmount::ZERO { + post_splice_holder_balance.checked_sub(counterparty_selected_channel_reserve).ok_or( + format!( + "Our post-splice channel balance {} is smaller than their selected v2 reserve {}", + post_splice_holder_balance, + counterparty_selected_channel_reserve, + ), + )?; + } + + if their_funding_contribution != SignedAmount::ZERO { + post_splice_counterparty_balance.checked_sub(holder_selected_channel_reserve).ok_or( + format!( + "Their post-splice channel balance {} is smaller than our selected v2 reserve {}", + post_splice_counterparty_balance, + holder_selected_channel_reserve, + ), + )?; + } + + #[cfg(debug_assertions)] + { + let (old_holder_balance_msat, old_counterparty_balance_msat) = + *self.funding.holder_prev_commitment_tx_balance.lock().unwrap(); + let (new_holder_balance_msat, new_counterparty_balance_msat) = + *candidate_scope.holder_prev_commitment_tx_balance.lock().unwrap(); + if new_holder_balance_msat < counterparty_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_holder_balance_msat, old_holder_balance_msat); + } + if new_counterparty_balance_msat < holder_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_counterparty_balance_msat, old_counterparty_balance_msat); + } + } + #[cfg(debug_assertions)] + { + let (old_holder_balance_msat, old_counterparty_balance_msat) = + *self.funding.counterparty_prev_commitment_tx_balance.lock().unwrap(); + let (new_holder_balance_msat, new_counterparty_balance_msat) = + *candidate_scope.counterparty_prev_commitment_tx_balance.lock().unwrap(); + if new_holder_balance_msat < counterparty_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_holder_balance_msat, old_holder_balance_msat); + } + if new_counterparty_balance_msat < holder_selected_channel_reserve.to_sat() * 1000 { + debug_assert_eq!(new_counterparty_balance_msat, old_counterparty_balance_msat); + } + } + + Ok(candidate_scope) + } + + fn resolve_queued_contribution<L: Logger>( + &self, feerate: FeeRate, logger: &L, + ) -> Result<(Option<SignedAmount>, Option<Amount>), ChannelError> { + let spliceable_balance = self + .get_next_splice_out_maximum(&self.funding) + .map_err(|e| { + log_info!( + logger, + "Cannot compute holder balance for channel {}: {}; \ + proceeding without contribution", + self.context.channel_id(), + e, + ); + }) + .ok(); + + let net_value = match spliceable_balance.and_then(|_| self.queued_funding_contribution()) { + Some(c) => { + match c.net_value_for_acceptor_at_feerate(feerate, spliceable_balance.unwrap()) { + Ok(net_value) => Some(net_value), + Err(FeeRateAdjustmentError::FeeRateTooHigh { .. }) => { + return Err(ChannelError::Abort(AbortReason::FeeRateTooHigh)); + }, + Err(e) => { + log_info!( + logger, + "Cannot accommodate initiator's feerate ({}) for channel {}: {}", + feerate, + self.context.channel_id(), + e, + ); + None + }, + } + }, + None => None, }; - let mut interactive_tx_constructor = funding_negotiation_context - .into_interactive_tx_constructor( - &self.context, - &splice_funding, - signer_provider, - entropy_source, - holder_node_id.clone(), + Ok((net_value, spliceable_balance)) + } + + pub(crate) fn splice_init<ES: EntropySource, L: Logger>( + &mut self, msg: &msgs::SpliceInit, entropy_source: &ES, holder_node_id: &PublicKey, + min_funding_satoshis: u64, logger: &L, + ) -> Result<msgs::SpliceAck, InteractiveTxMsgError> { + self.validate_splice_init(msg).map_err(|e| self.quiescent_negotiation_err(e))?; + + let feerate = FeeRate::from_sat_per_kwu(msg.funding_feerate_per_kw as u64); + let (queued_net_value, holder_balance) = self + .resolve_queued_contribution(feerate, logger) + .map_err(|e| self.quiescent_negotiation_err(e))?; + + let our_funding_contribution = queued_net_value.unwrap_or(SignedAmount::ZERO); + let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); + + // Rotate the pubkeys using the prev_funding_txid as a tweak + let prev_funding_txid = self.funding.get_funding_txid(); + let funding_pubkey = match prev_funding_txid { + None => { + debug_assert!(false); + self.funding.get_holder_pubkeys().funding_pubkey + }, + Some(prev_funding_txid) => self + .context + .holder_signer + .new_funding_pubkey(prev_funding_txid, &self.context.secp_ctx), + }; + let mut holder_pubkeys = self.funding.get_holder_pubkeys().clone(); + holder_pubkeys.funding_pubkey = funding_pubkey; + + let splice_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + msg.funding_pubkey, + holder_pubkeys, + min_funding_satoshis, + ) + .map_err(|e| { + self.quiescent_negotiation_err(ChannelError::Abort( + AbortReason::InvalidContribution(e), + )) + })?; + + // Adjust for the feerate and clone so we can store it for future RBF re-use. + let (adjusted_contribution, our_funding_inputs, our_funding_outputs) = + if queued_net_value.is_some() { + let adjusted_contribution = self + .take_queued_funding_contribution() + .expect("queued_funding_contribution was Some") + .for_acceptor_at_feerate(feerate, holder_balance.unwrap()) + .expect("feerate compatibility already checked"); + let (inputs, outputs) = adjusted_contribution.clone().into_tx_parts(); + (Some(adjusted_contribution), inputs, outputs) + } else { + (None, Default::default(), Default::default()) + }; + + log_info!( + logger, + "Starting splice funding negotiation for channel {} after receiving splice_init; new channel value: {} sats (old: {} sats)", + self.context.channel_id, + splice_funding.get_value_satoshis(), + self.funding.get_value_satoshis(), + ); + + let new_funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey; + let prev_funding_input = self.funding.to_splice_funding_input(); + let funding_negotiation = FundingNegotiation::for_acceptor( + splice_funding, + &self.context, + entropy_source, + holder_node_id, + our_funding_contribution, + prev_funding_input, + msg.locktime, + msg.funding_feerate_per_kw, + our_funding_inputs, + our_funding_outputs, + ); + self.pending_splice = Some(PendingFunding { + funding_negotiation: Some(funding_negotiation), + negotiation_contribution: adjusted_contribution, + negotiated_candidates: Vec::new(), + received_funding_txid: None, + sent_funding_txid: None, + last_funding_feerate_sat_per_1000_weight: None, + }); + + Ok(msgs::SpliceAck { + channel_id: self.context.channel_id, + funding_contribution_satoshis: our_funding_contribution.to_sat(), + funding_pubkey: new_funding_pubkey, + require_confirmed_inputs: None, + }) + } + + /// Checks during handling tx_init_rbf for an existing splice + fn validate_tx_init_rbf<F: FeeEstimator>( + &self, msg: &msgs::TxInitRbf, fee_estimator: &LowerBoundedFeeEstimator<F>, + ) -> Result<(ChannelPublicKeys, PublicKey), ChannelError> { + if !self.context.is_live() { + return Err(ChannelError::WarnAndDisconnect( + "RBF requested on a channel that is not live".to_owned(), + )); + } + if !self.context.channel_state.is_quiescent() { + return Err(ChannelError::WarnAndDisconnect("Quiescence needed for RBF".to_owned())); + } + + if self.holder_commitment_point.current_point().is_none() { + return Err(ChannelError::Abort(AbortReason::InternalError( + "Commitment point needs to be advanced once before RBF".into(), + ))); + } + + self.is_rbf_compatible() + .map_err(|msg| ChannelError::Abort(AbortReason::RbfUnavailable(msg)))?; + + let (pending_splice, last_candidate) = self + .pending_splice + .as_ref() + .filter(|pending_splice| !pending_splice.negotiated_candidates.is_empty()) + .map(|pending_splice| { + ( + pending_splice, + pending_splice.negotiated_candidates.last().expect("checked above"), + ) + }) + .ok_or_else(|| { + ChannelError::Abort(AbortReason::RbfUnavailable( + "No pending splice available to RBF".into(), + )) + })?; + + if pending_splice.funding_negotiation.is_some() { + return Err(ChannelError::Abort(AbortReason::NegotiationInProgress)); + } + + if pending_splice.received_funding_txid.is_some() { + return Err(ChannelError::Abort(AbortReason::RbfUnavailable( + "Already received splice_locked".into(), + ))); + } + + if pending_splice.sent_funding_txid.is_some() { + return Err(ChannelError::Abort(AbortReason::RbfUnavailable( + "Already sent splice_locked".into(), + ))); + } + + let prev_feerate = + pending_splice.last_funding_feerate_sat_per_1000_weight.unwrap_or_else(|| { + fee_estimator.bounded_sat_per_1000_weight(ConfirmationTarget::UrgentOnChainSweep) + }); + let new_feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64); + if new_feerate < PendingFunding::min_rbf_feerate_above(prev_feerate) { + return Err(ChannelError::Abort(AbortReason::InsufficientRbfFeerate)); + } + + if !pending_splice.is_rbf_feerate_sufficient(msg.feerate_sat_per_1000_weight, fee_estimator) + { + return Err(ChannelError::Abort(AbortReason::InsufficientRbfFeerate)); + } + + // Reuse funding pubkeys from the last negotiated candidate since all RBF candidates + // for the same splice share the same funding output script. + Ok(( + last_candidate.funding.get_holder_pubkeys().clone(), + *last_candidate.funding.counterparty_funding_pubkey(), + )) + } + + pub(crate) fn tx_init_rbf<ES: EntropySource, F: FeeEstimator, L: Logger>( + &mut self, msg: &msgs::TxInitRbf, entropy_source: &ES, holder_node_id: &PublicKey, + fee_estimator: &LowerBoundedFeeEstimator<F>, min_funding_satoshis: u64, logger: &L, + ) -> Result<msgs::TxAckRbf, InteractiveTxMsgError> { + let (holder_pubkeys, counterparty_funding_pubkey) = self + .validate_tx_init_rbf(msg, fee_estimator) + .map_err(|e| self.quiescent_negotiation_err(e))?; + + let feerate = FeeRate::from_sat_per_kwu(msg.feerate_sat_per_1000_weight as u64); + let (queued_net_value, holder_balance) = self + .resolve_queued_contribution(feerate, logger) + .map_err(|e| self.quiescent_negotiation_err(e))?; + + // If no queued contribution, try prior contribution from previous negotiation. + // Failing here means the RBF would erase our splice — reject it. + let prior_net_value = if queued_net_value.is_some() { + None + } else if let Some(prior) = self + .pending_splice + .as_ref() + .and_then(|pending_splice| pending_splice.latest_contribution()) + { + let net_value = holder_balance + .ok_or_else(|| ChannelError::Abort(AbortReason::InsufficientRbfFeerate)) + .and_then(|holder_balance| { + prior + .net_value_for_acceptor_at_feerate(feerate, holder_balance) + .map_err(|_| ChannelError::Abort(AbortReason::InsufficientRbfFeerate)) + }) + .map_err(|e| self.quiescent_negotiation_err(e))?; + Some(net_value) + } else { + None + }; + + let our_funding_contribution = queued_net_value.or(prior_net_value); + let our_funding_contribution = our_funding_contribution.unwrap_or(SignedAmount::ZERO); + + let their_funding_contribution = match msg.funding_output_contribution { + Some(value) => SignedAmount::from_sat(value), + None => SignedAmount::ZERO, + }; + + let rbf_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + holder_pubkeys, + min_funding_satoshis, ) - .map_err(|err| { - ChannelError::WarnAndDisconnect(format!( - "Failed to start interactive transaction construction, {:?}", - err + .map_err(|e| { + self.quiescent_negotiation_err(ChannelError::Abort( + AbortReason::InvalidContribution(e), )) })?; - debug_assert!(interactive_tx_constructor.take_initiator_first_message().is_none()); - // TODO(splicing): if quiescent_action is set, integrate what the user wants to do into the - // counterparty-initiated splice. For always-on nodes this probably isn't a useful - // optimization, but for often-offline nodes it may be, as we may connect and immediately - // go into splicing from both sides. + // Consume the appropriate contribution source. + let (our_funding_inputs, our_funding_outputs) = if queued_net_value.is_some() { + let adjusted_contribution = self + .take_queued_funding_contribution() + .expect("queued_funding_contribution was Some") + .for_acceptor_at_feerate(feerate, holder_balance.unwrap()) + .expect("feerate compatibility already checked"); + self.pending_splice + .as_mut() + .expect("pending_splice is Some") + .negotiation_contribution = Some(adjusted_contribution.clone()); + adjusted_contribution.into_tx_parts() + } else if prior_net_value.is_some() { + let prior_contribution = self + .pending_splice + .as_ref() + .expect("pending_splice is Some") + .latest_contribution() + .expect("prior_net_value was Some") + .clone(); + let adjusted_contribution = prior_contribution + .for_acceptor_at_feerate(feerate, holder_balance.unwrap()) + .expect("feerate compatibility already checked"); + self.pending_splice + .as_mut() + .expect("pending_splice is Some") + .negotiation_contribution = Some(adjusted_contribution.clone()); + adjusted_contribution.into_tx_parts() + } else { + Default::default() + }; - let new_funding_pubkey = splice_funding.get_holder_pubkeys().funding_pubkey; - self.pending_splice = Some(PendingFunding { - funding_negotiation: Some(FundingNegotiation::ConstructingTransaction { - funding: splice_funding, - interactive_tx_constructor, - }), - negotiated_candidates: Vec::new(), - received_funding_txid: None, - sent_funding_txid: None, - }); + log_info!( + logger, + "Starting RBF funding negotiation for channel {} after receiving tx_init_rbf; channel value: {} sats", + self.context.channel_id, + rbf_funding.get_value_satoshis(), + ); - Ok(msgs::SpliceAck { + let prev_funding_input = self.funding.to_splice_funding_input(); + let funding_negotiation = FundingNegotiation::for_acceptor( + rbf_funding, + &self.context, + entropy_source, + holder_node_id, + our_funding_contribution, + prev_funding_input, + msg.locktime, + msg.feerate_sat_per_1000_weight, + our_funding_inputs, + our_funding_outputs, + ); + let pending_splice = self.pending_splice.as_mut().expect("pending_splice should exist"); + pending_splice.funding_negotiation = Some(funding_negotiation); + + Ok(msgs::TxAckRbf { channel_id: self.context.channel_id, - funding_contribution_satoshis: our_funding_contribution.to_sat(), - funding_pubkey: new_funding_pubkey, - require_confirmed_inputs: None, + funding_output_contribution: if our_funding_contribution != SignedAmount::ZERO { + Some(our_funding_contribution.to_sat()) + } else { + None + }, }) } + fn validate_tx_ack_rbf( + &self, msg: &msgs::TxAckRbf, min_funding_satoshis: u64, + ) -> Result<FundingScope, ChannelError> { + let pending_splice = self + .pending_splice + .as_ref() + .ok_or_else(|| ChannelError::Ignore("Channel is not in pending splice".to_owned()))?; + + let (funding_negotiation_context, _) = pending_splice.awaiting_ack_context("tx_ack_rbf")?; + + let our_funding_contribution = funding_negotiation_context.our_funding_contribution; + let their_funding_contribution = match msg.funding_output_contribution { + Some(value) => SignedAmount::from_sat(value), + None => SignedAmount::ZERO, + }; + + let last_candidate = pending_splice.negotiated_candidates.last().ok_or_else(|| { + ChannelError::Abort(AbortReason::RbfUnavailable( + "No pending splice available to RBF".into(), + )) + })?; + let holder_pubkeys = last_candidate.funding.get_holder_pubkeys().clone(); + let counterparty_funding_pubkey = *last_candidate.funding.counterparty_funding_pubkey(); + + let new_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + counterparty_funding_pubkey, + holder_pubkeys, + min_funding_satoshis, + ) + .map_err(|e| ChannelError::Abort(AbortReason::InvalidContribution(e)))?; + + Ok(new_funding) + } + + pub(crate) fn tx_ack_rbf<ES: EntropySource, L: Logger>( + &mut self, msg: &msgs::TxAckRbf, entropy_source: &ES, holder_node_id: &PublicKey, + min_funding_satoshis: u64, logger: &L, + ) -> Result<Option<InteractiveTxMessageSend>, ChannelError> { + let rbf_funding = self.validate_tx_ack_rbf(msg, min_funding_satoshis)?; + + log_info!( + logger, + "Starting RBF funding negotiation for channel {} after receiving tx_ack_rbf; channel value: {} sats", + self.context.channel_id, + rbf_funding.get_value_satoshis(), + ); + + let pending_splice = self + .pending_splice + .as_mut() + .expect("pending_splice existence validated in validate_tx_ack_rbf"); + let funding_negotiation_context = pending_splice + .take_awaiting_ack_context("tx_ack_rbf") + .expect("awaiting ack state validated in validate_tx_ack_rbf"); + + let (funding_negotiation, tx_msg_opt) = FundingNegotiation::for_initiator( + rbf_funding, + &self.context, + funding_negotiation_context, + entropy_source, + holder_node_id, + ); + pending_splice.funding_negotiation = Some(funding_negotiation); + + Ok(tx_msg_opt) + } + pub(crate) fn splice_ack<ES: EntropySource, L: Logger>( - &mut self, msg: &msgs::SpliceAck, signer_provider: &SP, entropy_source: &ES, - holder_node_id: &PublicKey, logger: &L, + &mut self, msg: &msgs::SpliceAck, entropy_source: &ES, holder_node_id: &PublicKey, + min_funding_satoshis: u64, logger: &L, ) -> Result<Option<InteractiveTxMessageSend>, ChannelError> { - let splice_funding = self.validate_splice_ack(msg)?; + let splice_funding = self.validate_splice_ack(msg, min_funding_satoshis)?; log_info!( logger, @@ -12547,89 +13999,71 @@ where self.funding.get_value_satoshis(), ); - let pending_splice = - self.pending_splice.as_mut().expect("We should have returned an error earlier!"); - // TODO: Good candidate for a let else statement once MSRV >= 1.65 - let funding_negotiation_context = - if let Some(FundingNegotiation::AwaitingAck { context, .. }) = - pending_splice.funding_negotiation.take() - { - context - } else { - panic!("We should have returned an error earlier!"); - }; - - let mut interactive_tx_constructor = funding_negotiation_context - .into_interactive_tx_constructor( - &self.context, - &splice_funding, - signer_provider, - entropy_source, - holder_node_id.clone(), - ) - .map_err(|err| { - ChannelError::WarnAndDisconnect(format!( - "Failed to start interactive transaction construction, {:?}", - err - )) - })?; - let tx_msg_opt = interactive_tx_constructor.take_initiator_first_message(); - debug_assert!(self.context.interactive_tx_signing_session.is_none()); - pending_splice.funding_negotiation = Some(FundingNegotiation::ConstructingTransaction { - funding: splice_funding, - interactive_tx_constructor, - }); + let pending_splice = self + .pending_splice + .as_mut() + .expect("pending_splice existence validated in validate_splice_ack"); + let funding_negotiation_context = pending_splice + .take_awaiting_ack_context("splice_ack") + .expect("awaiting ack state validated in validate_splice_ack"); + + let (funding_negotiation, tx_msg_opt) = FundingNegotiation::for_initiator( + splice_funding, + &self.context, + funding_negotiation_context, + entropy_source, + holder_node_id, + ); + pending_splice.funding_negotiation = Some(funding_negotiation); Ok(tx_msg_opt) } - fn validate_splice_ack(&self, msg: &msgs::SpliceAck) -> Result<FundingScope, ChannelError> { - // TODO(splicing): Add check that we are the splice (quiescence) initiator - + fn validate_splice_ack( + &self, msg: &msgs::SpliceAck, min_funding_satoshis: u64, + ) -> Result<FundingScope, ChannelError> { let pending_splice = self .pending_splice .as_ref() .ok_or_else(|| ChannelError::Ignore("Channel is not in pending splice".to_owned()))?; - let (funding_negotiation_context, new_holder_funding_key) = match &pending_splice - .funding_negotiation - { - Some(FundingNegotiation::AwaitingAck { context, new_holder_funding_key }) => { - (context, new_holder_funding_key) - }, - Some(FundingNegotiation::ConstructingTransaction { .. }) - | Some(FundingNegotiation::AwaitingSignatures { .. }) => { - return Err(ChannelError::WarnAndDisconnect( - "Got unexpected splice_ack; splice negotiation already in progress".to_owned(), - )); - }, - None => { - return Err(ChannelError::Ignore( - "Got unexpected splice_ack; no splice negotiation in progress".to_owned(), - )); - }, - }; + let (funding_negotiation_context, new_holder_funding_key) = + pending_splice.awaiting_ack_context("splice_ack")?; let our_funding_contribution = funding_negotiation_context.our_funding_contribution; let their_funding_contribution = SignedAmount::from_sat(msg.funding_contribution_satoshis); - self.validate_splice_contributions(our_funding_contribution, their_funding_contribution) - .map_err(|e| ChannelError::WarnAndDisconnect(e))?; let mut new_keys = self.funding.get_holder_pubkeys().clone(); new_keys.funding_pubkey = *new_holder_funding_key; - Ok(FundingScope::for_splice( - &self.funding, - &self.context, - our_funding_contribution, - their_funding_contribution, - msg.funding_pubkey, - new_keys, - )) + let new_funding = self + .validate_splice_contributions( + our_funding_contribution, + their_funding_contribution, + msg.funding_pubkey, + new_keys, + min_funding_satoshis, + ) + .map_err(|e| ChannelError::Abort(AbortReason::InvalidContribution(e)))?; + + Ok(new_funding) } + /// The balances returned here should only be used to check that both parties still hold + /// their respective reserves *after* a splice. This function also checks that both local + /// and remote commitments still have at least one output after the splice, which is + /// particularly relevant for zero-reserve channels. + /// + /// Do NOT use this to determine how much the holder can splice out of the channel. The balance + /// of the holder after a splice is not necessarily equal to the funds they can splice out + /// of the channel due to the v2 reserve, and the zero-reserve-at-least-one-output + /// requirements. Note you cannot simply subtract out the reserve, as splicing funds out + /// of the channel changes the reserve the holder must keep in the channel. + /// + /// See [`FundedChannel::get_next_splice_out_maximum`] for the maximum value of the next + /// splice out of the holder's balance. fn get_holder_counterparty_balances_floor_incl_fee( &self, funding: &FundingScope, ) -> Result<(Amount, Amount), String> { @@ -12640,7 +14074,17 @@ where // We are not interested in dust exposure let dust_exposure_limiting_feerate = None; - let local_commitment_stats = self + // Different dust limits on the local and remote commitments cause the commitment + // transaction fee to be different depending on the commitment, so we grab the floor + // of both balances across both commitments here. + // + // `get_channel_stats` also checks for at least one output on the commitment given + // these parameters. This is particularly relevant for zero-reserve channels. + // + // This "at-least-one-output" check is why we still run both checks on + // zero-fee-commitment channels, even though those channels don't suffer from the + // commitment transaction fee asymmetry. + let (local_stats, _local_htlcs) = self .context .get_next_local_commitment_stats( funding, @@ -12648,15 +14092,12 @@ where include_counterparty_unknown_htlcs, addl_nondust_htlc_count, self.context.feerate_per_kw, + true, dust_exposure_limiting_feerate, ) - .map_err(|()| "Balance after HTLCs and anchors exhausted on local commitment")?; - let (holder_balance_on_local_msat, counterparty_balance_on_local_msat) = - local_commitment_stats - .get_holder_counterparty_balances_incl_fee_msat() - .map_err(|()| "Channel funder cannot afford the fee on local commitment")?; + .map_err(|()| "Balance exhausted on local commitment")?; - let remote_commitment_stats = self + let (remote_stats, _remote_htlcs) = self .context .get_next_remote_commitment_stats( funding, @@ -12664,25 +14105,85 @@ where include_counterparty_unknown_htlcs, addl_nondust_htlc_count, self.context.feerate_per_kw, + true, dust_exposure_limiting_feerate, ) - .map_err(|()| "Balance after HTLCs and anchors exhausted on remote commitment")?; - let (holder_balance_on_remote_msat, counterparty_balance_on_remote_msat) = - remote_commitment_stats - .get_holder_counterparty_balances_incl_fee_msat() - .map_err(|()| "Channel funder cannot afford the fee on remote commitment")?; + .map_err(|()| "Balance exhausted on remote commitment")?; let holder_balance_floor = Amount::from_sat( - cmp::min(holder_balance_on_local_msat, holder_balance_on_remote_msat) / 1000, + cmp::min( + local_stats.commitment_stats.holder_balance_msat, + remote_stats.commitment_stats.holder_balance_msat, + ) / 1000, ); let counterparty_balance_floor = Amount::from_sat( - cmp::min(counterparty_balance_on_local_msat, counterparty_balance_on_remote_msat) - / 1000, + cmp::min( + local_stats.commitment_stats.counterparty_balance_msat, + remote_stats.commitment_stats.counterparty_balance_msat, + ) / 1000, ); Ok((holder_balance_floor, counterparty_balance_floor)) } + /// Determines the maximum value that the holder can splice out of the channel, accounting + /// for the updated reserves after said splice. This maximum also makes sure the local + /// commitment retains at least one output after the splice, which is particularly relevant + /// for zero-reserve channels. + fn get_next_splice_out_maximum(&self, funding: &FundingScope) -> Result<Amount, String> { + let include_counterparty_unknown_htlcs = true; + // We are not interested in dust exposure + let dust_exposure_limiting_feerate = None; + + // When reading the available balances, we take the remote's view of the pending + // HTLCs, see `tx_builder` for further details + let (remote_stats, _remote_htlcs) = self + .context + .get_next_remote_commitment_stats( + funding, + None, // htlc_candidate + include_counterparty_unknown_htlcs, + 0, + self.context.feerate_per_kw, + false, + dust_exposure_limiting_feerate, + ) + .map_err(|()| "Balance exhausted on remote commitment")?; + + let next_splice_out_maximum_sat = + remote_stats.available_balances.next_splice_out_maximum_sat; + + #[cfg(debug_assertions)] + if !self.context.is_waiting_on_peer_pending_channel_update() + && !self.context.is_monitor_or_signer_pending_channel_update() + { + // After this max splice out, validation passes, accounting for the updated reserves + self.validate_splice_contributions( + SignedAmount::from_sat(-(next_splice_out_maximum_sat as i64)), + SignedAmount::ZERO, + funding.counterparty_funding_pubkey().clone(), + funding.get_holder_pubkeys().clone(), + // When the counterparty's contribution is non-negative, we don't validate + // the post splice channel value against `min_funding_satoshis` + 0, + ) + .unwrap(); + // Splice-out an additional satoshi, and validation fails! + self.validate_splice_contributions( + SignedAmount::from_sat(-((next_splice_out_maximum_sat + 1) as i64)), + SignedAmount::ZERO, + funding.counterparty_funding_pubkey().clone(), + funding.get_holder_pubkeys().clone(), + // When the counterparty's contribution is non-negative, we don't validate + // the post splice channel value against `min_funding_satoshis` + 0, + ) + .unwrap_err(); + } + + Ok(Amount::from_sat(next_splice_out_maximum_sat)) + } + pub fn splice_locked<NS: NodeSigner, L: Logger>( &mut self, msg: &msgs::SpliceLocked, node_signer: &NS, chain_hash: ChainHash, user_config: &UserConfig, block_height: u32, logger: &L, @@ -12699,7 +14200,7 @@ where if !pending_splice .negotiated_candidates .iter() - .any(|funding| funding.get_funding_txid() == Some(msg.splice_txid)) + .any(|candidate| candidate.funding.get_funding_txid() == Some(msg.splice_txid)) { let err = "unknown splice funding txid"; return Err(ChannelError::close(err.to_string())); @@ -12790,7 +14291,12 @@ where return Err((LocalHTLCFailureReason::ZeroAmount, "Cannot send 0-msat HTLC".to_owned())); } - let available_balances = self.get_available_balances(fee_estimator); + let available_balances = self.get_available_balances(fee_estimator).map_err(|()| { + ( + LocalHTLCFailureReason::ChannelBalanceOverdrawn, + "Channel balance overdrawn".to_owned(), + ) + })?; if amount_msat < available_balances.next_outbound_htlc_minimum_msat { return Err(( LocalHTLCFailureReason::HTLCMinimum, @@ -12869,9 +14375,9 @@ where self.context.pending_outbound_htlcs.push(OutboundHTLCOutput { htlc_id: self.context.next_holder_htlc_id, amount_msat, - payment_hash: payment_hash.clone(), + payment_hash, cltv_expiry, - state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet.clone())), + state: OutboundHTLCState::LocalAnnounced(Box::new(onion_routing_packet)), source, blinding_point, skimmed_fee_msat, @@ -12884,22 +14390,31 @@ where Ok(true) } - #[rustfmt::skip] + /// Gets the available balances, see [`AvailableBalances`]'s fields for more info. + /// + /// Returns `Err` if some party cannot currently pay for the HTLCs outbound from said party, and the anchors and + /// transaction fee if they are the funder. pub(super) fn get_available_balances<F: FeeEstimator>( &self, fee_estimator: &LowerBoundedFeeEstimator<F>, - ) -> AvailableBalances { - core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) - .map(|funding| self.context.get_available_balances_for_scope(funding, fee_estimator)) - .reduce(|acc, e| { - AvailableBalances { - inbound_capacity_msat: acc.inbound_capacity_msat.min(e.inbound_capacity_msat), - outbound_capacity_msat: acc.outbound_capacity_msat.min(e.outbound_capacity_msat), - next_outbound_htlc_limit_msat: acc.next_outbound_htlc_limit_msat.min(e.next_outbound_htlc_limit_msat), - next_outbound_htlc_minimum_msat: acc.next_outbound_htlc_minimum_msat.max(e.next_outbound_htlc_minimum_msat), - } + ) -> Result<AvailableBalances, ()> { + let init = self.context.get_available_balances_for_scope(&self.funding, fee_estimator)?; + self.pending_funding().try_fold(init, |acc, funding| { + let e = self.context.get_available_balances_for_scope(funding, fee_estimator)?; + Ok(AvailableBalances { + inbound_capacity_msat: acc.inbound_capacity_msat.min(e.inbound_capacity_msat), + outbound_capacity_msat: acc.outbound_capacity_msat.min(e.outbound_capacity_msat), + next_outbound_htlc_limit_msat: acc + .next_outbound_htlc_limit_msat + .min(e.next_outbound_htlc_limit_msat), + next_outbound_htlc_minimum_msat: acc + .next_outbound_htlc_minimum_msat + .max(e.next_outbound_htlc_minimum_msat), + dust_exposure_msat: acc.dust_exposure_msat.max(e.dust_exposure_msat), + next_splice_out_maximum_sat: acc + .next_splice_out_maximum_sat + .min(e.next_splice_out_maximum_sat), }) - .expect("At least one FundingScope is always provided") + }) } fn build_commitment_no_status_check<L: Logger>(&mut self, logger: &L) -> ChannelMonitorUpdate { @@ -12945,7 +14460,7 @@ where } self.context.resend_order = RAACommitmentOrder::RevokeAndACKFirst; - let update = if self.pending_funding().is_empty() { + let update = if self.negotiated_candidates().is_empty() { let (htlcs_ref, counterparty_commitment_tx) = self.build_commitment_no_state_update(&self.funding, logger); let htlc_outputs = htlcs_ref @@ -12976,7 +14491,7 @@ where } else { let mut htlc_data = None; let commitment_txs = core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) + .chain(self.pending_funding()) .map(|funding| { let (htlcs_ref, counterparty_commitment_tx) = self.build_commitment_no_state_update(funding, logger); @@ -13038,7 +14553,7 @@ where &self, logger: &L, ) -> Result<Vec<msgs::CommitmentSigned>, ChannelError> { core::iter::once(&self.funding) - .chain(self.pending_funding().iter()) + .chain(self.pending_funding()) .map(|funding| self.send_commitment_no_state_update_for_funding(funding, logger)) .collect::<Result<Vec<_>, ChannelError>>() } @@ -13057,51 +14572,44 @@ where ); let counterparty_commitment_tx = commitment_data.tx; - match &self.context.holder_signer { - ChannelSignerType::Ecdsa(ecdsa) => { - let (signature, htlc_signatures); - - { - let res = ecdsa.sign_counterparty_commitment( - &funding.channel_transaction_parameters, - &counterparty_commitment_tx, - commitment_data.inbound_htlc_preimages, - commitment_data.outbound_htlc_preimages, - &self.context.secp_ctx, - ).map_err(|_| ChannelError::Ignore("Failed to get signatures for new commitment_signed".to_owned()))?; - signature = res.0; - htlc_signatures = res.1; - - let trusted_tx = counterparty_commitment_tx.trust(); - log_trace!(logger, "Signed remote commitment tx {} (txid {}) with redeemscript {} -> {}", - encode::serialize_hex(&trusted_tx.built_transaction().transaction), - &trusted_tx.txid(), encode::serialize_hex(&funding.get_funding_redeemscript()), - log_bytes!(signature.serialize_compact()[..])); - - let counterparty_keys = trusted_tx.keys(); - debug_assert_eq!(htlc_signatures.len(), trusted_tx.nondust_htlcs().len()); - for (ref htlc_sig, ref htlc) in htlc_signatures.iter().zip(trusted_tx.nondust_htlcs()) { - log_trace!(logger, "Signed remote HTLC tx {} with redeemscript {} with pubkey {} -> {}", - encode::serialize_hex(&chan_utils::build_htlc_transaction(&trusted_tx.txid(), trusted_tx.negotiated_feerate_per_kw(), funding.get_holder_selected_contest_delay(), htlc, funding.get_channel_type(), &counterparty_keys.broadcaster_delayed_payment_key, &counterparty_keys.revocation_key)), - encode::serialize_hex(&chan_utils::get_htlc_redeemscript(&htlc, funding.get_channel_type(), &counterparty_keys)), - log_bytes!(counterparty_keys.broadcaster_htlc_key.to_public_key().serialize()), - log_bytes!(htlc_sig.serialize_compact()[..])); - } - } + let (signature, htlc_signatures); - Ok(msgs::CommitmentSigned { - channel_id: self.context.channel_id, - signature, - htlc_signatures, - funding_txid: funding.get_funding_txo().map(|funding_txo| funding_txo.txid), - #[cfg(taproot)] - partial_signature_with_nonce: None, - }) - }, - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!() + { + let res = self.context.holder_signer + .sign_counterparty_commitment( + &funding.channel_transaction_parameters, + &counterparty_commitment_tx, + commitment_data.inbound_htlc_preimages, + commitment_data.outbound_htlc_preimages, + &self.context.secp_ctx, + ) + .map_err(|_| ChannelError::Ignore("Failed to get signatures for new commitment_signed".to_owned()))?; + signature = res.0; + htlc_signatures = res.1; + + let trusted_tx = counterparty_commitment_tx.trust(); + log_trace!(logger, "Signed remote commitment tx {} (txid {}) with redeemscript {} -> {}", + encode::serialize_hex(&trusted_tx.built_transaction().transaction), + &trusted_tx.txid(), encode::serialize_hex(&funding.get_funding_redeemscript()), + log_bytes!(signature.serialize_compact()[..])); + + let counterparty_keys = trusted_tx.keys(); + debug_assert_eq!(htlc_signatures.len(), trusted_tx.nondust_htlcs().len()); + for (ref htlc_sig, ref htlc) in htlc_signatures.iter().zip(trusted_tx.nondust_htlcs()) { + log_trace!(logger, "Signed remote HTLC tx {} with redeemscript {} with pubkey {} -> {}", + encode::serialize_hex(&chan_utils::build_htlc_transaction(&trusted_tx.txid(), trusted_tx.negotiated_feerate_per_kw(), funding.get_holder_selected_contest_delay(), htlc, funding.get_channel_type(), &counterparty_keys.broadcaster_delayed_payment_key, &counterparty_keys.revocation_key)), + encode::serialize_hex(&chan_utils::get_htlc_redeemscript(&htlc, funding.get_channel_type(), &counterparty_keys)), + log_bytes!(counterparty_keys.broadcaster_htlc_key.to_public_key().serialize()), + log_bytes!(htlc_sig.serialize_compact()[..])); + } } + + Ok(msgs::CommitmentSigned { + channel_id: self.context.channel_id, + signature, + htlc_signatures, + funding_txid: funding.get_funding_txo().map(|funding_txo| funding_txo.txid), + }) } /// Adds a pending outbound HTLC to this channel, and builds a new remote commitment @@ -13172,7 +14680,12 @@ where target_feerate_sats_per_kw: Option<u32>, override_shutdown_script: Option<ShutdownScript>, logger: &L, ) -> Result< - (msgs::Shutdown, Option<ChannelMonitorUpdate>, Vec<(HTLCSource, PaymentHash)>), + ( + msgs::Shutdown, + Option<ChannelMonitorUpdate>, + Vec<(HTLCSource, PaymentHash)>, + Option<SpliceFundingFailed>, + ), APIError, > { let logger = WithChannelContext::from(logger, &self.context, None); @@ -13246,9 +14759,6 @@ where // From here on out, we may not fail! self.context.target_closing_feerate_sats_per_kw = target_feerate_sats_per_kw; self.context.channel_state.set_local_shutdown_sent(); - if self.context.channel_state.is_awaiting_quiescence() { - self.context.channel_state.clear_awaiting_quiescence(); - } self.context.local_initiated_shutdown = Some(()); self.context.update_time_counter += 1; @@ -13297,7 +14807,9 @@ where "we can't both complete shutdown and return a monitor update" ); - Ok((shutdown, monitor_update, dropped_outbound_htlcs)) + let splice_funding_failed = self.abandon_quiescent_action(); + + Ok((shutdown, monitor_update, dropped_outbound_htlcs, splice_funding_failed)) } // Miscellaneous utilities @@ -13328,100 +14840,82 @@ where #[rustfmt::skip] pub fn propose_quiescence<L: Logger>( &mut self, logger: &L, action: QuiescentAction, - ) -> Result<Option<msgs::Stfu>, &'static str> { + ) -> Result<Option<msgs::Stfu>, QuiescentError> { log_debug!(logger, "Attempting to initiate quiescence"); if !self.context.is_usable() { - return Err("Channel is not in a usable state to propose quiescence"); + debug_assert!( + self.context.channel_state.is_local_shutdown_sent() + || self.context.channel_state.is_remote_shutdown_sent(), + "splice_channel should have prevented reaching propose_quiescence on a non-ready channel" + ); + log_debug!(logger, "Channel is not in a usable state to propose quiescence"); + return Err(match action { + QuiescentAction::Splice { contribution, .. } => QuiescentError::FailSplice( + self.splice_funding_failed_for(contribution), + NegotiationFailureReason::ChannelClosing, + ), + #[cfg(any(test, fuzzing, feature = "_test_utils"))] + QuiescentAction::DoNothing => QuiescentError::DoNothing, + }); } + if self.quiescent_action.is_some() { - return Err("Channel already has a pending quiescent action and cannot start another"); + debug_assert!( + false, + "callers must not invoke propose_quiescence with {:?} while quiescent_action is set", + action, + ); + log_debug!( + logger, + "Channel already has a pending quiescent action and cannot start another", + ); + return Err(match action { + #[cfg(any(test, fuzzing, feature = "_test_utils"))] + QuiescentAction::DoNothing => QuiescentError::DoNothing, + QuiescentAction::Splice { contribution, .. } => QuiescentError::FailSplice( + self.splice_funding_failed_for(contribution), + NegotiationFailureReason::Unknown, + ), + }); } + // Since we don't have a pending quiescent action, we should never be in a state where we + // sent `stfu` without already having become quiescent. + debug_assert!(!self.context.channel_state.is_local_stfu_sent()); self.quiescent_action = Some(action); - if self.context.channel_state.is_quiescent() - || self.context.channel_state.is_awaiting_quiescence() - || self.context.channel_state.is_local_stfu_sent() - { - log_debug!(logger, "Channel is either pending quiescence or already quiescent"); + if self.context.channel_state.is_quiescent() { + log_debug!(logger, "Channel is already quiescent"); return Ok(None); } - self.context.channel_state.set_awaiting_quiescence(); - if self.context.is_live() { - match self.send_stfu(logger) { - Ok(stfu) => Ok(Some(stfu)), - Err(e) => { - log_debug!(logger, "{e}"); - Ok(None) - }, - } - } else { - log_debug!(logger, "Waiting for peer reconnection to send stfu"); - Ok(None) - } - } - - // Assumes we are either awaiting quiescence or our counterparty has requested quiescence. - #[rustfmt::skip] - pub fn send_stfu<L: Logger>(&mut self, logger: &L) -> Result<msgs::Stfu, &'static str> { - debug_assert!(!self.context.channel_state.is_local_stfu_sent()); - debug_assert!( - self.context.channel_state.is_awaiting_quiescence() - || self.context.channel_state.is_remote_stfu_sent() - ); - debug_assert!(self.context.is_live()); - - if self.context.is_waiting_on_peer_pending_channel_update() - || self.context.is_monitor_or_signer_pending_channel_update() - { - return Err("We cannot send `stfu` while state machine is pending") - } - - let initiator = if self.context.channel_state.is_remote_stfu_sent() { - // We may have also attempted to initiate quiescence. - self.context.channel_state.clear_awaiting_quiescence(); - self.context.channel_state.clear_remote_stfu_sent(); - self.context.channel_state.set_quiescent(); - // We are sending an stfu in response to our couterparty's stfu, but had not yet sent - // our own stfu (even if `awaiting_quiescence` was set). Thus, the counterparty is the - // initiator and they can do "something fundamental". - false - } else { - log_debug!(logger, "Sending stfu as quiescence initiator"); - debug_assert!(self.context.channel_state.is_awaiting_quiescence()); - self.context.channel_state.clear_awaiting_quiescence(); - self.context.channel_state.set_local_stfu_sent(); - true - }; - - Ok(msgs::Stfu { channel_id: self.context.channel_id, initiator }) + Ok(self.try_send_stfu(false, logger)) } #[rustfmt::skip] pub fn stfu<L: Logger>( &mut self, msg: &msgs::Stfu, logger: &L - ) -> Result<Option<StfuResponse>, ChannelError> { + ) -> Result<Option<StfuResponse>, (ChannelError, QuiescentError)> { if self.context.channel_state.is_quiescent() { - return Err(ChannelError::Warn("Channel is already quiescent".to_owned())); + return Err((ChannelError::Warn("Channel is already quiescent".to_owned()), QuiescentError::DoNothing)); } if self.context.channel_state.is_remote_stfu_sent() { - return Err(ChannelError::Warn( + return Err((ChannelError::Warn( "Peer sent `stfu` when they already sent it and we've yet to become quiescent".to_owned() - )); + ), QuiescentError::DoNothing)); } if !self.context.is_live() { - return Err(ChannelError::Warn( + return Err((ChannelError::Warn( "Peer sent `stfu` when we were not in a live state".to_owned() - )); + ), QuiescentError::DoNothing)); } if !self.context.channel_state.is_local_stfu_sent() { if !msg.initiator { - return Err(ChannelError::WarnAndDisconnect( + return Err((ChannelError::WarnAndDisconnect( "Peer sent unexpected `stfu` without signaling as initiator".to_owned() - )); + ), QuiescentError::DoNothing)); } // We don't check `is_waiting_on_peer_pending_channel_update` prior to setting the flag @@ -13432,10 +14926,7 @@ where self.context.channel_state.set_remote_stfu_sent(); log_debug!(logger, "Received counterparty stfu proposing quiescence"); - return self - .send_stfu(logger) - .map(|stfu| Some(StfuResponse::Stfu(stfu))) - .map_err(|e| ChannelError::Ignore(e.to_owned())); + return Ok(self.try_send_stfu(false, logger).map(|stfu| StfuResponse::Stfu(stfu))) } // We already sent `stfu` and are now processing theirs. It may be in response to ours, or @@ -13454,9 +14945,9 @@ where // have a monitor update pending if we've processed a message from the counterparty, but // we don't consider this when becoming quiescent since the states are not mutually // exclusive. - return Err(ChannelError::WarnAndDisconnect( + return Err((ChannelError::WarnAndDisconnect( "Received counterparty stfu while having pending counterparty updates".to_owned() - )); + ), QuiescentError::DoNothing)); } self.context.channel_state.clear_local_stfu_sent(); @@ -13472,26 +14963,74 @@ where match self.quiescent_action.take() { None => { debug_assert!(false); - return Err(ChannelError::WarnAndDisconnect( + return Err((ChannelError::WarnAndDisconnect( "Internal Error: Didn't have anything to do after reaching quiescence".to_owned() - )); + ), QuiescentError::DoNothing)); }, - Some(QuiescentAction::Splice(instructions)) => { - if self.pending_splice.is_some() { - self.quiescent_action = Some(QuiescentAction::Splice(instructions)); - - return Err(ChannelError::WarnAndDisconnect( - format!( - "Channel {} cannot be spliced as it already has a splice pending", + Some(QuiescentAction::Splice { contribution, locktime }) => { + // Re-validate the contribution now that we're quiescent and + // balances are stable. Outbound HTLCs may have been sent between + // funding_contributed and quiescence, reducing the holder's + // balance. If invalid, disconnect and return the contribution so + // the user can reclaim their inputs. + let our_funding_contribution = contribution.net_value(); + let unsigned_contribution = our_funding_contribution.unsigned_abs(); + if let Err(e) = self.get_next_splice_out_maximum(&self.funding) + .and_then(|splice_max| splice_max + .to_sat() + .checked_add_signed(our_funding_contribution.to_sat()) + .ok_or(format!("Our splice-out value of {unsigned_contribution} is greater than the maximum {splice_max}")) + ) + { + let failed = self.splice_funding_failed_for(contribution); + return Err(( + ChannelError::WarnAndDisconnect(format!( + "Channel {} contribution no longer valid at quiescence: {}", self.context.channel_id(), + e, + )), + QuiescentError::FailSplice( + failed, + NegotiationFailureReason::ContributionInvalid, ), )); } + let prior_contribution = contribution.clone(); + let prev_funding_input = self.funding.to_splice_funding_input(); + let our_funding_contribution = contribution.net_value(); + let funding_feerate_per_kw = contribution.feerate().to_sat_per_kwu() as u32; + let (our_funding_inputs, our_funding_outputs) = contribution.into_tx_parts(); + + let context = FundingNegotiationContext { + is_initiator: true, + our_funding_contribution, + funding_tx_locktime: locktime, + funding_feerate_sat_per_1000_weight: funding_feerate_per_kw, + shared_funding_input: Some(prev_funding_input), + our_funding_inputs, + our_funding_outputs, + }; + + if self.pending_splice.is_some() { + if let Err(e) = self.can_initiate_rbf() { + let failed = self.splice_funding_failed_for(prior_contribution); + return Err(( + ChannelError::WarnAndDisconnect(e), + QuiescentError::FailSplice( + failed, + NegotiationFailureReason::CannotInitiateRbf, + ), + )); + } + let tx_init_rbf = self.send_tx_init_rbf(context, prior_contribution); + return Ok(Some(StfuResponse::TxInitRbf(tx_init_rbf))); + } - let splice_init = self.send_splice_init(instructions); + let splice_init = self.send_splice_init(context, prior_contribution); + debug_assert!(self.pending_splice.is_some()); return Ok(Some(StfuResponse::SpliceInit(splice_init))); }, - #[cfg(any(test, fuzzing))] + #[cfg(any(test, fuzzing, feature = "_test_utils"))] Some(QuiescentAction::DoNothing) => { // In quiescence test we want to just hang out here, letting the test manually // leave quiescence. @@ -13502,44 +15041,92 @@ where Ok(None) } - pub fn try_send_stfu<L: Logger>( - &mut self, logger: &L, - ) -> Result<Option<msgs::Stfu>, ChannelError> { + pub fn try_send_stfu<L: Logger>(&mut self, is_retry: bool, logger: &L) -> Option<msgs::Stfu> { // We must never see both stfu flags set, we always set the quiescent flag instead. debug_assert!( !(self.context.channel_state.is_local_stfu_sent() && self.context.channel_state.is_remote_stfu_sent()) ); + // We only need to send `stfu` when we're awaiting quiescence and haven't sent it yet, or + // in response to a counterparty one. + if self.quiescent_action.is_none() && !self.context.channel_state.is_remote_stfu_sent() { + return None; + } + if self.context.channel_state.is_local_stfu_sent() + || self.context.channel_state.is_quiescent() + { + return None; + } + + let logger_level = if is_retry { LoggerLevel::Trace } else { LoggerLevel::Debug }; if !self.context.is_live() { - return Ok(None); + log_given_level!(logger, logger_level, "Waiting for peer reconnection to send stfu"); + return None; } - // We need to send our `stfu`, either because we're trying to initiate quiescence, or the - // counterparty is and we've yet to send ours. - if self.context.channel_state.is_awaiting_quiescence() - || (self.context.channel_state.is_remote_stfu_sent() - && !self.context.channel_state.is_local_stfu_sent()) + if self.context.is_waiting_on_peer_pending_channel_update() + || self.context.is_monitor_or_signer_pending_channel_update() { - return self - .send_stfu(logger) - .map(|stfu| Some(stfu)) - .map_err(|e| ChannelError::Ignore(e.to_owned())); + log_given_level!( + logger, + logger_level, + "Waiting for state machine pending changes to complete before sending stfu" + ); + return None; } - // We're either: - // - already quiescent - // - in a state where quiescence is not possible - // - not currently trying to become quiescent - Ok(None) + let initiator = if self.context.channel_state.is_remote_stfu_sent() { + // Since we may have also attempted to initiate quiescence but the counterparty + // initiated first, we'll retry after we're no longer quiescent. + self.context.channel_state.clear_remote_stfu_sent(); + self.context.channel_state.set_quiescent(); + false + } else if let Some(action) = self.quiescent_action.as_ref() { + #[allow(irrefutable_let_patterns)] + if let QuiescentAction::Splice { contribution, .. } = action { + if self.pending_splice.is_some() { + match self.can_initiate_rbf() { + Err(msg) => { + log_given_level!( + logger, + logger_level, + "Waiting on sending stfu for splice RBF: {msg}" + ); + return None; + }, + Ok(min_rbf_feerate) if contribution.feerate() < min_rbf_feerate => { + log_given_level!( + logger, + logger_level, + "Waiting for splice to lock: feerate {} below minimum RBF feerate {}", + contribution.feerate(), + min_rbf_feerate, + ); + return None; + }, + _ => {}, + } + } + } + + log_debug!(logger, "Sending stfu as quiescence initiator"); + self.context.channel_state.set_local_stfu_sent(); + true + } else { + debug_assert!( + false, + "Either we have a pending quiescent action or need to respond to the counterparty" + ); + false + }; + + Some(msgs::Stfu { channel_id: self.context.channel_id, initiator }) } - #[cfg(any(test, fuzzing))] - #[rustfmt::skip] pub fn exit_quiescence(&mut self) -> bool { // Make sure we either finished the quiescence handshake and are quiescent, or we never // attempted to initiate quiescence at all. - debug_assert!(!self.context.channel_state.is_awaiting_quiescence()); debug_assert!(!self.context.channel_state.is_local_stfu_sent()); debug_assert!(!self.context.channel_state.is_remote_stfu_sent()); @@ -13549,6 +15136,14 @@ where was_quiescent } + fn quiescent_negotiation_err(&mut self, err: ChannelError) -> InteractiveTxMsgError { + if matches!(err, ChannelError::Abort(_)) { + debug_assert!(self.context.channel_state.is_quiescent()); + self.exit_quiescence(); + } + InteractiveTxMsgError::new(err, None) + } + pub fn remove_legacy_scids_before_block(&mut self, height: u32) -> alloc::vec::Drain<'_, u64> { let end = self .funding @@ -13591,26 +15186,47 @@ impl<SP: SignerProvider> OutboundV1Channel<SP> { } #[allow(dead_code)] // TODO(dual_funding): Remove once opending V2 channels is enabled. - #[rustfmt::skip] pub fn new<ES: EntropySource, F: FeeEstimator, L: Logger>( - fee_estimator: &LowerBoundedFeeEstimator<F>, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, their_features: &InitFeatures, - channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, current_chain_height: u32, - outbound_scid_alias: u64, temporary_channel_id: Option<ChannelId>, logger: L + fee_estimator: &LowerBoundedFeeEstimator<F>, entropy_source: &ES, signer_provider: &SP, + counterparty_node_id: PublicKey, their_features: &InitFeatures, + channel_value_satoshis: u64, push_msat: u64, user_id: u128, config: &UserConfig, + current_chain_height: u32, outbound_scid_alias: u64, + temporary_channel_id: Option<ChannelId>, logger: L, + trusted_channel_features: Option<TrustedChannelFeatures>, ) -> Result<OutboundV1Channel<SP>, APIError> { - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(channel_value_satoshis, config); - if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS { + // At this point, we do not know what `dust_limit_satoshis` the counterparty will want for themselves, + // so we set the channel reserve with no regard for their dust limit, and fail the channel if they want + // a dust limit higher than our selected reserve. + let their_dust_limit_satoshis = 0; + let is_0reserve = trusted_channel_features.is_some_and(|f| f.is_0reserve()); + let holder_selected_channel_reserve_satoshis = + get_holder_selected_channel_reserve_satoshis( + channel_value_satoshis, + their_dust_limit_satoshis, + config, + is_0reserve, + ) + .map_err(|()| APIError::APIMisuseError { + err: format!( + "The channel value {channel_value_satoshis} is smaller than \ + {MIN_THEIR_CHAN_RESERVE_SATOSHIS}" + ), + })?; + if holder_selected_channel_reserve_satoshis < MIN_CHAN_DUST_LIMIT_SATOSHIS && !is_0reserve { // Protocol level safety check in place, although it should never happen because - // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` - return Err(APIError::APIMisuseError { err: format!("Holder selected channel reserve below \ - implemention limit dust_limit_satoshis {}", holder_selected_channel_reserve_satoshis) }); + // of `MIN_THEIR_CHAN_RESERVE_SATOSHIS` and `MIN_CHANNEL_VALUE_SATOSHIS` + return Err(APIError::APIMisuseError { + err: format!( + "Holder selected channel reserve below implementation limit dust_limit_satoshis {holder_selected_channel_reserve_satoshis}" + ), + }); } let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); - let temporary_channel_id_fn = temporary_channel_id.map(|id| { - move |_: &ChannelPublicKeys| id - }); + let temporary_channel_id_fn = + temporary_channel_id.map(|id| move |_: &ChannelPublicKeys| id); let (funding, context) = ChannelContext::new_for_outbound_channel( fee_estimator, @@ -13632,7 +15248,10 @@ impl<SP: SignerProvider> OutboundV1Channel<SP> { )?; let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, - holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), + holder_commitment_point: HolderCommitmentPoint::new( + &context.holder_signer, + &context.secp_ctx, + ), }; // We initialize `signer_pending_open_channel` to false, and leave setting the flag @@ -13648,16 +15267,19 @@ impl<SP: SignerProvider> OutboundV1Channel<SP> { self.context.counterparty_next_commitment_transaction_number, &self.context.counterparty_next_commitment_point.unwrap(), false, false, logger); let counterparty_initial_commitment_tx = commitment_data.tx; - let signature = match &self.context.holder_signer { - // TODO (taproot|arik): move match into calling method for Taproot - ChannelSignerType::Ecdsa(ecdsa) => { - let channel_parameters = &self.funding.channel_transaction_parameters; - ecdsa.sign_counterparty_commitment(channel_parameters, &counterparty_initial_commitment_tx, Vec::new(), Vec::new(), &self.context.secp_ctx) - .map(|(sig, _)| sig).ok() - }, - // TODO (taproot|arik) - #[cfg(taproot)] - _ => todo!() + let signature = { + let channel_parameters = &self.funding.channel_transaction_parameters; + self.context + .holder_signer + .sign_counterparty_commitment( + channel_parameters, + &counterparty_initial_commitment_tx, + Vec::new(), + Vec::new(), + &self.context.secp_ctx, + ) + .map(|(sig, _)| sig) + .ok() }; if signature.is_some() && self.context.signer_pending_funding { @@ -13673,10 +15295,6 @@ impl<SP: SignerProvider> OutboundV1Channel<SP> { funding_txid: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().txid, funding_output_index: self.funding.channel_transaction_parameters.funding_outpoint.as_ref().unwrap().index, signature, - #[cfg(taproot)] - partial_signature_with_nonce: None, - #[cfg(taproot)] - next_local_nonce: None, }) } @@ -13818,7 +15436,7 @@ impl<SP: SignerProvider> OutboundV1Channel<SP> { /// Handles a funding_signed message from the remote end. /// If this call is successful, broadcast the funding transaction (and not before!) pub fn funding_signed<L: Logger>( - mut self, msg: &msgs::FundingSigned, best_block: BestBlock, signer_provider: &SP, + mut self, msg: &msgs::FundingSigned, best_block: BlockLocator, signer_provider: &SP, logger: &L, ) -> Result< (FundedChannel<SP>, ChannelMonitor<SP::EcdsaSigner>), @@ -13966,26 +15584,47 @@ pub(super) fn channel_type_from_open_channel( impl<SP: SignerProvider> InboundV1Channel<SP> { /// Creates a new channel from a remote sides' request for one. /// Assumes chain_hash has already been checked and corresponds with what we expect! - #[rustfmt::skip] pub fn new<ES: EntropySource, F: FeeEstimator, L: Logger>( fee_estimator: &LowerBoundedFeeEstimator<F>, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, their_features: &InitFeatures, msg: &msgs::OpenChannel, user_id: u128, config: &UserConfig, - current_chain_height: u32, logger: &L, is_0conf: bool, + current_chain_height: u32, logger: &L, + trusted_channel_features: Option<TrustedChannelFeatures>, ) -> Result<InboundV1Channel<SP>, ChannelError> { - let logger = WithContext::from(logger, Some(counterparty_node_id), Some(msg.common_fields.temporary_channel_id), None); + let logger = WithContext::from( + logger, + Some(counterparty_node_id), + Some(msg.common_fields.temporary_channel_id), + None, + ); // First check the channel type is known, failing before we do anything else if we don't // support this channel type. - let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; + let channel_type = + channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; - let holder_selected_channel_reserve_satoshis = get_holder_selected_channel_reserve_satoshis(msg.common_fields.funding_satoshis, config); + let holder_selected_channel_reserve_satoshis = + get_holder_selected_channel_reserve_satoshis( + msg.common_fields.funding_satoshis, + msg.common_fields.dust_limit_satoshis, + config, + trusted_channel_features.is_some_and(|f| f.is_0reserve()), + ) + .map_err(|()| { + ChannelError::close(format!( + "The channel value {} is smaller than either their dust \ + limit {}, or {MIN_THEIR_CHAN_RESERVE_SATOSHIS}", + msg.common_fields.funding_satoshis, msg.common_fields.dust_limit_satoshis, + )) + })?; let counterparty_pubkeys = ChannelPublicKeys { funding_pubkey: msg.common_fields.funding_pubkey, revocation_basepoint: RevocationBasepoint::from(msg.common_fields.revocation_basepoint), payment_point: msg.common_fields.payment_basepoint, - delayed_payment_basepoint: DelayedPaymentBasepoint::from(msg.common_fields.delayed_payment_basepoint), - htlc_basepoint: HtlcBasepoint::from(msg.common_fields.htlc_basepoint) + delayed_payment_basepoint: DelayedPaymentBasepoint::from( + msg.common_fields.delayed_payment_basepoint, + ), + htlc_basepoint: HtlcBasepoint::from(msg.common_fields.htlc_basepoint), }; let (funding, context) = ChannelContext::new_for_inbound_channel( @@ -13998,9 +15637,8 @@ impl<SP: SignerProvider> InboundV1Channel<SP> { config, current_chain_height, &&logger, - is_0conf, + trusted_channel_features, 0, - counterparty_pubkeys, channel_type, holder_selected_channel_reserve_satoshis, @@ -14010,9 +15648,13 @@ impl<SP: SignerProvider> InboundV1Channel<SP> { )?; let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, - holder_commitment_point: HolderCommitmentPoint::new(&context.holder_signer, &context.secp_ctx), + holder_commitment_point: HolderCommitmentPoint::new( + &context.holder_signer, + &context.secp_ctx, + ), }; - let chan = Self { funding, context, unfunded_context, signer_pending_accept_channel: false }; + let chan = + Self { funding, context, unfunded_context, signer_pending_accept_channel: false }; Ok(chan) } @@ -14081,8 +15723,6 @@ impl<SP: SignerProvider> InboundV1Channel<SP> { channel_type: Some(self.funding.get_channel_type().clone()), }, channel_reserve_satoshis: self.funding.holder_selected_channel_reserve_satoshis, - #[cfg(taproot)] - next_local_nonce: None, }) } @@ -14098,7 +15738,7 @@ impl<SP: SignerProvider> InboundV1Channel<SP> { } pub fn funding_created<L: Logger>( - mut self, msg: &msgs::FundingCreated, best_block: BestBlock, signer_provider: &SP, + mut self, msg: &msgs::FundingCreated, best_block: BlockLocator, signer_provider: &SP, logger: &L, ) -> Result< (FundedChannel<SP>, Option<msgs::FundingSigned>, ChannelMonitor<SP::EcdsaSigner>), @@ -14221,9 +15861,9 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { pub fn new_outbound<ES: EntropySource, F: FeeEstimator, L: Logger>( fee_estimator: &LowerBoundedFeeEstimator<F>, entropy_source: &ES, signer_provider: &SP, counterparty_node_id: PublicKey, their_features: &InitFeatures, funding_satoshis: u64, - funding_inputs: Vec<FundingTxInput>, user_id: u128, config: &UserConfig, + funding_inputs: Vec<ConfirmedUtxo>, user_id: u128, config: &UserConfig, current_chain_height: u32, outbound_scid_alias: u64, funding_confirmation_target: ConfirmationTarget, - logger: L, + logger: L, trusted_channel_features: Option<TrustedChannelFeatures>, ) -> Result<Self, APIError> { let channel_keys_id = signer_provider.generate_channel_keys_id(false, user_id); let holder_signer = signer_provider.derive_channel_signer(channel_keys_id); @@ -14233,8 +15873,13 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { }); let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); - + funding_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, trusted_channel_features.is_some_and(|f| f.is_0reserve()) + ).map_err(|()| APIError::APIMisuseError { + err: format!( + "The channel value {funding_satoshis} is smaller than their dust \ + limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}" + ) + })?; let funding_feerate_sat_per_1000_weight = fee_estimator.bounded_sat_per_1000_weight(funding_confirmation_target); let funding_tx_locktime = LockTime::from_height(current_chain_height) .map_err(|_| APIError::APIMisuseError { @@ -14272,7 +15917,6 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { shared_funding_input: None, our_funding_inputs: funding_inputs, our_funding_outputs: Vec::new(), - change_script: None, }; let chan = Self { funding, @@ -14314,11 +15958,11 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { debug_assert!(false, "Tried to send an open_channel2 for a channel that has already advanced"); } - let first_per_commitment_point = self.context.holder_signer.as_ref() + let first_per_commitment_point = self.context.holder_signer .get_per_commitment_point(self.unfunded_context.transaction_number(), &self.context.secp_ctx) .expect("TODO: async signing is not yet supported for commitment points in v2 channel establishment"); - let second_per_commitment_point = self.context.holder_signer.as_ref() + let second_per_commitment_point = self.context.holder_signer .get_per_commitment_point(self.unfunded_context.transaction_number() - 1, &self.context.secp_ctx) .expect("TODO: async signing is not yet supported for commitment points in v2 channel establishment"); @@ -14352,6 +15996,7 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { second_per_commitment_point, locktime: self.funding_negotiation_context.funding_tx_locktime.to_consensus_u32(), require_confirmed_inputs: None, + disable_channel_reserve: (self.funding.holder_selected_channel_reserve_satoshis == 0).then_some(()), } } @@ -14364,7 +16009,7 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { fee_estimator: &LowerBoundedFeeEstimator<F>, entropy_source: &ES, signer_provider: &SP, holder_node_id: PublicKey, counterparty_node_id: PublicKey, our_supported_features: &ChannelTypeFeatures, their_features: &InitFeatures, msg: &msgs::OpenChannelV2, - user_id: u128, config: &UserConfig, current_chain_height: u32, logger: &L, + user_id: u128, config: &UserConfig, current_chain_height: u32, logger: &L, trusted_channel_features: Option<TrustedChannelFeatures>, ) -> Result<Self, ChannelError> { // TODO(dual_funding): Take these as input once supported let (our_funding_contribution, our_funding_contribution_sats) = (SignedAmount::ZERO, 0u64); @@ -14373,9 +16018,16 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { let channel_value_satoshis = our_funding_contribution_sats.saturating_add(msg.common_fields.funding_satoshis); let counterparty_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, msg.common_fields.dust_limit_satoshis); + channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS, msg.disable_channel_reserve.is_some() + ).map_err(|()| ChannelError::close(format!( + "The channel value {channel_value_satoshis} is smaller than our dust limit {MIN_CHAN_DUST_LIMIT_SATOSHIS}" + )))?; + let their_dust_limit_satoshis = msg.common_fields.dust_limit_satoshis; let holder_selected_channel_reserve_satoshis = get_v2_channel_reserve_satoshis( - channel_value_satoshis, MIN_CHAN_DUST_LIMIT_SATOSHIS); + channel_value_satoshis, their_dust_limit_satoshis, trusted_channel_features.is_some_and(|f| f.is_0reserve()) + ).map_err(|()| ChannelError::close(format!( + "The channel value {channel_value_satoshis} is smaller than their dust limit {their_dust_limit_satoshis}" + )))?; let channel_type = channel_type_from_open_channel(&msg.common_fields, our_supported_features)?; @@ -14397,7 +16049,7 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { config, current_chain_height, logger, - false, + trusted_channel_features, our_funding_contribution_sats, counterparty_pubkeys, channel_type, @@ -14419,14 +16071,13 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { shared_funding_input: None, our_funding_inputs: our_funding_inputs.clone(), our_funding_outputs: Vec::new(), - change_script: None, }; let shared_funding_output = TxOut { value: Amount::from_sat(funding.get_value_satoshis()), script_pubkey: funding.get_funding_redeemscript().to_p2wsh(), }; - let interactive_tx_constructor = Some(InteractiveTxConstructor::new( + let interactive_tx_constructor = Some(InteractiveTxConstructor::new_for_inbound( InteractiveTxConstructorArgs { entropy_source, holder_node_id, @@ -14434,16 +16085,12 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { channel_id: context.channel_id, feerate_sat_per_kw: funding_negotiation_context.funding_feerate_sat_per_1000_weight, funding_tx_locktime: funding_negotiation_context.funding_tx_locktime, - is_initiator: false, inputs_to_contribute: our_funding_inputs, shared_funding_input: None, shared_funding_output: SharedOwnedOutput::new(shared_funding_output, our_funding_contribution_sats), outputs_to_contribute: funding_negotiation_context.our_funding_outputs.clone(), } - ).map_err(|err| { - let reason = ClosureReason::ProcessingError { err: err.reason.to_string() }; - ChannelError::Close((err.reason.to_string(), reason)) - })?); + )); let unfunded_context = UnfundedChannelContext { unfunded_channel_age_ticks: 0, @@ -14488,10 +16135,10 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { /// [`msgs::AcceptChannelV2`]: crate::ln::msgs::AcceptChannelV2 #[allow(dead_code)] // TODO(dual_funding): Remove once V2 channels is enabled. fn generate_accept_channel_v2_message(&self) -> msgs::AcceptChannelV2 { - let first_per_commitment_point = self.context.holder_signer.as_ref().get_per_commitment_point( + let first_per_commitment_point = self.context.holder_signer.get_per_commitment_point( self.unfunded_context.transaction_number(), &self.context.secp_ctx) .expect("TODO: async signing is not yet supported for commitment points in v2 channel establishment"); - let second_per_commitment_point = self.context.holder_signer.as_ref().get_per_commitment_point( + let second_per_commitment_point = self.context.holder_signer.get_per_commitment_point( self.unfunded_context.transaction_number() - 1, &self.context.secp_ctx) .expect("TODO: async signing is not yet supported for commitment points in v2 channel establishment"); let keys = self.funding.get_holder_pubkeys(); @@ -14521,6 +16168,8 @@ impl<SP: SignerProvider> PendingV2Channel<SP> { as u64, second_per_commitment_point, require_confirmed_inputs: None, + disable_channel_reserve: (self.funding.holder_selected_channel_reserve_satoshis == 0) + .then_some(()), } } @@ -14646,14 +16295,9 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> { match channel_state { ChannelState::AwaitingChannelReady(_) => {}, ChannelState::ChannelReady(_) => { - if self.quiescent_action.is_some() { - // If we're trying to get quiescent to do something, try again when we - // reconnect to the peer. - channel_state.set_awaiting_quiescence(); - } channel_state.clear_local_stfu_sent(); channel_state.clear_remote_stfu_sent(); - if self.should_reset_pending_splice_state(false) + if self.should_reset_pending_splice_state(true) || !self.has_pending_splice_awaiting_signatures() { // We shouldn't be quiescent anymore upon reconnecting if: @@ -14694,6 +16338,7 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> { } } let mut removed_htlc_attribution_data: Vec<&Option<AttributionData>> = Vec::new(); + #[cfg_attr(not(test), allow(unused_mut))] let mut inbound_committed_update_adds: Vec<&InboundUpdateAdd> = Vec::new(); (self.context.pending_inbound_htlcs.len() as u64 - dropped_inbound_htlcs).write(writer)?; for htlc in self.context.pending_inbound_htlcs.iter() { @@ -14714,9 +16359,10 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> { 2u8.write(writer)?; htlc_resolution.write(writer)?; }, - &InboundHTLCState::Committed { ref update_add_htlc } => { + &InboundHTLCState::Committed { update_add_htlc: ref _update_add } => { 3u8.write(writer)?; - inbound_committed_update_adds.push(update_add_htlc); + #[cfg(test)] + inbound_committed_update_adds.push(_update_add); }, &InboundHTLCState::LocalRemoved(ref removal_reason) => { 4u8.write(writer)?; @@ -14993,15 +16639,11 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> { None }; - let mut old_max_in_flight_percent_config = UserConfig::default().channel_handshake_config; - old_max_in_flight_percent_config.max_inbound_htlc_value_in_flight_percent_of_channel = - MAX_IN_FLIGHT_PERCENT_LEGACY; - let max_in_flight_msat = get_holder_max_htlc_value_in_flight_msat( + let legacy_max_in_flight_msat = get_legacy_default_holder_max_htlc_value_in_flight_msat( self.funding.get_value_satoshis(), - &old_max_in_flight_percent_config, ); let serialized_holder_htlc_max_in_flight = - if self.context.holder_max_htlc_value_in_flight_msat != max_in_flight_msat { + if self.context.holder_max_htlc_value_in_flight_msat != legacy_max_in_flight_msat { Some(self.context.holder_max_htlc_value_in_flight_msat) } else { None @@ -15031,6 +16673,11 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> { } let is_manual_broadcast = Some(self.context.is_manual_broadcast); + // We prevent downgrades from 0.3 only in the case where the holder-selected reserve + // is 0, as we've had support for counterparty selected 0-reserves in prior + // releases. + let has_0reserve = + (self.funding.holder_selected_channel_reserve_satoshis == 0).then_some(()); let holder_commitment_point_previous_revoked = self.holder_commitment_point.previous_revoked_point(); let holder_commitment_point_last_revoked = @@ -15039,10 +16686,18 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> { let holder_commitment_point_next = self.holder_commitment_point.next_point(); let holder_commitment_point_pending_next = self.holder_commitment_point.pending_next_point; - // We don't have to worry about resetting the pending `FundingNegotiation` because we - // can only read `FundingNegotiation::AwaitingSignatures` variants anyway. - let pending_splice = - self.pending_splice.as_ref().filter(|_| !self.should_reset_pending_splice_state(false)); + // Avoid writing any negotiations that are not at the signing stage yet, as they cannot be + // resumed on reestablishment, but keep any already-negotiated candidates. + let reset_funding_negotiation = self.should_reset_pending_splice_state(true); + let should_persist_pending_splice = + !reset_funding_negotiation || !self.negotiated_candidates().is_empty(); + let pending_splice = should_persist_pending_splice + .then(|| ()) + .and_then(|_| self.pending_splice.as_ref()) + .map(|pending_funding| PendingFundingWriteable { + pending_funding, + reset_funding_negotiation, + }); let monitor_pending_tx_signatures = self.context.monitor_pending_tx_signatures.then_some(()); @@ -15097,9 +16752,10 @@ impl<SP: SignerProvider> Writeable for FundedChannel<SP> { (61, fulfill_attribution_data, optional_vec), // Added in 0.2 (63, holder_commitment_point_current, option), // Added in 0.2 (64, pending_splice, option), // Added in 0.2 - (65, self.quiescent_action, option), // Added in 0.2 + // 65 was previously used for quiescent_action (67, pending_outbound_held_htlc_flags, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags, optional_vec), // Added in 0.2 + (70, has_0reserve, option), // Added in 0.3 to prevent downgrades (71, holder_commitment_point_previous_revoked, option), // Added in 0.3 (73, holder_commitment_point_last_revoked, option), // Added in 0.3 (75, inbound_committed_update_adds, optional_vec), @@ -15427,11 +17083,9 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> let mut holder_selected_channel_reserve_satoshis = Some( get_legacy_default_holder_selected_channel_reserve_satoshis(channel_value_satoshis), ); + let mut holder_max_htlc_value_in_flight_msat = - Some(get_holder_max_htlc_value_in_flight_msat( - channel_value_satoshis, - &UserConfig::default().channel_handshake_config, - )); + Some(get_legacy_default_holder_max_htlc_value_in_flight_msat(channel_value_satoshis)); // Prior to supporting channel type negotiation, all of our channels were static_remotekey // only, so we default to that if none was written. let mut channel_type = Some(ChannelTypeFeatures::only_static_remote_key()); @@ -15473,6 +17127,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> let mut malformed_htlcs: Option<Vec<(u64, u16, [u8; 32])>> = None; let mut monitor_pending_update_adds: Option<Vec<msgs::UpdateAddHTLC>> = None; + let mut _has_0reserve: Option<()> = None; let mut holder_commitment_point_previous_revoked_opt: Option<PublicKey> = None; let mut holder_commitment_point_last_revoked_opt: Option<PublicKey> = None; let mut holder_commitment_point_current_opt: Option<PublicKey> = None; @@ -15487,7 +17142,6 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> let mut minimum_depth_override: Option<u32> = None; let mut pending_splice: Option<PendingFunding> = None; - let mut quiescent_action = None; let mut pending_outbound_held_htlc_flags_opt: Option<Vec<Option<()>>> = None; let mut holding_cell_held_htlc_flags_opt: Option<Vec<Option<()>>> = None; @@ -15541,9 +17195,10 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> (61, fulfill_attribution_data, optional_vec), // Added in 0.2 (63, holder_commitment_point_current_opt, option), // Added in 0.2 (64, pending_splice, option), // Added in 0.2 - (65, quiescent_action, upgradable_option), // Added in 0.2 + // 65 quiescent_action: Added in 0.2; removed in 0.3 (67, pending_outbound_held_htlc_flags_opt, optional_vec), // Added in 0.2 (69, holding_cell_held_htlc_flags_opt, optional_vec), // Added in 0.2 + (70, _has_0reserve, option), // Added in 0.3 to prevent downgrades (71, holder_commitment_point_previous_revoked_opt, option), // Added in 0.3 (73, holder_commitment_point_last_revoked_opt, option), // Added in 0.3 (75, inbound_committed_update_adds_opt, optional_vec), @@ -15875,9 +17530,9 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> .unwrap(), #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((0, 0)), + holder_prev_commitment_tx_balance: Mutex::new((0, 0)), #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((0, 0)), + counterparty_prev_commitment_tx_balance: Mutex::new((0, 0)), #[cfg(any(test, fuzzing))] next_local_fee: Mutex::new(PredictedNextFee::default()), @@ -15910,7 +17565,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> latest_monitor_update_id, - holder_signer: ChannelSignerType::Ecdsa(holder_signer), + holder_signer, shutdown_scriptpubkey, destination_script, @@ -15982,6 +17637,7 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> announcement_sigs, workaround_lnd_bug_4006: None, + funding_locked_txid_sent_in_reestablish: None, sent_message_awaiting_response: None, latest_inbound_scid_alias, @@ -16006,16 +17662,16 @@ impl<'a, 'b, 'c, ES: EntropySource, SP: SignerProvider> }, holder_commitment_point, pending_splice, - quiescent_action, + quiescent_action: None, }) } } fn duration_since_epoch() -> Option<Duration> { - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let now = None; - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let now = Some( std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) @@ -16040,28 +17696,26 @@ pub(crate) fn hold_time_since(send_timestamp: Option<Duration>) -> Option<u32> { mod tests { use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::transaction::OutPoint; - use crate::chain::BestBlock; - use crate::ln::chan_utils::{self, commit_tx_fee_sat, ChannelTransactionParameters}; - use crate::ln::channel::{ - AwaitingChannelReadyFlags, ChannelState, FundedChannel, HTLCCandidate, HTLCInitiator, - HTLCUpdateAwaitingACK, InboundHTLCOutput, InboundHTLCState, InboundUpdateAdd, - InboundV1Channel, OutboundHTLCOutput, OutboundHTLCState, OutboundV1Channel, - }; + use crate::chain::BlockLocator; + use crate::ln::chan_utils::{self, commit_tx_fee_sat}; use crate::ln::channel::{ - MAX_FUNDING_SATOSHIS_NO_WUMBO, MIN_THEIR_CHAN_RESERVE_SATOSHIS, - TOTAL_BITCOIN_SUPPLY_SATOSHIS, + AwaitingChannelReadyFlags, ChannelState, FundedChannel, HTLCUpdateAwaitingACK, + InboundHTLCOutput, InboundHTLCState, InboundUpdateAdd, InboundV1Channel, + OutboundHTLCOutput, OutboundHTLCState, OutboundV1Channel, WithChannelContext, + MIN_THEIR_CHAN_RESERVE_SATOSHIS, }; use crate::ln::channel_keys::{RevocationBasepoint, RevocationKey}; - use crate::ln::channelmanager::{self, HTLCSource, PaymentId}; - use crate::ln::funding::FundingTxInput; + use crate::ln::channelmanager::{self, HTLCSource, PaymentId, TrustedChannelFeatures}; use crate::ln::msgs; use crate::ln::msgs::{ChannelUpdate, UnsignedChannelUpdate, MAX_VALUE_MSAT}; use crate::ln::onion_utils::{AttributionData, LocalHTLCFailureReason}; use crate::ln::script::ShutdownScript; use crate::prelude::*; use crate::routing::router::{Path, RouteHop}; + use crate::sign::tx_builder::HTLCAmountDirection; #[cfg(ldk_test_vectors)] use crate::sign::{ChannelSigner, EntropySource, InMemorySigner, SignerProvider}; + #[cfg(ldk_test_vectors)] use crate::sync::Mutex; #[cfg(ldk_test_vectors)] use crate::types::features::ChannelTypeFeatures; @@ -16085,7 +17739,7 @@ mod tests { use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{Transaction, TxOut, Version}; - use bitcoin::{ScriptBuf, WPubkeyHash, WitnessProgram, WitnessVersion}; + use bitcoin::{WitnessProgram, WitnessVersion}; use std::cmp; fn dummy_inbound_update_add() -> InboundUpdateAdd { @@ -16106,15 +17760,6 @@ mod tests { assert!(ChannelState::ChannelReady(ChannelReadyFlags::new()) < ChannelState::ShutdownComplete); } - #[test] - fn test_max_funding_satoshis_no_wumbo() { - assert_eq!(TOTAL_BITCOIN_SUPPLY_SATOSHIS, 21_000_000 * 100_000_000); - assert!( - MAX_FUNDING_SATOSHIS_NO_WUMBO <= TOTAL_BITCOIN_SUPPLY_SATOSHIS, - "MAX_FUNDING_SATOSHIS_NO_WUMBO is greater than all satoshis in existence" - ); - } - #[cfg(ldk_test_vectors)] struct Keys { signer: crate::sign::InMemorySigner, @@ -16130,8 +17775,6 @@ mod tests { #[cfg(ldk_test_vectors)] impl SignerProvider for Keys { type EcdsaSigner = InMemorySigner; - #[cfg(taproot)] - type TaprootSigner = InMemorySigner; fn generate_channel_keys_id(&self, _inbound: bool, _user_channel_id: u128) -> [u8; 32] { self.signer.channel_keys_id() @@ -16205,6 +17848,7 @@ mod tests { 42, None, &logger, + None, ); match res { Err(APIError::IncompatibleShutdownScript { script }) => { @@ -16231,7 +17875,7 @@ mod tests { let node_a_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&bounded_fee_estimator, &&keys_provider, &&keys_provider, node_a_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&bounded_fee_estimator, &&keys_provider, &&keys_provider, node_a_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger, None).unwrap(); // Now change the fee so we can check that the fee in the open_channel message is the // same as the old fee. @@ -16252,7 +17896,7 @@ mod tests { let network = Network::Testnet; let keys_provider = TestKeysInterface::new(&seed, network); let logger = TestLogger::new(); - let best_block = BestBlock::from_network(network); + let best_block = BlockLocator::from_network(network); // Go through the flow of opening a channel between two nodes, making sure // they have different dust limits. @@ -16261,13 +17905,13 @@ mod tests { let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let mut config = UserConfig::default(); config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger, None).unwrap(); // Create Node B's channel by receiving Node A's open_channel message // Make sure A's dust limit is as we expect. let open_channel_msg = node_a_chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap(); let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap()); - let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, /*is_0conf=*/false).unwrap(); + let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, None).unwrap(); // Node B --> Node A: accept channel, explicitly setting B's dust limit. let mut accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap(); @@ -16320,8 +17964,8 @@ mod tests { // Make sure when Node A calculates their local commitment transaction, none of the HTLCs pass // the dust limit check. - let htlc_candidate = HTLCCandidate::new(htlc_amount_msat, HTLCInitiator::LocalOffered); - let local_commit_tx_fee = node_a_chan.context.next_local_commit_tx_fee_msat(&node_a_chan.funding, htlc_candidate, None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amount_msat, outbound: true }; + let local_commit_tx_fee = node_a_chan.context.get_next_local_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), false, 0, node_a_chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000; let local_commit_fee_0_htlcs = commit_tx_fee_sat(node_a_chan.context.feerate_per_kw, 0, node_a_chan.funding.get_channel_type()) * 1000; assert_eq!(local_commit_tx_fee, local_commit_fee_0_htlcs); @@ -16329,15 +17973,15 @@ mod tests { // of the HTLCs are seen to be above the dust limit. node_a_chan.funding.channel_transaction_parameters.is_outbound_from_holder = false; let remote_commit_fee_3_htlcs = commit_tx_fee_sat(node_a_chan.context.feerate_per_kw, 3, node_a_chan.funding.get_channel_type()) * 1000; - let htlc_candidate = HTLCCandidate::new(htlc_amount_msat, HTLCInitiator::LocalOffered); - let remote_commit_tx_fee = node_a_chan.context.next_remote_commit_tx_fee_msat(&node_a_chan.funding, Some(htlc_candidate), None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amount_msat, outbound: true }; + let remote_commit_tx_fee = node_a_chan.context.get_next_remote_commitment_stats(&node_a_chan.funding, Some(htlc_candidate), false, 0, node_a_chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000; assert_eq!(remote_commit_tx_fee, remote_commit_fee_3_htlcs); } #[test] #[rustfmt::skip] fn test_timeout_vs_success_htlc_dust_limit() { - // Make sure that when `next_remote_commit_tx_fee_msat` and `next_local_commit_tx_fee_msat` + // Make sure that when `get_next_local/remote_commitment_stats` // calculate the real dust limits for HTLCs (i.e. the dust limit given by the counterparty // *plus* the fees paid for the HTLC) they don't swap `HTLC_SUCCESS_TX_WEIGHT` for // `HTLC_TIMEOUT_TX_WEIGHT`, and vice versa. @@ -16352,7 +17996,8 @@ mod tests { let node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let mut config = UserConfig::default(); config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; - let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&fee_est, &&keys_provider, &&keys_provider, node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&fee_est, &&keys_provider, &&keys_provider, node_id, &channelmanager::provided_init_features(&config), 10_000_000, 100_000_000, 42, &config, 0, 42, None, &logger, None).unwrap(); + chan.context.counterparty_max_htlc_value_in_flight_msat = 1_000_000_000; let commitment_tx_fee_0_htlcs = commit_tx_fee_sat(chan.context.feerate_per_kw, 0, chan.funding.get_channel_type()) * 1000; let commitment_tx_fee_1_htlc = commit_tx_fee_sat(chan.context.feerate_per_kw, 1, chan.funding.get_channel_type()) * 1000; @@ -16363,28 +18008,28 @@ mod tests { // If HTLC_SUCCESS_TX_WEIGHT and HTLC_TIMEOUT_TX_WEIGHT were swapped: then this HTLC would be // counted as dust when it shouldn't be. let htlc_amt_above_timeout = (htlc_timeout_tx_fee_sat + chan.context.holder_dust_limit_satoshis + 1) * 1000; - let htlc_candidate = HTLCCandidate::new(htlc_amt_above_timeout, HTLCInitiator::LocalOffered); - let commitment_tx_fee = chan.context.next_local_commit_tx_fee_msat(&chan.funding, htlc_candidate, None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amt_above_timeout, outbound: true }; + let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_1_htlc); // If swapped: this HTLC would be counted as non-dust when it shouldn't be. let dust_htlc_amt_below_success = (htlc_success_tx_fee_sat + chan.context.holder_dust_limit_satoshis - 1) * 1000; - let htlc_candidate = HTLCCandidate::new(dust_htlc_amt_below_success, HTLCInitiator::RemoteOffered); - let commitment_tx_fee = chan.context.next_local_commit_tx_fee_msat(&chan.funding, htlc_candidate, None); + let htlc_candidate = HTLCAmountDirection { amount_msat: dust_htlc_amt_below_success, outbound: false }; + let commitment_tx_fee = chan.context.get_next_local_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_0_htlcs); chan.funding.channel_transaction_parameters.is_outbound_from_holder = false; // If swapped: this HTLC would be counted as non-dust when it shouldn't be. let dust_htlc_amt_above_timeout = (htlc_timeout_tx_fee_sat + chan.context.counterparty_dust_limit_satoshis + 1) * 1000; - let htlc_candidate = HTLCCandidate::new(dust_htlc_amt_above_timeout, HTLCInitiator::LocalOffered); - let commitment_tx_fee = chan.context.next_remote_commit_tx_fee_msat(&chan.funding, Some(htlc_candidate), None); + let htlc_candidate = HTLCAmountDirection { amount_msat: dust_htlc_amt_above_timeout, outbound: true }; + let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_0_htlcs); // If swapped: this HTLC would be counted as dust when it shouldn't be. let htlc_amt_below_success = (htlc_success_tx_fee_sat + chan.context.counterparty_dust_limit_satoshis - 1) * 1000; - let htlc_candidate = HTLCCandidate::new(htlc_amt_below_success, HTLCInitiator::RemoteOffered); - let commitment_tx_fee = chan.context.next_remote_commit_tx_fee_msat(&chan.funding, Some(htlc_candidate), None); + let htlc_candidate = HTLCAmountDirection { amount_msat: htlc_amt_below_success, outbound: false }; + let commitment_tx_fee = chan.context.get_next_remote_commitment_stats(&chan.funding, Some(htlc_candidate), false, 0, chan.context.feerate_per_kw, false, None).unwrap().0.commitment_stats.commit_tx_fee_sat * 1000; assert_eq!(commitment_tx_fee, commitment_tx_fee_1_htlc); } @@ -16397,7 +18042,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let seed = [42; 32]; let network = Network::Testnet; - let best_block = BestBlock::from_network(network); + let best_block = BlockLocator::from_network(network); let chain_hash = ChainHash::using_genesis_block(network); let keys_provider = TestKeysInterface::new(&seed, network); @@ -16406,12 +18051,12 @@ mod tests { // Create Node A's channel pointing to Node B's pubkey let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger, None).unwrap(); // Create Node B's channel by receiving Node A's open_channel message let open_channel_msg = node_a_chan.get_open_channel(chain_hash, &&logger).unwrap(); let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap()); - let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, /*is_0conf=*/false).unwrap(); + let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, None).unwrap(); // Node B --> Node A: accept channel let accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap(); @@ -16448,8 +18093,13 @@ mod tests { } #[test] - #[rustfmt::skip] fn test_configured_holder_max_htlc_value_in_flight() { + do_test_configured_holder_max_htlc_value_in_flight(true); + do_test_configured_holder_max_htlc_value_in_flight(false); + } + + #[rustfmt::skip] + fn do_test_configured_holder_max_htlc_value_in_flight(announce_channel: bool) { let test_est = TestFeeEstimator::new(15000); let feeest = LowerBoundedFeeEstimator::new(&test_est); let logger = TestLogger::new(); @@ -16461,23 +18111,59 @@ mod tests { let inbound_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap()); let mut config_2_percent = UserConfig::default(); - config_2_percent.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 2; + config_2_percent.channel_handshake_config.announce_for_forwarding = announce_channel; + if announce_channel { + config_2_percent + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 2; + } else { + config_2_percent + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage = 2; + } let mut config_99_percent = UserConfig::default(); - config_99_percent.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 99; + config_99_percent.channel_handshake_config.announce_for_forwarding = announce_channel; + if announce_channel { + config_99_percent + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 99; + } else { + config_99_percent + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage = 99; + } let mut config_0_percent = UserConfig::default(); - config_0_percent.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 0; + config_0_percent.channel_handshake_config.announce_for_forwarding = announce_channel; + if announce_channel { + config_0_percent + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 0; + } else { + config_0_percent + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage = 0; + } let mut config_101_percent = UserConfig::default(); - config_101_percent.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 101; + config_101_percent.channel_handshake_config.announce_for_forwarding = announce_channel; + if announce_channel { + config_101_percent + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 101; + } else { + config_101_percent + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage = 101; + } // Test that `OutboundV1Channel::new` creates a channel with the correct value for // `holder_max_htlc_value_in_flight_msat`, when configured with a valid percentage value, // which is set to the lower bound + 1 (2%) of the `channel_value`. - let mut chan_1 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_2_percent), 10000000, 100000, 42, &config_2_percent, 0, 42, None, &logger).unwrap(); + let mut chan_1 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_2_percent), 10000000, 100000, 42, &config_2_percent, 0, 42, None, &logger, None).unwrap(); let chan_1_value_msat = chan_1.funding.get_value_satoshis() * 1000; assert_eq!(chan_1.context.holder_max_htlc_value_in_flight_msat, (chan_1_value_msat as f64 * 0.02) as u64); // Test with the upper bound - 1 of valid values (99%). - let chan_2 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_99_percent), 10000000, 100000, 42, &config_99_percent, 0, 42, None, &logger).unwrap(); + let chan_2 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_99_percent), 10000000, 100000, 42, &config_99_percent, 0, 42, None, &logger, None).unwrap(); let chan_2_value_msat = chan_2.funding.get_value_satoshis() * 1000; assert_eq!(chan_2.context.holder_max_htlc_value_in_flight_msat, (chan_2_value_msat as f64 * 0.99) as u64); @@ -16486,38 +18172,38 @@ mod tests { // Test that `InboundV1Channel::new` creates a channel with the correct value for // `holder_max_htlc_value_in_flight_msat`, when configured with a valid percentage value, // which is set to the lower bound - 1 (2%) of the `channel_value`. - let chan_3 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_2_percent), &channelmanager::provided_init_features(&config_2_percent), &chan_1_open_channel_msg, 7, &config_2_percent, 0, &&logger, /*is_0conf=*/false).unwrap(); + let chan_3 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_2_percent), &channelmanager::provided_init_features(&config_2_percent), &chan_1_open_channel_msg, 7, &config_2_percent, 0, &&logger, None).unwrap(); let chan_3_value_msat = chan_3.funding.get_value_satoshis() * 1000; assert_eq!(chan_3.context.holder_max_htlc_value_in_flight_msat, (chan_3_value_msat as f64 * 0.02) as u64); // Test with the upper bound - 1 of valid values (99%). - let chan_4 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_99_percent), &channelmanager::provided_init_features(&config_99_percent), &chan_1_open_channel_msg, 7, &config_99_percent, 0, &&logger, /*is_0conf=*/false).unwrap(); + let chan_4 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_99_percent), &channelmanager::provided_init_features(&config_99_percent), &chan_1_open_channel_msg, 7, &config_99_percent, 0, &&logger, None).unwrap(); let chan_4_value_msat = chan_4.funding.get_value_satoshis() * 1000; assert_eq!(chan_4.context.holder_max_htlc_value_in_flight_msat, (chan_4_value_msat as f64 * 0.99) as u64); // Test that `OutboundV1Channel::new` uses the lower bound of the configurable percentage values (1%) - // if `max_inbound_htlc_value_in_flight_percent_of_channel` is set to a value less than 1. - let chan_5 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_0_percent), 10000000, 100000, 42, &config_0_percent, 0, 42, None, &logger).unwrap(); + // if `(un)announced_channel_max_inbound_htlc_value_in_flight_percentage` is set to a value less than 1. + let chan_5 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_0_percent), 10000000, 100000, 42, &config_0_percent, 0, 42, None, &logger, None).unwrap(); let chan_5_value_msat = chan_5.funding.get_value_satoshis() * 1000; assert_eq!(chan_5.context.holder_max_htlc_value_in_flight_msat, (chan_5_value_msat as f64 * 0.01) as u64); // Test that `OutboundV1Channel::new` uses the upper bound of the configurable percentage values - // (100%) if `max_inbound_htlc_value_in_flight_percent_of_channel` is set to a larger value + // (100%) if `(un)announced_channel_max_inbound_htlc_value_in_flight_percentage` is set to a larger value // than 100. - let chan_6 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_101_percent), 10000000, 100000, 42, &config_101_percent, 0, 42, None, &logger).unwrap(); + let chan_6 = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&config_101_percent), 10000000, 100000, 42, &config_101_percent, 0, 42, None, &logger, None).unwrap(); let chan_6_value_msat = chan_6.funding.get_value_satoshis() * 1000; assert_eq!(chan_6.context.holder_max_htlc_value_in_flight_msat, chan_6_value_msat); // Test that `InboundV1Channel::new` uses the lower bound of the configurable percentage values (1%) - // if `max_inbound_htlc_value_in_flight_percent_of_channel` is set to a value less than 1. - let chan_7 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_0_percent), &channelmanager::provided_init_features(&config_0_percent), &chan_1_open_channel_msg, 7, &config_0_percent, 0, &&logger, /*is_0conf=*/false).unwrap(); + // if `(un)announced_channel_max_inbound_htlc_value_in_flight_percentage` is set to a value less than 1. + let chan_7 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_0_percent), &channelmanager::provided_init_features(&config_0_percent), &chan_1_open_channel_msg, 7, &config_0_percent, 0, &&logger, None).unwrap(); let chan_7_value_msat = chan_7.funding.get_value_satoshis() * 1000; assert_eq!(chan_7.context.holder_max_htlc_value_in_flight_msat, (chan_7_value_msat as f64 * 0.01) as u64); // Test that `InboundV1Channel::new` uses the upper bound of the configurable percentage values - // (100%) if `max_inbound_htlc_value_in_flight_percent_of_channel` is set to a larger value + // (100%) if `(un)announced_channel_max_inbound_htlc_value_in_flight_percentage` is set to a larger value // than 100. - let chan_8 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_101_percent), &channelmanager::provided_init_features(&config_101_percent), &chan_1_open_channel_msg, 7, &config_101_percent, 0, &&logger, /*is_0conf=*/false).unwrap(); + let chan_8 = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&config_101_percent), &channelmanager::provided_init_features(&config_101_percent), &chan_1_open_channel_msg, 7, &config_101_percent, 0, &&logger, None).unwrap(); let chan_8_value_msat = chan_8.funding.get_value_satoshis() * 1000; assert_eq!(chan_8.context.holder_max_htlc_value_in_flight_msat, chan_8_value_msat); } @@ -16543,6 +18229,10 @@ mod tests { // to channel value test_self_and_counterparty_channel_reserve(10_000_000, 0.50, 0.50); test_self_and_counterparty_channel_reserve(10_000_000, 0.60, 0.50); + + // Make sure we correctly handle reserves greater than the channel value + test_self_and_counterparty_channel_reserve(100_000, 1.1, 0.30); + test_self_and_counterparty_channel_reserve(100_000, 0.30, 1.1); } #[rustfmt::skip] @@ -16560,9 +18250,21 @@ mod tests { let mut outbound_node_config = UserConfig::default(); outbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (outbound_selected_channel_reserve_perc * 1_000_000.0) as u32; - let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&outbound_node_config), channel_value_satoshis, 100_000, 42, &outbound_node_config, 0, 42, None, &logger).unwrap(); + let mut chan = OutboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, outbound_node_id, &channelmanager::provided_init_features(&outbound_node_config), channel_value_satoshis, 100_000, 42, &outbound_node_config, 0, 42, None, &logger, None).unwrap(); + + let outbound_capped_reserve_perc = if outbound_selected_channel_reserve_perc.lt(&1.0) { + outbound_selected_channel_reserve_perc + } else { + 1.0 + }; + + let inbound_capped_reserve_perc = if inbound_selected_channel_reserve_perc.lt(&1.0) { + inbound_selected_channel_reserve_perc + } else { + 1.0 + }; - let expected_outbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * outbound_selected_channel_reserve_perc) as u64); + let expected_outbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * outbound_capped_reserve_perc) as u64); assert_eq!(chan.funding.holder_selected_channel_reserve_satoshis, expected_outbound_selected_chan_reserve); let chan_open_channel_msg = chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap(); @@ -16570,15 +18272,15 @@ mod tests { inbound_node_config.channel_handshake_config.their_channel_reserve_proportional_millionths = (inbound_selected_channel_reserve_perc * 1_000_000.0) as u32; if outbound_selected_channel_reserve_perc + inbound_selected_channel_reserve_perc < 1.0 { - let chan_inbound_node = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, /*is_0conf=*/false).unwrap(); + let chan_inbound_node = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, None).unwrap(); - let expected_inbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * inbound_selected_channel_reserve_perc) as u64); + let expected_inbound_selected_chan_reserve = cmp::max(MIN_THEIR_CHAN_RESERVE_SATOSHIS, (chan.funding.get_value_satoshis() as f64 * inbound_capped_reserve_perc) as u64); assert_eq!(chan_inbound_node.funding.holder_selected_channel_reserve_satoshis, expected_inbound_selected_chan_reserve); assert_eq!(chan_inbound_node.funding.counterparty_selected_channel_reserve_satoshis.unwrap(), expected_outbound_selected_chan_reserve); } else { // Channel Negotiations failed - let result = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, /*is_0conf=*/false); + let result = InboundV1Channel::<&TestKeysInterface>::new(&&fee_est, &&keys_provider, &&keys_provider, inbound_node_id, &channelmanager::provided_channel_type_features(&inbound_node_config), &channelmanager::provided_init_features(&outbound_node_config), &chan_open_channel_msg, 7, &inbound_node_config, 0, &&logger, None); assert!(result.is_err()); } } @@ -16592,20 +18294,20 @@ mod tests { let secp_ctx = Secp256k1::new(); let seed = [42; 32]; let network = Network::Testnet; - let best_block = BestBlock::from_network(network); + let best_block = BlockLocator::from_network(network); let chain_hash = ChainHash::using_genesis_block(network); let keys_provider = TestKeysInterface::new(&seed, network); // Create Node A's channel pointing to Node B's pubkey let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let config = UserConfig::default(); - let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger).unwrap(); + let mut node_a_chan = OutboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_init_features(&config), 10000000, 100000, 42, &config, 0, 42, None, &logger, None).unwrap(); // Create Node B's channel by receiving Node A's open_channel message // Make sure A's dust limit is as we expect. let open_channel_msg = node_a_chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap(); let node_b_node_id = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[7; 32]).unwrap()); - let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, /*is_0conf=*/false).unwrap(); + let mut node_b_chan = InboundV1Channel::<&TestKeysInterface>::new(&feeest, &&keys_provider, &&keys_provider, node_b_node_id, &channelmanager::provided_channel_type_features(&config), &channelmanager::provided_init_features(&config), &open_channel_msg, 7, &config, 0, &&logger, None).unwrap(); // Node B --> Node A: accept channel, explicitly setting B's dust limit. let mut accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap(); @@ -16670,7 +18372,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let seed = [42; 32]; let network = Network::Testnet; - let best_block = BestBlock::from_network(network); + let best_block = BlockLocator::from_network(network); let keys_provider = TestKeysInterface::new(&seed, network); let node_b_node_id = @@ -16691,6 +18393,7 @@ mod tests { 42, None, &logger, + None, ) .unwrap(); let open_channel_msg = &outbound_chan @@ -16708,7 +18411,7 @@ mod tests { &config, 0, &&logger, - false, + None, ) .unwrap(); outbound_chan @@ -16981,7 +18684,7 @@ mod tests { ChannelPublicKeys, CounterpartyChannelTransactionParameters, HolderCommitmentTransaction, }; - use crate::ln::channel::HTLCOutputInCommitment; + use crate::ln::channel::{HTLCOutputInCommitment, PredictedNextFee}; use crate::ln::channel_keys::{DelayedPaymentBasepoint, HtlcBasepoint}; use crate::sign::{ecdsa::EcdsaChannelSigner, ChannelDerivationParameters, HTLCDescriptor}; use crate::sync::Arc; @@ -17047,6 +18750,7 @@ mod tests { 42, None, &*logger, + None, ) .unwrap(); // Nothing uses their network key in this test chan.context.holder_dust_limit_satoshis = 546; @@ -17110,6 +18814,8 @@ mod tests { macro_rules! test_commitment { ( $counterparty_sig_hex: expr, $sig_hex: expr, $tx_hex: expr, $($remain:tt)* ) => { chan.funding.channel_transaction_parameters.channel_type_features = ChannelTypeFeatures::only_static_remote_key(); + chan.funding.next_local_fee = Mutex::new(PredictedNextFee::default()); + chan.funding.next_remote_fee = Mutex::new(PredictedNextFee::default()); test_commitment_common!(chan, logger, secp_ctx, signer, holder_pubkeys, per_commitment_point, $counterparty_sig_hex, $sig_hex, $tx_hex, &ChannelTypeFeatures::only_static_remote_key(), $($remain)*); }; } @@ -17117,6 +18823,8 @@ mod tests { macro_rules! test_commitment_with_anchors { ( $counterparty_sig_hex: expr, $sig_hex: expr, $tx_hex: expr, $($remain:tt)* ) => { chan.funding.channel_transaction_parameters.channel_type_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + chan.funding.next_local_fee = Mutex::new(PredictedNextFee::default()); + chan.funding.next_remote_fee = Mutex::new(PredictedNextFee::default()); test_commitment_common!(chan, logger, secp_ctx, signer, holder_pubkeys, per_commitment_point, $counterparty_sig_hex, $sig_hex, $tx_hex, &ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(), $($remain)*); }; } @@ -17767,6 +19475,7 @@ mod tests { 0, None, &*logger, + None, ) .unwrap(); @@ -18318,7 +20027,7 @@ mod tests { let secp_ctx = Secp256k1::new(); let seed = [42; 32]; let network = Network::Testnet; - let best_block = BestBlock::from_network(network); + let best_block = BlockLocator::from_network(network); let chain_hash = ChainHash::using_genesis_block(network); let keys_provider = TestKeysInterface::new(&seed, network); @@ -18342,7 +20051,8 @@ mod tests { 0, 42, None, - &logger + &logger, + None, ).unwrap(); let open_channel_msg = node_a_chan.get_open_channel(ChainHash::using_genesis_block(network), &&logger).unwrap(); @@ -18359,7 +20069,8 @@ mod tests { &config, 0, &&logger, - true, // Allow node b to send a 0conf channel_ready. + // Allow node b to send a 0conf channel_ready. + Some(TrustedChannelFeatures::ZeroConf), ).unwrap(); let accept_channel_msg = node_b_chan.accept_inbound_channel(&&logger).unwrap(); @@ -18394,7 +20105,7 @@ mod tests { &&logger, ).map_err(|_| ()).unwrap(); let node_b_updates = node_b_chan.monitor_updating_restored( - &&logger, + &WithChannelContext::from(&logger, &node_b_chan.context, None), &&keys_provider, chain_hash, &config, @@ -18409,7 +20120,7 @@ mod tests { ); let (mut node_a_chan, _) = if let Ok(res) = res { res } else { panic!(); }; let node_a_updates = node_a_chan.monitor_updating_restored( - &&logger, + &WithChannelContext::from(&logger, &node_a_chan.context, None), &&keys_provider, chain_hash, &config, @@ -18441,339 +20152,4 @@ mod tests { assert_eq!(node_a_chan.context.channel_state, ChannelState::AwaitingChannelReady(AwaitingChannelReadyFlags::THEIR_CHANNEL_READY)); assert!(node_a_chan.check_get_channel_ready(0, &&logger).is_some()); } - - #[test] - #[rustfmt::skip] - fn test_estimate_v2_funding_transaction_fee() { - use crate::ln::channel::estimate_v2_funding_transaction_fee; - - let one_input = [funding_input_sats(1_000)]; - let two_inputs = [funding_input_sats(1_000), funding_input_sats(1_000)]; - - // 2 inputs, initiator, 2000 sat/kw feerate - assert_eq!( - estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 2000), - if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }, - ); - - // higher feerate - assert_eq!( - estimate_v2_funding_transaction_fee(&two_inputs, &[], true, false, 3000), - if cfg!(feature = "grind_signatures") { 2268 } else { 2274 }, - ); - - // only 1 input - assert_eq!( - estimate_v2_funding_transaction_fee(&one_input, &[], true, false, 2000), - if cfg!(feature = "grind_signatures") { 970 } else { 972 }, - ); - - // 0 inputs - assert_eq!( - estimate_v2_funding_transaction_fee(&[], &[], true, false, 2000), - 428, - ); - - // not initiator - assert_eq!( - estimate_v2_funding_transaction_fee(&[], &[], false, false, 2000), - 0, - ); - - // splice initiator - assert_eq!( - estimate_v2_funding_transaction_fee(&one_input, &[], true, true, 2000), - if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }, - ); - - // splice acceptor - assert_eq!( - estimate_v2_funding_transaction_fee(&one_input, &[], false, true, 2000), - if cfg!(feature = "grind_signatures") { 542 } else { 544 }, - ); - } - - #[rustfmt::skip] - fn funding_input_sats(input_value_sats: u64) -> FundingTxInput { - let prevout = TxOut { - value: Amount::from_sat(input_value_sats), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - }; - let prevtx = Transaction { - input: vec![], output: vec![prevout], - version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, - }; - - FundingTxInput::new_p2wpkh(prevtx, 0).unwrap() - } - - fn funding_output_sats(output_value_sats: u64) -> TxOut { - TxOut { - value: Amount::from_sat(output_value_sats), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - } - } - - #[test] - #[rustfmt::skip] - fn test_check_v2_funding_inputs_sufficient() { - use crate::ln::channel::check_v2_funding_inputs_sufficient; - - // positive case, inputs well over intended contribution - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // Net splice-in - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(200_000), - ], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // Net splice-out - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2526 } else { 2532 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(400_000), - ], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // Net splice-out, inputs insufficient to cover fees - { - let expected_fee = if cfg!(feature = "grind_signatures") { 113670 } else { 113940 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[ - funding_output_sats(400_000), - ], - true, - true, - 90000, - ), - Err(format!( - "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", - Amount::from_sat(expected_fee), - )), - ); - } - - // negative case, inputs clearly insufficient - { - let expected_fee = if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(220_000), - &[ - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ), - Err(format!( - "Total input amount 0.00100000 BTC is lower than needed for splice-in contribution 0.00220000 BTC, considering fees of {}. Need more inputs.", - Amount::from_sat(expected_fee), - )), - ); - } - - // barely covers - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2278 } else { 2284 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(300_000 - expected_fee - 20), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - - // higher fee rate, does not cover - { - let expected_fee = if cfg!(feature = "grind_signatures") { 2506 } else { 2513 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(298032), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - true, - true, - 2200, - ), - Err(format!( - "Total input amount 0.00300000 BTC is lower than needed for splice-in contribution 0.00298032 BTC, considering fees of {}. Need more inputs.", - Amount::from_sat(expected_fee), - )), - ); - } - - // barely covers, less fees (no extra weight, not initiator) - { - let expected_fee = if cfg!(feature = "grind_signatures") { 1084 } else { 1088 }; - assert_eq!( - check_v2_funding_inputs_sufficient( - Amount::from_sat(300_000 - expected_fee - 20), - &[ - funding_input_sats(200_000), - funding_input_sats(100_000), - ], - &[], - false, - false, - 2000, - ).unwrap(), - Amount::from_sat(expected_fee), - ); - } - } - - fn get_pre_and_post( - pre_channel_value: u64, our_funding_contribution: i64, their_funding_contribution: i64, - ) -> (u64, u64) { - use crate::ln::channel::{FundingScope, PredictedNextFee}; - - let funding = FundingScope { - value_to_self_msat: 0, - counterparty_selected_channel_reserve_satoshis: None, - holder_selected_channel_reserve_satoshis: 0, - - #[cfg(debug_assertions)] - holder_max_commitment_tx_output: Mutex::new((0, 0)), - #[cfg(debug_assertions)] - counterparty_max_commitment_tx_output: Mutex::new((0, 0)), - - #[cfg(any(test, fuzzing))] - next_local_fee: Mutex::new(PredictedNextFee::default()), - #[cfg(any(test, fuzzing))] - next_remote_fee: Mutex::new(PredictedNextFee::default()), - - channel_transaction_parameters: ChannelTransactionParameters::test_dummy( - pre_channel_value, - ), - funding_transaction: None, - funding_tx_confirmed_in: None, - funding_tx_confirmation_height: 0, - short_channel_id: None, - minimum_depth_override: None, - }; - let post_channel_value = - funding.compute_post_splice_value(our_funding_contribution, their_funding_contribution); - (pre_channel_value, post_channel_value) - } - - #[test] - fn test_compute_post_splice_value() { - { - // increase, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 6_000, 0); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 15_000); - } - { - // increase, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 4_000, 2_000); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 15_000); - } - { - // increase, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, 0, 6_000); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 15_000); - } - { - // decrease, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -6_000, 0); - assert_eq!(pre_channel_value, 15_000); - assert_eq!(post_channel_value, 9_000); - } - { - // decrease, small amounts - let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, -4_000, -2_000); - assert_eq!(pre_channel_value, 15_000); - assert_eq!(post_channel_value, 9_000); - } - { - // increase and decrease - let (pre_channel_value, post_channel_value) = get_pre_and_post(15_000, 4_000, -2_000); - assert_eq!(pre_channel_value, 15_000); - assert_eq!(post_channel_value, 17_000); - } - let base2: u64 = 2; - let huge63i3 = (base2.pow(63) - 3) as i64; - assert_eq!(huge63i3, 9223372036854775805); - assert_eq!(-huge63i3, -9223372036854775805); - { - // increase, large amount - let (pre_channel_value, post_channel_value) = get_pre_and_post(9_000, huge63i3, 3); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 9223372036854784807); - } - { - // increase, large amounts - let (pre_channel_value, post_channel_value) = - get_pre_and_post(9_000, huge63i3, huge63i3); - assert_eq!(pre_channel_value, 9_000); - assert_eq!(post_channel_value, 9223372036854784807); - } - } } diff --git a/lightning/src/ln/channel_open_tests.rs b/lightning/src/ln/channel_open_tests.rs index 059639330f8..2c048c9906c 100644 --- a/lightning/src/ln/channel_open_tests.rs +++ b/lightning/src/ln/channel_open_tests.rs @@ -16,13 +16,16 @@ use crate::chain::{self, ChannelMonitorUpdateStatus}; use crate::events::{ClosureReason, Event, FundingInfo}; use crate::ln::channel::{ get_holder_selected_channel_reserve_satoshis, ChannelError, InboundV1Channel, - OutboundV1Channel, COINBASE_MATURITY, UNFUNDED_CHANNEL_AGE_LIMIT_TICKS, + OutboundV1Channel, COINBASE_MATURITY, MIN_THEIR_CHAN_RESERVE_SATOSHIS, + UNFUNDED_CHANNEL_AGE_LIMIT_TICKS, }; use crate::ln::channelmanager::{ - self, BREAKDOWN_TIMEOUT, MAX_UNFUNDED_CHANNEL_PEERS, MAX_UNFUNDED_CHANS_PER_PEER, + self, TrustedChannelFeatures, BREAKDOWN_TIMEOUT, MAX_UNFUNDED_CHANNEL_PEERS, + MAX_UNFUNDED_CHANS_PER_PEER, }; use crate::ln::msgs::{ - AcceptChannel, BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent, + AcceptChannel, BaseMessageHandler, ChannelMessageHandler, ErrorAction, ErrorMessage, + MessageSendEvent, }; use crate::ln::types::ChannelId; use crate::ln::{functional_test_utils::*, msgs}; @@ -46,6 +49,7 @@ use bitcoin::{Amount, Sequence, Transaction, TxIn, TxOut, Witness}; use lightning_macros::xtest; use lightning_types::features::ChannelTypeFeatures; +use types::string::UntrustedString; #[test] fn test_outbound_chans_unlimited() { @@ -157,10 +161,11 @@ fn test_0conf_limiting() { Event::OpenChannelRequest { temporary_channel_id, .. } => { nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( &temporary_channel_id, &last_random_pk, 23, + TrustedChannelFeatures::ZeroConf, None, ) .unwrap(); @@ -180,7 +185,8 @@ fn test_inbound_anchors_manual_acceptance() { fn test_inbound_anchors_config_overridden() { let overrides = ChannelConfigOverrides { handshake_overrides: Some(ChannelHandshakeConfigUpdate { - max_inbound_htlc_value_in_flight_percent_of_channel: Some(5), + announced_channel_max_inbound_htlc_value_in_flight_percentage: Some(5), + unannounced_channel_max_inbound_htlc_value_in_flight_percentage: None, htlc_minimum_msat: Some(1000), minimum_depth: Some(2), to_self_delay: Some(200), @@ -457,8 +463,7 @@ fn test_channel_resumption_fail_post_funding() { pub fn test_insane_channel_opens() { // Stand up a network of 2 nodes use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; - let mut legacy_cfg = test_legacy_channel_config(); - legacy_cfg.channel_handshake_limits.max_funding_satoshis = TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1; + let legacy_cfg = test_legacy_channel_config(); let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(legacy_cfg.clone())]); @@ -471,7 +476,8 @@ pub fn test_insane_channel_opens() { // funding satoshis let channel_value_sat = 31337; // same as funding satoshis let channel_reserve_satoshis = - get_holder_selected_channel_reserve_satoshis(channel_value_sat, &legacy_cfg); + get_holder_selected_channel_reserve_satoshis(channel_value_sat, 0, &legacy_cfg, false) + .unwrap(); let push_msat = (channel_value_sat - channel_reserve_satoshis) * 1000; // Have node0 initiate a channel to node1 with aforementioned parameters @@ -524,19 +530,6 @@ pub fn test_insane_channel_opens() { use crate::ln::channelmanager::MAX_LOCAL_BREAKDOWN_TIMEOUT; - // Test all mutations that would make the channel open message insane - insane_open_helper( - format!( - "Per our config, funding must be at most {}. It was {}", - TOTAL_BITCOIN_SUPPLY_SATOSHIS + 1, - TOTAL_BITCOIN_SUPPLY_SATOSHIS + 2 - ) - .as_str(), - |mut msg| { - msg.common_fields.funding_satoshis = TOTAL_BITCOIN_SUPPLY_SATOSHIS + 2; - msg - }, - ); insane_open_helper( format!( "Funding must be smaller than the total bitcoin supply. It was {}", @@ -563,7 +556,13 @@ pub fn test_insane_channel_opens() { }, ); - insane_open_helper("Peer never wants payout outputs?", |mut msg| { + let crazy_dust_limit = channel_value_sat + 1; + let expected_error_str = format!( + "Got non-closing error: The channel value \ + {channel_value_sat} is smaller than either their dust limit {crazy_dust_limit}, or \ + {MIN_THEIR_CHAN_RESERVE_SATOSHIS}" + ); + insane_open_helper(&expected_error_str, |mut msg| { msg.common_fields.dust_limit_satoshis = msg.common_fields.funding_satoshis + 1; msg }); @@ -894,8 +893,7 @@ pub fn bolt2_open_channel_sane_dust_limit() { nodes[0].node.create_channel(node_b_id, value_sats, push_msat, 42, None, None).unwrap(); let mut node0_to_1_send_open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); - node0_to_1_send_open_channel.common_fields.dust_limit_satoshis = 547; - node0_to_1_send_open_channel.channel_reserve_satoshis = 100001; + node0_to_1_send_open_channel.common_fields.dust_limit_satoshis = 10_001; nodes[1].node.handle_open_channel(node_a_id, &node0_to_1_send_open_channel); let events = nodes[1].node.get_and_clear_pending_events(); @@ -907,7 +905,7 @@ pub fn bolt2_open_channel_sane_dust_limit() { { Err(APIError::ChannelUnavailable { err }) => assert_eq!( err, - "dust_limit_satoshis (547) is greater than the implementation limit (546)" + "dust_limit_satoshis (10001) is greater than the implementation limit (10000)" ), _ => panic!(), }, @@ -952,6 +950,7 @@ pub fn test_user_configurable_csv_delay() { 42, None, &logger, + None, ) { match error { APIError::APIMisuseError { err } => { @@ -983,7 +982,7 @@ pub fn test_user_configurable_csv_delay() { &low_our_to_self_config, 0, &nodes[0].logger, - /*is_0conf=*/ false, + None, ) { match error { ChannelError::Close((err, _)) => { @@ -1043,7 +1042,7 @@ pub fn test_user_configurable_csv_delay() { &high_their_to_self_config, 0, &nodes[0].logger, - /*is_0conf=*/ false, + None, ) { match error { ChannelError::Close((err, _)) => { @@ -1082,7 +1081,8 @@ pub fn test_accept_inbound_channel_config_override() { let config_overrides = ChannelConfigOverrides { handshake_overrides: Some(ChannelHandshakeConfigUpdate { - max_inbound_htlc_value_in_flight_percent_of_channel: None, + announced_channel_max_inbound_htlc_value_in_flight_percentage: None, + unannounced_channel_max_inbound_htlc_value_in_flight_percentage: None, htlc_minimum_msat: None, minimum_depth: None, to_self_delay: None, @@ -2498,3 +2498,189 @@ fn test_fund_pending_channel() { }; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100_000); } + +#[xtest(feature = "_externalize_tests")] +fn test_holder_selected_0reserve_on_legacy_channel_is_not_allowed() { + { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut channel_config = test_default_channel_config(); + assert!(channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx); + let node_chanmgrs = create_node_chanmgrs( + 2, + &node_cfgs, + &[Some(channel_config.clone()), Some(channel_config.clone())], + ); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + let mut legacy_channel_config = test_default_channel_config(); + legacy_channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + legacy_channel_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = + false; + + // User tries to open a legacy 0-reserve channel with a config override, we fail + assert_eq!( + nodes[0] + .node + .create_channel_to_trusted_peer_0reserve( + node_b_id, + 100_000, + 0, + 42, + None, + Some(legacy_channel_config) + ) + .unwrap_err(), + APIError::APIMisuseError { + err: "0-reserve is not allowed on legacy channels".to_owned() + } + ); + } + { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut channel_config = test_default_channel_config(); + channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + channel_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let node_chanmgrs = create_node_chanmgrs( + 2, + &node_cfgs, + &[Some(channel_config.clone()), Some(channel_config.clone())], + ); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + // User tries to open a legacy 0-reserve channel from the default config, we fail + assert_eq!( + nodes[0] + .node + .create_channel_to_trusted_peer_0reserve(node_b_id, 100_000, 0, 42, None, None) + .unwrap_err(), + APIError::APIMisuseError { + err: "0-reserve is not allowed on legacy channels".to_owned() + } + ); + } + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut channel_config = test_default_channel_config(); + channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + channel_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let node_chanmgrs = create_node_chanmgrs( + 2, + &node_cfgs, + &[Some(channel_config.clone()), Some(channel_config.clone())], + ); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + nodes[0].node.create_channel(node_b_id, 100_000, 0, 42, None, None).unwrap(); + let mut open_channel_msg_0reserve = + get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + open_channel_msg_0reserve.channel_reserve_satoshis = 0; + assert_eq!( + open_channel_msg_0reserve.common_fields.channel_type, + Some(ChannelTypeFeatures::only_static_remote_key()) + ); + + // User accepts a legacy channel, and sets 0-reserve for the counterparty, we fail + nodes[1].node.handle_open_channel(node_a_id, &open_channel_msg_0reserve); + let events = nodes[1].node.get_and_clear_pending_events(); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id, .. } => { + let error = nodes[1] + .node + .accept_inbound_channel_from_trusted_peer( + &temporary_channel_id, + &node_a_id, + 42, + TrustedChannelFeatures::ZeroReserve, + None, + ) + .unwrap_err(); + assert_eq!( + error, + APIError::ChannelUnavailable { + err: "0-reserve is not allowed on legacy channels".to_owned() + } + ); + }, + _ => panic!("Unexpected event"), + } + let err_msg = get_err_msg(&nodes[1], &node_a_id); + assert_eq!( + err_msg, + ErrorMessage { + channel_id: open_channel_msg_0reserve.common_fields.temporary_channel_id, + data: "0-reserve is not allowed on legacy channels".to_string() + } + ); + + // But legacy channels where only the counterparty sets 0-reserve are ok! + // Here node 1 accepts 0-reserve from node 0, and node 1 sets some non-zero reserve... + handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel_msg_0reserve); + let mut accept_channel_msg = + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + // Override the reserve selected by node 1, make sure node 0 accepts too + accept_channel_msg.channel_reserve_satoshis = 0; + + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg); + + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + assert!( + matches!(events[0], Event::FundingGenerationReady { channel_value_satoshis: 100_000, user_channel_id: 42, counterparty_node_id, .. } if counterparty_node_id == node_b_id) + ); +} + +#[xtest(feature = "_externalize_tests")] +fn test_error_if_0reserve_negotiates_down_to_legacy() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let channel_config = test_default_channel_config(); + assert!(channel_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx); + let node_chanmgrs = create_node_chanmgrs( + 2, + &node_cfgs, + &[Some(channel_config.clone()), Some(channel_config.clone())], + ); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_b_id = nodes[1].node.get_our_node_id(); + + nodes[0] + .node + .create_channel_to_trusted_peer_0reserve(node_b_id, 100_000, 0, 42, None, None) + .unwrap(); + let open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + assert_eq!( + open_channel_msg.common_fields.channel_type, + Some(ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()) + ); + assert_eq!(open_channel_msg.channel_reserve_satoshis, 0); + + let reason = "Don't like your channel".to_owned(); + nodes[0].node.handle_error( + node_b_id, + &ErrorMessage { + channel_id: open_channel_msg.common_fields.temporary_channel_id, + data: reason.clone(), + }, + ); + + let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(reason) }; + let expected_closing = ExpectedCloseEvent::from_id_reason( + open_channel_msg.common_fields.temporary_channel_id, + false, + reason, + ); + check_closed_events(&nodes[0], &[expected_closing]); +} diff --git a/lightning/src/ln/channel_state.rs b/lightning/src/ln/channel_state.rs index c7277d18e3b..48379f9e4d0 100644 --- a/lightning/src/ln/channel_state.rs +++ b/lightning/src/ln/channel_state.rs @@ -12,10 +12,13 @@ use alloc::vec::Vec; use bitcoin::secp256k1::PublicKey; +use bitcoin::Txid; use crate::chain::chaininterface::{FeeEstimator, LowerBoundedFeeEstimator}; use crate::chain::transaction::OutPoint; use crate::ln::channel::Channel; +use crate::ln::channelmanager::PaymentId; +use crate::ln::funding::FundingContribution; use crate::ln::types::ChannelId; use crate::sign::SignerProvider; use crate::types::features::{ChannelTypeFeatures, InitFeatures}; @@ -106,7 +109,7 @@ pub struct InboundHTLCDetails { pub is_dust: bool, } -impl_writeable_tlv_based!(InboundHTLCDetails, { +impl_ser_tlv_based!(InboundHTLCDetails, { (0, htlc_id, required), (2, amount_msat, required), (4, cltv_expiry, required), @@ -158,6 +161,52 @@ impl_writeable_tlv_based_enum_upgradable!(OutboundHTLCStateDetails, (6, AwaitingRemoteRevokeToRemoveFailure) => {}, ); +/// Identifies an inbound HTLC. +#[derive(Clone, Debug, PartialEq)] +pub struct InboundHTLCReference { + /// The channel on which the HTLC was received. + pub channel_id: ChannelId, + /// The HTLC ID assigned by the inbound channel. + pub htlc_id: u64, +} + +impl_ser_tlv_based!(InboundHTLCReference, { + (0, channel_id, required), + (2, htlc_id, required), +}); + +/// Describes how an outbound HTLC originated. +#[derive(Clone, Debug, PartialEq)] +pub enum OutboundHTLCSource { + /// A locally initiated payment or probe. + Local { + /// The payment or probe identifier. + payment_id: PaymentId, + }, + /// A forward of a single inbound HTLC. + Forwarded { + /// The inbound HTLC. + inbound_htlc: InboundHTLCReference, + }, + /// A trampoline forward of one or more inbound HTLCs. + TrampolineForwarded { + /// The inbound HTLCs. + inbound_htlcs: Vec<InboundHTLCReference>, + }, +} + +impl_writeable_tlv_based_enum_upgradable!(OutboundHTLCSource, + (0, Local) => { + (0, payment_id, required), + }, + (2, Forwarded) => { + (0, inbound_htlc, required), + }, + (4, TrampolineForwarded) => { + (0, inbound_htlcs, required_vec), + }, +); + /// Exposes details around pending outbound HTLCs. #[derive(Clone, Debug, PartialEq)] pub struct OutboundHTLCDetails { @@ -173,6 +222,10 @@ pub struct OutboundHTLCDetails { /// The block height at which this HTLC expires. pub cltv_expiry: u32, /// The payment hash. + /// + /// A payment hash is not sufficient to correlate HTLCs in a multipart payment because multiple + /// parts sharing a payment hash may traverse the same channel. Use [`Self::source`] to correlate + /// the HTLC with its locally initiated payment or inbound HTLCs. pub payment_hash: PaymentHash, /// The state of the HTLC in the state machine. /// @@ -198,9 +251,15 @@ pub struct OutboundHTLCDetails { /// Note that dust limits are specific to each party. An HTLC can be dust for the local /// commitment transaction but not for the counterparty's commitment transaction and vice versa. pub is_dust: bool, + /// The source of this outbound HTLC. + /// + /// LDK will always fill this field in, but it will be `None` for objects serialized with LDK + /// versions prior to 0.4 or when downgrading to a version that does not understand the source + /// variant. + pub source: Option<OutboundHTLCSource>, } -impl_writeable_tlv_based!(OutboundHTLCDetails, { +impl_ser_tlv_based!(OutboundHTLCDetails, { (0, htlc_id, required), (2, amount_msat, required), (4, cltv_expiry, required), @@ -208,6 +267,7 @@ impl_writeable_tlv_based!(OutboundHTLCDetails, { (7, state, upgradable_option), (8, skimmed_fee_msat, required), (10, is_dust, required), + (11, source, upgradable_option), }); /// Information needed for constructing an invoice route hint for this channel. @@ -223,7 +283,7 @@ pub struct CounterpartyForwardingInfo { pub cltv_expiry_delta: u16, } -impl_writeable_tlv_based!(CounterpartyForwardingInfo, { +impl_ser_tlv_based!(CounterpartyForwardingInfo, { (2, fee_base_msat, required), (4, fee_proportional_millionths, required), (6, cltv_expiry_delta, required), @@ -258,7 +318,7 @@ pub struct ChannelCounterparty { pub outbound_htlc_maximum_msat: Option<u64>, } -impl_writeable_tlv_based!(ChannelCounterparty, { +impl_ser_tlv_based!(ChannelCounterparty, { (2, node_id, required), (4, features, required), (6, unspendable_punishment_reserve, required), @@ -275,7 +335,8 @@ impl_writeable_tlv_based!(ChannelCounterparty, { /// /// When a channel is spliced, most fields continue to refer to the original pre-splice channel /// state until the splice transaction reaches sufficient confirmations to be locked (and we -/// exchange `splice_locked` messages with our peer). See individual fields for details. +/// exchange `splice_locked` messages with our peer). See individual fields for details, and +/// [`SpliceDetails`] for how a splice is negotiated and locked. /// /// [`ChannelManager::list_channels`]: crate::ln::channelmanager::ChannelManager::list_channels /// [`ChannelManager::list_usable_channels`]: crate::ln::channelmanager::ChannelManager::list_usable_channels @@ -312,8 +373,10 @@ pub struct ChannelDetails { /// Note that if [`inbound_scid_alias`] is set, it must be used for invoices and inbound /// payments instead of this. See [`get_inbound_payment_scid`]. /// - /// For channels with [`confirmations_required`] set to `Some(0)`, [`outbound_scid_alias`] may - /// be used in place of this in outbound routes. See [`get_outbound_payment_scid`]. + /// For routing outbound payments, this value should not be used if [`outbound_scid_alias`] is + /// set. [`outbound_scid_alias`] provides a stable routing identifier across splices, whereas + /// this value will change when a splice confirms. + /// Use [`get_outbound_payment_scid`] to pick the appropriate value. /// /// When a channel is spliced, this continues to refer to the original pre-splice channel /// state until the splice transaction reaches sufficient confirmations to be locked (and we @@ -323,21 +386,17 @@ pub struct ChannelDetails { /// [`outbound_scid_alias`]: Self::outbound_scid_alias /// [`get_inbound_payment_scid`]: Self::get_inbound_payment_scid /// [`get_outbound_payment_scid`]: Self::get_outbound_payment_scid - /// [`confirmations_required`]: Self::confirmations_required pub short_channel_id: Option<u64>, /// An optional [`short_channel_id`] alias for this channel, randomly generated by us and - /// usable in place of [`short_channel_id`] to reference the channel in outbound routes when - /// the channel has not yet been confirmed (as long as [`confirmations_required`] is - /// `Some(0)`). + /// usable in place of [`short_channel_id`] to route outbound payments. Because this alias is + /// assigned at channel open and remains stable across splices, it should be used for routing + /// instead of the real [`short_channel_id`] (which changes each time a splice confirms). + /// See [`get_outbound_payment_scid`]. /// /// This will be `None` as long as the channel is not available for routing outbound payments. /// - /// When a channel is spliced, this continues to refer to the original pre-splice channel - /// state until the splice transaction reaches sufficient confirmations to be locked (and we - /// exchange `splice_locked` messages with our peer). - /// /// [`short_channel_id`]: Self::short_channel_id - /// [`confirmations_required`]: Self::confirmations_required + /// [`get_outbound_payment_scid`]: Self::get_outbound_payment_scid pub outbound_scid_alias: Option<u64>, /// An optional [`short_channel_id`] alias for this channel, randomly generated by our /// counterparty and usable in place of [`short_channel_id`] in invoice route hints. Our @@ -399,6 +458,8 @@ pub struct ChannelDetails { /// an upper-bound. This is intended for use when routing, allowing us to ensure we pick a /// route which is valid. pub next_outbound_htlc_minimum_msat: u64, + /// The maximum value of the next splice out from our channel balance. + pub next_splice_out_maximum_sat: u64, /// The available inbound capacity for the remote peer to send HTLCs to us. This does not /// include any pending HTLCs which are not yet fully resolved (and, thus, whose balance is not /// available for inclusion in new inbound HTLCs). @@ -479,6 +540,26 @@ pub struct ChannelDetails { /// /// This field will be `None` for objects serialized with LDK versions prior to 0.2.0. pub funding_redeem_script: Option<bitcoin::ScriptBuf>, + /// The current total dust exposure on this channel, in millisatoshis. + /// + /// This is the maximum of the dust exposure on the holder and counterparty commitment + /// transactions, and includes both the value of all pending HTLCs that are below the dust + /// threshold as well as the portion of commitment transaction fees that contribute to dust + /// exposure. + /// + /// The dust exposure is compared against + /// [`ChannelConfig::max_dust_htlc_exposure`] to determine whether new HTLCs can be + /// accepted or offered on this channel. + /// + /// This field will be `None` for objects serialized with LDK versions prior to 0.3. + /// + /// [`ChannelConfig::max_dust_htlc_exposure`]: crate::util::config::ChannelConfig::max_dust_htlc_exposure + pub current_dust_exposure_msat: Option<u64>, + /// Details of any pending splice attempts on this channel, or `None` if no splice is pending. + /// + /// See [`SpliceDetails`] for what is included. This will be `None` for objects serialized with + /// LDK versions prior to 0.3. + pub splice_details: Option<SpliceDetails>, } impl ChannelDetails { @@ -496,12 +577,16 @@ impl ChannelDetails { /// This should be used in [`Route`]s to describe the first hop or in other contexts where /// we're sending or forwarding a payment outbound over this channel. /// - /// This is either the [`ChannelDetails::short_channel_id`], if set, or the - /// [`ChannelDetails::outbound_scid_alias`]. See those for more information. + /// Returns [`outbound_scid_alias`] if set, otherwise [`short_channel_id`]. The alias is + /// preferred because when a splice confirms the real SCID changes, whereas the alias assigned + /// at channel open remains stable. + /// + /// [`outbound_scid_alias`]: ChannelDetails::outbound_scid_alias + /// [`short_channel_id`]: ChannelDetails::short_channel_id /// /// [`Route`]: crate::routing::router::Route pub fn get_outbound_payment_scid(&self) -> Option<u64> { - self.short_channel_id.or(self.outbound_scid_alias) + self.outbound_scid_alias.or(self.short_channel_id) } /// Gets the funding output for this channel, if available. @@ -525,7 +610,18 @@ impl ChannelDetails { ) -> Self { let context = channel.context(); let funding = channel.funding(); - let balance = channel.get_available_balances(fee_estimator); + let balance_result = channel.get_available_balances(fee_estimator); + let balance = balance_result.unwrap_or_else(|()| { + debug_assert!(false, "some channel balance has been overdrawn"); + crate::ln::channel::AvailableBalances { + inbound_capacity_msat: 0, + outbound_capacity_msat: 0, + next_outbound_htlc_limit_msat: 0, + next_outbound_htlc_minimum_msat: u64::MAX, + dust_exposure_msat: 0, + next_splice_out_maximum_sat: 0, + } + }); let (to_remote_reserve_satoshis, to_self_reserve_satoshis) = funding.get_holder_counterparty_selected_channel_reserve_satoshis(); #[allow(deprecated)] // TODO: Remove once balance_msat is removed. @@ -573,6 +669,7 @@ impl ChannelDetails { outbound_capacity_msat: balance.outbound_capacity_msat, next_outbound_htlc_limit_msat: balance.next_outbound_htlc_limit_msat, next_outbound_htlc_minimum_msat: balance.next_outbound_htlc_minimum_msat, + next_splice_out_maximum_sat: balance.next_splice_out_maximum_sat, user_channel_id: context.get_user_id(), confirmations_required: channel.minimum_depth(), confirmations: Some(funding.get_funding_tx_confirmations(best_block_height)), @@ -587,11 +684,15 @@ impl ChannelDetails { channel_shutdown_state: Some(context.shutdown_state()), pending_inbound_htlcs: context.get_pending_inbound_htlc_details(funding), pending_outbound_htlcs: context.get_pending_outbound_htlc_details(funding), + current_dust_exposure_msat: Some(balance.dust_exposure_msat), + splice_details: channel + .as_funded() + .and_then(|chan| chan.pending_splice_details(best_block_height)), } } } -impl_writeable_tlv_based!(ChannelDetails, { +impl_ser_tlv_based!(ChannelDetails, { (1, inbound_scid_alias, option), (2, channel_id, required), (3, channel_type, option), @@ -612,6 +713,7 @@ impl_writeable_tlv_based!(ChannelDetails, { (20, inbound_capacity_msat, required), (21, next_outbound_htlc_minimum_msat, (default_value, 0)), (22, confirmations_required, option), + (23, next_splice_out_maximum_sat, (default_value, u64::from(outbound_capacity_msat.0.unwrap()) / 1000)), (24, force_close_spend_delay, option), (26, is_outbound, required), (28, is_channel_ready, required), @@ -627,11 +729,231 @@ impl_writeable_tlv_based!(ChannelDetails, { (43, pending_inbound_htlcs, optional_vec), (45, pending_outbound_htlcs, optional_vec), (47, funding_redeem_script, option), + (49, current_dust_exposure_msat, option), + (51, splice_details, option), (_unused, user_channel_id, (static_value, _user_channel_id_low.unwrap_or(0) as u128 | ((_user_channel_id_high.unwrap_or(0) as u128) << 64) )), }); +/// Details of pending splice attempts on a channel, as returned in +/// [`ChannelDetails::splice_details`]. +/// +/// Every splice or RBF round on the channel that has not yet locked is reported as a +/// [`SpliceCandidateDetails`] in [`candidates`], from the moment a contribution is committed +/// through negotiation, signing, and confirmation; see [`SpliceCandidateStatus`] for the stages. +/// +/// A splice is initiated by calling [`ChannelManager::splice_channel`] to obtain a +/// [`FundingTemplate`], building a [`FundingContribution`] from it, and committing that +/// contribution with [`ChannelManager::funding_contributed`]. The contribution first appears as a +/// candidate awaiting quiescence; once the channel is quiescent it is negotiated with the +/// counterparty, and a completed negotiation produces a signed *candidate* splice transaction. +/// While a candidate has been negotiated but not yet locked, calling +/// [`ChannelManager::splice_channel`] again and contributing a higher-feerate replacement RBFs it, +/// adding another candidate; the candidates all double-spend the same input, so at most one +/// confirms. A node sends `splice_locked` for a candidate once it has sufficient confirmations +/// (immediately, on a zero-conf channel), and considers the splice locked once it has both sent its +/// own `splice_locked` and received the counterparty's, at which point that candidate is promoted +/// to the channel's funding. The two sides may lock at different times, both because each counts +/// confirmations from its own chain view and because they may require different numbers of +/// confirmations. +/// +/// The counterparty may also initiate a splice or RBF. Such a round is reported here as well, so a +/// candidate may appear that we did not initiate; our [`contribution`] to it is `None` unless we +/// added funds of our own. +/// +/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel +/// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed +/// [`FundingTemplate`]: crate::ln::funding::FundingTemplate +/// [`candidates`]: Self::candidates +/// [`contribution`]: SpliceCandidateDetails::contribution +#[derive(Clone, Debug, PartialEq)] +pub struct SpliceDetails { + /// The splice and RBF rounds on this channel that have not yet locked, in order: any negotiated + /// candidates awaiting confirmation (oldest first), the round currently under negotiation (if + /// any), and a contribution we have committed but not yet begun negotiating (last). + /// + /// More than one entry indicates an in-flight negotiation and/or RBF replacements alongside + /// negotiated candidates; the candidates all double-spend the same input, so at most one + /// ultimately confirms. + /// + /// Note that entries before [`SpliceCandidateStatus::AwaitingSignatures`] do not survive a + /// restart, as they reflect in-memory negotiation state. + pub candidates: Vec<SpliceCandidateDetails>, + /// The negotiated candidate that has confirmed on-chain (or, on a zero-conf channel, that we + /// have locked at zero confirmations), if any, along with its confirmation progress. + /// + /// At most one candidate can confirm, as the candidates all double-spend the same input, so + /// this identifies the single confirming candidate rather than tracking confirmations on each. + pub confirmed_candidate: Option<ConfirmedSpliceCandidate>, + /// The txid announced in the `splice_locked` received from the counterparty, i.e., the + /// candidate that they consider to have sufficient confirmations. + /// + /// Unlike the `splice_locked` we sent (see [`ConfirmedSpliceCandidate::splice_locked_sent`]), + /// this need not match [`confirmed_candidate`]: during a reorg, our counterparty may observe a + /// different candidate confirm. + /// + /// [`confirmed_candidate`]: Self::confirmed_candidate + pub received_splice_locked_txid: Option<Txid>, +} + +impl_ser_tlv_based!(SpliceDetails, { + (1, candidates, required_vec), + (3, confirmed_candidate, option), + (5, received_splice_locked_txid, option), +}); + +/// A single splice or RBF round on a channel, as reported in [`SpliceDetails::candidates`]. +/// +/// The stage this round has reached is given by [`status`]; the details it carries (initiator, +/// feerate, value, txid) become available as it progresses and are accessed through the +/// [`SpliceCandidateStatus`] variant rather than as separate optional fields. +/// +/// [`status`]: Self::status +#[derive(Clone, Debug, PartialEq)] +pub struct SpliceCandidateDetails { + /// Our contribution to this round, or `None` if we did not contribute (a counterparty-only + /// round). + /// + /// Once a round includes our contribution, every later round does as well: RBF attempts carry + /// the contribution forward (possibly adjusted to a new feerate) rather than dropping it, + /// preserving the splice intention. + /// + /// Note that [`FundingContribution::feerate`] is the feerate used when selecting the + /// contribution's inputs, which is not necessarily the exact feerate of the negotiated + /// transaction. + pub contribution: Option<FundingContribution>, + /// The stage this round has reached. + pub status: SpliceCandidateStatus, +} + +impl_ser_tlv_based!(SpliceCandidateDetails, { + (1, contribution, option), + (3, status, required), +}); + +/// The stage a splice or RBF round has reached, as reported in [`SpliceCandidateDetails::status`]. +/// +/// A round committed via [`ChannelManager::funding_contributed`] begins in one of the `WaitingOn*` +/// statuses, advances through the negotiation statuses once the channel is quiescent, and finally +/// reaches [`Negotiated`] once signed. +/// +/// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed +/// [`Negotiated`]: Self::Negotiated +#[derive(Clone, Debug, PartialEq)] +pub enum SpliceCandidateStatus { + /// We have committed a contribution and are awaiting quiescence before it begins negotiating — + /// the first splice on the channel if there are no other candidates, or an RBF replacing an + /// existing candidate otherwise. If the counterparty initiates a round first, the contribution + /// may instead be included in that round. + WaitingOnQuiescence, + /// We have committed a contribution but cannot replace the pending candidate via RBF (our + /// contribution's feerate is too low, the channel is zero-conf, or a candidate is already + /// locking). It will be spliced once the pending candidate locks or, when only the feerate + /// prevents the RBF, sooner if the counterparty initiates an RBF that the contribution can + /// be included in. + WaitingOnLock, + /// We have proposed this round to the counterparty and are awaiting their acknowledgement. + AwaitingAck { + /// Whether we are the initiator of this round. When both sides want to splice, a tie-break at + /// quiescence decides which is the initiator and which is the acceptor. The initiator pays the + /// fees for the transaction's common fields and for the shared input and output (the previous + /// and new channel funding). + is_initiator: bool, + /// The feerate of the splice transaction under negotiation, denominated in satoshi per 1000 + /// weight units. + funding_feerate_sat_per_1000_weight: u32, + }, + /// The splice transaction is being interactively constructed. + ConstructingTransaction { + /// Whether we are the initiator of this round. When both sides want to splice, a tie-break at + /// quiescence decides which is the initiator and which is the acceptor. The initiator pays the + /// fees for the transaction's common fields and for the shared input and output (the previous + /// and new channel funding). + is_initiator: bool, + /// The feerate of the splice transaction under negotiation, denominated in satoshi per 1000 + /// weight units. + funding_feerate_sat_per_1000_weight: u32, + /// The value, in satoshis, of the channel once this round confirms and is promoted. + new_channel_value_satoshis: u64, + }, + /// The splice transaction has been negotiated and is awaiting signatures from both + /// counterparties. + AwaitingSignatures { + /// Whether we are the initiator of this round. When both sides want to splice, a tie-break at + /// quiescence decides which is the initiator and which is the acceptor. The initiator pays the + /// fees for the transaction's common fields and for the shared input and output (the previous + /// and new channel funding). + is_initiator: bool, + /// The feerate of the splice transaction under negotiation, denominated in satoshi per 1000 + /// weight units. + funding_feerate_sat_per_1000_weight: u32, + /// The value, in satoshis, of the channel once this round confirms and is promoted. + new_channel_value_satoshis: u64, + /// The txid of the splice transaction. + txid: Txid, + }, + /// The splice transaction has been signed and is awaiting sufficient on-chain confirmations for + /// both counterparties to exchange `splice_locked`. + Negotiated { + /// The txid of the splice transaction. + txid: Txid, + /// The value, in satoshis, of the channel once this candidate confirms and is promoted. + new_channel_value_satoshis: u64, + }, +} + +impl_ser_tlv_based_enum!(SpliceCandidateStatus, + (1, WaitingOnQuiescence) => {}, + (3, WaitingOnLock) => {}, + (5, AwaitingAck) => { + (1, is_initiator, required), + (3, funding_feerate_sat_per_1000_weight, required), + }, + (7, ConstructingTransaction) => { + (1, is_initiator, required), + (3, funding_feerate_sat_per_1000_weight, required), + (5, new_channel_value_satoshis, required), + }, + (9, AwaitingSignatures) => { + (1, is_initiator, required), + (3, funding_feerate_sat_per_1000_weight, required), + (5, new_channel_value_satoshis, required), + (7, txid, required), + }, + (11, Negotiated) => { + (1, txid, required), + (3, new_channel_value_satoshis, required), + }, +); + +/// The confirmation progress of the negotiated splice candidate that has confirmed on-chain, as +/// exposed in [`SpliceDetails::confirmed_candidate`]. +/// +/// At most one candidate can confirm, as the candidates all double-spend the same input, so this +/// identifies the single confirming candidate by its txid. +#[derive(Clone, Debug, PartialEq)] +pub struct ConfirmedSpliceCandidate { + /// The txid of the candidate that has confirmed on-chain. This matches the txid of the + /// [`SpliceCandidateStatus::Negotiated`] entry in [`SpliceDetails::candidates`] that confirmed. + pub txid: Txid, + /// The current number of confirmations of the candidate's transaction. + pub confirmations: u32, + /// The number of confirmations required before `splice_locked` can be sent for the candidate. + pub confirmations_required: u32, + /// Whether we have sent `splice_locked` for this candidate, i.e., we consider it to have + /// sufficient confirmations. The `splice_locked` we sent always refers to this confirmed + /// candidate, so it is tracked here rather than as a separate txid. + pub splice_locked_sent: bool, +} + +impl_ser_tlv_based!(ConfirmedSpliceCandidate, { + (1, txid, required), + (3, confirmations, required), + (5, confirmations_required, required), + (7, splice_locked_sent, required), +}); + #[derive(Clone, Copy, Debug, PartialEq, Eq)] /// Further information on the details of the channel shutdown. /// Upon channels being forced closed (i.e. commitment transaction confirmation detected @@ -654,7 +976,7 @@ pub enum ChannelShutdownState { ShutdownComplete, } -impl_writeable_tlv_based_enum!(ChannelShutdownState, +impl_ser_tlv_based_enum!(ChannelShutdownState, (0, NotShuttingDown) => {}, (2, ShutdownInitiated) => {}, (4, ResolvingHTLCs) => {}, @@ -673,8 +995,8 @@ mod tests { ln::{ chan_utils::make_funding_redeemscript, channel_state::{ - InboundHTLCDetails, InboundHTLCStateDetails, OutboundHTLCDetails, - OutboundHTLCStateDetails, + InboundHTLCDetails, InboundHTLCReference, InboundHTLCStateDetails, + OutboundHTLCDetails, OutboundHTLCSource, OutboundHTLCStateDetails, }, types::ChannelId, }, @@ -684,7 +1006,10 @@ mod tests { }, }; - use super::{ChannelCounterparty, ChannelDetails, ChannelShutdownState}; + use super::{ + ChannelCounterparty, ChannelDetails, ChannelShutdownState, ConfirmedSpliceCandidate, + SpliceCandidateDetails, SpliceCandidateStatus, SpliceDetails, + }; #[test] fn test_channel_details_serialization() { @@ -716,6 +1041,7 @@ mod tests { outbound_capacity_msat: 24_300, next_outbound_htlc_limit_msat: 20_000, next_outbound_htlc_minimum_msat: 132, + next_splice_out_maximum_sat: 20, inbound_capacity_msat: 42, unspendable_punishment_reserve: Some(8273), confirmations_required: Some(5), @@ -746,7 +1072,40 @@ mod tests { state: Some(OutboundHTLCStateDetails::AwaitingRemoteRevokeToAdd), skimmed_fee_msat: Some(42), is_dust: false, + source: Some(OutboundHTLCSource::TrampolineForwarded { + inbound_htlcs: vec![ + InboundHTLCReference { channel_id: ChannelId([5; 32]), htlc_id: 11 }, + InboundHTLCReference { channel_id: ChannelId([6; 32]), htlc_id: 12 }, + ], + }), }], + current_dust_exposure_msat: Some(150_000), + splice_details: Some(SpliceDetails { + // A reachable arrangement: a negotiated candidate we have confirmed and sent + // `splice_locked` for, followed by a committed contribution that cannot yet be spliced + // (that candidate is locking) and so waits. There is at most one in-flight round and at + // most one `WaitingOn*` entry, which is always last. + candidates: vec![ + SpliceCandidateDetails { + contribution: None, + status: SpliceCandidateStatus::Negotiated { + txid: bitcoin::Txid::from_slice(&[7; 32]).unwrap(), + new_channel_value_satoshis: 60_000, + }, + }, + SpliceCandidateDetails { + contribution: None, + status: SpliceCandidateStatus::WaitingOnLock, + }, + ], + confirmed_candidate: Some(ConfirmedSpliceCandidate { + txid: bitcoin::Txid::from_slice(&[7; 32]).unwrap(), + confirmations: 6, + confirmations_required: 6, + splice_locked_sent: true, + }), + received_splice_locked_txid: None, + }), }; let mut buffer = Vec::new(); channel_details.write(&mut buffer).unwrap(); diff --git a/lightning/src/ln/channel_type_tests.rs b/lightning/src/ln/channel_type_tests.rs index 2b069a6d314..77caa8a2bc4 100644 --- a/lightning/src/ln/channel_type_tests.rs +++ b/lightning/src/ln/channel_type_tests.rs @@ -144,6 +144,7 @@ fn test_zero_conf_channel_type_support() { 42, None, &logger, + None, ) .unwrap(); @@ -167,7 +168,7 @@ fn test_zero_conf_channel_type_support() { &config, 0, &&logger, - /*is_0conf=*/ false, + None, ); assert!(res.is_ok()); } @@ -244,6 +245,7 @@ fn do_test_supports_channel_type(config: UserConfig, expected_channel_type: Chan 42, None, &logger, + None, ) .unwrap(); assert_eq!( @@ -265,6 +267,7 @@ fn do_test_supports_channel_type(config: UserConfig, expected_channel_type: Chan 42, None, &logger, + None, ) .unwrap(); @@ -282,7 +285,7 @@ fn do_test_supports_channel_type(config: UserConfig, expected_channel_type: Chan &config, 0, &&logger, - /*is_0conf=*/ false, + None, ) .unwrap(); @@ -330,6 +333,7 @@ fn test_rejects_if_channel_type_not_set() { 42, None, &logger, + None, ) .unwrap(); @@ -350,7 +354,7 @@ fn test_rejects_if_channel_type_not_set() { &config, 0, &&logger, - /*is_0conf=*/ false, + None, ); assert!(channel_b.is_err()); @@ -368,7 +372,7 @@ fn test_rejects_if_channel_type_not_set() { &config, 0, &&logger, - /*is_0conf=*/ false, + None, ) .unwrap(); @@ -416,6 +420,7 @@ fn test_rejects_if_channel_type_differ() { 42, None, &logger, + None, ) .unwrap(); @@ -434,7 +439,7 @@ fn test_rejects_if_channel_type_differ() { &config, 0, &&logger, - /*is_0conf=*/ false, + None, ) .unwrap(); @@ -499,6 +504,7 @@ fn test_rejects_simple_anchors_channel_type() { 42, None, &logger, + None, ) .unwrap(); @@ -518,7 +524,7 @@ fn test_rejects_simple_anchors_channel_type() { &config, 0, &&logger, - /*is_0conf=*/ false, + None, ); assert!(res.is_err()); @@ -540,6 +546,7 @@ fn test_rejects_simple_anchors_channel_type() { 42, None, &logger, + None, ) .unwrap(); @@ -558,7 +565,7 @@ fn test_rejects_simple_anchors_channel_type() { &config, 0, &&logger, - /*is_0conf=*/ false, + None, ) .unwrap(); diff --git a/lightning/src/ln/channelmanager.rs b/lightning/src/ln/channelmanager.rs index e840d705b8e..5dbbc19be07 100644 --- a/lightning/src/ln/channelmanager.rs +++ b/lightning/src/ln/channelmanager.rs @@ -26,7 +26,7 @@ use bitcoin::transaction::Transaction; use bitcoin::hash_types::{BlockHash, Txid}; use bitcoin::hashes::hmac::Hmac; use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::{Hash, HashEngine, HmacEngine}; +use bitcoin::hashes::{Hash as CryptoHash, HashEngine, HmacEngine}; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; @@ -43,28 +43,29 @@ use crate::chain::chaininterface::{ TransactionType, }; use crate::chain::channelmonitor::{ - Balance, ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, MonitorEvent, + ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, MonitorEvent, WithChannelMonitor, ANTI_REORG_DELAY, CLTV_CLAIM_BUFFER, HTLC_FAIL_BACK_BUFFER, LATENCY_GRACE_PERIOD_BLOCKS, MAX_BLOCKS_FOR_CONF, }; use crate::chain::transaction::{OutPoint, TransactionData}; -use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Watch}; +use crate::chain::{BlockLocator, ChannelMonitorUpdateStatus, Confirm, Watch}; use crate::events::{ self, ClosureReason, Event, EventHandler, EventsProvider, HTLCHandlingFailureType, InboundChannelFunds, PaymentFailureReason, ReplayEvent, }; use crate::events::{FundingInfo, PaidBolt12Invoice}; use crate::ln::chan_utils::selected_commitment_sat_per_1000_weight; -#[cfg(any(test, fuzzing))] +#[cfg(any(test, fuzzing, feature = "_test_utils"))] use crate::ln::channel::QuiescentAction; +use crate::ln::channel::QuiescentError; use crate::ln::channel::{ self, hold_time_since, Channel, ChannelError, ChannelUpdateStatus, DisconnectResult, - FundedChannel, FundingTxSigned, InboundUpdateAdd, InboundV1Channel, OutboundV1Channel, - PendingV2Channel, ReconnectionMsg, ShutdownResult, SpliceFundingFailed, StfuResponse, + FundedChannel, FundingTxSigned, InboundV1Channel, InteractiveTxMsgError, OutboundHop, + OutboundV1Channel, PendingV2Channel, ReconnectionMsg, ShutdownResult, StfuResponse, UpdateFulfillCommitFetch, WithChannelContext, }; -use crate::ln::channel_state::ChannelDetails; -use crate::ln::funding::SpliceContribution; +use crate::ln::channel_state::{ChannelDetails, InboundHTLCReference, OutboundHTLCSource}; +use crate::ln::funding::{FundingContribution, FundingTemplate}; use crate::ln::inbound_payment; use crate::ln::interactivetxs::InteractiveTxMessageSend; use crate::ln::msgs; @@ -83,14 +84,13 @@ use crate::ln::onion_utils::{ }; use crate::ln::onion_utils::{process_fulfill_attribution_data, AttributionData}; use crate::ln::our_peer_storage::{EncryptedOurPeerStorage, PeerStorageMonitorHolder}; -#[cfg(test)] use crate::ln::outbound_payment; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::outbound_payment::PaymentSendFailure; use crate::ln::outbound_payment::{ - Bolt11PaymentError, Bolt12PaymentError, OutboundPayments, PendingOutboundPayment, - ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, RetryableInvoiceRequest, - RetryableSendFailure, SendAlongPathArgs, StaleExpiration, + Bolt11PaymentError, Bolt12PaymentError, NextTrampolineHopInfo, OutboundPayments, + PendingOutboundPayment, ProbeSendFailure, RecipientCustomTlvs, RecipientOnionFields, Retry, + RetryableInvoiceRequest, RetryableSendFailure, SendAlongPathArgs, StaleExpiration, }; use crate::ln::types::ChannelId; use crate::offers::async_receive_offer_cache::AsyncReceiveOfferCache; @@ -112,9 +112,9 @@ use crate::onion_message::messenger::{ MessageRouter, MessageSendInstructions, Responder, ResponseInstruction, }; use crate::onion_message::offers::{OffersMessage, OffersMessageHandler}; -use crate::routing::gossip::NodeId; +use crate::routing::gossip::{NodeId, RoutingFees}; use crate::routing::router::{ - BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, + compute_fees, BlindedTail, FixedRouter, InFlightHtlcs, Path, Payee, PaymentParameters, Route, RouteParameters, RouteParametersConfig, Router, }; use crate::sign::ecdsa::EcdsaChannelSigner; @@ -141,15 +141,6 @@ use crate::util::wakers::{Future, Notifier}; #[cfg(test)] use crate::blinded_path::payment::BlindedPaymentPath; -#[cfg(feature = "dnssec")] -use { - crate::blinded_path::message::DNSResolverContext, - crate::onion_message::dns_resolution::{ - DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, - }, - crate::onion_message::messenger::Destination, -}; - #[cfg(c_bindings)] use { crate::offers::offer::OfferWithDerivedMetadataBuilder, @@ -183,6 +174,7 @@ use crate::ln::script::ShutdownScript; use core::borrow::Borrow; use core::cell::RefCell; use core::convert::Infallible; +use core::hash::{Hash, Hasher}; use core::ops::Deref; use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use core::time::Duration; @@ -230,11 +222,12 @@ pub enum PendingHTLCRouting { }, /// An HTLC which should be forwarded on to another Trampoline node. TrampolineForward { - /// The onion shared secret we build with the sender (or the preceding Trampoline node) used - /// to decrypt the onion. + /// The onion shared secret we build with the node that forwarded us this trampoline + /// forward (either the original sender, or a preceding Trampoline node), used to decrypt + /// the inner trampoline onion. /// /// This is later used to encrypt failure packets in the event that the HTLC is failed. - incoming_shared_secret: [u8; 32], + trampoline_shared_secret: [u8; 32], /// The onion which should be included in the forwarded HTLC, telling the next hop what to /// do with the HTLC. onion_packet: msgs::TrampolineOnionPacket, @@ -244,6 +237,12 @@ pub enum PendingHTLCRouting { blinded: Option<BlindedForward>, /// The absolute CLTV of the inbound HTLC incoming_cltv_expiry: u32, + /// MPP data for accumulating incoming HTLCs before dispatching an outbound payment. + incoming_multipath_data: Option<msgs::FinalOnionHopData>, + /// The amount that the next trampoline is expecting to receive. + next_trampoline_amt_msat: u64, + /// The CLTV expiry height that the next trampoline is expecting to receive. + next_trampoline_cltv_expiry: u32, }, /// The onion indicates that this is a payment for an invoice (supposedly) generated by us. /// @@ -449,7 +448,7 @@ pub(super) enum PendingHTLCStatus { pub(super) struct PendingAddHTLCInfo { pub(super) forward_info: PendingHTLCInfo, - // These fields are produced in `forward_htlcs()` and consumed in + // These fields are set before calling `forward_htlcs()` and consumed in // `process_pending_htlc_forwards()` for constructing the // `HTLCSource::PreviousHopData` for failed and forwarded // HTLCs. @@ -473,12 +472,16 @@ impl PendingAddHTLCInfo { PendingHTLCRouting::Receive { trampoline_shared_secret, .. } => { trampoline_shared_secret }, + PendingHTLCRouting::TrampolineForward { trampoline_shared_secret, .. } => { + Some(trampoline_shared_secret) + }, _ => None, }; HTLCPreviousHopData { prev_outbound_scid_alias: self.prev_outbound_scid_alias, user_channel_id: Some(self.prev_user_channel_id), + amount_msat: self.forward_info.incoming_amt_msat, outpoint: self.prev_funding_outpoint, channel_id: self.prev_channel_id, counterparty_node_id: Some(self.prev_counterparty_node_id), @@ -524,9 +527,8 @@ enum OnionPayload { Spontaneous(PaymentPreimage), } -/// HTLCs that are to us and can be failed/claimed by the user #[derive(PartialEq, Eq)] -struct ClaimableHTLC { +pub(super) struct MppPart { prev_hop: HTLCPreviousHopData, cltv_expiry: u32, /// The amount (in msats) of this MPP part @@ -534,25 +536,91 @@ struct ClaimableHTLC { /// The amount (in msats) that the sender intended to be sent in this MPP /// part (used for validating total MPP amount) sender_intended_value: u64, - onion_payload: OnionPayload, timer_ticks: u8, /// The total value received for a payment (sum of all MPP parts if the payment is a MPP). /// Gets set to the amount reported when pushing [`Event::PaymentClaimable`]. total_value_received: Option<u64>, - /// The sender intended sum total of all MPP parts specified in the onion - total_msat: u64, +} + +impl MppPart { + #[cfg(test)] + pub(super) fn new( + prev_hop: HTLCPreviousHopData, value: u64, sender_intended_value: u64, cltv_expiry: u32, + ) -> Self { + MppPart { + prev_hop, + cltv_expiry, + value, + sender_intended_value, + timer_ticks: 0, + total_value_received: None, + } + } + + /// Returns a boolean indicating whether the HTLC has timed out on chain, accounting for a buffer + /// that gives us time to resolve it. + fn check_onchain_timeout(&self, height: u32) -> bool { + height >= self.cltv_expiry - HTLC_FAIL_BACK_BUFFER + } +} + +impl PartialOrd for MppPart { + fn partial_cmp(&self, other: &MppPart) -> Option<cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for MppPart { + fn cmp(&self, other: &MppPart) -> cmp::Ordering { + let res = (self.prev_hop.channel_id, self.prev_hop.htlc_id) + .cmp(&(other.prev_hop.channel_id, other.prev_hop.htlc_id)); + if res.is_eq() { + debug_assert!(self == other, "MppParts from the same source should be identical"); + } + res + } +} + +trait HasMppPart { + fn mpp_part(&self) -> &MppPart; + fn mpp_part_mut(&mut self) -> &mut MppPart; +} + +impl HasMppPart for MppPart { + fn mpp_part(&self) -> &MppPart { + self + } + fn mpp_part_mut(&mut self) -> &mut MppPart { + self + } +} + +/// Represents an incoming HTLC that can be claimed or failed by the user. +#[derive(PartialEq, Eq)] +struct ClaimableHTLC { + mpp_part: MppPart, + onion_payload: OnionPayload, /// The extra fee our counterparty skimmed off the top of this HTLC. counterparty_skimmed_fee_msat: Option<u64>, } +impl HasMppPart for ClaimableHTLC { + fn mpp_part(&self) -> &MppPart { + &self.mpp_part + } + fn mpp_part_mut(&mut self) -> &mut MppPart { + &mut self.mpp_part + } +} + impl From<&ClaimableHTLC> for events::ClaimedHTLC { fn from(val: &ClaimableHTLC) -> Self { events::ClaimedHTLC { - counterparty_node_id: val.prev_hop.counterparty_node_id, - channel_id: val.prev_hop.channel_id, - user_channel_id: val.prev_hop.user_channel_id.unwrap_or(0), - cltv_expiry: val.cltv_expiry, - value_msat: val.value, + counterparty_node_id: val.mpp_part.prev_hop.counterparty_node_id, + channel_id: val.mpp_part.prev_hop.channel_id, + user_channel_id: val.mpp_part.prev_hop.user_channel_id.unwrap_or(0), + cltv_expiry: val.mpp_part.cltv_expiry, + value_msat: val.mpp_part.value, counterparty_skimmed_fee_msat: val.counterparty_skimmed_fee_msat.unwrap_or(0), } } @@ -565,12 +633,7 @@ impl PartialOrd for ClaimableHTLC { } impl Ord for ClaimableHTLC { fn cmp(&self, other: &ClaimableHTLC) -> cmp::Ordering { - let res = (self.prev_hop.channel_id, self.prev_hop.htlc_id) - .cmp(&(other.prev_hop.channel_id, other.prev_hop.htlc_id)); - if res.is_eq() { - debug_assert!(self == other, "ClaimableHTLCs from the same source should be identical"); - } - res + self.mpp_part.cmp(&other.mpp_part) } } @@ -578,7 +641,7 @@ impl Ord for ClaimableHTLC { /// a payment and ensure idempotency in LDK. /// /// This is not exported to bindings users as we just use [u8; 32] directly -#[derive(Hash, Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] pub struct PaymentId(pub [u8; Self::LENGTH]); impl PaymentId { @@ -610,6 +673,13 @@ impl Borrow<[u8]> for PaymentId { } } +impl Hash for PaymentId { + fn hash<H: Hasher>(&self, state: &mut H) { + let slice: &[u8] = self.borrow(); + Hash::hash(slice, state); + } +} + impl_fmt_traits! { impl fmt_traits for PaymentId { const LENGTH: usize = 32; @@ -632,7 +702,7 @@ impl Readable for PaymentId { /// An identifier used to uniquely identify an intercepted HTLC to LDK. /// /// This is not exported to bindings users as we just use [u8; 32] directly -#[derive(Hash, Copy, Clone, PartialEq, Eq)] +#[derive(Copy, Clone, PartialEq, Eq)] pub struct InterceptId(pub [u8; 32]); impl InterceptId { @@ -652,6 +722,14 @@ impl Borrow<[u8]> for InterceptId { &self.0[..] } } + +impl Hash for InterceptId { + fn hash<H: Hasher>(&self, state: &mut H) { + let slice: &[u8] = self.borrow(); + Hash::hash(slice, state); + } +} + impl_fmt_traits! { impl fmt_traits for InterceptId { const LENGTH: usize = 32; @@ -686,6 +764,20 @@ pub struct OptionalBolt11PaymentParams { /// will ultimately fail once all pending paths have failed (generating an /// [`Event::PaymentFailed`]). pub retry_strategy: Retry, + /// If the payment being made from this node is part of a larger MPP payment from multiple + /// nodes (i.e. because a single payment is being made from multiple wallets), you can specify + /// the total amount being paid here. + /// + /// If this is set, it must be at least the [`Bolt11Invoice::amount_milli_satoshis`] for the + /// invoice provided to [`ChannelManager::pay_for_bolt11_invoice`]. Further, if this is set, + /// the `amount_msats` provided to [`ChannelManager::pay_for_bolt11_invoice`] is allowed to be + /// lower than [`Bolt11Invoice::amount_milli_satoshis`] (as the payment we're making may be a + /// small part of the amount needed to meet the invoice's minimum). + /// + /// If this is lower than the `amount_msats` passed to + /// [`ChannelManager::pay_for_bolt11_invoice`] the call will fail with + /// [`Bolt11PaymentError::InvalidAmount`]. + pub declared_total_mpp_value_msat_override: Option<u64>, } impl Default for OptionalBolt11PaymentParams { @@ -697,16 +789,12 @@ impl Default for OptionalBolt11PaymentParams { retry_strategy: Retry::Timeout(core::time::Duration::from_secs(2)), #[cfg(not(feature = "std"))] retry_strategy: Retry::Attempts(3), + declared_total_mpp_value_msat_override: None, } } } -/// Optional arguments to [`ChannelManager::pay_for_offer`] -#[cfg_attr( - feature = "dnssec", - doc = "and [`ChannelManager::pay_for_offer_from_human_readable_name`]" -)] -/// . +/// Optional arguments to [`ChannelManager::pay_for_offer`]. /// /// These fields will often not need to be set, and the provided [`Self::default`] can be used. pub struct OptionalOfferPaymentParams { @@ -742,21 +830,33 @@ impl Default for OptionalOfferPaymentParams { pub(crate) enum SentHTLCId { PreviousHopData { prev_outbound_scid_alias: u64, htlc_id: u64 }, OutboundRoute { session_priv: [u8; SECRET_KEY_SIZE] }, + TrampolineForward { session_priv: [u8; SECRET_KEY_SIZE] }, } impl SentHTLCId { + /// Creates an identifier for the [`HTLCSource`] provided. Note that for MPP trampoline payments + /// each outgoing HTLC will have a distinct identifier. pub(crate) fn from_source(source: &HTLCSource) -> Self { match source { HTLCSource::PreviousHopData(hop_data) => Self::PreviousHopData { prev_outbound_scid_alias: hop_data.prev_outbound_scid_alias, htlc_id: hop_data.htlc_id, }, + HTLCSource::TrampolineForward { + ref outbound_payment, + .. + } => Self::TrampolineForward { + session_priv: outbound_payment + .as_ref() + .map(|o| o.session_priv.secret_bytes()) + .expect("trying to identify a trampoline payment that we have no outbound_payment tracked for"), + }, HTLCSource::OutboundRoute { session_priv, .. } => { Self::OutboundRoute { session_priv: session_priv.secret_bytes() } }, } } } -impl_writeable_tlv_based_enum!(SentHTLCId, +impl_ser_tlv_based_enum!(SentHTLCId, (0, PreviousHopData) => { (0, prev_outbound_scid_alias, required), (2, htlc_id, required), @@ -764,22 +864,40 @@ impl_writeable_tlv_based_enum!(SentHTLCId, (2, OutboundRoute) => { (0, session_priv, required), }, + (4, TrampolineForward) => { + (0, session_priv, required), + }, ); -// (src_outbound_scid_alias, src_counterparty_node_id, src_funding_outpoint, src_chan_id, src_user_chan_id) -type PerSourcePendingForward = - (u64, PublicKey, OutPoint, ChannelId, u128, Vec<(PendingHTLCInfo, u64)>); - type FailedHTLCForward = (HTLCSource, PaymentHash, HTLCFailReason, HTLCHandlingFailureType); mod fuzzy_channelmanager { use super::*; + /// Information about a HTLC sent as part of a (possibly MPP) payment to the next trampoline. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct TrampolineDispatch { + /// The payment ID used for the outbound payment. + pub payment_id: PaymentId, + /// The path used for the outbound payment. + pub path: Path, + /// The session private key used for inter-trampoline outer onions. + pub session_priv: SecretKey, + } + /// Tracks the inbound corresponding to an outbound HTLC - #[allow(clippy::derive_hash_xor_eq)] // Our Hash is faithful to the data, we just don't have SecretKey::hash + #[allow(clippy::derive_hash_xor_eq, dead_code)] // Our Hash is faithful to the data, we just don't have SecretKey::hash #[derive(Clone, Debug, PartialEq, Eq)] pub enum HTLCSource { PreviousHopData(HTLCPreviousHopData), + TrampolineForward { + /// We might be forwarding an incoming payment that was received over MPP, and therefore + /// need to store the vector of corresponding `HTLCPreviousHopData` values. + previous_hop_data: Vec<HTLCPreviousHopData>, + /// Track outbound payment details once the payment has been dispatched, will be `None` + /// when waiting for incoming MPP to accumulate. + outbound_payment: Option<TrampolineDispatch>, + }, OutboundRoute { path: Path, session_priv: SecretKey, @@ -794,11 +912,60 @@ mod fuzzy_channelmanager { }, } + impl HTLCSource { + pub(crate) fn to_outbound(&self) -> OutboundHTLCSource { + let inbound_htlc = |prev_hop: &HTLCPreviousHopData| InboundHTLCReference { + channel_id: prev_hop.channel_id, + htlc_id: prev_hop.htlc_id, + }; + match self { + Self::OutboundRoute { payment_id, .. } => { + OutboundHTLCSource::Local { payment_id: *payment_id } + }, + Self::PreviousHopData(prev_hop) => { + OutboundHTLCSource::Forwarded { inbound_htlc: inbound_htlc(prev_hop) } + }, + Self::TrampolineForward { previous_hop_data, .. } => { + OutboundHTLCSource::TrampolineForwarded { + inbound_htlcs: previous_hop_data.iter().map(inbound_htlc).collect(), + } + }, + } + } + + pub fn failure_type( + &self, counterparty_node: PublicKey, channel_id: ChannelId, + ) -> HTLCHandlingFailureType { + match self { + // We won't actually emit an event with HTLCHandlingFailure if our source is an + // OutboundRoute, but `fail_htlc_backwards_internal` requires that we provide it. + HTLCSource::PreviousHopData(_) | HTLCSource::OutboundRoute { .. } => { + HTLCHandlingFailureType::Forward { + node_id: Some(counterparty_node), + channel_id, + } + }, + HTLCSource::TrampolineForward { .. } => { + HTLCHandlingFailureType::TrampolineForward {} + }, + } + } + + pub(crate) fn previous_hop_data(&self) -> &[HTLCPreviousHopData] { + match self { + HTLCSource::PreviousHopData(prev_hop) => core::slice::from_ref(prev_hop), + HTLCSource::TrampolineForward { previous_hop_data, .. } => &previous_hop_data[..], + HTLCSource::OutboundRoute { .. } => &[], + } + } + } + /// Tracks the inbound corresponding to an outbound HTLC #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct HTLCPreviousHopData { pub prev_outbound_scid_alias: u64, pub user_channel_id: Option<u128>, + pub amount_msat: Option<u64>, pub htlc_id: u64, pub incoming_packet_shared_secret: [u8; 32], pub phantom_shared_secret: Option<[u8; 32]>, @@ -814,6 +981,17 @@ mod fuzzy_channelmanager { /// channel remains unconfirmed for too long. pub cltv_expiry: Option<u32>, } + + impl HTLCPreviousHopData { + pub(super) fn htlc_locator(&self, amount_msat: Option<u64>) -> events::HTLCLocator { + events::HTLCLocator { + channel_id: self.channel_id, + amount_msat, + user_channel_id: self.user_channel_id, + node_id: self.counterparty_node_id, + } + } + } } #[cfg(fuzzing)] pub use self::fuzzy_channelmanager::*; @@ -821,7 +999,7 @@ pub use self::fuzzy_channelmanager::*; pub(crate) use self::fuzzy_channelmanager::*; #[allow(clippy::derive_hash_xor_eq)] // Our Hash is faithful to the data, we just don't have SecretKey::hash -impl core::hash::Hash for HTLCSource { +impl Hash for HTLCSource { fn hash<H: core::hash::Hasher>(&self, hasher: &mut H) { match self { HTLCSource::PreviousHopData(prev_hop_data) => { @@ -842,6 +1020,15 @@ impl core::hash::Hash for HTLCSource { first_hop_htlc_msat.hash(hasher); bolt12_invoice.hash(hasher); }, + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment } => { + 2u8.hash(hasher); + previous_hop_data.hash(hasher); + if let Some(payment) = outbound_payment { + payment.payment_id.hash(hasher); + payment.path.hash(hasher); + payment.session_priv[..].hash(hasher); + } + }, } } } @@ -939,6 +1126,7 @@ struct MsgHandleErrInternal { shutdown_finish: Option<(ShutdownResult, Option<(msgs::ChannelUpdate, NodeId, NodeId)>)>, tx_abort: Option<msgs::TxAbort>, } + impl MsgHandleErrInternal { fn send_err_msg_no_close(err: String, channel_id: ChannelId) -> Self { Self { @@ -954,6 +1142,21 @@ impl MsgHandleErrInternal { } } + fn unreachable_no_such_peer(counterparty_node_id: &PublicKey, channel_id: ChannelId) -> Self { + debug_assert!(false); + let err = + format!("No such peer for the passed counterparty_node_id {counterparty_node_id}"); + Self::send_err_msg_no_close(err, channel_id) + } + + fn no_such_channel_for_peer(counterparty_node_id: &PublicKey, channel_id: ChannelId) -> Self { + let err = format!( + "Got a message for a channel from the wrong node! No such channel_id {} for the passed counterparty_node_id {}", + channel_id, counterparty_node_id + ); + Self::send_err_msg_no_close(err, channel_id) + } + fn from_no_close(err: msgs::LightningError) -> Self { Self { err, closes_channel: false, shutdown_finish: None, tx_abort: None } } @@ -981,7 +1184,7 @@ impl MsgHandleErrInternal { fn from_chan_no_close(err: ChannelError, channel_id: ChannelId) -> Self { let tx_abort = match &err { - &ChannelError::Abort(reason) => Some(reason.into_tx_abort_msg(channel_id)), + ChannelError::Abort(reason) => Some(reason.clone().into_tx_abort_msg(channel_id)), _ => None, }; let err = match err { @@ -1027,6 +1230,13 @@ impl MsgHandleErrInternal { fn closes_channel(&self) -> bool { self.closes_channel } + + /// Whether the holding cell should be released after handling this error. This is inferred + /// from the presence of a `tx_abort`, which is sent when aborting an interactive transaction + /// negotiation that was conducted during quiescence. + fn needs_holding_cell_release(&self) -> bool { + self.tx_abort.is_some() + } } /// For events which result in both a RevokeAndACK and a CommitmentUpdate, by default they should @@ -1051,6 +1261,25 @@ pub(super) enum ChannelReadyOrder { SignaturesFirst, } +/// Determines whether splice `tx_signatures` should be sent before or after other messages when +/// resuming a channel. +/// +/// The ordering matters because exchanging `tx_signatures` ends splice quiescence. A normal +/// commitment update generated after quiescence cannot be processed by the peer until it has +/// received our `tx_signatures`. Similarly, if the peer's signature exchange is incomplete, it +/// cannot process `splice_locked` until the exchange adds the splice transaction to its negotiated +/// candidates. However, an initial `commitment_signed` for the splice funding must itself be +/// exchanged before the corresponding funding signatures. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(super) enum TxSignaturesOrder { + /// Send `tx_signatures` before a normal commitment update or `splice_locked` so the peer + /// completes the signature exchange first. + SignaturesFirst, + /// Send `tx_signatures` after an initial splice `commitment_signed` establishes the new funding + /// state. + CommitmentFirst, +} + /// Information about a payment which is currently being claimed. #[derive(Clone, Debug, PartialEq, Eq)] struct ClaimingPayment { @@ -1059,7 +1288,7 @@ struct ClaimingPayment { receiver_node_id: PublicKey, htlcs: Vec<events::ClaimedHTLC>, sender_intended_value: Option<u64>, - onion_fields: Option<RecipientOnionFields>, + onion_fields: RecipientOnionFields, payment_id: Option<PaymentId>, /// When we claim and generate a [`Event::PaymentClaimed`], we want to block any /// payment-preimage-removing RAA [`ChannelMonitorUpdate`]s until the [`Event::PaymentClaimed`] @@ -1071,20 +1300,21 @@ struct ClaimingPayment { /// outpoint), allowing us to remove this field. durable_preimage_channel: Option<(OutPoint, PublicKey, ChannelId)>, } -impl_writeable_tlv_based!(ClaimingPayment, { +impl_ser_tlv_based!(ClaimingPayment, { (0, amount_msat, required), (1, durable_preimage_channel, option), (2, payment_purpose, required), (4, receiver_node_id, required), (5, htlcs, optional_vec), (7, sender_intended_value, option), - (9, onion_fields, option), + // onion_fields was added (and always set for new payments) in 0.0.124 + (9, onion_fields, (required: ReadableArgs, amount_msat.0.unwrap())), (11, payment_id, option), }); struct ClaimablePayment { purpose: events::PaymentPurpose, - onion_fields: Option<RecipientOnionFields>, + onion_fields: RecipientOnionFields, htlcs: Vec<ClaimableHTLC>, } @@ -1092,7 +1322,9 @@ impl ClaimablePayment { fn inbound_payment_id(&self, secret: &[u8; 32]) -> PaymentId { PaymentId::for_inbound_from_htlcs( secret, - self.htlcs.iter().map(|htlc| (htlc.prev_hop.channel_id, htlc.prev_hop.htlc_id)), + self.htlcs + .iter() + .map(|htlc| (htlc.mpp_part.prev_hop.channel_id, htlc.mpp_part.prev_hop.htlc_id)), ) } @@ -1102,9 +1334,45 @@ impl ClaimablePayment { fn receiving_channel_ids(&self) -> Vec<(ChannelId, Option<u128>)> { self.htlcs .iter() - .map(|htlc| (htlc.prev_hop.channel_id, htlc.prev_hop.user_channel_id)) + .map(|htlc| (htlc.mpp_part.prev_hop.channel_id, htlc.mpp_part.prev_hop.user_channel_id)) .collect() } + + /// Returns the total counterparty skimmed fee across all HTLCs. + fn total_counterparty_skimmed_msat(&self) -> u64 { + self.htlcs.iter().map(|htlc| htlc.counterparty_skimmed_fee_msat.unwrap_or(0)).sum() + } +} + +/// Increments MPP timeout tick for all HTLCs and returns a boolean indicating whether the HTLC +/// set has hit its MPP timeout. Will return false if the set has reached the sender's intended +/// total, as the MPP has completed in this case. +fn check_mpp_timeout<'a>( + htlcs: impl Iterator<Item = &'a mut MppPart>, onion_fields: &RecipientOnionFields, +) -> bool { + // This condition determining whether the MPP is complete here must match exactly the condition + // used in `process_pending_htlc_forwards`. + let total_mpp_value = onion_fields.total_mpp_amount_msat; + let mut total_intended_recvd_value = 0; + let mut timed_out = false; + for htlc in htlcs { + total_intended_recvd_value += htlc.sender_intended_value; + htlc.timer_ticks += 1; + if htlc.timer_ticks >= MPP_TIMEOUT_TICKS { + timed_out = true; + } + } + if total_intended_recvd_value >= total_mpp_value { + return false; + } + + timed_out +} + +/// Tracks trampoline HTLCs being accumulated before forwarding. +struct TrampolinePayment { + onion_fields: RecipientOnionFields, + htlcs: Vec<MppPart>, } /// Represent the channel funding transaction type. @@ -1199,7 +1467,7 @@ impl ClaimablePayments { let mut receiver_node_id = node_signer.get_node_id(Recipient::Node) .expect("Failed to get node_id for node recipient"); for htlc in payment.htlcs.iter() { - if htlc.prev_hop.phantom_shared_secret.is_some() { + if htlc.mpp_part.prev_hop.phantom_shared_secret.is_some() { let phantom_pubkey = node_signer.get_node_id(Recipient::PhantomNode) .expect("Failed to get node_id for phantom node recipient"); receiver_node_id = phantom_pubkey; @@ -1207,12 +1475,11 @@ impl ClaimablePayments { } } - if let Some(RecipientOnionFields { custom_tlvs, .. }) = &payment.onion_fields { - if !custom_tlvs_known && custom_tlvs.iter().any(|(typ, _)| typ % 2 == 0) { - log_info!(logger, "Rejecting payment with payment hash {} as we cannot accept payment with unknown even TLVs: {}", - &payment_hash, log_iter!(custom_tlvs.iter().map(|(typ, _)| typ).filter(|typ| *typ % 2 == 0))); - return Err(payment.htlcs); - } + let custom_tlvs = &payment.onion_fields.custom_tlvs; + if !custom_tlvs_known && custom_tlvs.iter().any(|(typ, _)| typ % 2 == 0) { + log_info!(logger, "Rejecting payment with payment hash {} as we cannot accept payment with unknown even TLVs: {}", + &payment_hash, log_iter!(custom_tlvs.iter().map(|(typ, _)| typ).filter(|typ| *typ % 2 == 0))); + return Err(payment.htlcs); } let payment_id = payment.inbound_payment_id(inbound_payment_id_secret); @@ -1225,23 +1492,23 @@ impl ClaimablePayments { }) .or_insert_with(|| { let htlcs = payment.htlcs.iter().map(events::ClaimedHTLC::from).collect(); - let sender_intended_value = payment.htlcs.first().map(|htlc| htlc.total_msat); + let sender_intended_value = payment.onion_fields.total_mpp_amount_msat; // Pick an "arbitrary" channel to block RAAs on until the `PaymentSent` // event is processed, specifically the last channel to get claimed. let durable_preimage_channel = payment.htlcs.last().map_or(None, |htlc| { - if let Some(node_id) = htlc.prev_hop.counterparty_node_id { - Some((htlc.prev_hop.outpoint, node_id, htlc.prev_hop.channel_id)) + if let Some(node_id) = htlc.mpp_part.prev_hop.counterparty_node_id { + Some((htlc.mpp_part.prev_hop.outpoint, node_id, htlc.mpp_part.prev_hop.channel_id)) } else { None } }); debug_assert!(durable_preimage_channel.is_some()); ClaimingPayment { - amount_msat: payment.htlcs.iter().map(|source| source.value).sum(), + amount_msat: payment.htlcs.iter().map(|source| source.mpp_part.value).sum(), payment_purpose: payment.purpose, receiver_node_id, htlcs, - sender_intended_value, + sender_intended_value: Some(sender_intended_value), onion_fields: payment.onion_fields, payment_id: Some(payment_id), durable_preimage_channel, @@ -1283,6 +1550,11 @@ enum BackgroundEvent { channel_id: ChannelId, highest_update_id_completed: u64, }, + /// A channel had blocked monitor updates waiting on startup. If the updates were blocked on + /// an MPP claim blocker not written to disk, we may be able to unblock them now. + /// + /// This event is never written to disk. + AttemptUnblockMonitorUpdates { counterparty_node_id: PublicKey, channel_id: ChannelId }, } /// A pointer to a channel that is unblocked when an event is surfaced @@ -1346,23 +1618,23 @@ pub(crate) enum MonitorUpdateCompletionAction { /// completes a monitor update containing the payment preimage. In that case, after the inbound /// edge completes, we will surface an [`Event::PaymentForwarded`] as well as unblock the /// outbound edge. - EmitEventAndFreeOtherChannel { - event: events::Event, - downstream_counterparty_and_funding_outpoint: Option<EventUnblockedChannel>, + EmitEventOptionAndFreeOtherChannel { + event: Option<events::Event>, + downstream_counterparty_and_funding_outpoint: EventUnblockedChannel, }, /// Indicates we should immediately resume the operation of another channel, unless there is /// some other reason why the channel is blocked. In practice this simply means immediately /// removing the [`RAAMonitorUpdateBlockingAction`] provided from the blocking set. /// - /// This is usually generated when we've forwarded an HTLC and want to block the outbound edge - /// from completing a monitor update which removes the payment preimage until the inbound edge + /// This is generated when we've forwarded an HTLC and want to block the outbound edge from + /// completing a monitor update which removes the payment preimage until the inbound edge /// completes a monitor update containing the payment preimage. However, we use this variant - /// instead of [`Self::EmitEventAndFreeOtherChannel`] when we discover that the claim was in - /// fact duplicative and we simply want to resume the outbound edge channel immediately. + /// instead of [`Self::EmitEventOptionAndFreeOtherChannel`] when we discover that the claim was + /// in fact duplicative and we simply want to resume the outbound edge channel immediately. /// /// This variant should thus never be written to disk, as it is processed inline rather than /// stored for later processing. - FreeOtherChannelImmediately { + FreeDuplicateClaimImmediately { downstream_counterparty_node_id: PublicKey, blocking_action: RAAMonitorUpdateBlockingAction, downstream_channel_id: ChannelId, @@ -1374,21 +1646,19 @@ impl_writeable_tlv_based_enum_upgradable!(MonitorUpdateCompletionAction, (0, payment_hash, required), (9999999999, pending_mpp_claim, (static_value, None)), }, - // Note that FreeOtherChannelImmediately should never be written - we were supposed to free + // Note that FreeDuplicateClaimImmediately should never be written - we were supposed to free // *immediately*. However, for simplicity we implement read/write here. - (1, FreeOtherChannelImmediately) => { + (1, FreeDuplicateClaimImmediately) => { (0, downstream_counterparty_node_id, required), (4, blocking_action, upgradable_required), (5, downstream_channel_id, required), }, - (2, EmitEventAndFreeOtherChannel) => { - (0, event, upgradable_required), - // LDK prior to 0.0.116 did not have this field as the monitor update application order was - // required by clients. If we downgrade to something prior to 0.0.116 this may result in - // monitor updates which aren't properly blocked or resumed, however that's fine - we don't - // support async monitor updates even in LDK 0.0.116 and once we do we'll require no - // downgrades to prior versions. - (1, downstream_counterparty_and_funding_outpoint, upgradable_option), + (2, EmitEventOptionAndFreeOtherChannel) => { + // LDK prior to 0.3 required this field. It will not be present for trampoline payments + // with multiple incoming HTLCS, so nodes cannot downgrade while trampoline payments + // are in the process of being resolved. + (0, event, upgradable_option), + (1, downstream_counterparty_and_funding_outpoint, upgradable_required), }, ); @@ -1400,12 +1670,14 @@ enum PostMonitorUpdateChanResume { Blocked { update_actions: Vec<MonitorUpdateCompletionAction> }, /// Channel was fully unblocked and has been resumed. Contains remaining data to process. Unblocked { + needs_persist: bool, channel_id: ChannelId, counterparty_node_id: PublicKey, + funding_txo: OutPoint, + user_channel_id: u128, unbroadcasted_batch_funding_txid: Option<Txid>, update_actions: Vec<MonitorUpdateCompletionAction>, - htlc_forwards: Option<PerSourcePendingForward>, - decode_update_add_htlcs: Option<(u64, Vec<msgs::UpdateAddHTLC>)>, + htlc_forwards: Vec<PendingAddHTLCInfo>, finalized_claimed_htlcs: Vec<(HTLCSource, Option<AttributionData>)>, failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>, committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, @@ -1420,7 +1692,7 @@ pub(crate) struct PaymentCompleteUpdate { htlc_id: SentHTLCId, } -impl_writeable_tlv_based!(PaymentCompleteUpdate, { +impl_ser_tlv_based!(PaymentCompleteUpdate, { (1, channel_funding_outpoint, required), (3, counterparty_node_id, required), (5, channel_id, required), @@ -1442,7 +1714,7 @@ pub(crate) enum EventCompletionAction { /// Note that this action will be dropped on downgrade to LDK prior to 0.2! ReleasePaymentCompleteChannelMonitorUpdate(PaymentCompleteUpdate), } -impl_writeable_tlv_based_enum!(EventCompletionAction, +impl_ser_tlv_based_enum!(EventCompletionAction, (0, ReleaseRAAChannelMonitorUpdate) => { (0, channel_funding_outpoint, option), (2, counterparty_node_id, required), @@ -1497,7 +1769,7 @@ struct MPPClaimHTLCSource { htlc_id: u64, } -impl_writeable_tlv_based!(MPPClaimHTLCSource, { +impl_ser_tlv_based!(MPPClaimHTLCSource, { (0, counterparty_node_id, required), (2, funding_txo, required), (4, channel_id, required), @@ -1516,7 +1788,7 @@ pub(crate) struct PaymentClaimDetails { claiming_payment: ClaimingPayment, } -impl_writeable_tlv_based!(PaymentClaimDetails, { +impl_ser_tlv_based!(PaymentClaimDetails, { (0, mpp_parts, required_vec), (2, claiming_payment, required), }); @@ -1923,9 +2195,8 @@ impl< /// detailed in the [`ChannelManagerReadArgs`] documentation. /// /// ``` -/// use bitcoin::BlockHash; /// use bitcoin::network::Network; -/// use lightning::chain::BestBlock; +/// use lightning::chain::BlockLocator; /// # use lightning::chain::channelmonitor::ChannelMonitor; /// use lightning::ln::channelmanager::{ChainParameters, ChannelManager, ChannelManagerReadArgs}; /// # use lightning::routing::gossip::NetworkGraph; @@ -1951,7 +2222,7 @@ impl< /// # entropy_source: &ES, /// # node_signer: &dyn lightning::sign::NodeSigner, /// # signer_provider: &lightning::sign::DynSignerProvider, -/// # best_block: lightning::chain::BestBlock, +/// # best_block: lightning::chain::BlockLocator, /// # current_timestamp: u32, /// # mut reader: R, /// # ) -> Result<(), lightning::ln::msgs::DecodeError> { @@ -1972,8 +2243,8 @@ impl< /// entropy_source, node_signer, signer_provider, fee_estimator, chain_monitor, tx_broadcaster, /// router, message_router, logger, config, channel_monitors.iter().collect(), /// ); -/// let (block_hash, channel_manager) = -/// <(BlockHash, ChannelManager<_, _, _, _, _, _, _, _, _>)>::read(&mut reader, args)?; +/// let (best_block, channel_manager) = +/// <(BlockLocator, ChannelManager<_, _, _, _, _, _, _, _, _>)>::read(&mut reader, args)?; /// /// // Update the ChannelManager and ChannelMonitors with the latest chain data /// // ... @@ -2540,9 +2811,10 @@ impl< /// [`read`], those channels will be force-closed based on the `ChannelMonitor` state and no funds /// will be lost (modulo on-chain transaction fees). /// -/// Note that the deserializer is only implemented for `(`[`BlockHash`]`, `[`ChannelManager`]`)`, which -/// tells you the last block hash which was connected. You should get the best block tip before using the manager. -/// See [`chain::Listen`] and [`chain::Confirm`] for more details. +/// Note that the deserializer is only implemented for `(`[`BlockLocator`]`, `[`ChannelManager`]`)`, +/// which provides a locator for the best chain as of the last write. You should sync to the +/// current best chain tip before using the manager. See [`chain::Listen`] and [`chain::Confirm`] +/// for more details. /// /// # `ChannelUpdate` Messages /// @@ -2607,7 +2879,6 @@ impl< /// [`peer_disconnected`]: msgs::BaseMessageHandler::peer_disconnected /// [`funding_created`]: msgs::FundingCreated /// [`funding_transaction_generated`]: Self::funding_transaction_generated -/// [`BlockHash`]: bitcoin::hash_types::BlockHash /// [`update_channel`]: chain::Watch::update_channel /// [`ChannelUpdate`]: msgs::ChannelUpdate /// [`read`]: ReadableArgs::read @@ -2635,9 +2906,9 @@ pub struct ChannelManager< flow: OffersMessageFlow<MR, L>, #[cfg(any(test, feature = "_test_utils"))] - pub(super) best_block: RwLock<BestBlock>, + pub(super) best_block: RwLock<BlockLocator>, #[cfg(not(any(test, feature = "_test_utils")))] - best_block: RwLock<BestBlock>, + best_block: RwLock<BlockLocator>, pub(super) secp_ctx: Secp256k1<secp256k1::All>, /// The session_priv bytes and retry metadata of outbound payments which are pending resolution. @@ -2685,6 +2956,16 @@ pub struct ChannelManager< /// [`ClaimablePayments`]' individual field docs for more info. claimable_payments: Mutex<ClaimablePayments>, + /// The sets of trampoline payments which are in the process of being accumulated on inbound + /// channel(s). + /// + /// Note that this map is currently not persisted, as there is ongoing work to refactor our + /// reload from disk depending only on channel managers. Until proper restart logic is added + /// we will "forget" about any HTLCs that are pending in this map on restart waiting for MPP + /// timeout. For this reason, we should not forward any trampoline HTLCs until properly + /// implemented. + awaiting_trampoline_forwards: Mutex<HashMap<PaymentHash, TrampolinePayment>>, + /// The set of outbound SCID aliases across all our channels, including unconfirmed channels /// and some closed channels which reached a usable state prior to being closed. This is used /// only to avoid duplicates, and is not persisted explicitly to disk, but rebuilt from the @@ -2745,12 +3026,12 @@ pub struct ChannelManager< #[cfg(any(test, feature = "_test_utils"))] pub(super) per_peer_state: FairRwLock<HashMap<PublicKey, Mutex<PeerState<SP>>>>, - /// We only support using one of [`ChannelMonitorUpdateStatus::InProgress`] and - /// [`ChannelMonitorUpdateStatus::Completed`] without restarting. Because the API does not - /// otherwise directly enforce this, we enforce it in non-test builds here by storing which one - /// is in use. - #[cfg(not(any(test, feature = "_externalize_tests")))] - monitor_update_type: AtomicUsize, + /// When set, disables the panic when `Watch::update_channel` returns `Completed` while + /// prior updates are still `InProgress`. Some legacy tests switch the persister between + /// `InProgress` and `Completed` mid-flight, which violates this contract but is otherwise + /// harmless in a test context. + #[cfg(test)] + pub(crate) skip_monitor_update_assertion: AtomicBool, /// The set of events which we need to give to the user to handle. In some cases an event may /// require some further action after the user handles it (currently only blocking a monitor @@ -2819,14 +3100,6 @@ pub struct ChannelManager< /// [`ConfirmationTarget::MinAllowedNonAnchorChannelRemoteFee`] estimate. last_days_feerates: Mutex<VecDeque<(u32, u32)>>, - #[cfg(feature = "_test_utils")] - /// In testing, it is useful be able to forge a name -> offer mapping so that we can pay an - /// offer generated in the test. - /// - /// This allows for doing so, validating proofs as normal, but, if they pass, replacing the - /// offer they resolve to to the given one. - pub testing_dnssec_proof_offer_resolution_override: Mutex<HashMap<HumanReadableName, Offer>>, - #[cfg(test)] pub(super) entropy_source: ES, #[cfg(not(test))] @@ -2853,7 +3126,7 @@ pub struct ChainParameters { /// The hash and height of the latest block successfully connected. /// /// Used to track on-chain channel funding outputs and send payments with reliable timelocks. - pub best_block: BestBlock, + pub best_block: BlockLocator, } #[derive(Copy, Clone, PartialEq)] @@ -2898,6 +3171,23 @@ impl<'a> PersistenceNotifierGuard<'a, fn() -> NotifyOption> { Self::optionally_notify(cm, || -> NotifyOption { NotifyOption::DoPersist }) } + fn manually_notify<F: FnOnce(), C: AChannelManager>( + cm: &'a C, f: F, + ) -> PersistenceNotifierGuard<'a, impl FnOnce() -> NotifyOption> { + let read_guard = cm.get_cm().total_consistency_lock.read().unwrap(); + let force_notify = cm.get_cm().process_background_events(); + + PersistenceNotifierGuard { + event_persist_notifier: &cm.get_cm().event_persist_notifier, + needs_persist_flag: &cm.get_cm().needs_persist_flag, + should_persist: Some(move || { + f(); + force_notify + }), + _read_guard: read_guard, + } + } + fn optionally_notify<F: FnOnce() -> NotifyOption, C: AChannelManager>( cm: &'a C, persist_check: F, ) -> PersistenceNotifierGuard<'a, impl FnOnce() -> NotifyOption> { @@ -3034,7 +3324,10 @@ const _CHECK_CLTV_EXPIRY_OFFCHAIN: () = assert!( ); /// The number of ticks of [`ChannelManager::timer_tick_occurred`] until expiry of incomplete MPPs +#[cfg(not(any(fuzzing, test, feature = "_test_utils")))] pub(crate) const MPP_TIMEOUT_TICKS: u8 = 3; +#[cfg(any(fuzzing, test, feature = "_test_utils"))] +pub(crate) const MPP_TIMEOUT_TICKS: u8 = 1; /// The number of ticks of [`ChannelManager::timer_tick_occurred`] where a peer is disconnected /// until we mark the channel disabled and gossip the update. @@ -3063,7 +3356,7 @@ const MAX_PEER_STORAGE_SIZE: usize = 1024; /// The maximum number of peers which we do not have a (funded) channel with. Once we reach this /// many peers we reject new (inbound) connections. -const MAX_NO_CHANNEL_PEERS: usize = 250; +const MAX_NO_CHANNEL_PEERS: usize = 2500; /// Used by [`ChannelManager::list_recent_payments`] to express the status of recent payments. /// These include payments that have yet to find a successful path, or have unresolved HTLCs. @@ -3086,9 +3379,15 @@ pub enum RecentPaymentDetails { /// Hash of the payment that is currently being sent but has yet to be fulfilled or /// abandoned. payment_hash: PaymentHash, - /// Total amount (in msat, excluding fees) across all paths for this payment, + /// Total amount (excluding fees) across all paths for this payment, /// not just the amount currently inflight. total_msat: u64, + /// Total routing fees of the HTLCs currently in-flight for this payment. + /// + /// `None` for payments serialized by LDK versions prior to 0.0.103. + pending_fee_msat: Option<u64>, + /// Whether this payment is a liquidity probe. + is_probe: bool, }, /// When a pending payment is fulfilled, we continue tracking it until all pending HTLCs have /// been resolved. Upon receiving [`Event::PaymentSent`], we delay for a few minutes before the @@ -3103,6 +3402,13 @@ pub enum RecentPaymentDetails { /// Hash of the payment that was claimed. `None` for serializations of [`ChannelManager`] /// made before LDK version 0.0.104. payment_hash: Option<PaymentHash>, + /// Total routing fees paid for this payment, as also reported via the `fee_paid_msat` + /// field of [`Event::PaymentSent`]. + /// + /// `None` for payments serialized by LDK versions prior to 0.3.0. + /// + /// [`Event::PaymentSent`]: events::Event::PaymentSent + fee_paid_msat: Option<u64>, }, /// After a payment's retries are exhausted per the provided [`Retry`], or it is explicitly /// abandoned via [`ChannelManager::abandon_payment`], it is marked as abandoned until all @@ -3116,6 +3422,8 @@ pub enum RecentPaymentDetails { payment_id: PaymentId, /// Hash of the payment that we have given up trying to send. payment_hash: PaymentHash, + /// Whether this payment is a liquidity probe. + is_probe: bool, }, } @@ -3304,8 +3612,12 @@ macro_rules! process_events_body { // TODO: This behavior should be documented. It's unintuitive that we query // ChannelMonitors when clearing other events. - if $self.process_pending_monitor_events() { - result = NotifyOption::DoPersist; + match $self.process_pending_monitor_events() { + NotifyOption::DoPersist => result = NotifyOption::DoPersist, + NotifyOption::SkipPersistHandleEvents + if result == NotifyOption::SkipPersistNoEvents => + result = NotifyOption::SkipPersistHandleEvents, + _ => {}, } } @@ -3391,6 +3703,50 @@ fn create_htlc_intercepted_event( }) } +/// Sets the features of the accepted channel in [`ChannelManager::accept_inbound_channel_from_trusted_peer`] +#[derive(Clone, Copy)] +pub enum TrustedChannelFeatures { + /// Accepts the incoming channel and (if the counterparty agrees), enables forwarding of payments immediately. + /// + /// This fully trusts that the counterparty has honestly and correctly constructed the funding transaction and + /// blindly assumes that it will eventually confirm. + /// + /// If it does not confirm before we decide to close the channel, or if the funding transaction + /// does not pay to the correct script the correct amount, *you will lose funds*. + ZeroConf, + /// Accepts the incoming channel and sets the reserve the counterparty must keep at all times in the channel to + /// zero. + /// + /// This allows the counterparty to spend their entire channel balance, and attempt to force-close the channel + /// with a revoked commitment transaction *for free*. + /// + /// Note that there is no guarantee that the counterparty accepts such a channel themselves. + /// + /// The zero-reserve feature is not allowed on legacy / anchorless channels. + ZeroReserve, + /// Sets the combination of [`TrustedChannelFeatures::ZeroConf`] and [`TrustedChannelFeatures::ZeroReserve`] + ZeroConfZeroReserve, +} + +impl TrustedChannelFeatures { + /// True if and only if `ZeroConf` is set + pub fn is_0conf(&self) -> bool { + match self { + TrustedChannelFeatures::ZeroConf | TrustedChannelFeatures::ZeroConfZeroReserve => true, + TrustedChannelFeatures::ZeroReserve => false, + } + } + /// True if and only if `ZeroReserve` is set + pub fn is_0reserve(&self) -> bool { + match self { + TrustedChannelFeatures::ZeroReserve | TrustedChannelFeatures::ZeroConfZeroReserve => { + true + }, + TrustedChannelFeatures::ZeroConf => false, + } + } +} + impl< M: chain::Watch<SP::EcdsaSigner>, T: BroadcasterInterface, @@ -3419,7 +3775,7 @@ impl< /// /// [`block_connected`]: chain::Listen::block_connected /// [`blocks_disconnected`]: chain::Listen::blocks_disconnected - /// [`params.best_block.block_hash`]: chain::BestBlock::block_hash + /// [`params.best_block.block_hash`]: chain::BlockLocator::block_hash #[rustfmt::skip] pub fn new( fee_est: F, chain_monitor: M, tx_broadcaster: T, router: R, message_router: MR, logger: L, @@ -3457,6 +3813,7 @@ impl< forward_htlcs: Mutex::new(new_hash_map()), decode_update_add_htlcs: Mutex::new(new_hash_map()), claimable_payments: Mutex::new(ClaimablePayments { claimable_payments: new_hash_map(), pending_claiming_payments: new_hash_map() }), + awaiting_trampoline_forwards: Mutex::new(new_hash_map()), pending_intercepted_htlcs: Mutex::new(new_hash_map()), short_to_chan_info: FairRwLock::new(new_hash_map()), @@ -3473,8 +3830,8 @@ impl< per_peer_state: FairRwLock::new(new_hash_map()), - #[cfg(not(any(test, feature = "_externalize_tests")))] - monitor_update_type: AtomicUsize::new(0), + #[cfg(test)] + skip_monitor_update_assertion: AtomicBool::new(false), pending_events: Mutex::new(VecDeque::new()), pending_events_processor: AtomicBool::new(false), @@ -3495,9 +3852,6 @@ impl< signer_provider, logger, - - #[cfg(feature = "_test_utils")] - testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), } } @@ -3602,10 +3956,60 @@ impl< /// [`Event::FundingGenerationReady::user_channel_id`]: events::Event::FundingGenerationReady::user_channel_id /// [`Event::FundingGenerationReady::temporary_channel_id`]: events::Event::FundingGenerationReady::temporary_channel_id /// [`Event::ChannelClosed::channel_id`]: events::Event::ChannelClosed::channel_id - #[rustfmt::skip] - pub fn create_channel(&self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, user_channel_id: u128, temporary_channel_id: Option<ChannelId>, override_config: Option<UserConfig>) -> Result<ChannelId, APIError> { - if channel_value_satoshis < 1000 { - return Err(APIError::APIMisuseError { err: format!("Channel value must be at least 1000 satoshis. It was {}", channel_value_satoshis) }); + pub fn create_channel( + &self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, + user_channel_id: u128, temporary_channel_id: Option<ChannelId>, + override_config: Option<UserConfig>, + ) -> Result<ChannelId, APIError> { + self.create_channel_internal( + their_network_key, + channel_value_satoshis, + push_msat, + user_channel_id, + temporary_channel_id, + override_config, + None, + ) + } + + /// Creates a new outbound channel to the given remote node and with the given value. + /// + /// The only difference between this method and [`ChannelManager::create_channel`] is that this method sets + /// the reserve the counterparty must keep at all times in the channel to zero. This allows the counterparty to + /// spend their entire channel balance, and attempt to force-close the channel with a revoked commitment + /// transaction *for free*. + /// + /// Note that there is no guarantee that the counterparty accepts such a channel. + /// + /// The zero-reserve feature is not allowed on legacy / anchorless channels. + pub fn create_channel_to_trusted_peer_0reserve( + &self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, + user_channel_id: u128, temporary_channel_id: Option<ChannelId>, + override_config: Option<UserConfig>, + ) -> Result<ChannelId, APIError> { + self.create_channel_internal( + their_network_key, + channel_value_satoshis, + push_msat, + user_channel_id, + temporary_channel_id, + override_config, + Some(TrustedChannelFeatures::ZeroReserve), + ) + } + + fn create_channel_internal( + &self, their_network_key: PublicKey, channel_value_satoshis: u64, push_msat: u64, + user_channel_id: u128, temporary_channel_id: Option<ChannelId>, + override_config: Option<UserConfig>, + trusted_channel_features: Option<TrustedChannelFeatures>, + ) -> Result<ChannelId, APIError> { + if channel_value_satoshis < crate::ln::channel::MIN_CHANNEL_VALUE_SATOSHIS { + return Err(APIError::APIMisuseError { + err: format!( + "Channel value must be at least 1000 satoshis. It was {channel_value_satoshis}" + ), + }); } let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -3614,17 +4018,24 @@ impl< let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(&their_network_key) - .ok_or_else(|| APIError::APIMisuseError{ err: format!("Not connected to node: {}", their_network_key) })?; + let peer_state_mutex = per_peer_state.get(&their_network_key).ok_or_else(|| { + APIError::APIMisuseError { err: format!("Not connected to node: {their_network_key}") } + })?; let mut peer_state = peer_state_mutex.lock().unwrap(); if !peer_state.is_connected { - return Err(APIError::APIMisuseError{ err: format!("Not connected to node: {}", their_network_key) }); + return Err(APIError::APIMisuseError { + err: format!("Not connected to node: {their_network_key}"), + }); } if let Some(temporary_channel_id) = temporary_channel_id { if peer_state.channel_by_id.contains_key(&temporary_channel_id) { - return Err(APIError::APIMisuseError{ err: format!("Channel with temporary channel ID {} already exists!", temporary_channel_id)}); + return Err(APIError::APIMisuseError { + err: format!( + "Channel with temporary channel ID {temporary_channel_id} already exists!" + ), + }); } } @@ -3632,15 +4043,23 @@ impl< let outbound_scid_alias = self.create_and_insert_outbound_scid_alias(); let their_features = &peer_state.latest_features; let config = self.config.read().unwrap(); - let config = if let Some(config) = &override_config { - config - } else { - &*config - }; - match OutboundV1Channel::new(&self.fee_estimator, &self.entropy_source, &self.signer_provider, their_network_key, - their_features, channel_value_satoshis, push_msat, user_channel_id, config, - self.best_block.read().unwrap().height, outbound_scid_alias, temporary_channel_id, &self.logger) - { + let config = if let Some(config) = &override_config { config } else { &*config }; + match OutboundV1Channel::new( + &self.fee_estimator, + &self.entropy_source, + &self.signer_provider, + their_network_key, + their_features, + channel_value_satoshis, + push_msat, + user_channel_id, + config, + self.best_block.read().unwrap().height, + outbound_scid_alias, + temporary_channel_id, + &self.logger, + trusted_channel_features, + ) { Ok(res) => res, Err(e) => { self.outbound_scid_aliases.lock().unwrap().remove(&outbound_scid_alias); @@ -3660,14 +4079,15 @@ impl< panic!("RNG is bad???"); } }, - hash_map::Entry::Vacant(entry) => { entry.insert(Channel::from(channel)); } + hash_map::Entry::Vacant(entry) => { + entry.insert(Channel::from(channel)); + }, } if let Some(msg) = res { - peer_state.pending_msg_events.push(MessageSendEvent::SendOpenChannel { - node_id: their_network_key, - msg, - }); + peer_state + .pending_msg_events + .push(MessageSendEvent::SendOpenChannel { node_id: their_network_key, msg }); } Ok(temporary_channel_id) } @@ -3798,18 +4218,30 @@ impl< PendingOutboundPayment::StaticInvoiceReceived { .. } => { Some(RecentPaymentDetails::AwaitingInvoice { payment_id: *payment_id }) }, - PendingOutboundPayment::Retryable { payment_hash, total_msat, .. } => { + PendingOutboundPayment::Retryable { payment_hash, total_msat, pending_fee_msat, .. } => { + let is_probe = outbound_payment::payment_is_probe(payment_hash, payment_id, self.probing_cookie_secret); Some(RecentPaymentDetails::Pending { payment_id: *payment_id, payment_hash: *payment_hash, total_msat: *total_msat, + pending_fee_msat: *pending_fee_msat, + is_probe, }) }, PendingOutboundPayment::Abandoned { payment_hash, .. } => { - Some(RecentPaymentDetails::Abandoned { payment_id: *payment_id, payment_hash: *payment_hash }) + let is_probe = outbound_payment::payment_is_probe(payment_hash, payment_id, self.probing_cookie_secret); + Some(RecentPaymentDetails::Abandoned { + payment_id: *payment_id, + payment_hash: *payment_hash, + is_probe, + }) }, - PendingOutboundPayment::Fulfilled { payment_hash, .. } => { - Some(RecentPaymentDetails::Fulfilled { payment_id: *payment_id, payment_hash: *payment_hash }) + PendingOutboundPayment::Fulfilled { payment_hash, fee_paid_msat, .. } => { + Some(RecentPaymentDetails::Fulfilled { + payment_id: *payment_id, + payment_hash: *payment_hash, + fee_paid_msat: *fee_paid_msat, + }) }, PendingOutboundPayment::Legacy { .. } => None }) @@ -3829,8 +4261,9 @@ impl< { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| APIError::ChannelUnavailable { err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") })?; + let peer_state_mutex = per_peer_state + .get(counterparty_node_id) + .ok_or_else(|| APIError::no_such_peer(counterparty_node_id))?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -3846,15 +4279,40 @@ impl< if let Some(chan) = chan_entry.get_mut().as_funded_mut() { let funding_txo_opt = chan.funding.get_funding_txo(); let their_features = &peer_state.latest_features; - let (shutdown_msg, mut monitor_update_opt, htlcs) = chan.get_shutdown( - &self.signer_provider, - their_features, - target_feerate_sats_per_1000_weight, - override_shutdown_script, - &self.logger, - )?; + let (shutdown_msg, mut monitor_update_opt, htlcs, splice_funding_failed) = + chan.get_shutdown( + &self.signer_provider, + their_features, + target_feerate_sats_per_1000_weight, + override_shutdown_script, + &self.logger, + )?; failed_htlcs = htlcs; + if let Some(splice_funding_failed) = splice_funding_failed { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + let mut pending_events = self.pending_events.lock().unwrap(); + if let Some(funding_info) = funding_info { + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: *chan_id, + funding_info, + }, + None, + )); + } + pending_events.push_back(( + events::Event::SpliceNegotiationFailed { + channel_id: *chan_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: chan.context().get_user_id(), + contribution: Some(contribution), + reason: events::NegotiationFailureReason::ChannelClosing, + }, + None, + )); + } + // We can send the `shutdown` message before updating the `ChannelMonitor` // here as we don't need the monitor update to complete until we send a // `shutdown_signed`, which we'll delay if we're pending a monitor update. @@ -3881,7 +4339,7 @@ impl< ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } } } else { @@ -3894,12 +4352,7 @@ impl< } }, hash_map::Entry::Vacant(_) => { - return Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {} not found for the passed counterparty node_id {}", - chan_id, counterparty_node_id, - ), - }); + return Err(APIError::no_such_channel_for_peer(chan_id, counterparty_node_id)); }, } } @@ -3907,12 +4360,9 @@ impl< for htlc_source in failed_htlcs.drain(..) { let failure_reason = LocalHTLCFailureReason::ChannelClosed; let reason = HTLCFailReason::from_failure_code(failure_reason); - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(*counterparty_node_id), - channel_id: *chan_id, - }; let (source, hash) = htlc_source; - self.fail_htlc_backwards_internal(&source, &hash, &reason, receiver, None); + let failure_type = source.failure_type(*counterparty_node_id, *chan_id); + self.fail_htlc_backwards_internal(&source, &hash, &reason, failure_type, None); } let _ = self.handle_error(shutdown_result, *counterparty_node_id); @@ -4018,7 +4468,7 @@ impl< ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } return; } else { @@ -4074,11 +4524,8 @@ impl< let (source, payment_hash, counterparty_node_id, channel_id) = htlc_source; let failure_reason = LocalHTLCFailureReason::ChannelClosed; let reason = HTLCFailReason::from_failure_code(failure_reason); - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id), - channel_id, - }; - self.fail_htlc_backwards_internal(&source, &payment_hash, &reason, receiver, None); + let failure_type = source.failure_type(counterparty_node_id, channel_id); + self.fail_htlc_backwards_internal(&source, &payment_hash, &reason, failure_type, None); } if let Some((_, funding_txo, _channel_id, monitor_update)) = shutdown_res.monitor_update { debug_assert!(false, "This should have been handled in `convert_channel_err`"); @@ -4096,7 +4543,7 @@ impl< // TODO: If we do the `in_flight_monitor_updates.is_empty()` check in // `convert_channel_err` we can skip the locks here. if shutdown_res.channel_funding_txo.is_some() { - self.channel_monitor_updated( + let _ = self.channel_monitor_updated( &shutdown_res.channel_id, None, &shutdown_res.counterparty_node_id, @@ -4150,15 +4597,23 @@ impl< )); if let Some(splice_funding_failed) = shutdown_res.splice_funding_failed.take() { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + if let Some(funding_info) = funding_info { + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: shutdown_res.channel_id, + funding_info, + }, + None, + )); + } pending_events.push_back(( - events::Event::SpliceFailed { + events::Event::SpliceNegotiationFailed { channel_id: shutdown_res.channel_id, counterparty_node_id: shutdown_res.counterparty_node_id, user_channel_id: shutdown_res.user_channel_id, - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + contribution: Some(contribution), + reason: events::NegotiationFailureReason::ChannelClosing, }, None, )); @@ -4194,11 +4649,7 @@ impl< ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = - per_peer_state.get(peer_node_id).ok_or_else(|| APIError::ChannelUnavailable { - err: format!( - "Can't find a peer matching the passed counterparty node_id {peer_node_id}", - ), - })?; + per_peer_state.get(peer_node_id).ok_or_else(|| APIError::no_such_peer(peer_node_id))?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; let logger = WithContext::from(&self.logger, Some(*peer_node_id), Some(*channel_id), None); @@ -4242,11 +4693,7 @@ impl< // events anyway. Ok(()) } else { - Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {channel_id} not found for the passed counterparty node_id {peer_node_id}", - ), - }) + Err(APIError::no_such_channel_for_peer(channel_id, peer_node_id)) } } @@ -4305,6 +4752,7 @@ impl< internal.map_err(|err_internal| { let mut msg_event = None; + let needs_holding_cell_release = err_internal.needs_holding_cell_release(); if let Some((shutdown_res, update_option)) = err_internal.shutdown_finish { let counterparty_node_id = shutdown_res.counterparty_node_id; @@ -4345,15 +4793,25 @@ impl< }); } - if let Some(msg_event) = msg_event { + let mut holding_cell_res = None; + if msg_event.is_some() || needs_holding_cell_release { let per_peer_state = self.per_peer_state.read().unwrap(); if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { let mut peer_state = peer_state_mutex.lock().unwrap(); - if peer_state.is_connected { - peer_state.pending_msg_events.push(msg_event); + if let Some(msg_event) = msg_event { + if peer_state.is_connected { + peer_state.pending_msg_events.push(msg_event); + } } + // We need to enqueue the `tx_abort` in `pending_msg_events` above before we + // enqueue any commitment updates generated by freeing holding cell HTLCs. + holding_cell_res = needs_holding_cell_release + .then(|| self.check_free_peer_holding_cells(&mut peer_state)); } } + if let Some(res) = holding_cell_res { + self.handle_holding_cell_free_result(res); + } // Return error in case higher-API need one err_internal.err @@ -4530,8 +4988,7 @@ impl< } /// Initiate a splice in order to add value to (splice-in) or remove value from (splice-out) - /// the channel. This will spend the channel's funding transaction output, effectively replacing - /// it with a new one. + /// the channel, or to RBF a pending splice transaction. /// /// # Required Feature Flags /// @@ -4539,62 +4996,23 @@ impl< /// channel (no matter the type) can be spliced, as long as the counterparty is currently /// connected. /// - /// # Arguments - /// - /// Provide a `contribution` to determine if value is spliced in or out. The splice initiator is - /// responsible for paying fees for common fields, shared inputs, and shared outputs along with - /// any contributed inputs and outputs. Fees are determined using `funding_feerate_per_kw` and - /// must be covered by the supplied inputs for splice-in or the channel balance for splice-out. - /// - /// An optional `locktime` for the funding transaction may be specified. If not given, the - /// current best block height is used. - /// - /// # Events - /// - /// Once the funding transaction has been constructed, an [`Event::SplicePending`] will be - /// emitted. At this point, any inputs contributed to the splice can only be re-spent if an - /// [`Event::DiscardFunding`] is seen. - /// - /// After initial signatures have been exchanged, [`Event::FundingTransactionReadyForSigning`] - /// will be generated and [`ChannelManager::funding_transaction_signed`] should be called. + /// # Return Value /// - /// If any failures occur while negotiating the funding transaction, an [`Event::SpliceFailed`] - /// will be emitted. Any contributed inputs no longer used will be included here and thus can - /// be re-spent. - /// - /// Once the splice has been locked by both counterparties, an [`Event::ChannelReady`] will be - /// emitted with the new funding output. At this point, a new splice can be negotiated by - /// calling `splice_channel` again on this channel. + /// Returns a [`FundingTemplate`] which should be used to obtain a [`FundingContribution`] + /// to pass to [`ChannelManager::funding_contributed`]. If a splice has been negotiated but + /// not yet locked, it can be replaced with a higher feerate transaction to speed up + /// confirmation via Replace By Fee (RBF). See [`FundingTemplate`] for details on building + /// a fresh contribution or reusing a prior one for RBF. #[rustfmt::skip] pub fn splice_channel( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option<u32>, - ) -> Result<(), APIError> { - let mut res = Ok(()); - PersistenceNotifierGuard::optionally_notify(self, || { - let result = self.internal_splice_channel( - channel_id, counterparty_node_id, contribution, funding_feerate_per_kw, locktime - ); - res = result; - match res { - Ok(_) => NotifyOption::DoPersist, - Err(_) => NotifyOption::SkipPersistNoEvents, - } - }); - res - } - - fn internal_splice_channel( - &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - contribution: SpliceContribution, funding_feerate_per_kw: u32, locktime: Option<u32>, - ) -> Result<(), APIError> { + ) -> Result<FundingTemplate, APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = match per_peer_state.get(counterparty_node_id).ok_or_else(|| { - APIError::ChannelUnavailable { - err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - } - }) { + let peer_state_mutex = match per_peer_state + .get(counterparty_node_id) + .ok_or_else(|| APIError::no_such_peer(counterparty_node_id)) + { Ok(p) => p, Err(e) => return Err(e), }; @@ -4613,23 +5031,9 @@ impl< // Look for the channel match peer_state.channel_by_id.entry(*channel_id) { - hash_map::Entry::Occupied(mut chan_phase_entry) => { - let locktime = locktime.unwrap_or_else(|| self.current_best_block().height); - if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let msg_opt = chan.splice_channel( - contribution, - funding_feerate_per_kw, - locktime, - &&logger, - )?; - if let Some(msg) = msg_opt { - peer_state.pending_msg_events.push(MessageSendEvent::SendStfu { - node_id: *counterparty_node_id, - msg, - }); - } - Ok(()) + hash_map::Entry::Occupied(chan_phase_entry) => { + if let Some(chan) = chan_phase_entry.get().as_funded() { + chan.splice_channel() } else { Err(APIError::ChannelUnavailable { err: format!( @@ -4639,104 +5043,90 @@ impl< }) } }, - hash_map::Entry::Vacant(_) => Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {} not found for the passed counterparty node_id {}", - channel_id, counterparty_node_id, - ), - }), + hash_map::Entry::Vacant(_) => { + Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)) + }, } } - #[cfg(test)] - pub(crate) fn abandon_splice( - &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, - ) -> Result<(), APIError> { - let mut res = Ok(()); - PersistenceNotifierGuard::optionally_notify(self, || { - let result = self.internal_abandon_splice(channel_id, counterparty_node_id); - res = result; - match res { - Ok(_) => NotifyOption::SkipPersistHandleEvents, - Err(_) => NotifyOption::SkipPersistNoEvents, - } - }); - res - } - - #[cfg(test)] - fn internal_abandon_splice( + /// Cancels an in-flight [`FundingContribution`]. + /// + /// This is primarily useful after receiving an [`Event::FundingTransactionReadyForSigning`] for + /// a [`FundingContribution`] you no longer wish to proceed with. This may be called for any + /// pending [`FundingContribution`] after its corresponding + /// [`ChannelManager::funding_contributed`] call up until + /// [`ChannelManager::funding_transaction_signed`]. + /// + /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect + /// `counterparty_node_id` is provided, or [`APIMisuseError`] otherwise with the error details. + /// + /// [`Event::FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning + /// [`ChannelUnavailable`]: APIError::ChannelUnavailable + /// [`APIMisuseError`]: APIError::APIMisuseError + pub fn cancel_funding_contributed( &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, ) -> Result<(), APIError> { - let per_peer_state = self.per_peer_state.read().unwrap(); - - let peer_state_mutex = match per_peer_state.get(counterparty_node_id).ok_or_else(|| { - APIError::ChannelUnavailable { - err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - } - }) { - Ok(p) => p, - Err(e) => return Err(e), - }; - - let mut peer_state_lock = peer_state_mutex.lock().unwrap(); - let peer_state = &mut *peer_state_lock; - - // Look for the channel - match peer_state.channel_by_id.entry(*channel_id) { - hash_map::Entry::Occupied(mut chan_phase_entry) => { - if !chan_phase_entry.get().context().is_connected() { - // TODO: We should probably support this, but right now `splice_channel` refuses when - // the peer is disconnected, so we just check it here. - return Err(APIError::ChannelUnavailable { - err: "Cannot abandon splice while peer is disconnected".to_owned(), - }); - } - - if let Some(chan) = chan_phase_entry.get_mut().as_funded_mut() { - let (tx_abort, splice_funding_failed) = chan.abandon_splice()?; - - peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort { - node_id: *counterparty_node_id, - msg: tx_abort, - }); + let mut result = Ok(()); + PersistenceNotifierGuard::manually_notify(self, || { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = match per_peer_state + .get(counterparty_node_id) + .ok_or_else(|| APIError::no_such_peer(counterparty_node_id)) + { + Ok(p) => p, + Err(e) => { + result = Err(e); + return; + }, + }; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; - if let Some(splice_funding_failed) = splice_funding_failed { - let pending_events = &mut self.pending_events.lock().unwrap(); - pending_events.push_back(( - events::Event::SpliceFailed { - channel_id: *channel_id, - counterparty_node_id: *counterparty_node_id, - user_channel_id: chan.context.get_user_id(), - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + match peer_state.channel_by_id.entry(*channel_id) { + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(channel) = chan_entry.get_mut().as_funded_mut() { + let err = match channel.cancel_funding_contributed() { + Ok(v) => v, + Err(e) => { + result = Err(e); + return; }, - None, - )); - } + }; + let user_channel_id = channel.context().get_user_id(); + mem::drop(peer_state_lock); + mem::drop(per_peer_state); - Ok(()) - } else { - Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {} is not funded, cannot abandon splice", - channel_id - ), - }) - } - }, - hash_map::Entry::Vacant(_) => Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {} not found for the passed counterparty node_id {}", - channel_id, counterparty_node_id, - ), - }), - } + let err = self.handle_interactive_tx_msg_err( + err, + *channel_id, + counterparty_node_id, + user_channel_id, + ); + let _ = self.handle_error(Err::<(), _>(err), *counterparty_node_id); + self.event_persist_notifier.notify(); + } else { + result = Err(APIError::ChannelUnavailable { + err: format!( + "Channel with id {} is not funded, cannot cancel splice", + channel_id + ), + }); + return; + } + }, + hash_map::Entry::Vacant(_) => { + result = + Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)); + return; + }, + } + }); + result } - fn forward_needs_intercept_to_known_chan(&self, outbound_chan: &FundedChannel<SP>) -> bool { + fn forward_needs_intercept_to_known_chan( + &self, prev_chan_public: bool, outbound_chan: &FundedChannel<SP>, + ) -> bool { let intercept_flags = self.config.read().unwrap().htlc_interception_flags; if !outbound_chan.context.should_announce() { if outbound_chan.context.is_connected() { @@ -4753,6 +5143,23 @@ impl< return true; } } + if prev_chan_public { + if outbound_chan.context.should_announce() { + if intercept_flags & (HTLCInterceptionFlags::FromPublicToPublicChannels as u8) != 0 + { + return true; + } + } else { + if intercept_flags & (HTLCInterceptionFlags::FromPublicToPrivateChannels as u8) != 0 + { + return true; + } + } + } else { + if intercept_flags & (HTLCInterceptionFlags::FromPrivateChannels as u8) != 0 { + return true; + } + } false } @@ -4846,8 +5253,9 @@ impl< } fn can_forward_htlc_should_intercept( - &self, msg: &msgs::UpdateAddHTLC, next_hop: &NextPacketDetails, + &self, msg: &msgs::UpdateAddHTLC, prev_chan_public: bool, next_hop: &NextPacketDetails, ) -> Result<bool, LocalHTLCFailureReason> { + let cur_height = self.best_block.read().unwrap().height + 1; let outgoing_scid = match next_hop.outgoing_connector { HopConnector::ShortChannelId(scid) => scid, HopConnector::Dummy => { @@ -4855,8 +5263,34 @@ impl< debug_assert!(false, "Dummy hop reached HTLC handling."); return Err(LocalHTLCFailureReason::InvalidOnionPayload); }, + // We can't make forwarding checks on trampoline forwards where we don't know the + // outgoing channel on receipt of the incoming htlc. Our trampoline logic will check + // our required delta and fee later on, so here we just check that the forwarding node + // did not "skim" off some of the sender's intended fee/cltv. HopConnector::Trampoline(_) => { - return Err(LocalHTLCFailureReason::InvalidTrampolineForward); + // We do not yet support reloading our trampoline HTLCs on restart, so we just + // fail them for now (except in tests). + #[cfg(not(test))] + { + return Err(LocalHTLCFailureReason::InvalidTrampolineForward); + } + + #[cfg(test)] + { + if msg.amount_msat < next_hop.outgoing_amt_msat { + return Err(LocalHTLCFailureReason::FeeInsufficient); + } + + check_incoming_htlc_cltv( + cur_height, + next_hop.outgoing_cltv_value, + msg.cltv_expiry, + 0, + )?; + + // TODO: add interception flag specifically for trampoline + return Ok(false); + } }, }; // TODO: We do the fake SCID namespace check a bunch of times here (and indirectly via @@ -4865,7 +5299,7 @@ impl< // times we do it. let intercept = match self.do_funded_channel_callback(outgoing_scid, |chan: &mut FundedChannel<SP>| { - let intercept = self.forward_needs_intercept_to_known_chan(chan); + let intercept = self.forward_needs_intercept_to_known_chan(prev_chan_public, chan); self.can_forward_htlc_to_outgoing_channel(chan, msg, next_hop, intercept)?; Ok(intercept) }) { @@ -4895,9 +5329,12 @@ impl< }, }; - let cur_height = self.best_block.read().unwrap().height + 1; - check_incoming_htlc_cltv(cur_height, next_hop.outgoing_cltv_value, msg.cltv_expiry)?; - + check_incoming_htlc_cltv( + cur_height, + next_hop.outgoing_cltv_value, + msg.cltv_expiry, + MIN_CLTV_EXPIRY_DELTA, + )?; Ok(intercept) } @@ -5119,15 +5556,14 @@ impl< #[cfg(any(test, feature = "_externalize_tests"))] pub(crate) fn test_send_payment_along_path( &self, path: &Path, payment_hash: &PaymentHash, recipient_onion: RecipientOnionFields, - total_value: u64, cur_height: u32, payment_id: PaymentId, - keysend_preimage: &Option<PaymentPreimage>, session_priv_bytes: [u8; 32], + cur_height: u32, payment_id: PaymentId, keysend_preimage: &Option<PaymentPreimage>, + session_priv_bytes: [u8; 32], ) -> Result<(), APIError> { let _lck = self.total_consistency_lock.read().unwrap(); self.send_payment_along_path(SendAlongPathArgs { path, payment_hash, recipient_onion: &recipient_onion, - total_value, cur_height, payment_id, keysend_preimage, @@ -5143,7 +5579,6 @@ impl< path, payment_hash, recipient_onion, - total_value, cur_height, payment_id, keysend_preimage, @@ -5168,7 +5603,6 @@ impl< &self.secp_ctx, &path, &session_priv, - total_value, recipient_onion, cur_height, payment_hash, @@ -5254,7 +5688,7 @@ impl< if let Some(data) = completion_data { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } if !update_completed { // Note that MonitorUpdateInProgress here indicates (per function @@ -5296,30 +5730,32 @@ impl< /// /// LDK will not automatically retry this payment, though it may be manually re-sent after an /// [`Event::PaymentFailed`] is generated. - #[rustfmt::skip] pub fn send_payment_with_route( - &self, mut route: Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, - payment_id: PaymentId + &self, route: Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, + payment_id: PaymentId, ) -> Result<(), RetryableSendFailure> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - let route_params = route.route_params.clone().unwrap_or_else(|| { - // Create a dummy route params since they're a required parameter but unused in this case - let (payee_node_id, cltv_delta) = route.paths.first() - .and_then(|path| path.hops.last().map(|hop| (hop.pubkey, hop.cltv_expiry_delta as u32))) - .unwrap_or_else(|| (PublicKey::from_slice(&[2; 32]).unwrap(), MIN_FINAL_CLTV_EXPIRY_DELTA as u32)); - let dummy_payment_params = PaymentParameters::from_node_id(payee_node_id, cltv_delta); - RouteParameters::from_payment_params_and_value(dummy_payment_params, route.get_total_amount()) - }); - if route.route_params.is_none() { route.route_params = Some(route_params.clone()); } + let route_params = route.route_params.clone(); let router = FixedRouter::new(route); let logger = WithContext::for_payment(&self.logger, None, None, Some(payment_hash), payment_id); - self.pending_outbound_payments - .send_payment(payment_hash, recipient_onion, payment_id, Retry::Attempts(0), - route_params, &&router, self.list_usable_channels(), || self.compute_inflight_htlcs(), - &self.entropy_source, &self.node_signer, best_block_height, - &self.pending_events, |args| self.send_payment_along_path(args), &logger) + self.pending_outbound_payments.send_payment( + payment_hash, + recipient_onion, + payment_id, + Retry::Attempts(0), + route_params, + &&router, + self.list_usable_channels(), + || self.compute_inflight_htlcs(), + &self.entropy_source, + &self.node_signer, + best_block_height, + &self.pending_events, + |args| self.send_payment_along_path(args), + &logger, + ) } /// Sends a payment to the route found using the provided [`RouteParameters`], retrying failed @@ -5387,7 +5823,7 @@ impl< pub(super) fn test_send_payment_internal( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, - recv_value_msat: Option<u64>, onion_session_privs: Vec<[u8; 32]>, + onion_session_privs: Vec<[u8; 32]>, ) -> Result<(), PaymentSendFailure> { let best_block_height = self.best_block.read().unwrap().height; let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -5397,7 +5833,6 @@ impl< recipient_onion, keysend_preimage, payment_id, - recv_value_msat, onion_session_privs, &self.node_signer, best_block_height, @@ -5442,22 +5877,51 @@ impl< self.pending_outbound_payments.test_set_payment_metadata(payment_id, new_payment_metadata); } + #[cfg(test)] + pub(super) fn test_handle_trampoline_htlc( + &self, mpp_part: MppPart, onion_fields: RecipientOnionFields, payment_hash: PaymentHash, + next_hop_info: NextTrampolineHopInfo, next_node_id: PublicKey, + ) -> Result<(), (HTLCSource, onion_utils::HTLCFailReason)> { + self.handle_trampoline_htlc( + mpp_part, + onion_fields, + payment_hash, + next_hop_info, + next_node_id, + ) + } + /// Pays a [`Bolt11Invoice`] associated with the `payment_id`. See [`Self::send_payment`] for more info. /// /// # Payment Id /// The invoice's `payment_hash().0` serves as a reliable choice for the `payment_id`. /// /// # Handling Invoice Amounts - /// Some invoices include a specific amount, while others require you to specify one. - /// - If the invoice **includes** an amount, user may provide an amount greater or equal to it - /// to allow for overpayments. - /// - If the invoice **doesn't include** an amount, you'll need to specify `amount_msats`. + /// Some invoices require a specific minimum amount (which can be fetched with + /// [`Bolt11Invoice::amount_milli_satoshis`]) while others allow you to pay any amount. + /// + /// - If the invoice **includes** an amount, `amount_msats` may be `None` to pay exactly + /// [`Bolt11Invoice::amount_milli_satoshis`] or may be `Some` with a value greater than or + /// equal to the [`Bolt11Invoice::amount_milli_satoshis`] to allow for deliberate overpayment + /// (e.g. for "tips"). + /// - If the invoice **doesn't include** an amount, `amount_msats` must be `Some`. + /// + /// In the special case that + /// [`OptionalBolt11PaymentParams::declared_total_mpp_value_msat_override`] is set, + /// `amount_msats` may be `Some` and lower than [`Bolt11Invoice::amount_milli_satoshis`]. See + /// the parameter for more details. /// /// If these conditions aren’t met, the function will return [`Bolt11PaymentError::InvalidAmount`]. /// /// # Custom Routing Parameters /// Users can customize routing parameters via [`RouteParametersConfig`]. /// To use default settings, call the function with [`RouteParametersConfig::default`]. + /// + /// In general, you should use the + /// [`bitcoin-payment-instructions` crate](https://docs.rs/bitcoin-payment-instructions/) to + /// resolve payment instructions strings (e.g. from QR codes, link opens, pasted instructions, + /// or typed instructions) into payment instructions and use this when the instructions resolve + /// to a BOLT 11 invoice. pub fn pay_for_bolt11_invoice( &self, invoice: &Bolt11Invoice, payment_id: PaymentId, amount_msats: Option<u64>, optional_params: OptionalBolt11PaymentParams, @@ -5950,12 +6414,12 @@ impl< /// which checks the correctness of the funding transaction given the associated channel. #[rustfmt::skip] fn funding_transaction_generated_intern<FundingOutput: FnMut(&OutboundV1Channel<SP>) -> Result<OutPoint, &'static str>>( - &self, temporary_channel_id: ChannelId, counterparty_node_id: PublicKey, funding_transaction: Transaction, is_batch_funding: bool, - mut find_funding_output: FundingOutput, is_manual_broadcast: bool, - ) -> Result<(), APIError> { + &self, temporary_channel_id: ChannelId, counterparty_node_id: PublicKey, funding_transaction: Transaction, is_batch_funding: bool, + mut find_funding_output: FundingOutput, is_manual_broadcast: bool, + ) -> Result<(), APIError> { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(&counterparty_node_id) - .ok_or_else(|| APIError::ChannelUnavailable { err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") })?; + .ok_or_else(|| APIError::no_such_peer(&counterparty_node_id))?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -6345,67 +6809,281 @@ impl< result } - /// Handles a signed funding transaction generated by interactive transaction construction and - /// provided by the client. Should only be called in response to a [`FundingTransactionReadyForSigning`] - /// event. + /// Emits events for a [`QuiescentError`], if applicable. + fn handle_quiescent_error( + &self, channel_id: ChannelId, counterparty_node_id: PublicKey, user_channel_id: u128, + error: QuiescentError, + ) { + match error { + QuiescentError::DoNothing => {}, + QuiescentError::DiscardFunding { inputs, outputs } => { + if !inputs.is_empty() || !outputs.is_empty() { + self.pending_events.lock().unwrap().push_back(( + events::Event::DiscardFunding { + channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }, + None, + )); + } + }, + QuiescentError::FailSplice(splice_funding_failed, reason) => { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + let pending_events = &mut self.pending_events.lock().unwrap(); + if let Some(funding_info) = funding_info { + pending_events.push_back(( + events::Event::DiscardFunding { channel_id, funding_info }, + None, + )); + } + pending_events.push_back(( + events::Event::SpliceNegotiationFailed { + channel_id, + counterparty_node_id, + user_channel_id, + reason, + contribution: Some(contribution), + }, + None, + )); + }, + } + } + + /// Adds or removes funds from the given channel as specified by a [`FundingContribution`]. /// - /// Do NOT broadcast the funding transaction yourself. When we have safely received our - /// counterparty's signature(s) the funding transaction will automatically be broadcast via the - /// [`BroadcasterInterface`] provided when this `ChannelManager` was constructed. + /// Used after [`ChannelManager::splice_channel`] by constructing a [`FundingContribution`] + /// from the returned [`FundingTemplate`] and passing it here. /// - /// `SIGHASH_ALL` MUST be used for all signatures when providing signatures, otherwise your - /// funds can be held hostage! + /// # Arguments /// - /// LDK checks the following: - /// * Each input spends an output that is one of P2WPKH, P2WSH, or P2TR. - /// These were already checked by LDK when the inputs to be contributed were provided. - /// * All signatures use the `SIGHASH_ALL` sighash type. - /// * P2WPKH and P2TR key path spends are valid (verifies signatures) + /// An optional `locktime` for the funding transaction may be specified. If not given, the + /// current best block height is used. /// - /// NOTE: - /// * When checking P2WSH spends, LDK tries to decode 70-72 byte witness elements as ECDSA - /// signatures with a sighash flag. If the internal DER-decoding fails, then LDK just - /// assumes it wasn't a signature and carries with checks. If the element can be decoded - /// as an ECDSA signature, the the sighash flag must be `SIGHASH_ALL`. - /// * When checking P2TR script-path spends, LDK assumes all elements of exactly 65 bytes - /// with the last byte matching any valid sighash flag byte are schnorr signatures and checks - /// that the sighash type is `SIGHASH_ALL`. If the last byte is not any valid sighash flag, the - /// element is assumed not to be a signature and is ignored. Elements of 64 bytes are not - /// checked because if they were schnorr signatures then they would implicitly be `SIGHASH_DEFAULT` - /// which is an alias of `SIGHASH_ALL`. + /// # Fee Estimation + /// + /// The splice initiator is responsible for paying fees for common fields, shared inputs, and + /// shared outputs along with any contributed inputs and outputs. When building a + /// [`FundingContribution`], fees are estimated at `min_feerate` assuming initiator + /// responsibility. Contributions fall into two cases: + /// - **input-backed contributions**: when wallet inputs are selected, those inputs pay for both + /// the requested value added to the channel and any explicit withdrawal outputs. For + /// example, a 60,000 sat input might add 50,000 sat to the channel, pay a 2,000 sat fee, + /// and return 8,000 sat as change. A later RBF first tries to preserve that 50,000 sat + /// value added and cover any higher fee or newly requested withdrawal from the original + /// 10,000 sat fee buffer (2,000 sat fee + 8,000 sat change). If that buffer is not enough, + /// the prior contribution cannot be reused without selecting new wallet inputs. + /// - **input-less contributions**: when no wallet inputs are selected, fees and explicit + /// withdrawal outputs are paid from the channel balance. For example, a pure splice-out that + /// withdraws 20,000 sat from a 100,000 sat holder balance leaves up to 80,000 sat available + /// for fees. A later RBF keeps the 20,000 sat withdrawal only while that remaining balance + /// can still cover the re-estimated fee. + /// + /// If the counterparty also initiates a splice and wins the tie-break, they become the + /// initiator and choose the feerate. The fee is then re-estimated at the counterparty's + /// feerate for only our contributed inputs and outputs, which may be higher or lower than the + /// original estimate. The contribution is dropped and the splice proceeds without it when: + /// - the counterparty's feerate is below `min_feerate` + /// - the counterparty's feerate is above `max_feerate` and the re-estimated fee exceeds the + /// original fee estimate + /// - the re-estimated fee exceeds the *fee buffer* regardless of `max_feerate` + /// + /// The fee buffer is the maximum fee that can be accommodated: + /// - **input-backed contributions**: the original fee plus any change output value + /// - **input-less contributions**: the channel balance minus the withdrawal outputs + /// + /// # Events + /// + /// Calling this method will commence the process of creating a new funding transaction for the + /// channel. Once the funding transaction has been constructed, an [`Event::SpliceNegotiated`] + /// will be emitted if the negotiated transaction includes local inputs or outputs. At this + /// point, any inputs contributed to the splice can only be re-spent if an + /// [`Event::DiscardFunding`] is seen. + /// + /// If any failures occur while negotiating the funding transaction, an + /// [`Event::SpliceNegotiationFailed`] will be emitted. Any contributed inputs no longer used + /// will be included in an [`Event::DiscardFunding`] and thus can be re-spent. If a + /// [`FundingTemplate`] was obtained while a previous splice was still being negotiated, its + /// [`min_rbf_feerate`][FundingTemplate::min_rbf_feerate] may be stale after the failure. + /// Call [`ChannelManager::splice_channel`] again to get a fresh template. + /// + /// After initial signatures have been exchanged, [`Event::FundingTransactionReadyForSigning`] + /// will be generated and [`ChannelManager::funding_transaction_signed`] should be called. + /// + /// Once the splice has been locked by both counterparties, an [`Event::ChannelReady`] will be + /// emitted with the new funding output. At this point, a new (non-RBF) splice can be negotiated by + /// calling [`ChannelManager::splice_channel`] again on this channel. + /// + /// # Errors /// /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect /// `counterparty_node_id` is provided. /// /// Returns [`APIMisuseError`] when a channel is not in a state where it is expecting funding - /// signatures or if any of the checks described above fail. + /// contribution. /// - /// [`FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning /// [`ChannelUnavailable`]: APIError::ChannelUnavailable /// [`APIMisuseError`]: APIError::APIMisuseError - pub fn funding_transaction_signed( - &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, transaction: Transaction, + pub fn funding_contributed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + contribution: FundingContribution, locktime: Option<u32>, ) -> Result<(), APIError> { - let mut funding_tx_signed_result = Ok(()); - let mut monitor_update_result: Option< - Result<PostMonitorUpdateChanResume, MsgHandleErrInternal>, - > = None; - + let mut result = Ok(()); PersistenceNotifierGuard::optionally_notify(self, || { + let push_discard_funding = |contribution: FundingContribution| { + let (inputs, outputs) = contribution.into_contributed_inputs_and_outputs(); + self.pending_events.lock().unwrap().push_back(( + events::Event::DiscardFunding { + channel_id: *channel_id, + funding_info: FundingInfo::Contribution { inputs, outputs }, + }, + None, + )); + }; + let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id); if peer_state_mutex_opt.is_none() { - funding_tx_signed_result = Err(APIError::ChannelUnavailable { - err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") - }); + push_discard_funding(contribution); + result = Err(APIError::no_such_peer(counterparty_node_id)); return NotifyOption::SkipPersistNoEvents; } - let mut peer_state_lock = peer_state_mutex_opt.unwrap().lock().unwrap(); - let peer_state = &mut *peer_state_lock; + let mut peer_state = peer_state_mutex_opt.unwrap().lock().unwrap(); - match peer_state.channel_by_id.entry(*channel_id) { - hash_map::Entry::Occupied(mut chan_entry) => { + match peer_state.channel_by_id.get_mut(channel_id) { + Some(channel) => match channel.as_funded_mut() { + Some(chan) => { + let locktime = bitcoin::absolute::LockTime::from_consensus( + locktime.unwrap_or_else(|| self.current_best_block().height), + ); + let logger = WithChannelContext::from(&self.logger, chan.context(), None); + match chan.funding_contributed( + contribution, + locktime, + &self.fee_estimator, + &&logger, + ) { + Ok(msg_opt) => { + if let Some(msg) = msg_opt { + peer_state.pending_msg_events.push( + MessageSendEvent::SendStfu { + node_id: *counterparty_node_id, + msg, + }, + ); + } + }, + Err(e) => { + result = Err(APIError::APIMisuseError { + err: match &e { + QuiescentError::DoNothing => format!( + "Duplicate funding contribution for channel {}", + channel_id, + ), + QuiescentError::DiscardFunding { .. } => format!( + "Channel {} already has a pending funding contribution", + channel_id, + ), + QuiescentError::FailSplice(..) => format!( + "Channel {} cannot accept funding contribution", + channel_id, + ), + }, + }); + self.handle_quiescent_error( + *channel_id, + *counterparty_node_id, + channel.context().get_user_id(), + e, + ); + }, + } + + return NotifyOption::DoPersist; + }, + None => { + push_discard_funding(contribution); + result = Err(APIError::APIMisuseError { + err: format!( + "Channel with id {} not expecting funding contribution", + channel_id + ), + }); + return NotifyOption::SkipPersistNoEvents; + }, + }, + None => { + push_discard_funding(contribution); + result = + Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)); + return NotifyOption::SkipPersistNoEvents; + }, + } + }); + + result + } + + /// Handles a signed funding transaction generated by interactive transaction construction and + /// provided by the client. Should only be called in response to a [`FundingTransactionReadyForSigning`] + /// event. + /// + /// Do NOT broadcast the funding transaction yourself. When we have safely received our + /// counterparty's signature(s) the funding transaction will automatically be broadcast via the + /// [`BroadcasterInterface`] provided when this `ChannelManager` was constructed. + /// + /// `SIGHASH_ALL` MUST be used for all signatures when providing signatures, otherwise your + /// funds can be held hostage! + /// + /// LDK checks the following: + /// * Each input spends an output that is one of P2WPKH, P2WSH, or P2TR. + /// These were already checked by LDK when the inputs to be contributed were provided. + /// * All signatures use the `SIGHASH_ALL` sighash type. + /// * P2WPKH and P2TR key path spends are valid (verifies signatures) + /// + /// NOTE: + /// * When checking P2WSH spends, LDK tries to decode 70-72 byte witness elements as ECDSA + /// signatures with a sighash flag. If the internal DER-decoding fails, then LDK just + /// assumes it wasn't a signature and carries with checks. If the element can be decoded + /// as an ECDSA signature, the the sighash flag must be `SIGHASH_ALL`. + /// * When checking P2TR script-path spends, LDK assumes all elements of exactly 65 bytes + /// with the last byte matching any valid sighash flag byte are schnorr signatures and checks + /// that the sighash type is `SIGHASH_ALL`. If the last byte is not any valid sighash flag, the + /// element is assumed not to be a signature and is ignored. Elements of 64 bytes are not + /// checked because if they were schnorr signatures then they would implicitly be `SIGHASH_DEFAULT` + /// which is an alias of `SIGHASH_ALL`. + /// + /// Returns [`ChannelUnavailable`] when a channel is not found or an incorrect + /// `counterparty_node_id` is provided. + /// + /// Returns [`APIMisuseError`] when a channel is not in a state where it is expecting funding + /// signatures or if any of the checks described above fail. + /// + /// [`FundingTransactionReadyForSigning`]: events::Event::FundingTransactionReadyForSigning + /// [`ChannelUnavailable`]: APIError::ChannelUnavailable + /// [`APIMisuseError`]: APIError::APIMisuseError + pub fn funding_transaction_signed( + &self, channel_id: &ChannelId, counterparty_node_id: &PublicKey, transaction: Transaction, + ) -> Result<(), APIError> { + let mut funding_tx_signed_result = Ok(()); + let mut monitor_update_result: Option< + Result<PostMonitorUpdateChanResume, MsgHandleErrInternal>, + > = None; + + PersistenceNotifierGuard::optionally_notify(self, || { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id); + if peer_state_mutex_opt.is_none() { + funding_tx_signed_result = Err(APIError::no_such_peer(counterparty_node_id)); + return NotifyOption::SkipPersistNoEvents; + } + + let mut peer_state_lock = peer_state_mutex_opt.unwrap().lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + match peer_state.channel_by_id.entry(*channel_id) { + hash_map::Entry::Occupied(mut chan_entry) => { let txid = transaction.compute_txid(); let witnesses: Vec<_> = transaction .input @@ -6443,18 +7121,20 @@ impl< ); } if let Some(splice_negotiated) = splice_negotiated { - self.pending_events.lock().unwrap().push_back(( - events::Event::SplicePending { - channel_id: *channel_id, - counterparty_node_id: *counterparty_node_id, - user_channel_id: chan.context().get_user_id(), - new_funding_txo: splice_negotiated.funding_txo, - channel_type: splice_negotiated.channel_type, - new_funding_redeem_script: splice_negotiated - .funding_redeem_script, - }, - None, - )); + if splice_negotiated.has_local_contribution { + self.pending_events.lock().unwrap().push_back(( + events::Event::SpliceNegotiated { + channel_id: *channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: chan.context().get_user_id(), + new_funding_txo: splice_negotiated.funding_txo, + channel_type: splice_negotiated.channel_type, + new_funding_redeem_script: splice_negotiated + .funding_redeem_script, + }, + None, + )); + } } if chan.context().is_connected() { @@ -6535,12 +7215,8 @@ impl< } }, hash_map::Entry::Vacant(_) => { - funding_tx_signed_result = Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {} not found for the passed counterparty node_id {}", - channel_id, counterparty_node_id - ), - }); + funding_tx_signed_result = + Err(APIError::no_such_channel_for_peer(channel_id, counterparty_node_id)); return NotifyOption::SkipPersistNoEvents; }, } @@ -6551,7 +7227,7 @@ impl< if let Some(monitor_update_result) = monitor_update_result { match monitor_update_result { Ok(post_update_data) => { - self.handle_post_monitor_update_chan_resume(post_update_data); + let _ = self.handle_post_monitor_update_chan_resume(post_update_data); }, Err(_) => { let _ = self.handle_error(monitor_update_result, *counterparty_node_id); @@ -6623,15 +7299,16 @@ impl< let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| APIError::ChannelUnavailable { err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") })?; + .ok_or_else(|| APIError::no_such_peer(counterparty_node_id))?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; for channel_id in channel_ids { if !peer_state.has_channel(channel_id) { - return Err(APIError::ChannelUnavailable { - err: format!("Channel with id {} not found for the passed counterparty node_id {}", channel_id, counterparty_node_id), - }); + return Err(APIError::no_such_channel_for_peer( + channel_id, + counterparty_node_id, + )); }; } for channel_id in channel_ids { @@ -6726,12 +7403,9 @@ impl< let outbound_scid_alias = { let peer_state_lock = self.per_peer_state.read().unwrap(); - let peer_state_mutex = - peer_state_lock.get(&next_node_id).ok_or_else(|| APIError::ChannelUnavailable { - err: format!( - "Can't find a peer matching the passed counterparty node_id {next_node_id}" - ), - })?; + let peer_state_mutex = peer_state_lock + .get(&next_node_id) + .ok_or_else(|| APIError::no_such_peer(&next_node_id))?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.get(next_hop_channel_id) { @@ -6764,11 +7438,10 @@ impl< logger, "Channel not found when attempting to forward intercepted HTLC" ); - return Err(APIError::ChannelUnavailable { - err: format!( - "Channel with id {next_hop_channel_id} not found for the passed counterparty node_id {next_node_id}" - ), - }); + return Err(APIError::no_such_channel_for_peer( + next_hop_channel_id, + &next_node_id, + )); }, } }; @@ -6810,15 +7483,16 @@ impl< ..payment.forward_info }; - let mut per_source_pending_forward = [( - payment.prev_outbound_scid_alias, - payment.prev_counterparty_node_id, - payment.prev_funding_outpoint, - payment.prev_channel_id, - payment.prev_user_channel_id, - vec![(pending_htlc_info, payment.prev_htlc_id)], - )]; - self.forward_htlcs(&mut per_source_pending_forward); + let forward = [PendingAddHTLCInfo { + prev_outbound_scid_alias: payment.prev_outbound_scid_alias, + prev_htlc_id: payment.prev_htlc_id, + prev_counterparty_node_id: payment.prev_counterparty_node_id, + prev_channel_id: payment.prev_channel_id, + prev_funding_outpoint: payment.prev_funding_outpoint, + prev_user_channel_id: payment.prev_user_channel_id, + forward_info: pending_htlc_info, + }]; + self.forward_htlcs(forward); Ok(()) } @@ -6884,34 +7558,29 @@ impl< 'outer_loop: for (incoming_scid_alias, update_add_htlcs) in decode_update_add_htlcs { // If any decoded update_add_htlcs were processed, we need to persist. should_persist = true; - let incoming_channel_details_opt = self.do_funded_channel_callback( - incoming_scid_alias, - |chan: &mut FundedChannel<SP>| { - let counterparty_node_id = chan.context.get_counterparty_node_id(); - let channel_id = chan.context.channel_id(); - let funding_txo = chan.funding.get_funding_txo().unwrap(); - let user_channel_id = chan.context.get_user_id(); - let accept_underpaying_htlcs = chan.context.config().accept_underpaying_htlcs; - ( - counterparty_node_id, - channel_id, - funding_txo, - user_channel_id, - accept_underpaying_htlcs, - ) - }, - ); let ( incoming_counterparty_node_id, incoming_channel_id, incoming_funding_txo, incoming_user_channel_id, incoming_accept_underpaying_htlcs, - ) = if let Some(incoming_channel_details) = incoming_channel_details_opt { - incoming_channel_details - } else { + incoming_chan_is_public, + ) = match self.do_funded_channel_callback( + incoming_scid_alias, + |chan: &mut FundedChannel<SP>| { + ( + chan.context.get_counterparty_node_id(), + chan.context.channel_id(), + chan.funding.get_funding_txo().unwrap(), + chan.context.get_user_id(), + chan.context.config().accept_underpaying_htlcs, + chan.context.should_announce(), + ) + }, + ) { + Some(incoming_channel_details) => incoming_channel_details, // The incoming channel no longer exists, HTLCs should be resolved onchain instead. - continue; + None => continue, }; let mut htlc_forwards = Vec::new(); @@ -7031,9 +7700,11 @@ impl< // Now process the HTLC on the outgoing channel if it's a forward. let mut intercept_forward = false; if let Some(next_packet_details) = next_packet_details_opt.as_ref() { - match self - .can_forward_htlc_should_intercept(&update_add_htlc, next_packet_details) - { + match self.can_forward_htlc_should_intercept( + &update_add_htlc, + incoming_chan_is_public, + next_packet_details, + ) { Err(reason) => { fail_htlc_continue_to_next!(reason); }, @@ -7049,7 +7720,7 @@ impl< next_packet_details_opt.map(|d| d.next_packet_pubkey), ) { Ok(info) => { - let to_pending_add = |info| PendingAddHTLCInfo { + let pending_add = PendingAddHTLCInfo { prev_outbound_scid_alias: incoming_scid_alias, prev_counterparty_node_id: incoming_counterparty_node_id, prev_funding_outpoint: incoming_funding_txo, @@ -7071,7 +7742,7 @@ impl< Some(incoming_channel_id), Some(update_add_htlc.payment_hash), ); - if info.routing.should_hold_htlc() { + if pending_add.forward_info.routing.should_hold_htlc() { let mut held_htlcs = self.pending_intercepted_htlcs.lock().unwrap(); let intercept_id = intercept_id(); match held_htlcs.entry(intercept_id) { @@ -7080,7 +7751,6 @@ impl< logger, "Intercepted held HTLC with id {intercept_id}, holding until the recipient is online" ); - let pending_add = to_pending_add(info); entry.insert(pending_add); }, hash_map::Entry::Occupied(_) => { @@ -7097,7 +7767,6 @@ impl< self.pending_intercepted_htlcs.lock().unwrap(); match pending_intercepts.entry(intercept_id) { hash_map::Entry::Vacant(entry) => { - let pending_add = to_pending_add(info); if let Ok(intercept_ev) = create_htlc_intercepted_event(intercept_id, &pending_add) { @@ -7137,7 +7806,7 @@ impl< }, } } else { - htlc_forwards.push((info, update_add_htlc.htlc_id)) + htlc_forwards.push(pending_add); } }, Err(inbound_err) => { @@ -7157,15 +7826,7 @@ impl< // Process all of the forwards and failures for the channel in which the HTLCs were // proposed to as a batch. - let pending_forwards = ( - incoming_scid_alias, - incoming_counterparty_node_id, - incoming_funding_txo, - incoming_channel_id, - incoming_user_channel_id, - htlc_forwards, - ); - self.forward_htlcs(&mut [pending_forwards]); + self.forward_htlcs(htlc_forwards); for (htlc_fail, failure_type, failure_reason) in htlc_fails.drain(..) { let failure = match htlc_fail { HTLCFailureMsg::Relay(fail_htlc) => HTLCForwardInfo::FailHTLC { @@ -7188,7 +7849,7 @@ impl< .push(failure); self.pending_events.lock().unwrap().push_back(( events::Event::HTLCHandlingFailed { - prev_channel_id: incoming_channel_id, + prev_channel_ids: vec![incoming_channel_id], failure_type, failure_reason: Some(failure_reason), }, @@ -7259,7 +7920,7 @@ impl< let mut new_events = VecDeque::new(); let mut failed_forwards = Vec::new(); - let mut phantom_receives: Vec<PerSourcePendingForward> = Vec::new(); + let mut phantom_receives: Vec<PendingAddHTLCInfo> = Vec::new(); let mut forward_htlcs = new_hash_map(); mem::swap(&mut forward_htlcs, &mut self.forward_htlcs.lock().unwrap()); @@ -7306,7 +7967,7 @@ impl< None, ); } - self.forward_htlcs(&mut phantom_receives); + self.forward_htlcs(phantom_receives); if self.check_free_holding_cells() { should_persist = NotifyOption::DoPersist; @@ -7326,7 +7987,7 @@ impl< fn forwarding_channel_not_found( &self, forward_infos: impl Iterator<Item = HTLCForwardInfo>, short_chan_id: u64, forwarding_counterparty: Option<PublicKey>, failed_forwards: &mut Vec<FailedHTLCForward>, - phantom_receives: &mut Vec<PerSourcePendingForward>, + phantom_receives: &mut Vec<PendingAddHTLCInfo>, ) { for forward_info in forward_infos { match forward_info { @@ -7373,6 +8034,8 @@ impl< }; failed_forwards.push(( + // This can't be a trampoline payment because we don't process them + // as forwards (we're the last/"receiving" onion node). HTLCSource::PreviousHopData(prev_hop), payment_hash, HTLCFailReason::reason(reason, err_data), @@ -7394,7 +8057,7 @@ impl< &onion_packet.public_key.unwrap(), &onion_packet.hop_data, onion_packet.hmac, - payment_hash, + Some(payment_hash), None, &self.node_signer, ); @@ -7402,7 +8065,8 @@ impl< Ok(res) => res, Err(onion_utils::OnionDecodeErr::Malformed { err_msg, reason }) => { let sha256_of_onion = - Sha256::hash(&onion_packet.hop_data).to_byte_array(); + <Sha256 as CryptoHash>::hash(&onion_packet.hop_data) + .to_byte_array(); // In this scenario, the phantom would have sent us an // `update_fail_malformed_htlc`, meaning here we encrypt the error as // if it came from us (the second-to-last hop) but contains the sha256 @@ -7448,14 +8112,15 @@ impl< current_height, ); match create_res { - Ok(info) => phantom_receives.push(( + Ok(info) => phantom_receives.push(PendingAddHTLCInfo { + forward_info: info, prev_outbound_scid_alias, + prev_htlc_id, prev_counterparty_node_id, - prev_funding_outpoint, prev_channel_id, + prev_funding_outpoint, prev_user_channel_id, - vec![(info, prev_htlc_id)], - )), + }), Err(InboundHTLCErr { reason, err_data, msg }) => { failure_handler( msg, @@ -7482,6 +8147,10 @@ impl< continue; } } else { + debug_assert!( + false, + "We only expect to handle regular forwards in forwarding_channel_not_found" + ); let msg = format!("Unknown short channel id {} for forward HTLC", short_chan_id); failure_handler( @@ -7507,7 +8176,7 @@ impl< fn process_forward_htlcs( &self, short_chan_id: u64, pending_forwards: &mut Vec<HTLCForwardInfo>, failed_forwards: &mut Vec<FailedHTLCForward>, - phantom_receives: &mut Vec<PerSourcePendingForward>, + phantom_receives: &mut Vec<PendingAddHTLCInfo>, ) { let mut forwarding_counterparty = None; @@ -7592,7 +8261,18 @@ impl< .values_mut() .filter_map(Channel::as_funded_mut) .filter_map(|chan| { - let balances = chan.get_available_balances(&self.fee_estimator); + let balances_result = chan.get_available_balances(&self.fee_estimator); + let balances = balances_result.unwrap_or_else(|()| { + debug_assert!(false, "some channel balance has been overdrawn"); + crate::ln::channel::AvailableBalances { + inbound_capacity_msat: 0, + outbound_capacity_msat: 0, + next_outbound_htlc_limit_msat: 0, + next_outbound_htlc_minimum_msat: u64::MAX, + dust_exposure_msat: 0, + next_splice_out_maximum_sat: 0, + } + }); let is_in_range = (balances.next_outbound_htlc_minimum_msat ..=balances.next_outbound_htlc_limit_msat) .contains(&outgoing_amt_msat); @@ -7766,6 +8446,286 @@ impl< } } + // Checks whether an incoming HTLC can be added to an in-progress MPP payment, verifying onion + // field compatibility and that the total value is sensible. On success, the HTLC is added to + // the htlc claimable set and Ok(true) is returned if all MPP parts have arrived. + fn check_incoming_mpp_part<H: HasMppPart + Ord>( + &self, htlc_set: &mut Vec<H>, payment_onion_fields: &mut RecipientOnionFields, new_htlc: H, + mut onion_fields: RecipientOnionFields, payment_hash: PaymentHash, + ) -> Result<bool, ()> { + let onions_compatible = payment_onion_fields.check_merge(&mut onion_fields); + if onions_compatible.is_err() { + return Err(()); + } + let mut total_intended_recvd_value = new_htlc.mpp_part().sender_intended_value; + for htlc in htlc_set.iter() { + total_intended_recvd_value += htlc.mpp_part().sender_intended_value; + if total_intended_recvd_value >= msgs::MAX_VALUE_MSAT { + break; + } + } + let total_mpp_value = payment_onion_fields.total_mpp_amount_msat; + // The condition determining whether an MPP is complete must match exactly the condition + // used in `timer_tick_occurred` + if total_intended_recvd_value >= msgs::MAX_VALUE_MSAT { + return Err(()); + } else if total_intended_recvd_value - new_htlc.mpp_part().sender_intended_value + >= total_mpp_value + { + log_trace!( + self.logger, + "Failing HTLC with payment_hash {} as payment is already claimable", + &payment_hash + ); + return Err(()); + } else if total_intended_recvd_value >= total_mpp_value { + htlc_set.push(new_htlc); + let amount_msat = htlc_set.iter().map(|htlc| htlc.mpp_part().value).sum(); + htlc_set + .iter_mut() + .for_each(|htlc| htlc.mpp_part_mut().total_value_received = Some(amount_msat)); + htlc_set.sort(); + Ok(true) + } else { + // Nothing to do - we haven't reached the total payment value yet, wait until we + // receive more MPP parts. + htlc_set.push(new_htlc); + Ok(false) + } + } + + // Handles the addition of a HTLC associated with a payment we're receiving. + fn handle_claimable_htlc( + &self, purpose: events::PaymentPurpose, claimable_htlc: ClaimableHTLC, + onion_fields: RecipientOnionFields, payment_hash: PaymentHash, receiver_node_id: PublicKey, + new_events: &mut VecDeque<(Event, Option<EventCompletionAction>)>, + ) -> Result<(), ()> { + let mut claimable_payments = self.claimable_payments.lock().unwrap(); + if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { + return Err(()); + } + + // We should not fail if we're adding the first htlc to a ClaimablePayment (as our + // validation compares fields across parts, and our first part can't overflow maximum + // msats because each htlc's amount is individually validated - overflow is only possible + // with multiple parts). + let mut first_claimable_htlc = false; + let ref mut claimable_payment = + claimable_payments.claimable_payments.entry(payment_hash).or_insert_with(|| { + first_claimable_htlc = true; + ClaimablePayment { + purpose: purpose.clone(), + htlcs: Vec::new(), + onion_fields: onion_fields.clone(), + } + }); + + let is_keysend = purpose.is_keysend(); + if purpose != claimable_payment.purpose { + let log_keysend = |keysend| if keysend { "keysend" } else { "non-keysend" }; + log_trace!(self.logger, "Failing new {} HTLC with payment_hash {} as we already had an existing {} HTLC with the same payment hash", log_keysend(is_keysend), &payment_hash, log_keysend(!is_keysend)); + debug_assert!(!first_claimable_htlc); + return Err(()); + } + + let htlc_expiry = claimable_htlc.mpp_part.cltv_expiry; + match self.check_incoming_mpp_part( + &mut claimable_payment.htlcs, + &mut claimable_payment.onion_fields, + claimable_htlc, + onion_fields, + payment_hash, + ) { + Ok(true) => { + let counterparty_skimmed_fee_msat = + claimable_payment.total_counterparty_skimmed_msat(); + let amount_msat: u64 = + claimable_payment.htlcs.iter().map(|h| h.mpp_part.value).sum(); + let total_sender_intended: u64 = + claimable_payment.htlcs.iter().map(|h| h.mpp_part.sender_intended_value).sum(); + debug_assert!( + total_sender_intended.saturating_sub(amount_msat) + <= counterparty_skimmed_fee_msat + ); + let claim_deadline = Some( + match claimable_payment.htlcs.iter().map(|h| h.mpp_part.cltv_expiry).min() { + Some(claim_deadline) => claim_deadline, + None => { + debug_assert!(false, "no htlcs in completed claimable_payment"); + htlc_expiry + }, + } - HTLC_FAIL_BACK_BUFFER, + ); + new_events.push_back(( + events::Event::PaymentClaimable { + receiver_node_id: Some(receiver_node_id), + payment_hash, + purpose, + amount_msat, + counterparty_skimmed_fee_msat, + receiving_channel_ids: claimable_payment.receiving_channel_ids(), + claim_deadline, + onion_fields: Some(claimable_payment.onion_fields.clone()), + payment_id: Some( + claimable_payment.inbound_payment_id(&self.inbound_payment_id_secret), + ), + }, + None, + )); + Ok(()) + }, + // No action if MPP hasn't completed yet. + Ok(false) => Ok(()), + Err(()) => { + debug_assert!(!first_claimable_htlc); + Err(()) + }, + } + } + + /// Handles the addition of a HTLC associated with a trampoline forward that we need to + /// accumulate on the incoming link before forwarding onwards. If the HTLC is failed, it + /// returns the source and error that should be used to fail the HTLC(s) back. + fn handle_trampoline_htlc( + &self, mpp_part: MppPart, onion_fields: RecipientOnionFields, payment_hash: PaymentHash, + next_hop_info: NextTrampolineHopInfo, _next_node_id: PublicKey, + ) -> Result<(), (HTLCSource, HTLCFailReason)> { + let mut trampoline_payments = self.awaiting_trampoline_forwards.lock().unwrap(); + + // We should not fail if we're adding the first htlc to a ClaimablePayment (as our + // validation compares fields across parts, and our first part can't overflow maximum + // msats because each htlc's amount is individually validated - overflow is only possible + // with multiple parts). + let mut first_trampoline_htlc = false; + trampoline_payments.entry(payment_hash).or_insert_with(|| { + first_trampoline_htlc = true; + TrampolinePayment { htlcs: Vec::new(), onion_fields: onion_fields.clone() } + }); + + // TODO: add restriction to specification that trampoline should be consistent across + // MPP parts? Currently, we'll accept a MPP trampoline payments that specify different + // next_node_id destinations (just forwarding to the last one that arrives). + + // If MPP hasn't fully arrived yet, return early (saving indentation below). Once it has + // arrived, remove the entry from the map so that all downstream paths consume it. + let prev_hop = mpp_part.prev_hop.clone(); + let check_result = { + let trampoline_payment = + trampoline_payments.get_mut(&payment_hash).expect("just inserted"); + self.check_incoming_mpp_part( + &mut trampoline_payment.htlcs, + &mut trampoline_payment.onion_fields, + mpp_part, + onion_fields, + payment_hash, + ) + }; + let trampoline_payment = match check_result { + Ok(false) => return Ok(()), + Err(()) => { + debug_assert!( + !first_trampoline_htlc, + "first trampoline HTLC should not fail check_incoming_mpp_part" + ); + return Err(( + // When we couldn't add a new HTLC, we just fail back our last received htlc, + // allowing others to wait for more MPP parts to arrive. + HTLCSource::TrampolineForward { + previous_hop_data: vec![prev_hop], + outbound_payment: None, + }, + HTLCFailReason::reason( + LocalHTLCFailureReason::InvalidTrampolineForward, + vec![], + ), + )); + }, + Ok(true) => trampoline_payments.remove(&payment_hash).expect("just inserted"), + }; + + let incoming_amt_msat: u64 = trampoline_payment.htlcs.iter().map(|h| h.value).sum(); + let incoming_cltv_expiry = + trampoline_payment.htlcs.iter().map(|h| h.cltv_expiry).min().unwrap(); + + // TODO: configure and advertise the fees and CLTV delta we require once specified. + let (forwarding_fee_proportional_millionths, forwarding_fee_base_msat, cltv_delta) = { + let config = self.config.read().unwrap(); + ( + config.channel_config.forwarding_fee_proportional_millionths, + config.channel_config.forwarding_fee_base_msat, + // Note that we must floor the user-set value with our overriding minimum because + // we don't have a specific channel to call the helper get_cltv_expiry_delta which + // performs this flooring for us. When we have a more concrete policy for + // trampoline, this can be accessed with a similar helper. + cmp::max(config.channel_config.cltv_expiry_delta, MIN_CLTV_EXPIRY_DELTA).into(), + ) + }; + let trampoline_source = || -> HTLCSource { + HTLCSource::TrampolineForward { + previous_hop_data: trampoline_payment + .htlcs + .iter() + .map(|htlc| htlc.prev_hop.clone()) + .collect(), + outbound_payment: None, + } + }; + let trampoline_failure = || -> HTLCFailReason { + let mut err_data = Vec::with_capacity(10); + err_data.extend_from_slice(&forwarding_fee_base_msat.to_be_bytes()); + err_data.extend_from_slice(&forwarding_fee_proportional_millionths.to_be_bytes()); + err_data.extend_from_slice(&(cltv_delta as u16).to_be_bytes()); + HTLCFailReason::reason( + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient, + err_data, + ) + }; + + // We need to pick the maximum fee that we'll charge as a trampoline node. This could + // be any trampoline fee policy - this isn't specified or advertised. To keep things + // simple, we just calculate the amount that we would have charged to forward the amount + // going to the trampoline with our default fees, and make sure we have at least that. + // The amount that we actually dispatch will be slightly more than the amount for the next + // trampoline (since it'll also include fees for subsequent hops), so we're actually + // charging a little less than we would if this were a regular forward of that amount. As + // use of trampoline grows, we can investigate more sophisticated options. + let routing_fees = RoutingFees { + base_msat: forwarding_fee_base_msat, + proportional_millionths: forwarding_fee_proportional_millionths, + }; + let our_forwarding_fee_msat = compute_fees(next_hop_info.amount_msat, routing_fees); + let _max_total_routing_fee_msat = match our_forwarding_fee_msat + .and_then(|our_fee| our_fee.checked_add(next_hop_info.amount_msat)) + .and_then(|total| incoming_amt_msat.checked_sub(total)) + { + Some(amount) => amount, + None => { + return Err((trampoline_source(), trampoline_failure())); + }, + }; + + let _max_total_cltv_expiry_delta = match next_hop_info + .cltv_expiry_height + .checked_add(cltv_delta) + .and_then(|total| incoming_cltv_expiry.checked_sub(total)) + { + Some(cltv_delta) => cltv_delta, + None => { + return Err((trampoline_source(), trampoline_failure())); + }, + }; + + log_debug!( + self.logger, + "Rejecting trampoline forward because we do not fully support forwarding yet.", + ); + + Err(( + trampoline_source(), + HTLCFailReason::reason(LocalHTLCFailureReason::TemporaryTrampolineFailure, vec![]), + )) + } + fn process_receive_htlcs( &self, pending_forwards: &mut Vec<HTLCForwardInfo>, new_events: &mut VecDeque<(Event, Option<EventCompletionAction>)>, @@ -7789,6 +8749,10 @@ impl< }, .. } = payment; + // We differentiate the received value from the sender intended value if + // possible so that we don't prematurely mark MPP payments completed if routing + // nodes overpay + let value = incoming_amt_msat.unwrap_or(outgoing_amt_msat); let blinded_failure = routing.blinded_failure(); let ( cltv_expiry, @@ -7816,6 +8780,7 @@ impl< payment_secret: Some(payment_data.payment_secret), payment_metadata, custom_tlvs, + total_mpp_amount_msat: payment_data.total_msat, }; ( incoming_cltv_expiry, @@ -7844,6 +8809,10 @@ impl< payment_secret: payment_data .as_ref() .map(|data| data.payment_secret), + total_mpp_amount_msat: payment_data + .as_ref() + .map(|data| data.total_msat) + .unwrap_or(outgoing_amt_msat), payment_metadata, custom_tlvs, }; @@ -7859,56 +8828,112 @@ impl< None, ) }, - _ => { - panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive"); - }, - }; - let claimable_htlc = ClaimableHTLC { - prev_hop, - // We differentiate the received value from the sender intended value - // if possible so that we don't prematurely mark MPP payments complete - // if routing nodes overpay - value: incoming_amt_msat.unwrap_or(outgoing_amt_msat), - sender_intended_value: outgoing_amt_msat, - timer_ticks: 0, - total_value_received: None, - total_msat: if let Some(data) = &payment_data { - data.total_msat - } else { - outgoing_amt_msat - }, - cltv_expiry, - onion_payload, - counterparty_skimmed_fee_msat: skimmed_fee_msat, - }; - - let mut committed_to_claimable = false; - - macro_rules! fail_htlc { - ($htlc: expr, $payment_hash: expr) => { - debug_assert!(!committed_to_claimable); - let err_data = invalid_payment_err_data( - $htlc.value, - self.best_block.read().unwrap().height, + PendingHTLCRouting::TrampolineForward { + onion_packet, + node_id: next_trampoline, + blinded, + incoming_cltv_expiry, + incoming_multipath_data, + next_trampoline_amt_msat, + next_trampoline_cltv_expiry, + .. + } => { + // Trampoline forwards only *need* to have MPP data if they're + // multi-part. + let onion_fields = match incoming_multipath_data { + Some(ref final_mpp) => RecipientOnionFields::secret_only( + final_mpp.payment_secret, + final_mpp.total_msat, + ), + None => RecipientOnionFields::spontaneous_empty(outgoing_amt_msat), + }; + + let next_hop_info = NextTrampolineHopInfo { + onion_packet, + blinding_point: blinded.and_then(|b| { + b.next_blinding_override.or_else(|| { + let encrypted_tlvs_ss = self + .node_signer + .ecdh(Recipient::Node, &b.inbound_blinding_point, None) + .unwrap() + .secret_bytes(); + onion_utils::next_hop_pubkey( + &self.secp_ctx, + b.inbound_blinding_point, + &encrypted_tlvs_ss, + ) + .ok() + }) + }), + amount_msat: next_trampoline_amt_msat, + cltv_expiry_height: next_trampoline_cltv_expiry, + }; + + // For trampoline forwards, construct MppPart directly and handle separately + // from claimable HTLCs. + let mpp_part = MppPart { + prev_hop, + cltv_expiry: incoming_cltv_expiry, + value, + sender_intended_value: outgoing_amt_msat, + timer_ticks: 0, + total_value_received: None, + }; + if let Err((htlc_source, failure_reason)) = self.handle_trampoline_htlc( + mpp_part, + onion_fields, + payment_hash, + next_hop_info, + next_trampoline, + ) { + failed_forwards.push(( + htlc_source, + payment_hash, + failure_reason, + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + continue 'next_forwardable_htlc; + }, + _ => { + panic!("short_channel_id == 0 should imply any pending_forward entries are of type Receive"); + }, + }; + let htlc_source = HTLCSource::PreviousHopData(HTLCPreviousHopData { + prev_outbound_scid_alias: prev_hop.prev_outbound_scid_alias, + user_channel_id: prev_hop.user_channel_id, + amount_msat: Some(value), + counterparty_node_id: prev_hop.counterparty_node_id, + channel_id: prev_channel_id, + outpoint: prev_funding_outpoint, + htlc_id: prev_hop.htlc_id, + incoming_packet_shared_secret: prev_hop.incoming_packet_shared_secret, + phantom_shared_secret, + trampoline_shared_secret, + blinded_failure, + cltv_expiry: Some(cltv_expiry), + }); + let claimable_htlc = ClaimableHTLC { + mpp_part: MppPart { + prev_hop, + cltv_expiry, + value, + sender_intended_value: outgoing_amt_msat, + timer_ticks: 0, + total_value_received: None, + }, + onion_payload, + counterparty_skimmed_fee_msat: skimmed_fee_msat, + }; + + macro_rules! fail_htlc { + ($payment_hash: expr) => { + let err_data = invalid_payment_err_data( + value, + self.best_block.read().unwrap().height, ); - let counterparty_node_id = $htlc.prev_hop.counterparty_node_id; - let incoming_packet_shared_secret = - $htlc.prev_hop.incoming_packet_shared_secret; - let prev_outbound_scid_alias = $htlc.prev_hop.prev_outbound_scid_alias; failed_forwards.push(( - HTLCSource::PreviousHopData(HTLCPreviousHopData { - prev_outbound_scid_alias, - user_channel_id: $htlc.prev_hop.user_channel_id, - counterparty_node_id, - channel_id: prev_channel_id, - outpoint: prev_funding_outpoint, - htlc_id: $htlc.prev_hop.htlc_id, - incoming_packet_shared_secret, - phantom_shared_secret, - trampoline_shared_secret, - blinded_failure, - cltv_expiry: Some(cltv_expiry), - }), + htlc_source, payment_hash, HTLCFailReason::reason( LocalHTLCFailureReason::IncorrectPaymentDetails, @@ -7919,7 +8944,8 @@ impl< continue 'next_forwardable_htlc; }; } - let phantom_shared_secret = claimable_htlc.prev_hop.phantom_shared_secret; + let phantom_shared_secret = + claimable_htlc.mpp_part.prev_hop.phantom_shared_secret; let mut receiver_node_id = self.our_network_pubkey; if phantom_shared_secret.is_some() { receiver_node_id = self @@ -7928,96 +8954,6 @@ impl< .expect("Failed to get node_id for phantom node recipient"); } - macro_rules! check_total_value { - ($purpose: expr) => {{ - let mut payment_claimable_generated = false; - let is_keysend = $purpose.is_keysend(); - let mut claimable_payments = self.claimable_payments.lock().unwrap(); - if claimable_payments.pending_claiming_payments.contains_key(&payment_hash) { - fail_htlc!(claimable_htlc, payment_hash); - } - let ref mut claimable_payment = claimable_payments.claimable_payments - .entry(payment_hash) - // Note that if we insert here we MUST NOT fail_htlc!() - .or_insert_with(|| { - committed_to_claimable = true; - ClaimablePayment { - purpose: $purpose.clone(), htlcs: Vec::new(), onion_fields: None, - } - }); - if $purpose != claimable_payment.purpose { - let log_keysend = |keysend| if keysend { "keysend" } else { "non-keysend" }; - log_trace!(self.logger, "Failing new {} HTLC with payment_hash {} as we already had an existing {} HTLC with the same payment hash", log_keysend(is_keysend), &payment_hash, log_keysend(!is_keysend)); - fail_htlc!(claimable_htlc, payment_hash); - } - if let Some(earlier_fields) = &mut claimable_payment.onion_fields { - if earlier_fields.check_merge(&mut onion_fields).is_err() { - fail_htlc!(claimable_htlc, payment_hash); - } - } else { - claimable_payment.onion_fields = Some(onion_fields); - } - let mut total_value = claimable_htlc.sender_intended_value; - let mut earliest_expiry = claimable_htlc.cltv_expiry; - for htlc in claimable_payment.htlcs.iter() { - total_value += htlc.sender_intended_value; - earliest_expiry = cmp::min(earliest_expiry, htlc.cltv_expiry); - if htlc.total_msat != claimable_htlc.total_msat { - log_trace!(self.logger, "Failing HTLCs with payment_hash {} as the HTLCs had inconsistent total values (eg {} and {})", - &payment_hash, claimable_htlc.total_msat, htlc.total_msat); - total_value = msgs::MAX_VALUE_MSAT; - } - if total_value >= msgs::MAX_VALUE_MSAT { break; } - } - // The condition determining whether an MPP is complete must - // match exactly the condition used in `timer_tick_occurred` - if total_value >= msgs::MAX_VALUE_MSAT { - fail_htlc!(claimable_htlc, payment_hash); - } else if total_value - claimable_htlc.sender_intended_value >= claimable_htlc.total_msat { - log_trace!(self.logger, "Failing HTLC with payment_hash {} as payment is already claimable", - &payment_hash); - fail_htlc!(claimable_htlc, payment_hash); - } else if total_value >= claimable_htlc.total_msat { - #[allow(unused_assignments)] { - committed_to_claimable = true; - } - claimable_payment.htlcs.push(claimable_htlc); - let amount_msat = - claimable_payment.htlcs.iter().map(|htlc| htlc.value).sum(); - claimable_payment.htlcs.iter_mut() - .for_each(|htlc| htlc.total_value_received = Some(amount_msat)); - let counterparty_skimmed_fee_msat = claimable_payment.htlcs.iter() - .map(|htlc| htlc.counterparty_skimmed_fee_msat.unwrap_or(0)).sum(); - debug_assert!(total_value.saturating_sub(amount_msat) <= - counterparty_skimmed_fee_msat); - claimable_payment.htlcs.sort(); - let payment_id = - claimable_payment.inbound_payment_id(&self.inbound_payment_id_secret); - new_events.push_back((events::Event::PaymentClaimable { - receiver_node_id: Some(receiver_node_id), - payment_hash, - purpose: $purpose, - amount_msat, - counterparty_skimmed_fee_msat, - receiving_channel_ids: claimable_payment.receiving_channel_ids(), - claim_deadline: Some(earliest_expiry - HTLC_FAIL_BACK_BUFFER), - onion_fields: claimable_payment.onion_fields.clone(), - payment_id: Some(payment_id), - }, None)); - payment_claimable_generated = true; - } else { - // Nothing to do - we haven't reached the total - // payment value yet, wait until we receive more - // MPP parts. - claimable_payment.htlcs.push(claimable_htlc); - #[allow(unused_assignments)] { - committed_to_claimable = true; - } - } - payment_claimable_generated - }} - } - // Check that the payment hash and secret are known. Note that we // MUST take care to handle the "unknown payment hash" and // "incorrect payment secret" cases here identically or we'd expose @@ -8029,6 +8965,7 @@ impl< let verify_res = inbound_payment::verify( payment_hash, &payment_data, + onion_fields.payment_metadata.as_mut(), self.highest_seen_timestamp.load(Ordering::Acquire) as u64, &self.inbound_payment_key, &self.logger, @@ -8037,7 +8974,7 @@ impl< Ok(result) => result, Err(()) => { log_trace!(self.logger, "Failing new HTLC with payment_hash {} as payment verification failed", &payment_hash); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); }, }; if let Some(min_final_cltv_expiry_delta) = min_final_cltv_expiry_delta { @@ -8047,12 +8984,12 @@ impl< if (cltv_expiry as u64) < expected_min_expiry_height { log_trace!(self.logger, "Failing new HTLC with payment_hash {} as its CLTV expiry was too soon (had {}, earliest expected {})", &payment_hash, cltv_expiry, expected_min_expiry_height); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); } } payment_preimage } else { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); } } else { None @@ -8068,14 +9005,24 @@ impl< let purpose = match from_parts_res { Ok(purpose) => purpose, Err(()) => { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); }, }; - check_total_value!(purpose); + + if let Err(()) = self.handle_claimable_htlc( + purpose, + claimable_htlc, + onion_fields, + payment_hash, + receiver_node_id, + new_events, + ) { + fail_htlc!(payment_hash); + } }, OnionPayload::Spontaneous(keysend_preimage) => { let purpose = if let Some(PaymentContext::AsyncBolt12Offer( - AsyncBolt12OfferContext { offer_nonce }, + AsyncBolt12OfferContext { offer_nonce, payment_metadata }, )) = payment_context { let payment_data = match payment_data { @@ -8085,7 +9032,7 @@ impl< false, "We checked that payment_data is Some above" ); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); }, }; @@ -8104,19 +9051,20 @@ impl< verified_invreq.amount_msats() { if payment_data.total_msat < invreq_amt_msat { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); } } verified_invreq }, None => { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); }, }; let payment_purpose_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: verified_invreq.offer_id(), invoice_request: verified_invreq.fields(), + payment_metadata, }); let from_parts_res = events::PaymentPurpose::from_parts( Some(keysend_preimage), @@ -8126,16 +9074,25 @@ impl< match from_parts_res { Ok(purpose) => purpose, Err(()) => { - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); }, } } else if payment_context.is_some() { log_trace!(self.logger, "Failing new HTLC with payment_hash {}: received a keysend payment to a non-async payments context {:#?}", payment_hash, payment_context); - fail_htlc!(claimable_htlc, payment_hash); + fail_htlc!(payment_hash); } else { events::PaymentPurpose::SpontaneousPayment(keysend_preimage) }; - check_total_value!(purpose); + if let Err(()) = self.handle_claimable_htlc( + purpose, + claimable_htlc, + onion_fields, + payment_hash, + receiver_node_id, + new_events, + ) { + fail_htlc!(payment_hash); + } }, } }, @@ -8188,12 +9145,18 @@ impl< // already been persisted to the monitor and can be applied to our internal // state such that the channel resumes operation if no new updates have been // made since. - self.channel_monitor_updated( + let _ = self.channel_monitor_updated( &channel_id, Some(highest_update_id_completed), &counterparty_node_id, ); }, + BackgroundEvent::AttemptUnblockMonitorUpdates { + counterparty_node_id, + channel_id, + } => { + self.handle_monitor_update_release(counterparty_node_id, channel_id, None); + }, } } NotifyOption::DoPersist @@ -8235,39 +9198,6 @@ impl< NotifyOption::DoPersist } - #[cfg(any(test, fuzzing, feature = "_externalize_tests"))] - /// In chanmon_consistency we want to sometimes do the channel fee updates done in - /// timer_tick_occurred, but we can't generate the disabled channel updates as it considers - /// these a fuzz failure (as they usually indicate a channel force-close, which is exactly what - /// it wants to detect). Thus, we have a variant exposed here for its benefit. - #[rustfmt::skip] - pub fn maybe_update_chan_fees(&self) { - PersistenceNotifierGuard::optionally_notify(self, || { - let mut should_persist = NotifyOption::SkipPersistNoEvents; - let mut feerate_cache = new_hash_map(); - - let per_peer_state = self.per_peer_state.read().unwrap(); - for (_cp_id, peer_state_mutex) in per_peer_state.iter() { - let mut peer_state_lock = peer_state_mutex.lock().unwrap(); - let peer_state = &mut *peer_state_lock; - for (chan_id, chan) in peer_state.channel_by_id.iter_mut() - .filter_map(|(chan_id, chan)| chan.as_funded_mut().map(|chan| (chan_id, chan))) - { - let channel_type = chan.funding.get_channel_type(); - let new_feerate = feerate_cache.get(channel_type).copied().or_else(|| { - let feerate = selected_commitment_sat_per_1000_weight(&self.fee_estimator, &channel_type); - feerate_cache.insert(channel_type.clone(), feerate); - Some(feerate) - }).unwrap(); - let chan_needs_persist = self.update_channel_fee(chan_id, chan, new_feerate); - if chan_needs_persist == NotifyOption::DoPersist { should_persist = NotifyOption::DoPersist; } - } - } - - should_persist - }); - } - /// Performs actions which should happen on startup and roughly once per minute thereafter. /// /// This currently includes: @@ -8475,52 +9405,67 @@ impl< self.claimable_payments.lock().unwrap().claimable_payments.retain( |payment_hash, payment| { if payment.htlcs.is_empty() { - // This should be unreachable debug_assert!(false); return false; } - if let OnionPayload::Invoice { .. } = payment.htlcs[0].onion_payload { - // Check if we've received all the parts we need for an MPP (the value of the parts adds to total_msat). - // In this case we're not going to handle any timeouts of the parts here. - // This condition determining whether the MPP is complete here must match - // exactly the condition used in `process_pending_htlc_forwards`. - let htlc_total_msat = - payment.htlcs.iter().map(|h| h.sender_intended_value).sum(); - if payment.htlcs[0].total_msat <= htlc_total_msat { - return true; - } else if payment.htlcs.iter_mut().any(|htlc| { - htlc.timer_ticks += 1; - return htlc.timer_ticks >= MPP_TIMEOUT_TICKS; - }) { - let htlcs = payment - .htlcs - .drain(..) - .map(|htlc: ClaimableHTLC| (htlc.prev_hop, *payment_hash)); - timed_out_mpp_htlcs.extend(htlcs); - return false; - } + let mpp_timeout = check_mpp_timeout( + payment.htlcs.iter_mut().map(|htlc| &mut htlc.mpp_part), + &payment.onion_fields, + ); + if mpp_timeout { + timed_out_mpp_htlcs.extend(payment.htlcs.drain(..).map(|h| { + ( + HTLCSource::PreviousHopData(h.mpp_part.prev_hop), + *payment_hash, + HTLCHandlingFailureType::Receive { payment_hash: *payment_hash }, + ) + })); } - true + return !mpp_timeout; }, ); - for htlc_source in timed_out_mpp_htlcs.drain(..) { - let source = HTLCSource::PreviousHopData(htlc_source.0.clone()); + self.awaiting_trampoline_forwards.lock().unwrap().retain(|payment_hash, payment| { + if payment.htlcs.is_empty() { + debug_assert!(false); + return false; + } + let mpp_timeout = + check_mpp_timeout(payment.htlcs.iter_mut(), &payment.onion_fields); + if mpp_timeout { + let previous_hop_data = + payment.htlcs.drain(..).map(|claimable| claimable.prev_hop).collect(); + + timed_out_mpp_htlcs.push(( + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment: None }, + *payment_hash, + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + !mpp_timeout + }); + + for (htlc_source, payment_hash, failure_type) in timed_out_mpp_htlcs.drain(..) { let failure_reason = LocalHTLCFailureReason::MPPTimeout; let reason = HTLCFailReason::from_failure_code(failure_reason); - let receiver = HTLCHandlingFailureType::Receive { payment_hash: htlc_source.1 }; - self.fail_htlc_backwards_internal(&source, &htlc_source.1, &reason, receiver, None); + self.fail_htlc_backwards_internal( + &htlc_source, + &payment_hash, + &reason, + failure_type, + None, + ); } for (err, counterparty_node_id) in handle_errors { let _ = self.handle_error(err, counterparty_node_id); } - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let duration_since_epoch = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let duration_since_epoch = Duration::from_secs( self.highest_seen_timestamp.load(Ordering::Acquire).saturating_sub(7200) as u64, ); @@ -8584,7 +9529,7 @@ impl< if let Some(payment) = removed_source { for htlc in payment.htlcs { let reason = self.get_htlc_fail_reason_from_failure_code(failure_code, &htlc); - let source = HTLCSource::PreviousHopData(htlc.prev_hop); + let source = HTLCSource::PreviousHopData(htlc.mpp_part.prev_hop); let receiver = HTLCHandlingFailureType::Receive { payment_hash: *payment_hash }; self.fail_htlc_backwards_internal(&source, &payment_hash, &reason, receiver, None); } @@ -8603,7 +9548,7 @@ impl< HTLCFailReason::from_failure_code(failure_code.into()) }, FailureCode::IncorrectOrUnknownPaymentDetails => { - let mut htlc_msat_height_data = htlc.value.to_be_bytes().to_vec(); + let mut htlc_msat_height_data = htlc.mpp_part.value.to_be_bytes().to_vec(); htlc_msat_height_data .extend_from_slice(&self.best_block.read().unwrap().height.to_be_bytes()); HTLCFailReason::reason(failure_code.into(), htlc_msat_height_data) @@ -8675,11 +9620,14 @@ impl< for (htlc_src, payment_hash) in htlcs_to_fail.drain(..) { let reason = HTLCFailReason::reason(failure_reason, onion_failure_data.clone()); - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id.clone()), - channel_id, - }; - self.fail_htlc_backwards_internal(&htlc_src, &payment_hash, &reason, receiver, None); + let failure_type = htlc_src.failure_type(*counterparty_node_id, channel_id); + self.fail_htlc_backwards_internal( + &htlc_src, + &payment_hash, + &reason, + failure_type, + None, + ); } } @@ -8699,6 +9647,19 @@ impl< debug_assert_ne!(peer.held_by_thread(), LockHeldState::HeldByThread); } + let push_forward_htlcs_failure = + |prev_outbound_scid_alias: u64, failure: HTLCForwardInfo| { + let mut forward_htlcs = self.forward_htlcs.lock().unwrap(); + match forward_htlcs.entry(prev_outbound_scid_alias) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(failure); + }, + hash_map::Entry::Vacant(entry) => { + entry.insert(vec![failure]); + }, + } + }; + //TODO: There is a timing attack here where if a node fails an HTLC back to us they can //identify whether we sent it or not based on the (I presume) very different runtime //between the branches here. We should make this async and move it into the forward HTLCs @@ -8765,49 +9726,90 @@ impl< if blinded_failure.is_some() { "blinded " } else { "" }, onion_error ); - // In case of trampoline + phantom we prioritize the trampoline failure over the phantom failure. - // TODO: Correctly wrap the error packet twice if failing back a trampoline + phantom HTLC. - let secondary_shared_secret = trampoline_shared_secret.or(*phantom_shared_secret); - let failure = match blinded_failure { - Some(BlindedFailure::FromIntroductionNode) => { - let blinded_onion_error = HTLCFailReason::reason( - LocalHTLCFailureReason::InvalidOnionBlinding, - vec![0; 32], - ); - let err_packet = blinded_onion_error.get_encrypted_failure_packet( - incoming_packet_shared_secret, - &secondary_shared_secret, - ); - HTLCForwardInfo::FailHTLC { htlc_id: *htlc_id, err_packet } - }, - Some(BlindedFailure::FromBlindedNode) => HTLCForwardInfo::FailMalformedHTLC { - htlc_id: *htlc_id, - failure_code: LocalHTLCFailureReason::InvalidOnionBlinding.failure_code(), - sha256_of_onion: [0; 32], - }, - None => { - let err_packet = onion_error.get_encrypted_failure_packet( - incoming_packet_shared_secret, - &secondary_shared_secret, - ); - HTLCForwardInfo::FailHTLC { htlc_id: *htlc_id, err_packet } - }, - }; - let mut forward_htlcs = self.forward_htlcs.lock().unwrap(); - match forward_htlcs.entry(*prev_outbound_scid_alias) { - hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(failure); + push_forward_htlcs_failure( + *prev_outbound_scid_alias, + get_htlc_forward_failure( + blinded_failure, + onion_error, + incoming_packet_shared_secret, + trampoline_shared_secret, + phantom_shared_secret, + *htlc_id, + ), + ); + + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::HTLCHandlingFailed { + prev_channel_ids: vec![*channel_id], + failure_type, + failure_reason: Some(onion_error.into()), }, - hash_map::Entry::Vacant(entry) => { - entry.insert(vec![failure]); + None, + )); + }, + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + let decoded_onion_failure = + onion_error.decode_onion_failure(&self.secp_ctx, &self.logger, &source); + log_trace!( + WithContext::from(&self.logger, None, None, Some(*payment_hash)), + "Trampoline forward failed downstream on {}", + if let Some(scid) = decoded_onion_failure.short_channel_id { + scid.to_string() + } else { + "unknown channel".to_string() }, + ); + // TODO: when we receive a failure from a single outgoing trampoline HTLC, we don't + // necessarily want to fail all of our incoming HTLCs back yet. We may have other + // outgoing HTLCs that need to resolve first. This will be tracked in our + // pending_outbound_payments in a followup. + for current_hop_data in previous_hop_data { + let HTLCPreviousHopData { + prev_outbound_scid_alias, + htlc_id, + incoming_packet_shared_secret, + blinded_failure, + channel_id, + trampoline_shared_secret, + .. + } = current_hop_data; + log_trace!( + WithContext::from(&self.logger, None, Some(*channel_id), Some(*payment_hash)), + "Failing {}HTLC with payment_hash {} backwards from us following Trampoline forwarding failure: {:?}", + if blinded_failure.is_some() { "blinded " } else { "" }, &payment_hash, onion_error + ); + let onion_error = HTLCFailReason::reason( + LocalHTLCFailureReason::TemporaryTrampolineFailure, + Vec::new(), + ); + debug_assert!( + trampoline_shared_secret.is_some(), + "trampoline hop should have secret" + ); + push_forward_htlcs_failure( + *prev_outbound_scid_alias, + get_htlc_forward_failure( + blinded_failure, + &onion_error, + incoming_packet_shared_secret, + &trampoline_shared_secret, + &None, + *htlc_id, + ), + ); } - mem::drop(forward_htlcs); + + // We only want to emit a single event for trampoline failures, so we do it once + // we've failed back all of our incoming HTLCs. let mut pending_events = self.pending_events.lock().unwrap(); pending_events.push_back(( events::Event::HTLCHandlingFailed { - prev_channel_id: *channel_id, + prev_channel_ids: previous_hop_data + .iter() + .map(|prev| prev.channel_id) + .collect(), failure_type, failure_reason: Some(onion_error.into()), }, @@ -8859,7 +9861,7 @@ impl< } fn claim_payment_internal(&self, payment_preimage: PaymentPreimage, custom_tlvs_known: bool) { - let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); + let payment_hash: PaymentHash = payment_preimage.into(); let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); @@ -8880,7 +9882,7 @@ impl< FailureCode::InvalidOnionPayload(None), &htlc, ); - let source = HTLCSource::PreviousHopData(htlc.prev_hop); + let source = HTLCSource::PreviousHopData(htlc.mpp_part.prev_hop); let receiver = HTLCHandlingFailureType::Receive { payment_hash }; self.fail_htlc_backwards_internal( &source, @@ -8901,28 +9903,21 @@ impl< // amount we told the user in the last `PaymentClaimable`. We also do a sanity-check that // the MPP parts all have the same `total_msat`. let mut claimable_amt_msat = 0; - let mut prev_total_msat = None; let mut expected_amt_msat = None; let mut valid_mpp = true; let mut errs = Vec::new(); let per_peer_state = self.per_peer_state.read().unwrap(); for htlc in sources.iter() { - if prev_total_msat.is_some() && prev_total_msat != Some(htlc.total_msat) { - log_error!(self.logger, "Somehow ended up with an MPP payment with different expected total amounts - this should not be reachable!"); - debug_assert!(false); - valid_mpp = false; - break; - } - prev_total_msat = Some(htlc.total_msat); - - if expected_amt_msat.is_some() && expected_amt_msat != htlc.total_value_received { + if expected_amt_msat.is_some() + && expected_amt_msat != htlc.mpp_part.total_value_received + { log_error!(self.logger, "Somehow ended up with an MPP payment with different received total amounts - this should not be reachable!"); debug_assert!(false); valid_mpp = false; break; } - expected_amt_msat = htlc.total_value_received; - claimable_amt_msat += htlc.value; + expected_amt_msat = htlc.mpp_part.total_value_received; + claimable_amt_msat += htlc.mpp_part.value; } mem::drop(per_peer_state); if sources.is_empty() || expected_amt_msat.is_none() { @@ -8943,12 +9938,12 @@ impl< let mpp_parts: Vec<_> = sources .iter() .filter_map(|htlc| { - if let Some(cp_id) = htlc.prev_hop.counterparty_node_id { + if let Some(cp_id) = htlc.mpp_part.prev_hop.counterparty_node_id { Some(MPPClaimHTLCSource { counterparty_node_id: cp_id, - funding_txo: htlc.prev_hop.outpoint, - channel_id: htlc.prev_hop.channel_id, - htlc_id: htlc.prev_hop.htlc_id, + funding_txo: htlc.mpp_part.prev_hop.outpoint, + channel_id: htlc.mpp_part.prev_hop.channel_id, + htlc_id: htlc.mpp_part.prev_hop.htlc_id, }) } else { None @@ -8974,11 +9969,11 @@ impl< for htlc in sources { let this_mpp_claim = pending_mpp_claim_ptr_opt.as_ref().map(|pending_mpp_claim| { - let counterparty_id = htlc.prev_hop.counterparty_node_id; + let counterparty_id = htlc.mpp_part.prev_hop.counterparty_node_id; let counterparty_id = counterparty_id .expect("Prior to upgrading to LDK 0.1, all pending HTLCs forwarded by LDK 0.0.123 or before must be resolved. It appears at least one claimable payment was not resolved. Please downgrade to LDK 0.0.125 and resolve the HTLC by claiming the payment prior to upgrading."); let claim_ptr = PendingMPPClaimPointer(Arc::clone(pending_mpp_claim)); - (counterparty_id, htlc.prev_hop.channel_id, claim_ptr) + (counterparty_id, htlc.mpp_part.prev_hop.channel_id, claim_ptr) }); let raa_blocker = pending_mpp_claim_ptr_opt.as_ref().map(|pending_claim| { RAAMonitorUpdateBlockingAction::ClaimedMPPPayment { @@ -8990,7 +9985,7 @@ impl< // non-zero value will not make a difference in the penalty that may be applied by the sender. If there // is a phantom hop, we need to double-process. let attribution_data = - if let Some(phantom_secret) = htlc.prev_hop.phantom_shared_secret { + if let Some(phantom_secret) = htlc.mpp_part.prev_hop.phantom_shared_secret { let attribution_data = process_fulfill_attribution_data(None, &phantom_secret, 0); Some(attribution_data) @@ -9000,12 +9995,12 @@ impl< let attribution_data = process_fulfill_attribution_data( attribution_data, - &htlc.prev_hop.incoming_packet_shared_secret, + &htlc.mpp_part.prev_hop.incoming_packet_shared_secret, 0, ); self.claim_funds_from_hop( - htlc.prev_hop, + &htlc.mpp_part.prev_hop, payment_preimage, payment_info.clone(), Some(attribution_data), @@ -9026,9 +10021,11 @@ impl< } } else { for htlc in sources { - let err_data = - invalid_payment_err_data(htlc.value, self.best_block.read().unwrap().height); - let source = HTLCSource::PreviousHopData(htlc.prev_hop); + let err_data = invalid_payment_err_data( + htlc.mpp_part.value, + self.best_block.read().unwrap().height, + ); + let source = HTLCSource::PreviousHopData(htlc.mpp_part.prev_hop); let reason = HTLCFailReason::reason( LocalHTLCFailureReason::IncorrectPaymentDetails, err_data, @@ -9046,13 +10043,137 @@ impl< } } + /// Claims funds for a forwarded HTLC where we are an intermediate hop. + /// + /// Processes attribution data, calculates fees earned, and emits a [`Event::PaymentForwarded`] + /// event upon successful claim. `make_payment_forwarded_event` is responsible for creating a + /// single [`Event::PaymentForwarded`] event that represents the forward. + fn claim_funds_from_htlc_forward_hop( + &self, payment_preimage: PaymentPreimage, + make_payment_forwarded_event: impl FnOnce(Option<u64>) -> Option<events::Event>, + startup_replay: bool, next_channel_counterparty_node_id: PublicKey, + next_channel_outpoint: OutPoint, next_channel_id: ChannelId, hop_data: HTLCPreviousHopData, + attribution_data: Option<AttributionData>, send_timestamp: Option<Duration>, + ) { + let _prev_channel_id = hop_data.channel_id; + let completed_blocker = RAAMonitorUpdateBlockingAction::from_prev_hop_data(&hop_data); + + // Obtain hold time, if available. + let hold_time = hold_time_since(send_timestamp).unwrap_or(0); + + // If attribution data was received from downstream, we shift it and get it ready for adding our hold + // time. Note that fulfilled HTLCs take a fast path to the incoming side. We don't need to wait for RAA + // to record the hold time like we do for failed HTLCs. + let attribution_data = process_fulfill_attribution_data( + attribution_data, + &hop_data.incoming_packet_shared_secret, + hold_time, + ); + + #[cfg(test)] + let claiming_chan_funding_outpoint = hop_data.outpoint; + self.claim_funds_from_hop( + &hop_data, + payment_preimage, + None, + Some(attribution_data), + |htlc_claim_value_msat, definitely_duplicate| { + let chan_to_release = EventUnblockedChannel { + counterparty_node_id: next_channel_counterparty_node_id, + funding_txo: next_channel_outpoint, + channel_id: next_channel_id, + blocking_action: completed_blocker, + }; + + if definitely_duplicate && startup_replay { + // On startup we may get redundant claims which are related to + // monitor updates still in flight. In that case, we shouldn't + // immediately free, but instead let that monitor update complete + // in the background. + #[cfg(test)] + { + let per_peer_state = self.per_peer_state.deadlocking_read(); + // The channel we'd unblock should already be closed, or... + let channel_closed = per_peer_state + .get(&next_channel_counterparty_node_id) + .map(|lck| lck.deadlocking_lock()) + .map(|peer| !peer.channel_by_id.contains_key(&next_channel_id)) + .unwrap_or(true); + let background_events = self.pending_background_events.lock().unwrap(); + // there should be a `BackgroundEvent` pending... + let matching_bg_event = + background_events.iter().any(|ev| { + match ev { + // to apply a monitor update that blocked the claiming channel, + BackgroundEvent::MonitorUpdateRegeneratedOnStartup { + funding_txo, + update, + .. + } => { + if *funding_txo == claiming_chan_funding_outpoint { + assert!( + update.updates.iter().any(|upd| { + if let ChannelMonitorUpdateStep::PaymentPreimage { + payment_preimage: update_preimage, .. + } = upd { + payment_preimage == *update_preimage + } else { false } + }), + "{:?}", + update + ); + true + } else { + false + } + }, + // or the monitor update has completed and will unblock + // immediately once we get going. + BackgroundEvent::MonitorUpdatesComplete { + channel_id, .. + } => *channel_id == _prev_channel_id, + BackgroundEvent::AttemptUnblockMonitorUpdates { .. } => false, + } + }); + assert!(channel_closed || matching_bg_event, "{:?}", *background_events); + } + (None, None) + } else if definitely_duplicate { + ( + Some(MonitorUpdateCompletionAction::FreeDuplicateClaimImmediately { + downstream_counterparty_node_id: chan_to_release.counterparty_node_id, + downstream_channel_id: chan_to_release.channel_id, + blocking_action: chan_to_release.blocking_action, + }), + None, + ) + } else { + let event = make_payment_forwarded_event(htlc_claim_value_msat); + if let Some(ref payment_forwarded) = event { + debug_assert!(matches!( + payment_forwarded, + &events::Event::PaymentForwarded { .. } + )); + } + ( + Some(MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { + event, + downstream_counterparty_and_funding_outpoint: chan_to_release, + }), + None, + ) + } + }, + ); + } + fn claim_funds_from_hop< ComplFunc: FnOnce( Option<u64>, bool, ) -> (Option<MonitorUpdateCompletionAction>, Option<RAAMonitorUpdateBlockingAction>), >( - &self, prev_hop: HTLCPreviousHopData, payment_preimage: PaymentPreimage, + &self, prev_hop: &HTLCPreviousHopData, payment_preimage: PaymentPreimage, payment_info: Option<PaymentClaimDetails>, attribution_data: Option<AttributionData>, completion_action: ComplFunc, ) { @@ -9166,7 +10287,7 @@ impl< ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } }, UpdateFulfillCommitFetch::DuplicateClaim {} => { @@ -9218,7 +10339,7 @@ impl< log_trace!(logger, "Completing monitor update completion action as claim was redundant: {:?}", action); - if let MonitorUpdateCompletionAction::FreeOtherChannelImmediately { + if let MonitorUpdateCompletionAction::FreeDuplicateClaimImmediately { downstream_counterparty_node_id: node_id, blocking_action: blocker, downstream_channel_id: channel_id, @@ -9226,12 +10347,12 @@ impl< { if let Some(peer_state_mtx) = per_peer_state.get(&node_id) { let mut peer_state = peer_state_mtx.lock().unwrap(); - if let Some(blockers) = peer_state + let entry = peer_state .actions_blocking_raa_monitor_updates - .get_mut(&channel_id) - { + .entry(channel_id); + if let btree_map::Entry::Occupied(mut entry) = entry { let mut found_blocker = false; - blockers.retain(|iter| { + entry.get_mut().retain(|iter| { // Note that we could actually be blocked, in // which case we need to only remove the one // blocker which was added duplicatively. @@ -9241,6 +10362,9 @@ impl< } *iter != blocker || !first_blocker }); + if entry.get().is_empty() { + entry.remove(); + } debug_assert!(found_blocker); } } else { @@ -9350,7 +10474,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let derived_key; let session_priv = if path.has_trampoline_hops() { let session_priv_hash = - Sha256::hash(&session_priv.secret_bytes()).to_byte_array(); + <Sha256 as CryptoHash>::hash(&session_priv.secret_bytes()).to_byte_array(); derived_key = SecretKey::from_slice(&session_priv_hash[..]).unwrap(); &derived_key } else { @@ -9378,7 +10502,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn claim_funds_internal( &self, source: HTLCSource, payment_preimage: PaymentPreimage, - forwarded_htlc_value_msat: Option<u64>, skimmed_fee_msat: Option<u64>, from_onchain: bool, + forwarded_htlc_value_msat: u64, skimmed_fee_msat: Option<u64>, from_onchain: bool, next_channel_counterparty_node_id: PublicKey, next_channel_outpoint: OutPoint, next_channel_id: ChannelId, next_user_channel_id: Option<u128>, attribution_data: Option<AttributionData>, send_timestamp: Option<Duration>, @@ -9441,138 +10565,103 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, HTLCSource::PreviousHopData(hop_data) => { - let prev_channel_id = hop_data.channel_id; - let prev_user_channel_id = hop_data.user_channel_id; - let prev_node_id = hop_data.counterparty_node_id; - let completed_blocker = - RAAMonitorUpdateBlockingAction::from_prev_hop_data(&hop_data); - - // Obtain hold time, if available. - let hold_time = hold_time_since(send_timestamp).unwrap_or(0); - - // If attribution data was received from downstream, we shift it and get it ready for adding our hold - // time. Note that fulfilled HTLCs take a fast path to the incoming side. We don't need to wait for RAA - // to record the hold time like we do for failed HTLCs. - let attribution_data = process_fulfill_attribution_data( + let event_prev_hop_data = hop_data.clone(); + self.claim_funds_from_htlc_forward_hop( + payment_preimage, + |htlc_claim_value_msat: Option<u64>| -> Option<events::Event> { + let total_fee_earned_msat = + if let Some(claimed_htlc_value) = htlc_claim_value_msat { + Some(claimed_htlc_value - forwarded_htlc_value_msat) + } else { + None + }; + debug_assert!( + skimmed_fee_msat <= total_fee_earned_msat, + "skimmed_fee_msat must always be included in total_fee_earned_msat" + ); + let prev_htlc_amount_msat = + event_prev_hop_data.amount_msat.or(htlc_claim_value_msat); + + Some(events::Event::PaymentForwarded { + prev_htlcs: vec![ + event_prev_hop_data.htlc_locator(prev_htlc_amount_msat) + ], + next_htlcs: vec![events::HTLCLocator { + channel_id: next_channel_id, + amount_msat: Some(forwarded_htlc_value_msat), + user_channel_id: next_user_channel_id, + node_id: Some(next_channel_counterparty_node_id), + }], + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx: from_onchain, + outbound_amount_forwarded_msat: forwarded_htlc_value_msat, + }) + }, + startup_replay, + next_channel_counterparty_node_id, + next_channel_outpoint, + next_channel_id, + hop_data, attribution_data, - &hop_data.incoming_packet_shared_secret, - hold_time, + send_timestamp, + ); + }, + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + // Only emit a single event for trampoline claims. + let mut event_prev_htlcs = Some( + previous_hop_data.iter().map(|hop| hop.htlc_locator(hop.amount_msat)).collect(), ); - - #[cfg(test)] - let claiming_chan_funding_outpoint = hop_data.outpoint; - self.claim_funds_from_hop( - hop_data, - payment_preimage, - None, - Some(attribution_data), - |htlc_claim_value_msat, definitely_duplicate| { - let chan_to_release = Some(EventUnblockedChannel { - counterparty_node_id: next_channel_counterparty_node_id, - funding_txo: next_channel_outpoint, - channel_id: next_channel_id, - blocking_action: completed_blocker, - }); - - if definitely_duplicate && startup_replay { - // On startup we may get redundant claims which are related to - // monitor updates still in flight. In that case, we shouldn't - // immediately free, but instead let that monitor update complete - // in the background. - #[cfg(test)] - { - let per_peer_state = self.per_peer_state.deadlocking_read(); - // The channel we'd unblock should already be closed, or... - let channel_closed = per_peer_state - .get(&next_channel_counterparty_node_id) - .map(|lck| lck.deadlocking_lock()) - .map(|peer| !peer.channel_by_id.contains_key(&next_channel_id)) - .unwrap_or(true); - let background_events = - self.pending_background_events.lock().unwrap(); - // there should be a `BackgroundEvent` pending... - let matching_bg_event = - background_events.iter().any(|ev| { - match ev { - // to apply a monitor update that blocked the claiming channel, - BackgroundEvent::MonitorUpdateRegeneratedOnStartup { - funding_txo, update, .. - } => { - if *funding_txo == claiming_chan_funding_outpoint { - assert!(update.updates.iter().any(|upd| - if let ChannelMonitorUpdateStep::PaymentPreimage { - payment_preimage: update_preimage, .. - } = upd { - payment_preimage == *update_preimage - } else { false } - ), "{:?}", update); - true - } else { false } - }, - // or the monitor update has completed and will unblock - // immediately once we get going. - BackgroundEvent::MonitorUpdatesComplete { - channel_id, .. - } => - *channel_id == prev_channel_id, - } - }); - assert!( - channel_closed || matching_bg_event, - "{:?}", - *background_events - ); - } - (None, None) - } else if definitely_duplicate { - if let Some(other_chan) = chan_to_release { - (Some(MonitorUpdateCompletionAction::FreeOtherChannelImmediately { - downstream_counterparty_node_id: other_chan.counterparty_node_id, - downstream_channel_id: other_chan.channel_id, - blocking_action: other_chan.blocking_action, - }), None) - } else { - (None, None) - } - } else { - let total_fee_earned_msat = - if let Some(forwarded_htlc_value) = forwarded_htlc_value_msat { - if let Some(claimed_htlc_value) = htlc_claim_value_msat { - Some(claimed_htlc_value - forwarded_htlc_value) - } else { - None - } - } else { - None - }; - debug_assert!( - skimmed_fee_msat <= total_fee_earned_msat, - "skimmed_fee_msat must always be included in total_fee_earned_msat" - ); - ( - Some(MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { - event: events::Event::PaymentForwarded { - prev_channel_id: Some(prev_channel_id), - next_channel_id: Some(next_channel_id), - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id: Some(next_channel_counterparty_node_id), - total_fee_earned_msat, - skimmed_fee_msat, - claim_from_onchain_tx: from_onchain, - outbound_amount_forwarded_msat: forwarded_htlc_value_msat, - }, - downstream_counterparty_and_funding_outpoint: chan_to_release, - }), - None, - ) - } - }, - ); - }, - } - } + for (i, current_previous_hop_data) in previous_hop_data.into_iter().enumerate() { + self.claim_funds_from_htlc_forward_hop( + payment_preimage, + |_: Option<u64>| -> Option<events::Event> { + if i == 0 { + let Some(prev_htlcs) = event_prev_htlcs.take() else { + debug_assert!( + false, + "trampoline forward event already emitted" + ); + return None; + }; + Some(events::Event::PaymentForwarded { + prev_htlcs, + // TODO: When trampoline payments are tracked in our + // pending_outbound_payments, we'll be able to provide all the + // outgoing htlcs for this forward. + next_htlcs: vec![events::HTLCLocator { + channel_id: next_channel_id, + amount_msat: Some(forwarded_htlc_value_msat), + user_channel_id: next_user_channel_id, + node_id: Some(next_channel_counterparty_node_id), + }], + // TODO: When trampoline payments are tracked in our + // pending_outbound_payments, we'll be able to lookup our total + // fee earnings. + total_fee_earned_msat: None, + skimmed_fee_msat, + claim_from_onchain_tx: from_onchain, + // TODO: When trampoline payments are tracked in our + // pending_outbound_payments, set to the total amount sent (not + // just the amount of the outgoing htlc that was first settled). + outbound_amount_forwarded_msat: forwarded_htlc_value_msat, + }) + } else { + None + } + }, + startup_replay, + next_channel_counterparty_node_id, + next_channel_outpoint, + next_channel_id, + current_previous_hop_data, + attribution_data.clone(), + send_timestamp, + ); + } + }, + } + } /// Gets the node_id held by this ChannelManager pub fn get_our_node_id(&self) -> PublicKey { @@ -9582,11 +10671,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// Handles actions which need to complete after a [`ChannelMonitorUpdate`] has been applied /// which can happen after the per-peer state lock has been dropped. fn post_monitor_update_unlock( - &self, channel_id: ChannelId, counterparty_node_id: PublicKey, - unbroadcasted_batch_funding_txid: Option<Txid>, - update_actions: Vec<MonitorUpdateCompletionAction>, - htlc_forwards: Option<PerSourcePendingForward>, - decode_update_add_htlcs: Option<(u64, Vec<msgs::UpdateAddHTLC>)>, + &self, channel_id: ChannelId, counterparty_node_id: PublicKey, funding_txo: OutPoint, + user_channel_id: u128, unbroadcasted_batch_funding_txid: Option<Txid>, + update_actions: Vec<MonitorUpdateCompletionAction>, htlc_forwards: Vec<PendingAddHTLCInfo>, finalized_claimed_htlcs: Vec<(HTLCSource, Option<AttributionData>)>, failed_htlcs: Vec<(HTLCSource, PaymentHash, HTLCFailReason)>, committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, @@ -9646,21 +10733,25 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ self.handle_monitor_update_completion_actions(update_actions); - if let Some(forwards) = htlc_forwards { - self.forward_htlcs(&mut [forwards][..]); - } - if let Some(decode) = decode_update_add_htlcs { - self.push_decode_update_add_htlcs(decode); - } + self.forward_htlcs(htlc_forwards); self.finalize_claims(finalized_claimed_htlcs); for failure in failed_htlcs { - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id), - channel_id, - }; - self.fail_htlc_backwards_internal(&failure.0, &failure.1, &failure.2, receiver, None); + let failure_type = failure.0.failure_type(counterparty_node_id, channel_id); + self.fail_htlc_backwards_internal( + &failure.0, + &failure.1, + &failure.2, + failure_type, + None, + ); } - self.prune_persisted_inbound_htlc_onions(committed_outbound_htlc_sources); + self.prune_persisted_inbound_htlc_onions( + channel_id, + counterparty_node_id, + funding_txo, + user_channel_id, + committed_outbound_htlc_sources, + ); } fn handle_monitor_update_completion_actions< @@ -9755,7 +10846,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ receiver_node_id: Some(receiver_node_id), htlcs, sender_intended_total_msat, - onion_fields, + onion_fields: Some(onion_fields), payment_id, }; let action = if let Some((outpoint, counterparty_node_id, channel_id)) = @@ -9785,20 +10876,20 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } }, - MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { + MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { event, downstream_counterparty_and_funding_outpoint, } => { - self.pending_events.lock().unwrap().push_back((event, None)); - if let Some(unblocked) = downstream_counterparty_and_funding_outpoint { - self.handle_monitor_update_release( - unblocked.counterparty_node_id, - unblocked.channel_id, - Some(unblocked.blocking_action), - ); + if let Some(event) = event { + self.pending_events.lock().unwrap().push_back((event, None)); } + self.handle_monitor_update_release( + downstream_counterparty_and_funding_outpoint.counterparty_node_id, + downstream_counterparty_and_funding_outpoint.channel_id, + Some(downstream_counterparty_and_funding_outpoint.blocking_action), + ); }, - MonitorUpdateCompletionAction::FreeOtherChannelImmediately { + MonitorUpdateCompletionAction::FreeDuplicateClaimImmediately { downstream_counterparty_node_id, downstream_channel_id, blocking_action, @@ -9840,11 +10931,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // During startup, we push monitor updates as background events through to here in // order to replay updates that were in-flight when we shut down. Thus, we have to // filter for uniqueness here. - let update_idx = - in_flight_updates.iter().position(|upd| upd == &new_update).unwrap_or_else(|| { - in_flight_updates.push(new_update); - in_flight_updates.len() - 1 - }); + let existing_idx = in_flight_updates.iter().position(|upd| upd == &new_update); + let is_replay = existing_idx.is_some(); + let update_idx = existing_idx.unwrap_or_else(|| { + in_flight_updates.push(new_update); + in_flight_updates.len() - 1 + }); if self.background_events_processed_since_startup.load(Ordering::Acquire) { let update_res = @@ -9855,6 +10947,22 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if update_completed { let _ = in_flight_updates.remove(update_idx); } + // A Watch implementation must not return Completed while prior updates are + // still InProgress, as this would violate the async persistence contract. + // We skip this check for replayed updates (startup background events) + // because during startup replay, the remaining in-flight updates may not + // have been submitted to the Watch yet and will be processed by subsequent + // background events. This is specifically necessary when switching from + // async to sync persistence across a restart: the replayed update + // returns Completed from the now-sync Watch while earlier in-flight + // updates are still queued as background events. + #[cfg(test)] + let skip_check = self.skip_monitor_update_assertion.load(Ordering::Relaxed); + #[cfg(not(test))] + let skip_check = false; + if !skip_check && !is_replay && update_completed && !in_flight_updates.is_empty() { + panic!("Watch::update_channel returned Completed while prior updates are still InProgress"); + } (update_completed, update_completed && in_flight_updates.is_empty()) } else { // We blindly assume that the ChannelMonitorUpdate will be regenerated on startup if we @@ -9920,23 +11028,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ panic!("{}", err_str); }, ChannelMonitorUpdateStatus::InProgress => { - #[cfg(not(any(test, feature = "_externalize_tests")))] - if self.monitor_update_type.swap(1, Ordering::Relaxed) == 2 { - panic!("Cannot use both ChannelMonitorUpdateStatus modes InProgress and Completed without restart"); - } log_debug!( logger, "ChannelMonitor update in flight, holding messages until the update completes.", ); false }, - ChannelMonitorUpdateStatus::Completed => { - #[cfg(not(any(test, feature = "_externalize_tests")))] - if self.monitor_update_type.swap(2, Ordering::Relaxed) == 1 { - panic!("Cannot use both ChannelMonitorUpdateStatus modes InProgress and Completed without restart"); - } - true - }, + ChannelMonitorUpdateStatus::Completed => true, } } @@ -10042,7 +11140,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// /// If the channel has no more blocked monitor updates, this resumes normal operation by /// calling [`Self::handle_channel_resumption`] and returns the remaining work to process - /// after locks are released. If blocked updates remain, only the update actions are returned. + /// after locks are released. If blocked updates remain, only the update actions are returned + /// and the caller should persist if any are present. + /// + /// This method also determines whether the prepared work mutates `ChannelManager` state in a + /// way that should be persisted before returning control to the caller. /// /// Note: This method takes individual fields from [`PeerState`] rather than the whole struct /// to avoid borrow checker issues when the channel is borrowed from `peer_state.channel_by_id`. @@ -10077,7 +11179,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } else { log_debug!(logger, "Channel is open and awaiting update, resuming it"); let updates = chan.monitor_updating_restored( - &&logger, + &logger, &self.node_signer, self.chain_hash, &*self.config.read().unwrap(), @@ -10104,6 +11206,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ None }; + let unbroadcasted_batch_funding_txid = + chan.context.unbroadcasted_batch_funding_txid(&chan.funding); + let mut needs_persist = updates.requires_channel_manager_persistence + || !update_actions.is_empty() + || unbroadcasted_batch_funding_txid.is_some(); + let (htlc_forwards, decode_update_add_htlcs) = self.handle_channel_resumption( pending_msg_events, chan, @@ -10115,24 +11223,30 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ updates.funding_broadcastable, updates.channel_ready, updates.announcement_sigs, - updates.tx_signatures, + updates.funding_tx_signed, None, updates.channel_ready_order, + TxSignaturesOrder::SignaturesFirst, ); + needs_persist |= !htlc_forwards.is_empty(); + if let Some(upd) = channel_update { pending_msg_events.push(upd); } - let unbroadcasted_batch_funding_txid = - chan.context.unbroadcasted_batch_funding_txid(&chan.funding); + if let Some(update_adds) = decode_update_add_htlcs { + self.push_decode_update_add_htlcs(update_adds); + } PostMonitorUpdateChanResume::Unblocked { + needs_persist, channel_id: chan_id, counterparty_node_id, + funding_txo: chan.funding_outpoint(), + user_channel_id: chan.context.get_user_id(), unbroadcasted_batch_funding_txid, update_actions, htlc_forwards, - decode_update_add_htlcs, finalized_claimed_htlcs: updates.finalized_claimed_htlcs, failed_htlcs: updates.failed_htlcs, committed_outbound_htlc_sources: updates.committed_outbound_htlc_sources, @@ -10144,7 +11258,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// HTLC set on `ChannelManager` read. If an HTLC has been irrevocably forwarded to the outbound /// edge, we no longer need to persist the inbound edge's onion and can prune it here. fn prune_persisted_inbound_htlc_onions( - &self, committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, + &self, outbound_channel_id: ChannelId, outbound_node_id: PublicKey, + outbound_funding_txo: OutPoint, outbound_user_channel_id: u128, + committed_outbound_htlc_sources: Vec<(HTLCPreviousHopData, u64)>, ) { let per_peer_state = self.per_peer_state.read().unwrap(); for (source, outbound_amt_msat) in committed_outbound_htlc_sources { @@ -10161,7 +11277,17 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if let Some(chan) = peer_state.channel_by_id.get_mut(&source.channel_id).and_then(|c| c.as_funded_mut()) { - chan.prune_inbound_htlc_onion(source.htlc_id, source, outbound_amt_msat); + chan.prune_inbound_htlc_onion( + source.htlc_id, + &source, + OutboundHop { + amt_msat: outbound_amt_msat, + channel_id: outbound_channel_id, + node_id: outbound_node_id, + funding_txo: outbound_funding_txo, + user_channel_id: outbound_user_channel_id, + }, + ); } } } @@ -10185,10 +11311,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state = per_peer_state.get(&cp_id).map(|state| state.lock().unwrap()).unwrap(); let chan = peer_state.channel_by_id.get(&chan_id).and_then(|c| c.as_funded()).unwrap(); - chan.inbound_committed_unresolved_htlcs() - .iter() - .filter(|(_, htlc)| matches!(htlc, InboundUpdateAdd::WithOnion { .. })) - .count() + chan.inbound_htlcs_pending_decode().count() } #[cfg(test)] @@ -10206,7 +11329,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// Processes the [`PostMonitorUpdateChanResume`] returned by /// [`Self::try_resume_channel_post_monitor_update`], handling update actions and any /// remaining work that requires locks to be released (e.g., forwarding HTLCs, failing HTLCs). - fn handle_post_monitor_update_chan_resume(&self, data: PostMonitorUpdateChanResume) { + /// + /// Returns whether the completed work mutated `ChannelManager` state in a way that should be + /// persisted before returning control to the caller. In other words, this method executes the + /// prepared post-monitor-update work and reports whether the caller should treat monitor + /// completion as requiring `ChannelManager` persistence. + #[must_use = "callers must either persist when true or explicitly discard the result"] + fn handle_post_monitor_update_chan_resume(&self, data: PostMonitorUpdateChanResume) -> bool { debug_assert_ne!(self.per_peer_state.held_by_thread(), LockHeldState::HeldByThread); #[cfg(debug_assertions)] for (_, peer) in self.per_peer_state.read().unwrap().iter() { @@ -10215,15 +11344,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ match data { PostMonitorUpdateChanResume::Blocked { update_actions } => { + let needs_persist = !update_actions.is_empty(); self.handle_monitor_update_completion_actions(update_actions); + needs_persist }, PostMonitorUpdateChanResume::Unblocked { + needs_persist, channel_id, counterparty_node_id, + funding_txo, + user_channel_id, unbroadcasted_batch_funding_txid, update_actions, htlc_forwards, - decode_update_add_htlcs, finalized_claimed_htlcs, failed_htlcs, committed_outbound_htlc_sources, @@ -10231,14 +11364,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ self.post_monitor_update_unlock( channel_id, counterparty_node_id, + funding_txo, + user_channel_id, unbroadcasted_batch_funding_txid, update_actions, htlc_forwards, - decode_update_add_htlcs, finalized_claimed_htlcs, failed_htlcs, committed_outbound_htlc_sources, ); + needs_persist }, } } @@ -10252,31 +11387,38 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ pending_forwards: Vec<(PendingHTLCInfo, u64)>, pending_update_adds: Vec<msgs::UpdateAddHTLC>, funding_broadcastable: Option<Transaction>, channel_ready: Option<msgs::ChannelReady>, announcement_sigs: Option<msgs::AnnouncementSignatures>, - tx_signatures: Option<msgs::TxSignatures>, tx_abort: Option<msgs::TxAbort>, - channel_ready_order: ChannelReadyOrder, - ) -> (Option<(u64, PublicKey, OutPoint, ChannelId, u128, Vec<(PendingHTLCInfo, u64)>)>, Option<(u64, Vec<msgs::UpdateAddHTLC>)>) { + mut funding_tx_signed: Option<FundingTxSigned>, tx_abort: Option<msgs::TxAbort>, + channel_ready_order: ChannelReadyOrder, tx_signatures_order: TxSignaturesOrder, + ) -> (Vec<PendingAddHTLCInfo>, Option<(u64, Vec<msgs::UpdateAddHTLC>)>) { let logger = WithChannelContext::from(&self.logger, &channel.context, None); - log_trace!(logger, "Handling channel resumption with {} RAA, {} commitment update, {} pending forwards, {} pending update_add_htlcs, {}broadcasting funding, {} channel ready, {} announcement, {} tx_signatures, {} tx_abort", + log_trace!(logger, "Handling channel resumption with {} RAA, {} commitment update, {} pending forwards, {} pending update_add_htlcs, {}broadcasting funding, {} channel ready, {} announcement, {} tx_signatures, {} tx_abort, {} splice_locked", if raa.is_some() { "an" } else { "no" }, if commitment_update.is_some() { "a" } else { "no" }, pending_forwards.len(), pending_update_adds.len(), if funding_broadcastable.is_some() { "" } else { "not " }, if channel_ready.is_some() { "sending" } else { "without" }, if announcement_sigs.is_some() { "sending" } else { "without" }, - if tx_signatures.is_some() { "sending" } else { "without" }, + if funding_tx_signed.as_ref().map(|v| v.tx_signatures.is_some()).unwrap_or(false) { "sending" } else { "without" }, if tx_abort.is_some() { "sending" } else { "without" }, + if funding_tx_signed.as_ref().map(|v| v.splice_locked.is_some()).unwrap_or(false) { "sending" } else { "without" }, ); let counterparty_node_id = channel.context.get_counterparty_node_id(); let outbound_scid_alias = channel.context.outbound_scid_alias(); - let mut htlc_forwards = None; + let mut htlc_forwards = Vec::new(); if !pending_forwards.is_empty() { - htlc_forwards = Some(( - outbound_scid_alias, channel.context.get_counterparty_node_id(), - channel.funding.get_funding_txo().unwrap(), channel.context.channel_id(), - channel.context.get_user_id(), pending_forwards - )); + htlc_forwards = pending_forwards.into_iter().map(|(forward_info, prev_htlc_id)| { + PendingAddHTLCInfo { + forward_info, + prev_outbound_scid_alias: outbound_scid_alias, + prev_htlc_id, + prev_counterparty_node_id: counterparty_node_id, + prev_channel_id: channel.context.channel_id(), + prev_funding_outpoint: channel.funding.get_funding_txo().unwrap(), + prev_user_channel_id: channel.context.get_user_id(), + } + }).collect(); } let mut decode_update_add_htlcs = None; if !pending_update_adds.is_empty() { @@ -10297,6 +11439,27 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + if let Some(funding_tx_signed) = funding_tx_signed.as_ref() { + // These [`FundingTxSigned`] fields are only expected as a result of calling + // [`ChannelManager::funding_transaction_signed`]. + debug_assert!(funding_tx_signed.commitment_signed.is_none()); + debug_assert!(funding_tx_signed.counterparty_initial_commitment_signed_result.is_none()); + } + if let TxSignaturesOrder::SignaturesFirst = tx_signatures_order { + if let Some(msg) = funding_tx_signed.as_mut().and_then(|v| v.tx_signatures.take()) { + pending_msg_events.push(MessageSendEvent::SendTxSignatures { + node_id: counterparty_node_id, + msg, + }); + } + } + if let Some(msg) = funding_tx_signed.as_mut().and_then(|v| v.splice_locked.take()) { + pending_msg_events.push(MessageSendEvent::SendSpliceLocked { + node_id: counterparty_node_id, + msg, + }); + } + macro_rules! handle_cs { () => { if let Some(update) = commitment_update { pending_msg_events.push(MessageSendEvent::UpdateHTLCs { @@ -10325,11 +11488,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, } - if let Some(msg) = tx_signatures { - pending_msg_events.push(MessageSendEvent::SendTxSignatures { - node_id: counterparty_node_id, - msg, - }); + if let TxSignaturesOrder::CommitmentFirst = tx_signatures_order { + if let Some(msg) = funding_tx_signed.as_mut().and_then(|v| v.tx_signatures.take()) { + pending_msg_events.push(MessageSendEvent::SendTxSignatures { + node_id: counterparty_node_id, + msg, + }); + } } if let Some(msg) = tx_abort { pending_msg_events.push(MessageSendEvent::SendTxAbort { @@ -10354,6 +11519,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ self.send_channel_ready(pending_msg_events, channel, msg); } + // If we just finished a pending interactive funding negotiation and are ready to broadcast + // the transaction, `funding_broadcastable` will only contain the transaction for a + // dual-funded channel. Splice transactions need to be broadcast separately. if let Some(tx) = funding_broadcastable { if channel.context.is_manual_broadcast() { log_info!(logger, "Not broadcasting funding transaction with txid {} as it is manually managed", tx.compute_txid()); @@ -10368,31 +11536,61 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }; } else { + if let Some((funding_tx, tx_type)) = funding_tx_signed.as_ref().and_then(|v| v.funding_tx.as_ref()) { + debug_assert_eq!(&tx, funding_tx); + debug_assert!(matches!(tx_type, TransactionType::Funding { .. })); + } log_info!(logger, "Broadcasting funding transaction with txid {}", tx.compute_txid()); self.tx_broadcaster.broadcast_transactions(&[( &tx, TransactionType::Funding { channels: vec![(counterparty_node_id, channel.context.channel_id())] }, )]); } + } else if let Some((tx, tx_type)) = funding_tx_signed + .as_mut() + .and_then(|v| v.funding_tx.take()) + .filter(|(_, tx_type)| matches!(tx_type, TransactionType::InteractiveFunding { .. })) + { + log_info!(logger, "Broadcasting interactively funded transaction with txid {}", tx.compute_txid()); + self.tx_broadcaster.broadcast_transactions(&[(&tx, tx_type)]); } { let mut pending_events = self.pending_events.lock().unwrap(); emit_channel_pending_event!(pending_events, channel); emit_initial_channel_ready_event!(pending_events, channel); + if let Some(splice_negotiated) = funding_tx_signed + .as_mut() + .and_then(|v| v.splice_negotiated.take()) + { + if splice_negotiated.has_local_contribution { + pending_events.push_back(( + events::Event::SpliceNegotiated { + channel_id: channel.context.channel_id(), + counterparty_node_id, + user_channel_id: channel.context.get_user_id(), + new_funding_txo: splice_negotiated.funding_txo, + channel_type: splice_negotiated.channel_type, + new_funding_redeem_script: splice_negotiated.funding_redeem_script, + }, + None, + )); + } + } } (htlc_forwards, decode_update_add_htlcs) } #[rustfmt::skip] - fn channel_monitor_updated(&self, channel_id: &ChannelId, highest_applied_update_id: Option<u64>, counterparty_node_id: &PublicKey) { + #[must_use = "callers must either persist when true or explicitly discard the result"] + fn channel_monitor_updated(&self, channel_id: &ChannelId, highest_applied_update_id: Option<u64>, counterparty_node_id: &PublicKey) -> bool { debug_assert!(self.total_consistency_lock.try_write().is_err()); // Caller holds read lock let per_peer_state = self.per_peer_state.read().unwrap(); let mut peer_state_lock; let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id); - if peer_state_mutex_opt.is_none() { return } + if peer_state_mutex_opt.is_none() { return false; } peer_state_lock = peer_state_mutex_opt.unwrap().lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -10424,7 +11622,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } else { 0 }; if remaining_in_flight != 0 { - return; + return false; } if let Some(chan) = peer_state.channel_by_id @@ -10445,10 +11643,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(completion_data); + let needs_persist = self.handle_post_monitor_update_chan_resume(completion_data); self.handle_holding_cell_free_result(holding_cell_res); + needs_persist } else { log_trace!(logger, "Channel is open but not awaiting update"); + false } } else { let update_actions = peer_state.monitor_update_blocked_actions @@ -10456,7 +11656,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ log_trace!(logger, "Channel is closed, applying {} post-update actions", update_actions.len()); mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_monitor_update_completion_actions(update_actions); + if !update_actions.is_empty() { + self.handle_monitor_update_completion_actions(update_actions); + true + } else { + false + } } } @@ -10468,10 +11673,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// /// The `user_channel_id` parameter will be provided back in /// [`Event::ChannelClosed::user_channel_id`] to allow tracking of which events correspond - /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer_0conf` call. + /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer` call. /// /// Note that this method will return an error and reject the channel, if it requires support - /// for zero confirmations. Instead, `accept_inbound_channel_from_trusted_peer_0conf` must be + /// for zero confirmations. Instead, `accept_inbound_channel_from_trusted_peer` must be /// used to accept such channels. /// /// NOTE: LDK makes no attempt to prevent the counterparty from using non-standard inputs which @@ -10487,38 +11692,32 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ self.do_accept_inbound_channel( temporary_channel_id, counterparty_node_id, - false, + None, user_channel_id, config_overrides, ) } - /// Accepts a request to open a channel after a [`Event::OpenChannelRequest`], treating - /// it as confirmed immediately. + /// Accepts a request to open a channel after a [`Event::OpenChannelRequest`]. Unlike + /// [`ChannelManager::accept_inbound_channel`], this method allows some combination of the + /// zero-conf and zero-reserve features to be set for the channel, see a description of these + /// features in [`TrustedChannelFeatures`]. /// /// The `user_channel_id` parameter will be provided back in /// [`Event::ChannelClosed::user_channel_id`] to allow tracking of which events correspond - /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer_0conf` call. - /// - /// Unlike [`ChannelManager::accept_inbound_channel`], this method accepts the incoming channel - /// and (if the counterparty agrees), enables forwarding of payments immediately. - /// - /// This fully trusts that the counterparty has honestly and correctly constructed the funding - /// transaction and blindly assumes that it will eventually confirm. - /// - /// If it does not confirm before we decide to close the channel, or if the funding transaction - /// does not pay to the correct script the correct amount, *you will lose funds*. + /// with which `accept_inbound_channel`/`accept_inbound_channel_from_trusted_peer` call. /// /// [`Event::OpenChannelRequest`]: events::Event::OpenChannelRequest /// [`Event::ChannelClosed::user_channel_id`]: events::Event::ChannelClosed::user_channel_id - pub fn accept_inbound_channel_from_trusted_peer_0conf( + pub fn accept_inbound_channel_from_trusted_peer( &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, - user_channel_id: u128, config_overrides: Option<ChannelConfigOverrides>, + user_channel_id: u128, trusted_channel_features: TrustedChannelFeatures, + config_overrides: Option<ChannelConfigOverrides>, ) -> Result<(), APIError> { self.do_accept_inbound_channel( temporary_channel_id, counterparty_node_id, - true, + Some(trusted_channel_features), user_channel_id, config_overrides, ) @@ -10527,7 +11726,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// TODO(dual_funding): Allow contributions, pass intended amount and inputs fn do_accept_inbound_channel( &self, temporary_channel_id: &ChannelId, counterparty_node_id: &PublicKey, - accept_0conf: bool, user_channel_id: u128, + trusted_channel_features: Option<TrustedChannelFeatures>, user_channel_id: u128, config_overrides: Option<ChannelConfigOverrides>, ) -> Result<(), APIError> { let mut config = self.config.read().unwrap().clone(); @@ -10550,11 +11749,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { log_error!(logger, "Can't find peer matching the passed counterparty node_id"); - - let err_str = format!( - "Can't find a peer matching the passed counterparty node_id {counterparty_node_id}" - ); - APIError::ChannelUnavailable { err: err_str } + APIError::no_such_peer(counterparty_node_id) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -10580,7 +11775,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &config, best_block_height, &self.logger, - accept_0conf, + trusted_channel_features, ) .map_err(|err| { MsgHandleErrInternal::from_chan_no_close(err, *temporary_channel_id) @@ -10609,6 +11804,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &config, best_block_height, &self.logger, + trusted_channel_features, ) .map_err(|e| { let channel_id = open_channel_msg.common_fields.temporary_channel_id; @@ -10635,29 +11831,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, }; - // We have to match below instead of map_err on the above as in the map_err closure the borrow checker - // would consider peer_state moved even though we would bail out with the `?` operator. - let (channel_id, mut channel, message_send_event) = match res { - Ok(res) => res, - Err(err) => { - mem::drop(peer_state_lock); - mem::drop(per_peer_state); - // TODO(dunxen): Find/make less icky way to do this. - match self.handle_error( - Result::<(), MsgHandleErrInternal>::Err(err), - *counterparty_node_id, - ) { - Ok(_) => { - unreachable!("`handle_error` only returns Err as we've passed in an Err") - }, - Err(e) => { - return Err(APIError::ChannelUnavailable { err: e.err }); - }, - } - }, + let Ok((channel_id, mut channel, message_send_event)) = res else { + mem::drop(peer_state_lock); + mem::drop(per_peer_state); + let e = self.handle_error::<()>(res.map(|_| ()), *counterparty_node_id).unwrap_err(); + return Err(APIError::ChannelUnavailable { err: e.err }); }; - if accept_0conf { + if trusted_channel_features.is_some_and(|f| f.is_0conf()) { // This should have been correctly configured by the call to Inbound(V1/V2)Channel::new. debug_assert!(channel.minimum_depth().unwrap() == 0); } else if channel.funding().get_channel_type().requires_zero_conf() { @@ -10672,7 +11853,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }; debug_assert!(peer_state.is_connected); peer_state.pending_msg_events.push(send_msg_err_event); - let err_str = "Please use accept_inbound_channel_from_trusted_peer_0conf to accept channels with zero confirmations.".to_owned(); + let err_str = "Please use accept_inbound_channel_from_trusted_peer to accept channels with zero confirmations.".to_owned(); log_error!(logger, "{}", err_str); return Err(APIError::APIMisuseError { err: err_str }); @@ -10811,10 +11992,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - common_fields.temporary_channel_id) + MsgHandleErrInternal::unreachable_no_such_peer( + counterparty_node_id, + common_fields.temporary_channel_id, + ) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -10881,11 +12062,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // likely to be lost on restart! let (value, output_script, user_id) = { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.common_fields.temporary_channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer( + counterparty_node_id, + msg.common_fields.temporary_channel_id, + ) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.common_fields.temporary_channel_id) { @@ -10905,7 +12087,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.common_fields.temporary_channel_id)) + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.common_fields.temporary_channel_id)) } }; let mut pending_events = self.pending_events.lock().unwrap(); @@ -10925,49 +12107,61 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let best_block = *self.best_block.read().unwrap(); let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.temporary_channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer( + counterparty_node_id, + msg.temporary_channel_id, + ) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - let (mut chan, funding_msg_opt, monitor) = - match peer_state.channel_by_id.remove(&msg.temporary_channel_id) - .map(Channel::into_unfunded_inbound_v1) - { - Some(Ok(inbound_chan)) => { - let logger = WithChannelContext::from(&self.logger, &inbound_chan.context, None); - match inbound_chan.funding_created(msg, best_block, &self.signer_provider, &&logger) { - Ok(res) => res, - Err((inbound_chan, err)) => { - // We've already removed this inbound channel from the map in `PeerState` - // above so at this point we just need to clean up any lingering entries - // concerning this channel as it is safe to do so. - debug_assert!(matches!(err, ChannelError::Close(_))); - let mut chan = Channel::from(inbound_chan); - return Err(self.locked_handle_force_close( + let (mut chan, funding_msg_opt, monitor) = match peer_state + .channel_by_id + .remove(&msg.temporary_channel_id) + .map(Channel::into_unfunded_inbound_v1) + { + Some(Ok(inbound_chan)) => { + let logger = WithChannelContext::from(&self.logger, &inbound_chan.context, None); + match inbound_chan.funding_created(msg, best_block, &self.signer_provider, &&logger) + { + Ok(res) => res, + Err((inbound_chan, err)) => { + // We've already removed this inbound channel from the map in `PeerState` + // above so at this point we just need to clean up any lingering entries + // concerning this channel as it is safe to do so. + debug_assert!(matches!(err, ChannelError::Close(_))); + let mut chan = Channel::from(inbound_chan); + return Err(self + .locked_handle_force_close( &mut peer_state.closed_channel_monitor_update_ids, &mut peer_state.in_flight_monitor_updates, err, &mut chan, - ).1); - }, - } - }, - Some(Err(mut chan)) => { - let err_msg = format!("Got an unexpected funding_created message from peer with counterparty_node_id {}", counterparty_node_id); - let err = ChannelError::close(err_msg); - return Err(self.locked_handle_force_close( + ) + .1); + }, + } + }, + Some(Err(mut chan)) => { + let err_msg = format!("Got an unexpected funding_created message from peer with counterparty_node_id {}", counterparty_node_id); + let err = ChannelError::close(err_msg); + return Err(self + .locked_handle_force_close( &mut peer_state.closed_channel_monitor_update_ids, &mut peer_state.in_flight_monitor_updates, err, &mut chan, - ).1); - }, - None => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.temporary_channel_id)) - }; + ) + .1); + }, + None => { + return Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.temporary_channel_id, + )) + }, + }; let funded_channel_id = chan.context.channel_id(); @@ -11013,7 +12207,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } } else { unreachable!("This must be a funded channel as we just inserted it."); @@ -11111,11 +12305,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ #[rustfmt::skip] fn internal_peer_storage(&self, counterparty_node_id: PublicKey, msg: msgs::PeerStorage) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(&counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), ChannelId([0; 32])) - })?; + let peer_state_mutex = per_peer_state.get(&counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(&counterparty_node_id, ChannelId([0; 32])) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -11149,11 +12341,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn internal_funding_signed(&self, counterparty_node_id: &PublicKey, msg: &msgs::FundingSigned) -> Result<(), MsgHandleErrInternal> { let best_block = *self.best_block.read().unwrap(); let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -11187,7 +12377,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } Ok(()) }, @@ -11198,21 +12388,41 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } + fn handle_interactive_tx_msg_err( + &self, err: InteractiveTxMsgError, channel_id: ChannelId, counterparty_node_id: &PublicKey, + user_channel_id: u128, + ) -> MsgHandleErrInternal { + let (err, splice_failure) = err.into_parts(); + if let Some((splice_funding_failed, reason)) = splice_failure { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + let pending_events = &mut self.pending_events.lock().unwrap(); + if let Some(funding_info) = funding_info { + pending_events + .push_back((events::Event::DiscardFunding { channel_id, funding_info }, None)); + } + pending_events.push_back(( + events::Event::SpliceNegotiationFailed { + channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id, + contribution: Some(contribution), + reason, + }, + None, + )); + } + MsgHandleErrInternal::from_chan_no_close(err, channel_id) + } + fn internal_tx_msg< - HandleTxMsgFn: Fn( - &mut Channel<SP>, - ) -> Result<InteractiveTxMessageSend, (ChannelError, Option<SpliceFundingFailed>)>, + HandleTxMsgFn: Fn(&mut Channel<SP>) -> Result<InteractiveTxMessageSend, InteractiveTxMsgError>, >( &self, counterparty_node_id: &PublicKey, channel_id: ChannelId, tx_msg_handler: HandleTxMsgFn, - ) -> Result<NotifyOption, MsgHandleErrInternal> { + ) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - channel_id, - ) + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, channel_id) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -11223,37 +12433,29 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ Ok(msg_send) => { let msg_send_event = msg_send.into_msg_send_event(*counterparty_node_id); peer_state.pending_msg_events.push(msg_send_event); - Ok(NotifyOption::SkipPersistHandleEvents) + Ok(()) }, - Err((error, splice_funding_failed)) => { - if let Some(splice_funding_failed) = splice_funding_failed { - let pending_events = &mut self.pending_events.lock().unwrap(); - pending_events.push_back((events::Event::SpliceFailed { - channel_id, - counterparty_node_id: *counterparty_node_id, - user_channel_id: channel.context().get_user_id(), - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type.clone(), - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, - }, None)); - } - Err(MsgHandleErrInternal::from_chan_no_close(error, channel_id)) + Err(err) => { + let user_channel_id = channel.context().get_user_id(); + Err(self.handle_interactive_tx_msg_err( + err, + channel_id, + counterparty_node_id, + user_channel_id, + )) }, } }, - hash_map::Entry::Vacant(_) => { - Err(MsgHandleErrInternal::send_err_msg_no_close(format!( - "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", - counterparty_node_id), channel_id) - ) - } + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + channel_id, + )), } } fn internal_tx_add_input( &self, counterparty_node_id: PublicKey, msg: &msgs::TxAddInput, - ) -> Result<NotifyOption, MsgHandleErrInternal> { + ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| { channel.tx_add_input(msg, &self.logger) }) @@ -11261,7 +12463,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn internal_tx_add_output( &self, counterparty_node_id: PublicKey, msg: &msgs::TxAddOutput, - ) -> Result<NotifyOption, MsgHandleErrInternal> { + ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| { channel.tx_add_output(msg, &self.logger) }) @@ -11269,7 +12471,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn internal_tx_remove_input( &self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveInput, - ) -> Result<NotifyOption, MsgHandleErrInternal> { + ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| { channel.tx_remove_input(msg, &self.logger) }) @@ -11277,22 +12479,19 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn internal_tx_remove_output( &self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveOutput, - ) -> Result<NotifyOption, MsgHandleErrInternal> { + ) -> Result<(), MsgHandleErrInternal> { self.internal_tx_msg(&counterparty_node_id, msg.channel_id, |channel: &mut Channel<SP>| { channel.tx_remove_output(msg, &self.logger) }) } - #[rustfmt::skip] - fn internal_tx_complete(&self, counterparty_node_id: PublicKey, msg: &msgs::TxComplete) -> Result<NotifyOption, MsgHandleErrInternal> { + fn internal_tx_complete( + &self, counterparty_node_id: PublicKey, msg: &msgs::TxComplete, + ) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(&counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(&counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(&counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { @@ -11300,12 +12499,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let chan = chan_entry.get_mut(); match chan.tx_complete(msg, &self.fee_estimator, &self.logger) { Ok(tx_complete_result) => { - let mut persist = NotifyOption::SkipPersistNoEvents; - - if let Some(interactive_tx_msg_send) = tx_complete_result.interactive_tx_msg_send { - let msg_send_event = interactive_tx_msg_send.into_msg_send_event(counterparty_node_id); + if let Some(interactive_tx_msg_send) = + tx_complete_result.interactive_tx_msg_send + { + let msg_send_event = + interactive_tx_msg_send.into_msg_send_event(counterparty_node_id); peer_state.pending_msg_events.push(msg_send_event); - persist = NotifyOption::SkipPersistHandleEvents; }; if let Some(unsigned_transaction) = tx_complete_result.event_unsigned_tx { @@ -11318,9 +12517,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, None, )); - // // We have a successful signing session that we need to persist. - persist = NotifyOption::DoPersist; + self.needs_persist_flag.store(true, Ordering::Release); + self.event_persist_notifier.notify() } if let Some(FundingTxSigned { @@ -11356,172 +12555,224 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); } if let Some(tx_signatures) = tx_signatures { - peer_state.pending_msg_events.push(MessageSendEvent::SendTxSignatures { - node_id: counterparty_node_id, - msg: tx_signatures, - }); + peer_state.pending_msg_events.push( + MessageSendEvent::SendTxSignatures { + node_id: counterparty_node_id, + msg: tx_signatures, + }, + ); } // We have a successful signing session that we need to persist. - persist = NotifyOption::DoPersist; + self.needs_persist_flag.store(true, Ordering::Release); + self.event_persist_notifier.notify() } - Ok(persist) + Ok(()) }, - Err((error, splice_funding_failed)) => { - if let Some(splice_funding_failed) = splice_funding_failed { - let pending_events = &mut self.pending_events.lock().unwrap(); - pending_events.push_back((events::Event::SpliceFailed { - channel_id: msg.channel_id, - counterparty_node_id, - user_channel_id: chan.context().get_user_id(), - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type.clone(), - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, - }, None)); - } - Err(MsgHandleErrInternal::from_chan_no_close(error, msg.channel_id)) + Err(err) => { + let user_channel_id = chan.context().get_user_id(); + Err(self.handle_interactive_tx_msg_err( + err, + msg.channel_id, + &counterparty_node_id, + user_channel_id, + )) }, } }, - hash_map::Entry::Vacant(_) => { - Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) - } + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::no_such_channel_for_peer( + &counterparty_node_id, + msg.channel_id, + )), } } - #[rustfmt::skip] - fn internal_tx_signatures(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxSignatures) - -> Result<(), MsgHandleErrInternal> { - let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - msg.channel_id) + fn internal_tx_signatures( + &self, counterparty_node_id: &PublicKey, msg: &msgs::TxSignatures, + ) -> Result<(), MsgHandleErrInternal> { + let (result, holding_cell_res) = { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) })?; - let mut peer_state_lock = peer_state_mutex.lock().unwrap(); - let peer_state = &mut *peer_state_lock; - match peer_state.channel_by_id.entry(msg.channel_id) { - hash_map::Entry::Occupied(mut chan_entry) => { - match chan_entry.get_mut().as_funded_mut() { - Some(chan) => { - let best_block_height = self.best_block.read().unwrap().height; - let FundingTxSigned { - commitment_signed, - counterparty_initial_commitment_signed_result, - tx_signatures, - funding_tx, - splice_negotiated, - splice_locked, - } = try_channel_entry!( - self, - peer_state, - chan.tx_signatures(msg, best_block_height, &self.logger), - chan_entry - ); + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_entry) => { + match chan_entry.get_mut().as_funded_mut() { + Some(chan) => { + let best_block_height = self.best_block.read().unwrap().height; + let FundingTxSigned { + commitment_signed, + counterparty_initial_commitment_signed_result, + tx_signatures, + funding_tx, + splice_negotiated, + splice_locked, + } = try_channel_entry!( + self, + peer_state, + chan.tx_signatures(msg, best_block_height, &self.logger), + chan_entry + ); - // We should never be sending a `commitment_signed` in response to their - // `tx_signatures`. - debug_assert!(commitment_signed.is_none()); - debug_assert!(counterparty_initial_commitment_signed_result.is_none()); + // We should never be sending a `commitment_signed` in response to their + // `tx_signatures`. + debug_assert!(commitment_signed.is_none()); + debug_assert!(counterparty_initial_commitment_signed_result.is_none()); - if let Some(tx_signatures) = tx_signatures { - peer_state.pending_msg_events.push(MessageSendEvent::SendTxSignatures { - node_id: *counterparty_node_id, - msg: tx_signatures, - }); - } - if let Some(splice_locked) = splice_locked { - peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceLocked { - node_id: *counterparty_node_id, - msg: splice_locked, - }); - } - if let Some((ref funding_tx, ref tx_type)) = funding_tx { - self.broadcast_interactive_funding(chan, funding_tx, Some(tx_type.clone()), &self.logger); - } - if let Some(splice_negotiated) = splice_negotiated { - self.pending_events.lock().unwrap().push_back(( - events::Event::SplicePending { - channel_id: msg.channel_id, - counterparty_node_id: *counterparty_node_id, - user_channel_id: chan.context.get_user_id(), - new_funding_txo: splice_negotiated.funding_txo, - channel_type: splice_negotiated.channel_type, - new_funding_redeem_script: splice_negotiated.funding_redeem_script, - }, - None, - )); - } - }, - None => { - let msg = "Got an unexpected tx_signatures message"; - let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; - let err = ChannelError::Close((msg.to_owned(), reason)); - try_channel_entry!(self, peer_state, Err(err), chan_entry) - }, - } - Ok(()) - }, - hash_map::Entry::Vacant(_) => { - Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + if let Some(tx_signatures) = tx_signatures { + peer_state.pending_msg_events.push( + MessageSendEvent::SendTxSignatures { + node_id: *counterparty_node_id, + msg: tx_signatures, + }, + ); + } + if let Some(splice_locked) = splice_locked { + peer_state.pending_msg_events.push( + MessageSendEvent::SendSpliceLocked { + node_id: *counterparty_node_id, + msg: splice_locked, + }, + ); + } + if let Some((ref funding_tx, ref tx_type)) = funding_tx { + self.broadcast_interactive_funding( + chan, + funding_tx, + Some(tx_type.clone()), + &self.logger, + ); + } + // We consider a splice negotiated when we exchange `tx_signatures`, + // which also terminates quiescence. + let needs_holding_cell_release = splice_negotiated.is_some(); + if let Some(splice_negotiated) = splice_negotiated { + if splice_negotiated.has_local_contribution { + self.pending_events.lock().unwrap().push_back(( + events::Event::SpliceNegotiated { + channel_id: msg.channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: chan.context.get_user_id(), + new_funding_txo: splice_negotiated.funding_txo, + channel_type: splice_negotiated.channel_type, + new_funding_redeem_script: splice_negotiated + .funding_redeem_script, + }, + None, + )); + } + } + let holding_cell_res = if needs_holding_cell_release { + self.check_free_peer_holding_cells(peer_state) + } else { + Vec::new() + }; + (Ok(()), holding_cell_res) + }, + None => { + let msg = "Got an unexpected tx_signatures message"; + let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; + let err = ChannelError::Close((msg.to_owned(), reason)); + try_channel_entry!(self, peer_state, Err(err), chan_entry) + }, + } + }, + hash_map::Entry::Vacant(_) => ( + Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )), + Vec::new(), + ), } - } + }; + + self.handle_holding_cell_free_result(holding_cell_res); + result } - #[rustfmt::skip] - fn internal_tx_abort(&self, counterparty_node_id: &PublicKey, msg: &msgs::TxAbort) - -> Result<NotifyOption, MsgHandleErrInternal> { - let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - msg.channel_id) + fn internal_tx_abort( + &self, counterparty_node_id: &PublicKey, msg: &msgs::TxAbort, + ) -> Result<NotifyOption, MsgHandleErrInternal> { + let (result, holding_cell_res) = { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) })?; - let mut peer_state_lock = peer_state_mutex.lock().unwrap(); - let peer_state = &mut *peer_state_lock; - match peer_state.channel_by_id.entry(msg.channel_id) { - hash_map::Entry::Occupied(mut chan_entry) => { - let res = chan_entry.get_mut().tx_abort(msg, &self.logger); - let (tx_abort, splice_failed) = try_channel_entry!(self, peer_state, res, chan_entry); + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Occupied(mut chan_entry) => { + let res = chan_entry.get_mut().tx_abort(msg, &self.logger); + let (tx_abort, splice_failed) = + try_channel_entry!(self, peer_state, res, chan_entry); - let persist = if tx_abort.is_some() || splice_failed.is_some() { - NotifyOption::DoPersist - } else { - NotifyOption::SkipPersistNoEvents - }; + let persist = if tx_abort.is_some() || splice_failed.is_some() { + NotifyOption::DoPersist + } else { + NotifyOption::SkipPersistNoEvents + }; - if let Some(tx_abort_msg) = tx_abort { - peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort { - node_id: *counterparty_node_id, - msg: tx_abort_msg, - }); - } + // Release any HTLCs held during quiescence now that we're + // exiting via tx_abort. + let needs_holding_cell_release = tx_abort.is_some(); + if let Some(tx_abort_msg) = tx_abort { + peer_state.pending_msg_events.push(MessageSendEvent::SendTxAbort { + node_id: *counterparty_node_id, + msg: tx_abort_msg, + }); + } - if let Some(splice_funding_failed) = splice_failed { - let pending_events = &mut self.pending_events.lock().unwrap(); - pending_events.push_back((events::Event::SpliceFailed { - channel_id: msg.channel_id, - counterparty_node_id: *counterparty_node_id, - user_channel_id: chan_entry.get().context().get_user_id(), - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, - }, None)); - } + if let Some(splice_funding_failed) = splice_failed { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + let pending_events = &mut self.pending_events.lock().unwrap(); + if let Some(funding_info) = funding_info { + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: msg.channel_id, + funding_info, + }, + None, + )); + } + pending_events.push_back(( + events::Event::SpliceNegotiationFailed { + channel_id: msg.channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: chan_entry.get().context().get_user_id(), + contribution: Some(contribution), + reason: events::NegotiationFailureReason::CounterpartyAborted { + msg: UntrustedString( + String::from_utf8_lossy(&msg.data).to_string(), + ), + }, + }, + None, + )); + } - Ok(persist) - }, - hash_map::Entry::Vacant(_) => { - Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + let holding_cell_res = if needs_holding_cell_release { + self.check_free_peer_holding_cells(peer_state) + } else { + Vec::new() + }; + (Ok(persist), holding_cell_res) + }, + hash_map::Entry::Vacant(_) => ( + Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )), + Vec::new(), + ), } - } + }; + + self.handle_holding_cell_free_result(holding_cell_res); + result } #[rustfmt::skip] @@ -11529,53 +12780,21 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // Note that the ChannelManager is NOT re-persisted on disk after this (unless we error // closing a channel), so any changes are likely to be lost on restart! let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let res = chan.channel_ready( - &msg, - &self.node_signer, - self.chain_hash, - &self.config.read().unwrap(), - &self.best_block.read().unwrap(), - &&logger + let res = self.internal_channel_ready_with_funded_channel( + counterparty_node_id, + msg, + chan, + &mut peer_state.pending_msg_events, ); - let announcement_sigs_opt = - try_channel_entry!(self, peer_state, res, chan_entry); - if let Some(announcement_sigs) = announcement_sigs_opt { - log_trace!(logger, "Sending announcement_signatures"); - peer_state.pending_msg_events.push(MessageSendEvent::SendAnnouncementSignatures { - node_id: counterparty_node_id.clone(), - msg: announcement_sigs, - }); - } else if chan.context.is_usable() { - // If we're sending an announcement_signatures, we'll send the (public) - // channel_update after sending a channel_announcement when we receive our - // counterparty's announcement_signatures. Thus, we only bother to send a - // channel_update here if the channel is not public, i.e. we're not sending an - // announcement_signatures. - log_trace!(logger, "Sending private initial channel_update for our counterparty"); - if let Ok((msg, _, _)) = self.get_channel_update_for_unicast(chan) { - peer_state.pending_msg_events.push(MessageSendEvent::SendChannelUpdate { - node_id: counterparty_node_id.clone(), - msg, - }); - } - } - - { - let mut pending_events = self.pending_events.lock().unwrap(); - emit_initial_channel_ready_event!(pending_events, chan); - } - + try_channel_entry!(self, peer_state, res, chan_entry); Ok(()) } else { try_channel_entry!(self, peer_state, Err(ChannelError::close( @@ -11583,11 +12802,54 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, hash_map::Entry::Vacant(_) => { - Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id)) } } } + #[rustfmt::skip] + fn internal_channel_ready_with_funded_channel( + &self, counterparty_node_id: &PublicKey, msg: &msgs::ChannelReady, + chan: &mut FundedChannel<SP>, pending_msg_events: &mut Vec<MessageSendEvent>, + ) -> Result<(), ChannelError> { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let announcement_sigs_opt = chan.channel_ready( + &msg, + &self.node_signer, + self.chain_hash, + &self.config.read().unwrap(), + &self.best_block.read().unwrap(), + &&logger + )?; + if let Some(announcement_sigs) = announcement_sigs_opt { + log_trace!(logger, "Sending announcement_signatures"); + pending_msg_events.push(MessageSendEvent::SendAnnouncementSignatures { + node_id: counterparty_node_id.clone(), + msg: announcement_sigs, + }); + } else if chan.context.is_usable() { + // If we're sending an announcement_signatures, we'll send the (public) + // channel_update after sending a channel_announcement when we receive our + // counterparty's announcement_signatures. Thus, we only bother to send a + // channel_update here if the channel is not public, i.e. we're not sending an + // announcement_signatures. + log_trace!(logger, "Sending private initial channel_update for our counterparty"); + if let Ok((msg, _, _)) = self.get_channel_update_for_unicast(chan) { + pending_msg_events.push(MessageSendEvent::SendChannelUpdate { + node_id: counterparty_node_id.clone(), + msg, + }); + } + } + + { + let mut pending_events = self.pending_events.lock().unwrap(); + emit_initial_channel_ready_event!(pending_events, chan); + } + + Ok(()) + } + fn internal_shutdown( &self, counterparty_node_id: &PublicKey, msg: &msgs::Shutdown, ) -> Result<(), MsgHandleErrInternal> { @@ -11595,14 +12857,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!( - "Can't find a peer matching the passed counterparty node_id {}", - counterparty_node_id - ), - msg.channel_id, - ) + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -11626,19 +12881,40 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } let funding_txo_opt = chan.funding.get_funding_txo(); - let (shutdown, monitor_update_opt, htlcs) = try_channel_entry!( - self, - peer_state, - chan.shutdown( - &self.logger, - &self.signer_provider, - &peer_state.latest_features, - &msg - ), - chan_entry + let res = chan.shutdown( + &self.logger, + &self.signer_provider, + &peer_state.latest_features, + &msg, ); + let (shutdown, monitor_update_opt, htlcs, splice_funding_failed) = + try_channel_entry!(self, peer_state, res, chan_entry); dropped_htlcs = htlcs; + if let Some(splice_funding_failed) = splice_funding_failed { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + let mut pending_events = self.pending_events.lock().unwrap(); + if let Some(funding_info) = funding_info { + pending_events.push_back(( + events::Event::DiscardFunding { + channel_id: msg.channel_id, + funding_info, + }, + None, + )); + } + pending_events.push_back(( + events::Event::SpliceNegotiationFailed { + channel_id: msg.channel_id, + counterparty_node_id: *counterparty_node_id, + user_channel_id: chan.context().get_user_id(), + contribution: Some(contribution), + reason: events::NegotiationFailureReason::ChannelClosing, + }, + None, + )); + } + if let Some(msg) = shutdown { // We can send the `shutdown` message before updating the `ChannelMonitor` // here as we don't need the monitor update to complete until we send a @@ -11661,7 +12937,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } } }, @@ -11681,17 +12957,17 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, } } else { - return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)); + return Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )); } } for htlc_source in dropped_htlcs.drain(..) { - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id.clone()), - channel_id: msg.channel_id, - }; - let reason = HTLCFailReason::from_failure_code(LocalHTLCFailureReason::ChannelClosed); let (source, hash) = htlc_source; - self.fail_htlc_backwards_internal(&source, &hash, &reason, receiver, None); + let failure_type = source.failure_type(*counterparty_node_id, msg.channel_id); + let reason = HTLCFailReason::from_failure_code(LocalHTLCFailureReason::ChannelClosed); + self.fail_htlc_backwards_internal(&source, &hash, &reason, failure_type, None); } Ok(()) @@ -11702,14 +12978,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!( - "Can't find a peer matching the passed counterparty node_id {}", - counterparty_node_id - ), - msg.channel_id, - ) + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) })?; let logger; let tx_err: Option<(_, Result<Infallible, _>)> = { @@ -11724,10 +12993,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ try_channel_entry!(self, peer_state, res, chan_entry); debug_assert_eq!(tx_shutdown_result.is_some(), chan.is_shutdown()); if let Some(msg) = closing_signed { - peer_state.pending_msg_events.push(MessageSendEvent::SendClosingSigned { - node_id: counterparty_node_id.clone(), - msg, - }); + peer_state.pending_msg_events.push( + MessageSendEvent::SendClosingSigned { + node_id: counterparty_node_id.clone(), + msg, + }, + ); } if let Some((tx, close_res)) = tx_shutdown_result { // We're done with this channel, we've got a signed closing transaction and @@ -11735,18 +13006,34 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // also implies there are no pending HTLCs left on the channel, so we can // fully delete it from tracking (the channel monitor is still around to // watch for old state broadcasts)! - let err = self.locked_handle_funded_coop_close(&mut peer_state.closed_channel_monitor_update_ids, &mut peer_state.in_flight_monitor_updates, close_res, chan); + let err = self.locked_handle_funded_coop_close( + &mut peer_state.closed_channel_monitor_update_ids, + &mut peer_state.in_flight_monitor_updates, + close_res, + chan, + ); chan_entry.remove(); Some((tx, Err(err))) } else { None } } else { - return try_channel_entry!(self, peer_state, Err(ChannelError::close( - "Got a closing_signed message for an unfunded channel!".into())), chan_entry); + return try_channel_entry!( + self, + peer_state, + Err(ChannelError::close( + "Got a closing_signed message for an unfunded channel!".into() + )), + chan_entry + ); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => { + return Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )) + }, } }; mem::drop(per_peer_state); @@ -11793,11 +13080,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // closing a channel), so any changes are likely to be lost on restart! let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { @@ -11809,7 +13094,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ "Got an update_add_htlc message for an unfunded channel!".into())), chan_entry); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id)) } Ok(()) } @@ -11822,51 +13107,67 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let (htlc_source, forwarded_htlc_value, skimmed_fee_msat, send_timestamp) = { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!( - "Can't find a peer matching the passed counterparty node_id {}", - counterparty_node_id - ), - msg.channel_id, - ) + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let res = try_channel_entry!(self, peer_state, chan.update_fulfill_htlc(&msg), chan_entry); - if let HTLCSource::PreviousHopData(prev_hop) = &res.0 { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let res = try_channel_entry!( + self, + peer_state, + chan.update_fulfill_htlc(&msg), + chan_entry + ); + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + for prev_hop in res.0.previous_hop_data() { log_trace!(logger, "Holding the next revoke_and_ack until the preimage is durably persisted in the inbound edge's ChannelMonitor", - ); - peer_state.actions_blocking_raa_monitor_updates.entry(msg.channel_id) + ); + peer_state + .actions_blocking_raa_monitor_updates + .entry(msg.channel_id) .or_insert_with(Vec::new) - .push(RAAMonitorUpdateBlockingAction::from_prev_hop_data(&prev_hop)); + .push(RAAMonitorUpdateBlockingAction::from_prev_hop_data(prev_hop)); } + // Note that we do not need to push an `actions_blocking_raa_monitor_updates` // entry here, even though we *do* need to block the next RAA monitor update. // We do this instead in the `claim_funds_internal` by attaching a // `ReleaseRAAChannelMonitorUpdate` action to the event generated when the // outbound HTLC is claimed. This is guaranteed to all complete before we // process the RAA as messages are processed from single peers serially. - funding_txo = chan.funding.get_funding_txo().expect("We won't accept a fulfill until funded"); + funding_txo = chan + .funding + .get_funding_txo() + .expect("We won't accept a fulfill until funded"); next_user_channel_id = chan.context.get_user_id(); res } else { - return try_channel_entry!(self, peer_state, Err(ChannelError::close( - "Got an update_fulfill_htlc message for an unfunded channel!".into())), chan_entry); + return try_channel_entry!( + self, + peer_state, + Err(ChannelError::close( + "Got an update_fulfill_htlc message for an unfunded channel!" + .into() + )), + chan_entry + ); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => { + return Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )) + }, } }; self.claim_funds_internal( htlc_source, msg.payment_preimage.clone(), - Some(forwarded_htlc_value), + forwarded_htlc_value, skimmed_fee_msat, false, *counterparty_node_id, @@ -11885,11 +13186,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // Note that the ChannelManager is NOT re-persisted on disk after this (unless we error // closing a channel), so any changes are likely to be lost on restart! let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { @@ -11901,7 +13200,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ "Got an update_fail_htlc message for an unfunded channel!".into())), chan_entry); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id)) } Ok(()) } @@ -11911,11 +13210,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // Note that the ChannelManager is NOT re-persisted on disk after this (unless we error // closing a channel), so any changes are likely to be lost on restart! let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { @@ -11932,19 +13229,18 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } Ok(()) }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id)) } } - #[rustfmt::skip] - fn internal_commitment_signed(&self, counterparty_node_id: &PublicKey, msg: &msgs::CommitmentSigned) -> Result<(), MsgHandleErrInternal> { + fn internal_commitment_signed( + &self, counterparty_node_id: &PublicKey, msg: &msgs::CommitmentSigned, + ) -> Result<(), MsgHandleErrInternal> { let best_block = *self.best_block.read().unwrap(); let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { @@ -11952,13 +13248,20 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let chan = chan_entry.get_mut(); let logger = WithChannelContext::from(&self.logger, &chan.context(), None); let funding_txo = chan.funding().get_funding_txo(); - let (monitor_opt, monitor_update_opt) = try_channel_entry!( - self, peer_state, chan.commitment_signed(msg, best_block, &self.signer_provider, &self.fee_estimator, &&logger), - chan_entry); + let res = chan.commitment_signed( + msg, + best_block, + &self.signer_provider, + &self.fee_estimator, + &&logger, + ); + let (monitor_opt, monitor_update_opt) = + try_channel_entry!(self, peer_state, res, chan_entry); if let Some(chan) = chan.as_funded_mut() { if let Some(monitor) = monitor_opt { - let monitor_res = self.chain_monitor.watch_channel(monitor.channel_id(), monitor); + let monitor_res = + self.chain_monitor.watch_channel(monitor.channel_id(), monitor); if let Ok(persist_state) = monitor_res { if let Some(data) = self.handle_initial_monitor( &mut peer_state.in_flight_monitor_updates, @@ -11970,10 +13273,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } } else { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let logger = + WithChannelContext::from(&self.logger, &chan.context, None); log_error!(logger, "Persisting initial ChannelMonitor failed, implying the channel ID was duplicated"); let msg = "Channel ID was a duplicate"; let reason = ClosureReason::ProcessingError { err: msg.to_owned() }; @@ -11992,24 +13296,25 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } } } Ok(()) }, - hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )), } } #[rustfmt::skip] fn internal_commitment_signed_batch(&self, counterparty_node_id: &PublicKey, channel_id: ChannelId, batch: Vec<msgs::CommitmentSigned>) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(channel_id) { @@ -12034,13 +13339,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } } } Ok(()) }, - hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), channel_id)) + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, channel_id)) } } @@ -12054,48 +13359,26 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ hash_map::Entry::Vacant(e) => { e.insert(update_add_htlcs.1); }, - } - } - - #[inline] - fn forward_htlcs(&self, per_source_pending_forwards: &mut [PerSourcePendingForward]) { - for &mut ( - prev_outbound_scid_alias, - prev_counterparty_node_id, - prev_funding_outpoint, - prev_channel_id, - prev_user_channel_id, - ref mut pending_forwards, - ) in per_source_pending_forwards - { - if !pending_forwards.is_empty() { - for (forward_info, prev_htlc_id) in pending_forwards.drain(..) { - let scid = match forward_info.routing { - PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id, - PendingHTLCRouting::TrampolineForward { .. } - | PendingHTLCRouting::Receive { .. } - | PendingHTLCRouting::ReceiveKeysend { .. } => 0, - }; - - let pending_add = PendingAddHTLCInfo { - prev_outbound_scid_alias, - prev_counterparty_node_id, - prev_funding_outpoint, - prev_channel_id, - prev_htlc_id, - prev_user_channel_id, - forward_info, - }; + } + } - match self.forward_htlcs.lock().unwrap().entry(scid) { - hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().push(HTLCForwardInfo::AddHTLC(pending_add)); - }, - hash_map::Entry::Vacant(entry) => { - entry.insert(vec![HTLCForwardInfo::AddHTLC(pending_add)]); - }, - } - } + #[inline] + fn forward_htlcs<I: IntoIterator<Item = PendingAddHTLCInfo>>(&self, pending_forwards: I) { + for htlc in pending_forwards.into_iter() { + let scid = match htlc.forward_info.routing { + PendingHTLCRouting::Forward { short_channel_id, .. } => short_channel_id, + PendingHTLCRouting::TrampolineForward { .. } + | PendingHTLCRouting::Receive { .. } + | PendingHTLCRouting::ReceiveKeysend { .. } => 0, + }; + + match self.forward_htlcs.lock().unwrap().entry(scid) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().push(HTLCForwardInfo::AddHTLC(htlc)); + }, + hash_map::Entry::Vacant(entry) => { + entry.insert(vec![HTLCForwardInfo::AddHTLC(htlc)]); + }, } } } @@ -12147,11 +13430,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn internal_revoke_and_ack(&self, counterparty_node_id: &PublicKey, msg: &msgs::RevokeAndACK) -> Result<(), MsgHandleErrInternal> { let (htlcs_to_fail, static_invoices) = { let per_peer_state = self.per_peer_state.read().unwrap(); - let mut peer_state_lock = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - }).map(|mtx| mtx.lock().unwrap())?; + let mut peer_state_lock = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + }).map(|mtx| mtx.lock().unwrap())?; let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { @@ -12177,7 +13458,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) { mem::drop(peer_state_lock); mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } } (htlcs_to_fail, static_invoices) @@ -12186,7 +13467,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ "Got a revoke_and_ack message for an unfunded channel!".into())), chan_entry); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id)) } }; self.fail_holding_cell_htlcs(htlcs_to_fail, msg.channel_id, counterparty_node_id); @@ -12200,11 +13481,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ #[rustfmt::skip] fn internal_update_fee(&self, counterparty_node_id: &PublicKey, msg: &msgs::UpdateFee) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { @@ -12217,7 +13496,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ "Got an update_fee message for an unfunded channel!".into())), chan_entry); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id)) } Ok(()) } @@ -12226,11 +13505,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ fn internal_stfu(&self, counterparty_node_id: &PublicKey, msg: &msgs::Stfu) -> Result<bool, MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - msg.channel_id - ) + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; @@ -12249,6 +13524,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ); let res = chan.stfu(&msg, &&logger); + let (res, quiescent_error) = match res { + Ok(resp) => (Ok(resp), QuiescentError::DoNothing), + Err((chan_err, quiescent_err)) => (Err(chan_err), quiescent_err), + }; + self.handle_quiescent_error( + chan_entry.get().context().channel_id(), + *counterparty_node_id, + chan_entry.get().context().get_user_id(), + quiescent_error, + ); let resp = try_channel_entry!(self, peer_state, res, chan_entry); match resp { None => Ok(false), @@ -12266,6 +13551,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); Ok(true) }, + Some(StfuResponse::TxInitRbf(msg)) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendTxInitRbf { + node_id: *counterparty_node_id, + msg, + }); + Ok(true) + }, } } else { let msg = "Peer sent `stfu` for an unfunded channel"; @@ -12275,9 +13567,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ return try_channel_entry!(self, peer_state, err, chan_entry); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close( - format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), - msg.channel_id + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id )) } } @@ -12285,11 +13575,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ #[rustfmt::skip] fn internal_announcement_signatures(&self, counterparty_node_id: &PublicKey, msg: &msgs::AnnouncementSignatures) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; match peer_state.channel_by_id.entry(msg.channel_id) { @@ -12307,18 +13595,22 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ msg, &self.config.read().unwrap(), ); - peer_state.pending_msg_events.push(MessageSendEvent::BroadcastChannelAnnouncement { - msg: try_channel_entry!(self, peer_state, res, chan_entry), - // Note that announcement_signatures fails if the channel cannot be announced, - // so get_channel_update_for_broadcast will never fail by the time we get here. - update_msg: Some(self.get_channel_update_for_broadcast(chan).unwrap().0), - }); + let announcement_msg = try_channel_entry!(self, peer_state, res, chan_entry); + // Note that announcement_signatures fails if the channel cannot be announced, + // so get_channel_update_for_broadcast will never fail by the time we get here. + let update_msg = self.get_channel_update_for_broadcast(chan).unwrap().0; + self.pending_broadcast_messages.lock().unwrap().push( + MessageSendEvent::BroadcastChannelAnnouncement { + msg: announcement_msg, + update_msg: Some(update_msg), + }, + ); } else { return try_channel_entry!(self, peer_state, Err(ChannelError::close( "Got an announcement_signatures message for an unfunded channel!".into())), chan_entry); } }, - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", counterparty_node_id), msg.channel_id)) + hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id)) } Ok(()) } @@ -12381,21 +13673,16 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ #[rustfmt::skip] fn internal_channel_reestablish(&self, counterparty_node_id: &PublicKey, msg: &msgs::ChannelReestablish) -> Result<(), MsgHandleErrInternal> { - let (inferred_splice_locked, need_lnd_workaround, holding_cell_res) = { + let (post_splice_locked_update, holding_cell_res) = { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), - msg.channel_id - ) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let logger = WithContext::from(&self.logger, Some(*counterparty_node_id), Some(msg.channel_id), None); let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - match peer_state.channel_by_id.entry(msg.channel_id) { + let post_splice_locked_update = match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { // Currently, we expect all holding cell update_adds to be dropped on peer @@ -12431,19 +13718,58 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } let need_lnd_workaround = chan.context.workaround_lnd_bug_4006.take(); + let inferred_splice_locked = responses.inferred_splice_locked; + let (tx_signatures_order, tx_signatures) = responses + .tx_signatures + .map(|(order, msg)| (order, Some(msg))) + .unwrap_or((TxSignaturesOrder::CommitmentFirst, None)); + let funding_tx_signed = if tx_signatures.is_some() || responses.splice_locked.is_some() { + Some(FundingTxSigned { + tx_signatures, + splice_locked: responses.splice_locked, + ..Default::default() + }) + } else { + None + }; let (htlc_forwards, decode_update_add_htlcs) = self.handle_channel_resumption( &mut peer_state.pending_msg_events, chan, responses.raa, responses.commitment_update, responses.commitment_order, Vec::new(), Vec::new(), None, responses.channel_ready, responses.announcement_sigs, - responses.tx_signatures, responses.tx_abort, responses.channel_ready_order, + funding_tx_signed, responses.tx_abort, responses.channel_ready_order, tx_signatures_order, ); - debug_assert!(htlc_forwards.is_none()); + debug_assert!(htlc_forwards.is_empty()); debug_assert!(decode_update_add_htlcs.is_none()); if let Some(upd) = channel_update { peer_state.pending_msg_events.push(upd); } - let holding_cell_res = self.check_free_peer_holding_cells(peer_state); - (responses.inferred_splice_locked, need_lnd_workaround, holding_cell_res) + if let Some(channel_ready_msg) = need_lnd_workaround { + let res = self.internal_channel_ready_with_funded_channel( + counterparty_node_id, + &channel_ready_msg, + chan, + &mut peer_state.pending_msg_events, + ); + try_channel_entry!(self, peer_state, res, chan_entry); + } + + // A reestablish may infer a missed `splice_locked`; apply it before freeing + // holding cells so we don't generate commitment updates against stale splice + // state. + if let Some(splice_locked) = inferred_splice_locked { + let result = self.internal_splice_locked_with_funded_channel( + counterparty_node_id, + &splice_locked, + chan, + &mut peer_state.in_flight_monitor_updates, + &mut peer_state.monitor_update_blocked_actions, + &mut peer_state.pending_msg_events, + peer_state.is_connected, + ); + try_channel_entry!(self, peer_state, result, chan_entry) + } else { + None + } } else { return try_channel_entry!(self, peer_state, Err(ChannelError::close( "Got a channel_reestablish message for an unfunded channel!".into())), chan_entry); @@ -12478,205 +13804,268 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ my_current_funding_locked: None, }, }); - return Err(MsgHandleErrInternal::send_err_msg_no_close( - format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", - counterparty_node_id), msg.channel_id) + return Err(MsgHandleErrInternal::no_such_channel_for_peer(counterparty_node_id, msg.channel_id) ) } - } - }; - - self.handle_holding_cell_free_result(holding_cell_res); + }; - if let Some(channel_ready_msg) = need_lnd_workaround { - self.internal_channel_ready(counterparty_node_id, &channel_ready_msg)?; - } + let holding_cell_res = self.check_free_peer_holding_cells(peer_state); + (post_splice_locked_update, holding_cell_res) + }; - if let Some(splice_locked) = inferred_splice_locked { - self.internal_splice_locked(counterparty_node_id, &splice_locked)?; + if let Some(data) = post_splice_locked_update { + let _ = self.handle_post_monitor_update_chan_resume(data); } + self.handle_holding_cell_free_result(holding_cell_res); Ok(()) } /// Handle incoming splice request, transition channel to splice-pending (unless some check fails). - #[rustfmt::skip] - fn internal_splice_init(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceInit) -> Result<(), MsgHandleErrInternal> { + fn internal_splice_init( + &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceInit, + ) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; - // TODO(splicing): Currently not possible to contribute on the splicing-acceptor side - let our_funding_contribution = 0i64; - // Look for the channel match peer_state.channel_by_id.entry(msg.channel_id) { - hash_map::Entry::Vacant(_) => return Err(MsgHandleErrInternal::send_err_msg_no_close(format!( - "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}, channel_id {}", - counterparty_node_id, msg.channel_id, - ), msg.channel_id)), + hash_map::Entry::Vacant(_) => { + return Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )) + }, hash_map::Entry::Occupied(mut chan_entry) => { if self.config.read().unwrap().reject_inbound_splices { let err = ChannelError::WarnAndDisconnect( - "Inbound channel splices are currently not allowed".to_owned() + "Inbound channel splices are currently not allowed".to_owned(), ); return Err(MsgHandleErrInternal::from_chan_no_close(err, msg.channel_id)); } if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { - let init_res = funded_channel.splice_init( - msg, our_funding_contribution, &self.signer_provider, &self.entropy_source, - &self.get_our_node_id(), &self.logger - ); - let splice_ack_msg = try_channel_entry!(self, peer_state, init_res, chan_entry); - peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { - node_id: *counterparty_node_id, - msg: splice_ack_msg, - }); - Ok(()) + let user_channel_id = funded_channel.context.get_user_id(); + match funded_channel.splice_init( + msg, + &self.entropy_source, + &self.get_our_node_id(), + self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis, + &self.logger, + ) { + Ok(splice_ack_msg) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendSpliceAck { + node_id: *counterparty_node_id, + msg: splice_ack_msg, + }); + Ok(()) + }, + Err(err) => { + debug_assert!(err.splice_funding_failed.is_none()); + Err(self.handle_interactive_tx_msg_err( + err, + msg.channel_id, + counterparty_node_id, + user_channel_id, + )) + }, + } + } else { + try_channel_entry!( + self, + peer_state, + Err(ChannelError::close("Channel is not funded, cannot be spliced".into())), + chan_entry + ) + } + }, + } + } + + /// Handle incoming tx_init_rbf, start a new round of interactive transaction construction. + fn internal_tx_init_rbf( + &self, counterparty_node_id: &PublicKey, msg: &msgs::TxInitRbf, + ) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + + match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => { + return Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )) + }, + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { + let user_channel_id = funded_channel.context.get_user_id(); + match funded_channel.tx_init_rbf( + msg, + &self.entropy_source, + &self.get_our_node_id(), + &self.fee_estimator, + self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis, + &self.logger, + ) { + Ok(tx_ack_rbf_msg) => { + peer_state.pending_msg_events.push(MessageSendEvent::SendTxAckRbf { + node_id: *counterparty_node_id, + msg: tx_ack_rbf_msg, + }); + Ok(()) + }, + Err(err) => { + debug_assert!(err.splice_funding_failed.is_none()); + Err(self.handle_interactive_tx_msg_err( + err, + msg.channel_id, + counterparty_node_id, + user_channel_id, + )) + }, + } } else { - try_channel_entry!(self, peer_state, Err(ChannelError::close("Channel is not funded, cannot be spliced".into())), chan_entry) + try_channel_entry!( + self, + peer_state, + Err( + ChannelError::close("Channel is not funded, cannot RBF splice".into(),) + ), + chan_entry + ) } }, } } /// Handle incoming splice request ack, transition channel to splice-pending (unless some check fails). - #[rustfmt::skip] - fn internal_splice_ack(&self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceAck) -> Result<(), MsgHandleErrInternal> { + fn internal_splice_ack( + &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceAck, + ) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); - let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close(format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}"), msg.channel_id) - })?; + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; // Look for the channel match peer_state.channel_by_id.entry(msg.channel_id) { - hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::send_err_msg_no_close(format!( - "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", - counterparty_node_id - ), msg.channel_id)), + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )), hash_map::Entry::Occupied(mut chan_entry) => { if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { let splice_ack_res = funded_channel.splice_ack( - msg, &self.signer_provider, &self.entropy_source, - &self.get_our_node_id(), &self.logger + msg, + &self.entropy_source, + &self.get_our_node_id(), + self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis, + &self.logger, ); - let tx_msg_opt = try_channel_entry!(self, peer_state, splice_ack_res, chan_entry); + let tx_msg_opt = + try_channel_entry!(self, peer_state, splice_ack_res, chan_entry); if let Some(tx_msg) = tx_msg_opt { - peer_state.pending_msg_events.push(tx_msg.into_msg_send_event(counterparty_node_id.clone())); + peer_state + .pending_msg_events + .push(tx_msg.into_msg_send_event(counterparty_node_id.clone())); } Ok(()) } else { - try_channel_entry!(self, peer_state, Err(ChannelError::close("Channel is not funded, cannot be spliced".into())), chan_entry) + try_channel_entry!( + self, + peer_state, + Err(ChannelError::close("Channel is not funded, cannot be spliced".into())), + chan_entry + ) } }, } } - fn internal_splice_locked( - &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceLocked, + fn internal_tx_ack_rbf( + &self, counterparty_node_id: &PublicKey, msg: &msgs::TxAckRbf, ) -> Result<(), MsgHandleErrInternal> { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { - debug_assert!(false); - MsgHandleErrInternal::send_err_msg_no_close( - format!( - "Can't find a peer matching the passed counterparty node_id {}", - counterparty_node_id - ), - msg.channel_id, - ) + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) })?; let mut peer_state_lock = peer_state_mutex.lock().unwrap(); let peer_state = &mut *peer_state_lock; // Look for the channel match peer_state.channel_by_id.entry(msg.channel_id) { + hash_map::Entry::Vacant(_) => Err(MsgHandleErrInternal::no_such_channel_for_peer( + counterparty_node_id, + msg.channel_id, + )), + hash_map::Entry::Occupied(mut chan_entry) => { + if let Some(ref mut funded_channel) = chan_entry.get_mut().as_funded_mut() { + let tx_ack_rbf_res = funded_channel.tx_ack_rbf( + msg, + &self.entropy_source, + &self.get_our_node_id(), + self.config.read().unwrap().channel_handshake_limits.min_funding_satoshis, + &self.logger, + ); + let tx_msg_opt = + try_channel_entry!(self, peer_state, tx_ack_rbf_res, chan_entry); + if let Some(tx_msg) = tx_msg_opt { + peer_state + .pending_msg_events + .push(tx_msg.into_msg_send_event(counterparty_node_id.clone())); + } + Ok(()) + } else { + try_channel_entry!( + self, + peer_state, + Err(ChannelError::close("Channel is not funded, cannot RBF splice".into())), + chan_entry + ) + } + }, + } + } + + fn internal_splice_locked( + &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceLocked, + ) -> Result<(), MsgHandleErrInternal> { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state_mutex = per_peer_state.get(counterparty_node_id).ok_or_else(|| { + MsgHandleErrInternal::unreachable_no_such_peer(counterparty_node_id, msg.channel_id) + })?; + let mut peer_state_lock = peer_state_mutex.lock().unwrap(); + let peer_state = &mut *peer_state_lock; + // Look for the channel + let post_update_data = match peer_state.channel_by_id.entry(msg.channel_id) { hash_map::Entry::Vacant(_) => { - let err = format!( - "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", + return Err(MsgHandleErrInternal::no_such_channel_for_peer( counterparty_node_id, - ); - return Err(MsgHandleErrInternal::send_err_msg_no_close(err, msg.channel_id)); + msg.channel_id, + )); }, hash_map::Entry::Occupied(mut chan_entry) => { if let Some(chan) = chan_entry.get_mut().as_funded_mut() { - let logger = WithChannelContext::from(&self.logger, &chan.context, None); - let result = chan.splice_locked( + let result = self.internal_splice_locked_with_funded_channel( + counterparty_node_id, msg, - &self.node_signer, - self.chain_hash, - &self.config.read().unwrap(), - self.best_block.read().unwrap().height, - &&logger, + chan, + &mut peer_state.in_flight_monitor_updates, + &mut peer_state.monitor_update_blocked_actions, + &mut peer_state.pending_msg_events, + peer_state.is_connected, ); - let splice_promotion = try_channel_entry!(self, peer_state, result, chan_entry); - if let Some(splice_promotion) = splice_promotion { - { - let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); - insert_short_channel_id!(short_to_chan_info, chan); - } - - { - let mut pending_events = self.pending_events.lock().unwrap(); - pending_events.push_back(( - events::Event::ChannelReady { - channel_id: chan.context.channel_id(), - user_channel_id: chan.context.get_user_id(), - counterparty_node_id: chan.context.get_counterparty_node_id(), - funding_txo: Some( - splice_promotion.funding_txo.into_bitcoin_outpoint(), - ), - channel_type: chan.funding.get_channel_type().clone(), - }, - None, - )); - splice_promotion.discarded_funding.into_iter().for_each( - |funding_info| { - let event = Event::DiscardFunding { - channel_id: chan.context.channel_id(), - funding_info, - }; - pending_events.push_back((event, None)); - }, - ); - } - - if let Some(announcement_sigs) = splice_promotion.announcement_sigs { - log_trace!(logger, "Sending announcement_signatures",); - peer_state.pending_msg_events.push( - MessageSendEvent::SendAnnouncementSignatures { - node_id: counterparty_node_id.clone(), - msg: announcement_sigs, - }, - ); - } - - if let Some(monitor_update) = splice_promotion.monitor_update { - if let Some(data) = self.handle_new_monitor_update( - &mut peer_state.in_flight_monitor_updates, - &mut peer_state.monitor_update_blocked_actions, - &mut peer_state.pending_msg_events, - peer_state.is_connected, - chan, - splice_promotion.funding_txo, - monitor_update, - ) { - mem::drop(peer_state_lock); - mem::drop(per_peer_state); - self.handle_post_monitor_update_chan_resume(data); - } - } - } + try_channel_entry!(self, peer_state, result, chan_entry) } else { return Err(MsgHandleErrInternal::send_err_msg_no_close( "Channel is not funded, cannot splice".to_owned(), @@ -12685,23 +14074,105 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, }; + mem::drop(peer_state_lock); + mem::drop(per_peer_state); + + if let Some(data) = post_update_data { + let _ = self.handle_post_monitor_update_chan_resume(data); + } + + Ok(()) + } + + fn internal_splice_locked_with_funded_channel( + &self, counterparty_node_id: &PublicKey, msg: &msgs::SpliceLocked, + chan: &mut FundedChannel<SP>, + in_flight_monitor_updates: &mut BTreeMap<ChannelId, (OutPoint, Vec<ChannelMonitorUpdate>)>, + monitor_update_blocked_actions: &mut BTreeMap< + ChannelId, + Vec<MonitorUpdateCompletionAction>, + >, + pending_msg_events: &mut Vec<MessageSendEvent>, is_connected: bool, + ) -> Result<Option<PostMonitorUpdateChanResume>, ChannelError> { + let logger = WithChannelContext::from(&self.logger, &chan.context, None); + let splice_promotion = chan.splice_locked( + msg, + &self.node_signer, + self.chain_hash, + &self.config.read().unwrap(), + self.best_block.read().unwrap().height, + &&logger, + )?; + let mut post_update_data = None; + if let Some(splice_promotion) = splice_promotion { + { + let mut short_to_chan_info = self.short_to_chan_info.write().unwrap(); + insert_short_channel_id!(short_to_chan_info, chan); + } + + { + let mut pending_events = self.pending_events.lock().unwrap(); + pending_events.push_back(( + events::Event::ChannelReady { + channel_id: chan.context.channel_id(), + user_channel_id: chan.context.get_user_id(), + counterparty_node_id: chan.context.get_counterparty_node_id(), + funding_txo: Some(splice_promotion.funding_txo.into_bitcoin_outpoint()), + channel_type: chan.funding.get_channel_type().clone(), + }, + None, + )); + splice_promotion.discarded_funding.into_iter().for_each(|funding_info| { + let event = Event::DiscardFunding { + channel_id: chan.context.channel_id(), + funding_info, + }; + pending_events.push_back((event, None)); + }); + } - Ok(()) + if let Some(announcement_sigs) = splice_promotion.announcement_sigs { + log_trace!(logger, "Sending announcement_signatures",); + pending_msg_events.push(MessageSendEvent::SendAnnouncementSignatures { + node_id: counterparty_node_id.clone(), + msg: announcement_sigs, + }); + } + + if let Some(monitor_update) = splice_promotion.monitor_update { + post_update_data = self.handle_new_monitor_update( + in_flight_monitor_updates, + monitor_update_blocked_actions, + pending_msg_events, + is_connected, + chan, + splice_promotion.funding_txo, + monitor_update, + ); + } + } + + Ok(post_update_data) } - /// Process pending events from the [`chain::Watch`], returning whether any events were processed. - fn process_pending_monitor_events(&self) -> bool { + /// Process pending events from the [`chain::Watch`], returning the appropriate + /// [`NotifyOption`] for persistence and event handling. + fn process_pending_monitor_events(&self) -> NotifyOption { debug_assert!(self.total_consistency_lock.try_write().is_err()); // Caller holds read lock let mut failed_channels: Vec<(Result<Infallible, _>, _)> = Vec::new(); let mut pending_monitor_events = self.chain_monitor.release_pending_monitor_events(); - let has_pending_monitor_events = !pending_monitor_events.is_empty(); + if pending_monitor_events.is_empty() { + return NotifyOption::SkipPersistNoEvents; + } + let mut needs_persist = false; for (funding_outpoint, channel_id, mut monitor_events, counterparty_node_id) in pending_monitor_events.drain(..) { for monitor_event in monitor_events.drain(..) { match monitor_event { MonitorEvent::HTLCEvent(htlc_update) => { + needs_persist = true; let logger = WithContext::from( &self.logger, Some(counterparty_node_id), @@ -12719,7 +14190,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ self.claim_funds_internal( htlc_update.source, preimage, - htlc_update.htlc_value_satoshis.map(|v| v * 1000), + htlc_update.htlc_value_satoshis * 1000, None, true, counterparty_node_id, @@ -12732,10 +14203,8 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } else { log_trace!(logger, "Failing HTLC from our monitor"); let failure_reason = LocalHTLCFailureReason::OnChainTimeout; - let receiver = HTLCHandlingFailureType::Forward { - node_id: Some(counterparty_node_id), - channel_id, - }; + let failure_type = + htlc_update.source.failure_type(counterparty_node_id, channel_id); let reason = HTLCFailReason::from_failure_code(failure_reason); let completion_update = Some(PaymentCompleteUpdate { counterparty_node_id, @@ -12747,13 +14216,14 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ &htlc_update.source, &htlc_update.payment_hash, &reason, - receiver, + failure_type, completion_update, ); } }, MonitorEvent::HolderForceClosed(_) | MonitorEvent::HolderForceClosedWithInfo { .. } => { + needs_persist = true; let per_peer_state = self.per_peer_state.read().unwrap(); if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { let mut peer_state_lock = peer_state_mutex.lock().unwrap(); @@ -12786,6 +14256,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, MonitorEvent::CommitmentTxConfirmed(_) => { + needs_persist = true; let per_peer_state = self.per_peer_state.read().unwrap(); if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { let mut peer_state_lock = peer_state_mutex.lock().unwrap(); @@ -12807,7 +14278,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, MonitorEvent::Completed { channel_id, monitor_update_id, .. } => { - self.channel_monitor_updated( + needs_persist |= self.channel_monitor_updated( &channel_id, Some(monitor_update_id), &counterparty_node_id, @@ -12821,7 +14292,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let _ = self.handle_error(err, counterparty_node_id); } - has_pending_monitor_events + if needs_persist { + NotifyOption::DoPersist + } else { + NotifyOption::SkipPersistHandleEvents + } } fn handle_holding_cell_free_result(&self, result: FreeHoldingCellsResult) { @@ -12831,7 +14306,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ); for (chan_id, cp_node_id, post_update_data, failed_htlcs) in result { if let Some(data) = post_update_data { - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } self.fail_holding_cell_htlcs(failed_htlcs, chan_id, &cp_node_id); @@ -12914,20 +14389,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ /// [`ChannelSigner`]: crate::sign::ChannelSigner pub fn signer_unblocked(&self, channel_opt: Option<(PublicKey, ChannelId)>) { let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let mut needs_holding_cell_release = false; // Returns whether we should remove this channel as it's just been closed. let unblock_chan = |chan: &mut Channel<SP>, - pending_msg_events: &mut Vec<MessageSendEvent>| + pending_msg_events: &mut Vec<MessageSendEvent>, + needs_holding_cell_release: &mut bool| -> Result<Option<ShutdownResult>, ChannelError> { let channel_id = chan.context().channel_id(); let outbound_scid_alias = chan.context().outbound_scid_alias(); let logger = WithChannelContext::from(&self.logger, &chan.context(), None); let node_id = chan.context().get_counterparty_node_id(); + let best_block_height = self.best_block.read().unwrap().height; let cbp = |htlc_id| { self.path_for_release_held_htlc(htlc_id, outbound_scid_alias, &channel_id, &node_id) }; - let msgs = chan.signer_maybe_unblocked(self.chain_hash, &&logger, cbp)?; - if let Some(msgs) = msgs { + let msgs = + chan.signer_maybe_unblocked(self.chain_hash, best_block_height, &&logger, cbp)?; + if let Some(mut msgs) = msgs { if chan.context().is_connected() { if let Some(msg) = msgs.open_channel { pending_msg_events.push(MessageSendEvent::SendOpenChannel { node_id, msg }); @@ -12967,7 +14446,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ pending_msg_events .push(MessageSendEvent::SendFundingSigned { node_id, msg }); } - if let Some(msg) = msgs.funding_commit_sig { + if let Some(msg) = msgs + .funding_tx_signed + .as_mut() + .and_then(|funding_tx_signed| funding_tx_signed.commitment_signed.take()) + { pending_msg_events.push(MessageSendEvent::UpdateHTLCs { node_id, channel_id, @@ -12981,7 +14464,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }, }); } - if let Some(msg) = msgs.tx_signatures { + if let Some(msg) = msgs + .funding_tx_signed + .as_mut() + .and_then(|funding_tx_signed| funding_tx_signed.tx_signatures.take()) + { pending_msg_events .push(MessageSendEvent::SendTxSignatures { node_id, msg }); } @@ -12994,6 +14481,58 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ if let Some(msg) = msgs.channel_ready { self.send_channel_ready(pending_msg_events, funded_chan, msg); } + debug_assert!(msgs + .funding_tx_signed + .as_ref() + .and_then(|funding_tx_signed| { + funding_tx_signed.counterparty_initial_commitment_signed_result.as_ref() + }) + .is_none()); + if let Some(msg) = msgs + .funding_tx_signed + .as_mut() + .and_then(|funding_tx_signed| funding_tx_signed.splice_locked.take()) + { + pending_msg_events + .push(MessageSendEvent::SendSpliceLocked { node_id, msg }); + } + if let Some((tx, tx_type)) = msgs + .funding_tx_signed + .as_mut() + .and_then(|funding_tx_signed| funding_tx_signed.funding_tx.take()) + { + debug_assert!(matches!( + tx_type, + TransactionType::InteractiveFunding { .. } + )); + log_info!( + logger, + "Broadcasting interactively funded transaction with txid {}", + tx.compute_txid(), + ); + self.tx_broadcaster.broadcast_transactions(&[(&tx, tx_type)]); + } + if let Some(splice_negotiated) = msgs + .funding_tx_signed + .as_mut() + .and_then(|funding_tx_signed| funding_tx_signed.splice_negotiated.take()) + { + *needs_holding_cell_release = true; + if splice_negotiated.has_local_contribution { + self.pending_events.lock().unwrap().push_back(( + events::Event::SpliceNegotiated { + channel_id, + counterparty_node_id: node_id, + user_channel_id: funded_chan.context.get_user_id(), + new_funding_txo: splice_negotiated.funding_txo, + channel_type: splice_negotiated.channel_type, + new_funding_redeem_script: splice_negotiated + .funding_redeem_script, + }, + None, + )); + } + } if let Some(broadcast_tx) = msgs.signed_closing_tx { log_info!(logger, "Broadcasting closing tx {}", log_tx!(broadcast_tx)); self.tx_broadcaster.broadcast_transactions(&[( @@ -13008,6 +14547,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // We don't know how to handle a channel_ready or signed_closing_tx for a // non-funded channel. debug_assert!(msgs.channel_ready.is_none()); + debug_assert!(msgs.funding_tx_signed.is_none()); debug_assert!(msgs.signed_closing_tx.is_none()); } Ok(msgs.shutdown_result) @@ -13031,7 +14571,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ peer_state.channel_by_id.retain(|_, chan| { let shutdown_result = match channel_opt { Some((_, channel_id)) if chan.context().channel_id() != channel_id => None, - _ => match unblock_chan(chan, &mut peer_state.pending_msg_events) { + _ => match unblock_chan( + chan, + &mut peer_state.pending_msg_events, + &mut needs_holding_cell_release, + ) { Ok(shutdown_result) => shutdown_result, Err(err) => { let (_, err) = self.locked_handle_force_close( @@ -13072,6 +14616,9 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); } drop(per_peer_state); + if needs_holding_cell_release { + self.check_free_holding_cells(); + } for (err, counterparty_node_id) in shutdown_results { let _ = self.handle_error(err, counterparty_node_id); } @@ -13184,24 +14731,18 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let logger = WithContext::from( &self.logger, Some(*counterparty_node_id), Some(*channel_id), None ); - match funded_chan.try_send_stfu(&&logger) { - Ok(None) => {}, - Ok(Some(stfu)) => { - pending_msg_events.push(MessageSendEvent::SendStfu { - node_id: chan.context().get_counterparty_node_id(), - msg: stfu, - }); - }, - Err(e) => { - log_debug!(logger, "Could not advance quiescence handshake: {}", e); - } + if let Some(stfu) = funded_chan.try_send_stfu(true, &&logger) { + pending_msg_events.push(MessageSendEvent::SendStfu { + node_id: chan.context().get_counterparty_node_id(), + msg: stfu, + }); } } } } } - #[cfg(any(test, fuzzing))] + #[cfg(any(test, fuzzing, feature = "_test_utils"))] #[rustfmt::skip] pub fn maybe_propose_quiescence(&self, counterparty_node_id: &PublicKey, channel_id: &ChannelId) -> Result<(), APIError> { let mut result = Ok(()); @@ -13211,9 +14752,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex_opt = per_peer_state.get(counterparty_node_id); if peer_state_mutex_opt.is_none() { - result = Err(APIError::ChannelUnavailable { - err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") - }); + result = Err(APIError::no_such_peer(counterparty_node_id)); return notify; } @@ -13238,7 +14777,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }); notify = NotifyOption::SkipPersistHandleEvents; }, - Err(msg) => log_trace!(logger, "{}", msg), + Err(e) => { + debug_assert!(matches!(e, QuiescentError::DoNothing)); + log_trace!(logger, "Failed to propose quiescence"); + }, } } else { result = Err(APIError::APIMisuseError { @@ -13247,10 +14789,10 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } }, hash_map::Entry::Vacant(_) => { - result = Err(APIError::ChannelUnavailable { - err: format!("Channel with id {} not found for the passed counterparty node_id {}", - channel_id, counterparty_node_id), - }); + result = Err(APIError::no_such_channel_for_peer( + channel_id, + counterparty_node_id, + )); }, } @@ -13260,7 +14802,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ result } - #[cfg(any(test, fuzzing))] + #[cfg(any(test, fuzzing, feature = "_test_utils"))] #[rustfmt::skip] pub fn exit_quiescence(&self, counterparty_node_id: &PublicKey, channel_id: &ChannelId) -> Result<bool, APIError> { let _read_guard = self.total_consistency_lock.read().unwrap(); @@ -13268,9 +14810,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ let initiator = { let per_peer_state = self.per_peer_state.read().unwrap(); let peer_state_mutex = per_peer_state.get(counterparty_node_id) - .ok_or_else(|| APIError::ChannelUnavailable { - err: format!("Can't find a peer matching the passed counterparty node_id {counterparty_node_id}") - })?; + .ok_or_else(|| APIError::no_such_peer(counterparty_node_id))?; let mut peer_state = peer_state_mutex.lock().unwrap(); match peer_state.channel_by_id.entry(*channel_id) { hash_map::Entry::Occupied(mut chan_entry) => { @@ -13282,10 +14822,12 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ }) } }, - hash_map::Entry::Vacant(_) => return Err(APIError::ChannelUnavailable { - err: format!("Channel with id {} not found for the passed counterparty node_id {}", - channel_id, counterparty_node_id), - }), + hash_map::Entry::Vacant(_) => { + return Err(APIError::no_such_channel_for_peer( + channel_id, + counterparty_node_id, + )) + }, } }; self.check_free_holding_cells(); @@ -13302,13 +14844,13 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ ) -> Result<Bolt11Invoice, SignOrCreationError<()>> { let Bolt11InvoiceParameters { amount_msats, description, invoice_expiry_delta_secs, min_final_cltv_expiry_delta, - payment_hash, + payment_hash, payment_metadata, } = params; let currency = Network::from_chain_hash(self.chain_hash).map(Into::into).unwrap_or(Currency::Bitcoin); - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let duration_since_epoch = { use std::time::SystemTime; SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) @@ -13318,7 +14860,7 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ // This may be up to 2 hours in the future because of bitcoin's block time rule or about // 10-30 minutes in the past if a block hasn't been found recently. This should be fine as // the default invoice expiration is 2 hours, though shorter expirations may be problematic. - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let duration_since_epoch = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); @@ -13328,22 +14870,24 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } - let (payment_hash, payment_secret) = match payment_hash { + let (payment_hash, payment_secret, payment_metadata) = match payment_hash { Some(payment_hash) => { - let payment_secret = self + let (payment_secret, payment_metadata) = self .create_inbound_payment_for_hash( payment_hash, amount_msats, invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32), min_final_cltv_expiry_delta, + payment_metadata, ) .map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; - (payment_hash, payment_secret) + (payment_hash, payment_secret, payment_metadata) }, None => { self .create_inbound_payment( amount_msats, invoice_expiry_delta_secs.unwrap_or(DEFAULT_EXPIRY_TIME as u32), min_final_cltv_expiry_delta, + payment_metadata, ) .map_err(|()| SignOrCreationError::CreationError(CreationError::InvalidAmount))? }, @@ -13382,7 +14926,11 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ invoice = invoice.private_route(hint); } - let raw_invoice = invoice.build_raw().map_err(|e| SignOrCreationError::CreationError(e))?; + let raw_invoice = if let Some(payment_metadata) = payment_metadata { + invoice.payment_metadata(payment_metadata).build_raw() + } else { + invoice.build_raw() + }.map_err(|e| SignOrCreationError::CreationError(e))?; let signature = self.node_signer.sign_invoice(&raw_invoice, Recipient::Node); raw_invoice @@ -13392,6 +14940,43 @@ This indicates a bug inside LDK. Please report this error at https://github.com/ } } +/// Constructs an HTLC forward failure for sending back to the previous hop, converting to a blinded +/// failure where appropriate. +/// +/// When both trampoline and phantom secrets are present, the trampoline secret takes priority +/// for error encryption. +fn get_htlc_forward_failure( + blinded_failure: &Option<BlindedFailure>, onion_error: &HTLCFailReason, + incoming_packet_shared_secret: &[u8; 32], trampoline_shared_secret: &Option<[u8; 32]>, + phantom_shared_secret: &Option<[u8; 32]>, htlc_id: u64, +) -> HTLCForwardInfo { + // TODO: Correctly wrap the error packet twice if failing back a trampoline + phantom HTLC. + let secondary_shared_secret = trampoline_shared_secret.or(*phantom_shared_secret); + match blinded_failure { + Some(BlindedFailure::FromIntroductionNode) => { + let blinded_onion_error = + HTLCFailReason::reason(LocalHTLCFailureReason::InvalidOnionBlinding, vec![0; 32]); + let err_packet = blinded_onion_error.get_encrypted_failure_packet( + incoming_packet_shared_secret, + &secondary_shared_secret, + ); + HTLCForwardInfo::FailHTLC { htlc_id, err_packet } + }, + Some(BlindedFailure::FromBlindedNode) => HTLCForwardInfo::FailMalformedHTLC { + htlc_id, + failure_code: LocalHTLCFailureReason::InvalidOnionBlinding.failure_code(), + sha256_of_onion: [0; 32], + }, + None => { + let err_packet = onion_error.get_encrypted_failure_packet( + incoming_packet_shared_secret, + &secondary_shared_secret, + ); + HTLCForwardInfo::FailHTLC { htlc_id, err_packet } + }, + } +} + /// Parameters used with [`create_bolt11_invoice`]. /// /// [`create_bolt11_invoice`]: ChannelManager::create_bolt11_invoice @@ -13424,6 +15009,13 @@ pub struct Bolt11InvoiceParameters { /// involving another protocol where the payment hash is also involved outside the scope of /// lightning. pub payment_hash: Option<PaymentHash>, + + /// The `payment_metadata` to include in the invoice. This is provided back to us in the payment + /// onion by the sender, available as [`RecipientOnionFields::payment_metadata`] via + /// [`Event::PaymentClaimable::onion_fields`]. + /// + /// The metadata itself is encrypted and HMAC'd before being stored in the BOLT 11 invoice. + pub payment_metadata: Option<Vec<u8>>, } impl Default for Bolt11InvoiceParameters { @@ -13434,6 +15026,7 @@ impl Default for Bolt11InvoiceParameters { invoice_expiry_delta_secs: None, min_final_cltv_expiry_delta: None, payment_hash: None, + payment_metadata: None, } } } @@ -13491,6 +15084,47 @@ macro_rules! create_offer_builder { ($self: ident, $builder: ty) => { Ok(builder.into()) } + + /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by any + /// [`ChannelManager`] (or [`OffersMessageFlow`]) using the same [`ExpandedKey`] (as returned + /// from [`NodeSigner::get_expanded_key`]). This allows any nodes participating in a BOLT 11 + /// "phantom node" cluster to also receive BOLT 12 payments. + /// + /// Note that, unlike with BOLT 11 invoices, BOLT 12 "phantom" offers do not in fact have any + /// "phantom node" appended to receiving paths. Instead, multiple blinded paths are simply + /// included which terminate at different final nodes. + /// + /// `other_nodes_channels` must be set to a list of each participating node's `node_id` (from + /// [`NodeSigner::get_node_id`] with a [`Recipient::Node`]) and its channels. + /// + /// `path_count_limit` is used to limit the number of blinded paths included in the resulting + /// [`Offer`]. Note that if this is less than the number of participating nodes (i.e. + /// `other_nodes_channels.len() + 1`) not all nodes will participate in receiving funds. + /// Because the parameterized [`MessageRouter`] will only get a chance to limit the number of + /// paths *per-node*, it is important to set this for offers that will be included in a QR + /// code. + /// + /// See [`Self::create_offer_builder`] for more details on the blinded path construction. + /// + /// [`ExpandedKey`]: inbound_payment::ExpandedKey + pub fn create_phantom_offer_builder( + &$self, other_nodes_channels: Vec<(PublicKey, Vec<ChannelDetails>)>, + path_count_limit: usize, + ) -> Result<$builder, Bolt12SemanticError> { + let mut peers = Vec::with_capacity(other_nodes_channels.len() + 1); + if !other_nodes_channels.iter().any(|(node_id, _)| *node_id == $self.get_our_node_id()) { + peers.push(($self.get_our_node_id(), $self.get_peers_for_blinded_path())); + } + for (node_id, peer_chans) in other_nodes_channels { + peers.push((node_id, Self::channel_details_to_forward_nodes(peer_chans))); + } + + let builder = $self.flow.create_phantom_offer_builder( + &$self.entropy_source, peers, path_count_limit + )?; + + Ok(builder.into()) + } } } macro_rules! create_refund_builder { ($self: ident, $builder: ty) => { @@ -13689,6 +15323,13 @@ impl< /// - the parameterized [`Router`] is unable to create a blinded reply path for the invoice /// request. /// + /// In general, you should use the + /// [`bitcoin-payment-instructions` crate](https://docs.rs/bitcoin-payment-instructions/) to + /// resolve payment instructions strings (e.g. from QR codes, link opens, pasted instructions, + /// or typed instructions) into payment instructions and use this when the instructions resolve + /// to a BOLT 12 offer or [`Self::pay_for_offer_from_hrn`] when they resolve to a BOLT 12 offer + /// via a human-readable name. + /// /// [`InvoiceRequest`]: crate::offers::invoice_request::InvoiceRequest /// [`InvoiceRequestBuilder`]: crate::offers::invoice_request::InvoiceRequestBuilder /// [`Bolt12Invoice`]: crate::offers::invoice::Bolt12Invoice @@ -13827,13 +15468,13 @@ impl< let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); self.flow.enqueue_invoice_request( - invoice_request.clone(), payment_id, nonce, + invoice_request.clone(), payment_id, self.get_peers_for_blinded_path() )?; let retryable_invoice_request = RetryableInvoiceRequest { invoice_request: invoice_request.clone(), - nonce, + nonce: Some(nonce), needs_retry: true, }; @@ -13877,9 +15518,11 @@ impl< refund, self.list_usable_channels(), |amount_msats, relative_expiry| { - self.create_inbound_payment(Some(amount_msats), relative_expiry, None) + self.create_inbound_payment(Some(amount_msats), relative_expiry, None, None) .map_err(|()| Bolt12SemanticError::InvalidAmount) + .map(|(preimage, secret, _no_metadata)| (preimage, secret)) }, + None, )?; let invoice = builder.allow_mpp().build_and_sign(secp_ctx)?; @@ -13888,74 +15531,8 @@ impl< Ok(invoice) } - /// Pays for an [`Offer`] looked up using [BIP 353] Human Readable Names resolved by the DNS - /// resolver(s) at `dns_resolvers` which resolve names according to [bLIP 32]. - /// - /// Because most wallets support on-chain or other payment schemes beyond only offers, this is - /// deprecated in favor of the [`bitcoin-payment-instructions`] crate, which can be used to - /// build an [`OfferFromHrn`] and call [`Self::pay_for_offer_from_hrn`]. Thus, this method is - /// deprecated. - /// - /// # Payment - /// - /// The provided `payment_id` is used to ensure that only one invoice is paid for the request - /// when received. See [Avoiding Duplicate Payments] for other requirements once the payment has - /// been sent. - /// - /// To revoke the request, use [`ChannelManager::abandon_payment`] prior to receiving the - /// invoice. If abandoned, or an invoice isn't received in a reasonable amount of time, the - /// payment will fail with an [`PaymentFailureReason::UserAbandoned`] or - /// [`PaymentFailureReason::InvoiceRequestExpired`], respectively. - /// - /// # Privacy - /// - /// For payer privacy, uses a derived payer id and uses [`MessageRouter::create_blinded_paths`] - /// to construct a [`BlindedMessagePath`] for the reply path. - /// - /// # Errors - /// - /// Errors if a duplicate `payment_id` is provided given the caveats in the aforementioned link. - /// - /// [BIP 353]: https://github.com/bitcoin/bips/blob/master/bip-0353.mediawiki - /// [bLIP 32]: https://github.com/lightning/blips/blob/master/blip-0032.md - /// [`OMNameResolver::resolve_name`]: crate::onion_message::dns_resolution::OMNameResolver::resolve_name - /// [`OMNameResolver::handle_dnssec_proof_for_uri`]: crate::onion_message::dns_resolution::OMNameResolver::handle_dnssec_proof_for_uri - /// [`bitcoin-payment-instructions`]: https://docs.rs/bitcoin-payment-instructions/ - /// [Avoiding Duplicate Payments]: #avoiding-duplicate-payments - /// [`BlindedMessagePath`]: crate::blinded_path::message::BlindedMessagePath - /// [`PaymentFailureReason::UserAbandoned`]: crate::events::PaymentFailureReason::UserAbandoned - /// [`PaymentFailureReason::InvoiceRequestRejected`]: crate::events::PaymentFailureReason::InvoiceRequestRejected - #[cfg(feature = "dnssec")] - #[deprecated(note = "Use bitcoin-payment-instructions and pay_for_offer_from_hrn instead")] - pub fn pay_for_offer_from_human_readable_name( - &self, name: HumanReadableName, amount_msats: u64, payment_id: PaymentId, - optional_params: OptionalOfferPaymentParams, dns_resolvers: Vec<Destination>, - ) -> Result<(), ()> { - let (onion_message, context) = - self.flow.hrn_resolver.resolve_name(payment_id, name, &self.entropy_source)?; - - let expiration = StaleExpiration::TimerTicks(1); - self.pending_outbound_payments.add_new_awaiting_offer( - payment_id, - expiration, - optional_params.retry_strategy, - optional_params.route_params_config, - amount_msats, - optional_params.payer_note, - )?; - - self.flow - .enqueue_dns_onion_message( - onion_message, - context, - dns_resolvers, - self.get_peers_for_blinded_path(), - ) - .map_err(|_| ()) - } - - /// Gets a payment secret and payment hash for use in an invoice given to a third party wishing - /// to pay us. + /// Gets a payment secret, payment hash, and encrypts the `payment_metadata` for use in an + /// invoice given to a third party wishing to pay us. /// /// This differs from [`create_inbound_payment_for_hash`] only in that it generates the /// [`PaymentHash`] and [`PaymentPreimage`] for you. @@ -13986,8 +15563,8 @@ impl< /// [`create_inbound_payment_for_hash`]: Self::create_inbound_payment_for_hash pub fn create_inbound_payment( &self, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32, - min_final_cltv_expiry_delta: Option<u16>, - ) -> Result<(PaymentHash, PaymentSecret), ()> { + min_final_cltv_expiry_delta: Option<u16>, payment_metadata: Option<Vec<u8>>, + ) -> Result<(PaymentHash, PaymentSecret, Option<Vec<u8>>), ()> { inbound_payment::create( &self.inbound_payment_key, min_value_msat, @@ -13995,11 +15572,12 @@ impl< &self.entropy_source, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, min_final_cltv_expiry_delta, + payment_metadata, ) } - /// Gets a [`PaymentSecret`] for a given [`PaymentHash`], for which the payment preimage is - /// stored external to LDK. + /// Gets a [`PaymentSecret`] for a given [`PaymentHash`] (for which the payment preimage is + /// stored external to LDK) and encrypts the `payment_metadata`. /// /// A [`PaymentClaimable`] event will only be generated if the [`PaymentSecret`] matches a /// payment secret fetched via this method or [`create_inbound_payment`], and which is at least @@ -14014,6 +15592,9 @@ impl< /// before a [`PaymentClaimable`] event will be generated, ensuring that we do not provide the /// sender "proof-of-payment" unless they have paid the required amount. /// + /// The returned secret commits to the `payment_metadata` and thus the invoice's metadata must + /// match what is provided here. + /// /// `invoice_expiry_delta_secs` describes the number of seconds that the invoice is valid for /// in excess of the current time. This should roughly match the expiry time set in the invoice. /// After this many seconds, we will remove the inbound payment, resulting in any attempts to @@ -14032,41 +15613,42 @@ impl< /// Note that a malicious eavesdropper can intuit whether an inbound payment was created by /// `create_inbound_payment` or `create_inbound_payment_for_hash` based on runtime. /// - /// # Note - /// - /// If you register an inbound payment with this method, then serialize the `ChannelManager`, then - /// deserialize it with a node running 0.0.103 and earlier, the payment will fail to be received. - /// /// Errors if `min_value_msat` is greater than total bitcoin supply. /// - /// If `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable - /// on versions of LDK prior to 0.0.114. - /// /// [`create_inbound_payment`]: Self::create_inbound_payment /// [`PaymentClaimable`]: events::Event::PaymentClaimable pub fn create_inbound_payment_for_hash( &self, payment_hash: PaymentHash, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32, min_final_cltv_expiry: Option<u16>, - ) -> Result<PaymentSecret, ()> { + payment_metadata: Option<Vec<u8>>, + ) -> Result<(PaymentSecret, Option<Vec<u8>>), ()> { inbound_payment::create_from_hash( &self.inbound_payment_key, min_value_msat, payment_hash, invoice_expiry_delta_secs, + &self.entropy_source, self.highest_seen_timestamp.load(Ordering::Acquire) as u64, min_final_cltv_expiry, + payment_metadata, ) } - /// Gets an LDK-generated payment preimage from a payment hash and payment secret that were - /// previously returned from [`create_inbound_payment`]. + /// Gets an LDK-generated payment preimage from a payment hash and secret and decrypts the + /// metadata (if any) that were previously returned from [`create_inbound_payment`]. /// /// [`create_inbound_payment`]: Self::create_inbound_payment - pub fn get_payment_preimage( + pub fn get_payment_preimage_decrypt_metadata( &self, payment_hash: PaymentHash, payment_secret: PaymentSecret, + payment_metadata: Option<&mut [u8]>, ) -> Result<PaymentPreimage, APIError> { let expanded_key = &self.inbound_payment_key; - inbound_payment::get_payment_preimage(payment_hash, payment_secret, expanded_key) + inbound_payment::get_payment_preimage( + payment_hash, + payment_secret, + payment_metadata, + expanded_key, + ) } /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively @@ -14097,9 +15679,9 @@ impl< } pub(super) fn duration_since_epoch(&self) -> Duration { - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let now = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let now = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); @@ -14107,6 +15689,43 @@ impl< now } + /// Converts a list of channels to a list of peers which may be suitable to receive onion + /// messages through. + fn channel_details_to_forward_nodes( + mut channel_list: Vec<ChannelDetails>, + ) -> Vec<MessageForwardNode> { + channel_list.sort_unstable_by_key(|chan| chan.counterparty.node_id); + let mut res = Vec::new(); + // TODO: When MSRV reaches 1.77 use chunk_by + let mut start = 0; + while start < channel_list.len() { + let counterparty_node_id = channel_list[start].counterparty.node_id; + let end = channel_list[start..] + .iter() + .position(|chan| chan.counterparty.node_id != counterparty_node_id) + .map(|pos| start + pos) + .unwrap_or(channel_list.len()); + + let peer_chans = &channel_list[start..end]; + if peer_chans.iter().any(|chan| chan.is_usable) + && peer_chans.iter().any(|c| c.counterparty.features.supports_onion_messages()) + { + res.push(MessageForwardNode { + node_id: peer_chans[0].counterparty.node_id, + short_channel_id: peer_chans + .iter() + .filter(|chan| chan.is_usable) + // Select the channel which has the highest local balance. We assume this + // channel is the most likely to stick around. + .max_by_key(|chan| chan.inbound_capacity_msat) + .and_then(|chan| chan.get_inbound_payment_scid()), + }) + } + start = end; + } + res + } + fn get_peers_for_blinded_path(&self) -> Vec<MessageForwardNode> { let per_peer_state = self.per_peer_state.read().unwrap(); per_peer_state @@ -14121,7 +15740,9 @@ impl< .iter() .filter(|(_, channel)| channel.context().is_usable()) .filter_map(|(_, channel)| channel.as_funded()) - .min_by_key(|funded_channel| funded_channel.context.channel_creation_height) + // Select the channel which has the highest local balance. We assume this + // channel is the most likely to stick around. + .max_by_key(|funded_channel| funded_channel.funding.get_value_to_self_msat()) .and_then(|funded_channel| funded_channel.get_inbound_scid()), }) .collect::<Vec<_>>() @@ -14234,6 +15855,22 @@ impl< self.process_pending_events(&event_handler); let collected_events = events.into_inner(); + // When both DiscardFunding and SpliceNegotiationFailed are emitted for the same + // channel, DiscardFunding must come first so that inputs are unlocked before any + // retry. Each pair is emitted adjacently under a single lock, so checking + // adjacent events is sufficient. + for window in collected_events.windows(2) { + if let events::Event::SpliceNegotiationFailed { channel_id, .. } = &window[0] { + if let events::Event::DiscardFunding { channel_id: cid, .. } = &window[1] { + assert!( + channel_id != cid, + "DiscardFunding must precede SpliceNegotiationFailed for channel {}", + channel_id, + ); + } + } + } + // To expand the coverage and make sure all events are properly serialised and deserialised, // we test all generated events round-trip: for event in &collected_events { @@ -14313,10 +15950,12 @@ impl< let peer_state = &mut *peer_state_lck; if let Some(blocker) = completed_blocker.take() { // Only do this on the first iteration of the loop. - if let Some(blockers) = peer_state.actions_blocking_raa_monitor_updates - .get_mut(&channel_id) - { - blockers.retain(|iter| iter != &blocker); + let entry = peer_state.actions_blocking_raa_monitor_updates.entry(channel_id); + if let btree_map::Entry::Occupied(mut entry) = entry { + entry.get_mut().retain(|iter| iter != &blocker); + if entry.get().is_empty() { + entry.remove(); + } } } @@ -14352,7 +15991,7 @@ impl< mem::drop(per_peer_state); if let Some(data) = post_update_data { - self.handle_post_monitor_update_chan_resume(data); + let _ = self.handle_post_monitor_update_chan_resume(data); } self.handle_holding_cell_free_result(holding_cell_res); @@ -14507,14 +16146,19 @@ impl< chan.peer_disconnected_is_resumable(&&logger); if let Some(splice_funding_failed) = splice_funding_failed { - splice_failed_events.push(events::Event::SpliceFailed { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + if let Some(funding_info) = funding_info { + splice_failed_events.push(events::Event::DiscardFunding { + channel_id: chan.context().channel_id(), + funding_info, + }); + } + splice_failed_events.push(events::Event::SpliceNegotiationFailed { channel_id: chan.context().channel_id(), counterparty_node_id, user_channel_id: chan.context().get_user_id(), - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + contribution: Some(contribution), + reason: events::NegotiationFailureReason::PeerDisconnected, }); } @@ -14579,11 +16223,14 @@ impl< &MessageSendEvent::HandleError { .. } => false, // Gossip &MessageSendEvent::SendChannelAnnouncement { .. } => false, - &MessageSendEvent::BroadcastChannelAnnouncement { .. } => true, - // [`ChannelManager::pending_broadcast_events`] holds the [`BroadcastChannelUpdate`] - // This check here is to ensure exhaustivity. + // [`ChannelManager::pending_broadcast_messages`] holds broadcast events, + // not per-peer queues. + &MessageSendEvent::BroadcastChannelAnnouncement { .. } => { + debug_assert!(false, "BroadcastChannelAnnouncement should be in pending_broadcast_messages"); + false + }, &MessageSendEvent::BroadcastChannelUpdate { .. } => { - debug_assert!(false, "This event shouldn't have been here"); + debug_assert!(false, "BroadcastChannelUpdate should be in pending_broadcast_messages"); false }, &MessageSendEvent::BroadcastNodeAnnouncement { .. } => true, @@ -14700,8 +16347,6 @@ impl< } } - log_debug!(logger, "Generating channel_reestablish events"); - let per_peer_state = self.per_peer_state.read().unwrap(); if let Some(peer_state_mutex) = per_peer_state.get(&counterparty_node_id) { let mut peer_state_lock = peer_state_mutex.lock().unwrap(); @@ -14719,22 +16364,45 @@ impl< let logger = WithChannelContext::from(&self.logger, &chan.context(), None); match chan.peer_connected_get_handshake(self.chain_hash, &&logger) { ReconnectionMsg::Reestablish(msg) => { + log_debug!( + logger, + "Generated channel_reestablish event for channel {}", + chan.context().channel_id() + ); pending_msg_events.push(MessageSendEvent::SendChannelReestablish { node_id: chan.context().get_counterparty_node_id(), msg, }) }, - ReconnectionMsg::Open(OpenChannelMessage::V1(msg)) => pending_msg_events - .push(MessageSendEvent::SendOpenChannel { + ReconnectionMsg::Open(OpenChannelMessage::V1(msg)) => { + log_debug!( + logger, + "Generated open_channel event for channel {}", + chan.context().channel_id() + ); + pending_msg_events.push(MessageSendEvent::SendOpenChannel { node_id: chan.context().get_counterparty_node_id(), msg, - }), - ReconnectionMsg::Open(OpenChannelMessage::V2(msg)) => pending_msg_events - .push(MessageSendEvent::SendOpenChannelV2 { + }); + }, + ReconnectionMsg::Open(OpenChannelMessage::V2(msg)) => { + log_debug!( + logger, + "Generated open_channel_v2 event for channel {}", + chan.context().channel_id() + ); + pending_msg_events.push(MessageSendEvent::SendOpenChannelV2 { node_id: chan.context().get_counterparty_node_id(), msg, - }), - ReconnectionMsg::None => {}, + }); + }, + ReconnectionMsg::None => { + log_debug!( + logger, + "Peer reconnected. No reconnection message for channel {}", + chan.context().channel_id() + ); + }, } } } @@ -14760,15 +16428,9 @@ impl< /// the chunks of `MessageSendEvent`s for different peers is random. I.e. if the array contains /// `MessageSendEvent`s for both `node_a` and `node_b`, the `MessageSendEvent`s for `node_a` /// will randomly be placed first or last in the returned array. - /// - /// Note that even though `BroadcastChannelAnnouncement` and `BroadcastChannelUpdate` - /// `MessageSendEvent`s are intended to be broadcasted to all peers, they will be placed among - /// the `MessageSendEvent`s to the specific peer they were generated under. fn get_and_clear_pending_msg_events(&self) -> Vec<MessageSendEvent> { let events = RefCell::new(Vec::new()); PersistenceNotifierGuard::optionally_notify(self, || { - let mut result = NotifyOption::SkipPersistNoEvents; - // This method is quite performance-sensitive. Not only is it called very often, but it // *is* the critical path between generating a message for a peer and giving it to the // `PeerManager` to send. Thus, we should avoid adding any more logic here than we @@ -14777,9 +16439,7 @@ impl< // TODO: This behavior should be documented. It's unintuitive that we query // ChannelMonitors when clearing other events. - if self.process_pending_monitor_events() { - result = NotifyOption::DoPersist; - } + let mut result = self.process_pending_monitor_events(); if self.maybe_generate_initial_closing_signed() { result = NotifyOption::DoPersist; @@ -14878,7 +16538,7 @@ impl< self.best_block_updated(header, height); } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { let _persistence_guard = PersistenceNotifierGuard::optionally_notify_skipping_background_events( self, @@ -14955,12 +16615,12 @@ impl< // See the docs for `ChannelManagerReadArgs` for more. let block_hash = header.block_hash(); - log_trace!(self.logger, "New best block: {} at height {}", block_hash, height); + log_info!(self.logger, "New best block: {} at height {}", block_hash, height); let _persistence_guard = PersistenceNotifierGuard::optionally_notify_skipping_background_events( self, || -> NotifyOption { NotifyOption::DoPersist }); - *self.best_block.write().unwrap() = BestBlock::new(block_hash, height); + self.best_block.write().unwrap().update_for_new_tip(block_hash, height); let mut min_anchor_feerate = None; let mut min_non_anchor_feerate = None; @@ -15120,8 +16780,8 @@ impl< for (source, payment_hash) in timed_out_pending_htlcs.drain(..) { let reason = LocalHTLCFailureReason::CLTVExpiryTooSoon; let data = self.get_htlc_inbound_temp_fail_data(reason); - timed_out_htlcs.push((source, payment_hash, HTLCFailReason::reason(reason, data), - HTLCHandlingFailureType::Forward { node_id: Some(funded_channel.context.get_counterparty_node_id()), channel_id: *channel_id })); + let failure_type = source.failure_type(funded_channel.context.get_counterparty_node_id(), *channel_id); + timed_out_htlcs.push((source, payment_hash, HTLCFailReason::reason(reason, data), failure_type)); } let logger = WithChannelContext::from(&self.logger, &funded_channel.context, None); match funding_confirmed_opt { @@ -15216,14 +16876,16 @@ impl< if let Some(announcement) = funded_channel.get_signed_channel_announcement( &self.node_signer, self.chain_hash, height, &self.config.read().unwrap(), ) { - pending_msg_events.push(MessageSendEvent::BroadcastChannelAnnouncement { - msg: announcement, - // Note that get_signed_channel_announcement fails - // if the channel cannot be announced, so - // get_channel_update_for_broadcast will never fail - // by the time we get here. - update_msg: Some(self.get_channel_update_for_broadcast(funded_channel).unwrap().0), - }); + self.pending_broadcast_messages.lock().unwrap().push( + MessageSendEvent::BroadcastChannelAnnouncement { + msg: announcement, + // Note that get_signed_channel_announcement + // fails if the channel cannot be announced, so + // get_channel_update_for_broadcast will never + // fail by the time we get here. + update_msg: Some(self.get_channel_update_for_broadcast(funded_channel).unwrap().0), + }, + ); } } } @@ -15271,39 +16933,63 @@ impl< } for (counterparty_node_id, channel_id) in to_process_monitor_update_actions { - self.channel_monitor_updated(&channel_id, None, &counterparty_node_id); + let _ = self.channel_monitor_updated(&channel_id, None, &counterparty_node_id); } if let Some(height) = height_opt { + // If height is approaching the number of blocks we think it takes us to get our + // commitment transaction confirmed before the HTLC expires, plus the number of blocks + // we generally consider it to take to do a commitment update, just give up on it and + // fail the HTLC. self.claimable_payments.lock().unwrap().claimable_payments.retain( |payment_hash, payment| { payment.htlcs.retain(|htlc| { - // If height is approaching the number of blocks we think it takes us to get - // our commitment transaction confirmed before the HTLC expires, plus the - // number of blocks we generally consider it to take to do a commitment update, - // just give up on it and fail the HTLC. - if height >= htlc.cltv_expiry - HTLC_FAIL_BACK_BUFFER { + let htlc_timed_out = htlc.mpp_part.check_onchain_timeout(height); + if htlc_timed_out { let reason = LocalHTLCFailureReason::PaymentClaimBuffer; timed_out_htlcs.push(( - HTLCSource::PreviousHopData(htlc.prev_hop.clone()), + HTLCSource::PreviousHopData(htlc.mpp_part.prev_hop.clone()), payment_hash.clone(), HTLCFailReason::reason( reason, - invalid_payment_err_data(htlc.value, height), + invalid_payment_err_data(htlc.mpp_part.value, height), ), HTLCHandlingFailureType::Receive { payment_hash: payment_hash.clone(), }, )); - false - } else { - true } + !htlc_timed_out }); !payment.htlcs.is_empty() // Only retain this entry if htlcs has at least one entry. }, ); + self.awaiting_trampoline_forwards.lock().unwrap().retain(|payment_hash, payment| { + if payment.htlcs.is_empty() { + debug_assert!(false); + return false; + } + let htlc_timed_out = + payment.htlcs.iter().any(|htlc| htlc.check_onchain_timeout(height)); + if htlc_timed_out { + let previous_hop_data = + payment.htlcs.drain(..).map(|claimable| claimable.prev_hop).collect(); + + let failure_reason = LocalHTLCFailureReason::CLTVExpiryTooSoon; + timed_out_htlcs.push(( + HTLCSource::TrampolineForward { previous_hop_data, outbound_payment: None }, + *payment_hash, + HTLCFailReason::reason( + failure_reason, + self.get_htlc_inbound_temp_fail_data(failure_reason), + ), + HTLCHandlingFailureType::TrampolineForward {}, + )); + } + !htlc_timed_out + }); + let mut intercepted_htlcs = self.pending_intercepted_htlcs.lock().unwrap(); intercepted_htlcs.retain(|_, htlc| { if height >= htlc.forward_info.outgoing_cltv_value - HTLC_FAIL_BACK_BUFFER { @@ -15374,7 +17060,7 @@ impl< /// Gets the latest best block which was connected either via the [`chain::Listen`] or /// [`chain::Confirm`] interfaces. - pub fn current_best_block(&self) -> BestBlock { + pub fn current_best_block(&self) -> BlockLocator { self.best_block.read().unwrap().clone() } @@ -15867,87 +17553,82 @@ impl< } fn handle_tx_add_input(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAddInput) { - let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _persistence_guard = PersistenceNotifierGuard::manually_notify(self, || { let res = self.internal_tx_add_input(counterparty_node_id, msg); - let persist = match &res { - Err(_) => NotifyOption::DoPersist, - Ok(persist) => *persist, - }; + debug_assert!(res.as_ref().err().map_or(true, |err| !err.closes_channel())); let _ = self.handle_error(res, counterparty_node_id); - persist + self.event_persist_notifier.notify(); }); } fn handle_tx_add_output(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAddOutput) { - let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _persistence_guard = PersistenceNotifierGuard::manually_notify(self, || { let res = self.internal_tx_add_output(counterparty_node_id, msg); - let persist = match &res { - Err(_) => NotifyOption::DoPersist, - Ok(persist) => *persist, - }; + debug_assert!(res.as_ref().err().map_or(true, |err| !err.closes_channel())); let _ = self.handle_error(res, counterparty_node_id); - persist + self.event_persist_notifier.notify(); }); } fn handle_tx_remove_input(&self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveInput) { - let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _persistence_guard = PersistenceNotifierGuard::manually_notify(self, || { let res = self.internal_tx_remove_input(counterparty_node_id, msg); - let persist = match &res { - Err(_) => NotifyOption::DoPersist, - Ok(persist) => *persist, - }; + debug_assert!(res.as_ref().err().map_or(true, |err| !err.closes_channel())); let _ = self.handle_error(res, counterparty_node_id); - persist + self.event_persist_notifier.notify(); }); } fn handle_tx_remove_output(&self, counterparty_node_id: PublicKey, msg: &msgs::TxRemoveOutput) { - let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let _persistence_guard = PersistenceNotifierGuard::manually_notify(self, || { let res = self.internal_tx_remove_output(counterparty_node_id, msg); + debug_assert!(res.as_ref().err().map_or(true, |err| !err.closes_channel())); + let _ = self.handle_error(res, counterparty_node_id); + self.event_persist_notifier.notify(); + }); + } + + fn handle_tx_complete(&self, counterparty_node_id: PublicKey, msg: &msgs::TxComplete) { + let _persistence_guard = PersistenceNotifierGuard::manually_notify(self, || { + let res = self.internal_tx_complete(counterparty_node_id, msg); + debug_assert!(res.as_ref().err().map_or(true, |err| !err.closes_channel())); + let _ = self.handle_error(res, counterparty_node_id); + self.event_persist_notifier.notify(); + }); + } + + fn handle_tx_signatures(&self, counterparty_node_id: PublicKey, msg: &msgs::TxSignatures) { + let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); + let res = self.internal_tx_signatures(&counterparty_node_id, msg); + let _ = self.handle_error(res, counterparty_node_id); + } + + fn handle_tx_init_rbf(&self, counterparty_node_id: PublicKey, msg: &msgs::TxInitRbf) { + let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { + let res = self.internal_tx_init_rbf(&counterparty_node_id, msg); let persist = match &res { - Err(_) => NotifyOption::DoPersist, - Ok(persist) => *persist, + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistHandleEvents, }; let _ = self.handle_error(res, counterparty_node_id); persist }); } - fn handle_tx_complete(&self, counterparty_node_id: PublicKey, msg: &msgs::TxComplete) { + fn handle_tx_ack_rbf(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAckRbf) { let _persistence_guard = PersistenceNotifierGuard::optionally_notify(self, || { - let res = self.internal_tx_complete(counterparty_node_id, msg); + let res = self.internal_tx_ack_rbf(&counterparty_node_id, msg); let persist = match &res { - Err(_) => NotifyOption::DoPersist, - Ok(persist) => *persist, + Err(e) if e.closes_channel() => NotifyOption::DoPersist, + Err(_) => NotifyOption::SkipPersistHandleEvents, + Ok(()) => NotifyOption::SkipPersistHandleEvents, }; let _ = self.handle_error(res, counterparty_node_id); persist }); } - fn handle_tx_signatures(&self, counterparty_node_id: PublicKey, msg: &msgs::TxSignatures) { - let _persistence_guard = PersistenceNotifierGuard::notify_on_drop(self); - let res = self.internal_tx_signatures(&counterparty_node_id, msg); - let _ = self.handle_error(res, counterparty_node_id); - } - - fn handle_tx_init_rbf(&self, counterparty_node_id: PublicKey, msg: &msgs::TxInitRbf) { - let err = Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone(), - )); - let _: Result<(), _> = self.handle_error(err, counterparty_node_id); - } - - fn handle_tx_ack_rbf(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAckRbf) { - let err = Err(MsgHandleErrInternal::send_err_msg_no_close( - "Dual-funded channels not supported".to_owned(), - msg.channel_id.clone(), - )); - let _: Result<(), _> = self.handle_error(err, counterparty_node_id); - } - fn handle_tx_abort(&self, counterparty_node_id: PublicKey, msg: &msgs::TxAbort) { // Note that we never need to persist the updated ChannelManager for an inbound // tx_abort message - interactive transaction construction does not need to @@ -15968,11 +17649,11 @@ impl< for (payment_id, retryable_invoice_request) in self.pending_outbound_payments.release_invoice_requests_awaiting_invoice() { - let RetryableInvoiceRequest { invoice_request, nonce, .. } = retryable_invoice_request; + let RetryableInvoiceRequest { invoice_request, .. } = retryable_invoice_request; let peers = self.get_peers_for_blinded_path(); let enqueue_invreq_res = - self.flow.enqueue_invoice_request(invoice_request, payment_id, nonce, peers); + self.flow.enqueue_invoice_request(invoice_request, payment_id, peers); if enqueue_invreq_res.is_err() { log_warn!( self.logger, @@ -16041,6 +17722,13 @@ impl< None => return None, }; + let payment_metadata = + if let Some(OffersContext::InvoiceRequest { payment_metadata, .. }) = &context { + payment_metadata.clone() + } else { + None + }; + let invoice_request = match self.flow.verify_invoice_request(invoice_request, context) { Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) => invoice_request, Ok(InvreqResponseInstructions::SendStaticInvoice { recipient_id, invoice_slot, invoice_request }) => { @@ -16057,8 +17745,11 @@ impl< self.create_inbound_payment( Some(amount_msats), relative_expiry, - None - ).map_err(|_| Bolt12SemanticError::InvalidAmount) + None, + None, + ) + .map_err(|_| Bolt12SemanticError::InvalidAmount) + .map(|(preimage, secret, _no_metadata)| (preimage, secret)) }; let (result, context) = match invoice_request { @@ -16068,6 +17759,7 @@ impl< &request, self.list_usable_channels(), get_payment_info, + payment_metadata, ); match result { @@ -16092,6 +17784,7 @@ impl< &request, self.list_usable_channels(), get_payment_info, + payment_metadata, ); match result { @@ -16307,6 +18000,18 @@ impl< htlc_id, } => { let _serialize_guard = PersistenceNotifierGuard::notify_on_drop(self); + // It's possible the release_held_htlc message raced ahead of us fully committing to the + // HTLC. If that's the case, update the pending update_add to indicate that the HTLC should + // be released immediately. + let released_pre_commitment_htlc = self + .do_funded_channel_callback(prev_outbound_scid_alias, |chan| { + chan.release_pending_inbound_held_htlc(htlc_id) + }) + .unwrap_or(false); + if released_pre_commitment_htlc { + return; + } + // It's possible the release_held_htlc message raced ahead of us transitioning the pending // update_add to `Self::pending_intercept_htlcs`. If that's the case, update the pending // update_add to indicate that the HTLC should be released immediately. @@ -16366,9 +18071,29 @@ impl< ); log_trace!(logger, "Releasing held htlc with intercept_id {}", intercept_id); + let prev_chan_public = { + let per_peer_state = self.per_peer_state.read().unwrap(); + let peer_state = per_peer_state + .get(&htlc.prev_counterparty_node_id) + .map(|mtx| mtx.lock().unwrap()); + let chan_state = peer_state + .as_ref() + .map(|state| state.channel_by_id.get(&htlc.prev_channel_id)) + .flatten(); + if let Some(chan_state) = chan_state { + chan_state.context().should_announce() + } else { + // If the inbound channel has closed since the HTLC was held, we really + // shouldn't forward it - forwarding it now would result in, at best, + // having to claim the HTLC on chain. Instead, drop the HTLC and let the + // counterparty claim their money on chain. + return; + } + }; + let should_intercept = self .do_funded_channel_callback(next_hop_scid, |chan| { - self.forward_needs_intercept_to_known_chan(chan) + self.forward_needs_intercept_to_known_chan(prev_chan_public, chan) }) .unwrap_or_else(|| self.forward_needs_intercept_to_unknown_chan(next_hop_scid)); @@ -16406,15 +18131,7 @@ impl< }, } } else { - let mut per_source_pending_forward = [( - htlc.prev_outbound_scid_alias, - htlc.prev_counterparty_node_id, - htlc.prev_funding_outpoint, - htlc.prev_channel_id, - htlc.prev_user_channel_id, - vec![(htlc.forward_info, htlc.prev_htlc_id)], - )]; - self.forward_htlcs(&mut per_source_pending_forward); + self.forward_htlcs([htlc]); } }, _ => return, @@ -16426,65 +18143,6 @@ impl< } } -#[cfg(feature = "dnssec")] -impl< - M: chain::Watch<SP::EcdsaSigner>, - T: BroadcasterInterface, - ES: EntropySource, - NS: NodeSigner, - SP: SignerProvider, - F: FeeEstimator, - R: Router, - MR: MessageRouter, - L: Logger, - > DNSResolverMessageHandler for ChannelManager<M, T, ES, NS, SP, F, R, MR, L> -{ - fn handle_dnssec_query( - &self, _message: DNSSECQuery, _responder: Option<Responder>, - ) -> Option<(DNSResolverMessage, ResponseInstruction)> { - None - } - - #[rustfmt::skip] - fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { - let offer_opt = self.flow.hrn_resolver.handle_dnssec_proof_for_offer(message, context); - #[cfg_attr(not(feature = "_test_utils"), allow(unused_mut))] - if let Some((completed_requests, mut offer)) = offer_opt { - for (name, payment_id) in completed_requests { - #[cfg(feature = "_test_utils")] - if let Some(replacement_offer) = self.testing_dnssec_proof_offer_resolution_override.lock().unwrap().remove(&name) { - // If we have multiple pending requests we may end up over-using the override - // offer, but tests can deal with that. - offer = replacement_offer; - } - if let Ok((amt_msats, payer_note)) = self.pending_outbound_payments.params_for_payment_awaiting_offer(payment_id) { - let offer_pay_res = - self.pay_for_offer_intern(&offer, None, Some(amt_msats), payer_note, payment_id, Some(name), - |retryable_invoice_request| { - self.pending_outbound_payments - .received_offer(payment_id, Some(retryable_invoice_request)) - .map_err(|_| Bolt12SemanticError::DuplicatePaymentId) - }); - if offer_pay_res.is_err() { - // The offer we tried to pay is the canonical current offer for the name we - // wanted to pay. If we can't pay it, there's no way to recover so fail the - // payment. - // Note that the PaymentFailureReason should be ignored for an - // AwaitingInvoice payment. - self.pending_outbound_payments.abandon_payment( - payment_id, PaymentFailureReason::RouteNotFound, &self.pending_events, - ); - } - } - } - } - } - - fn release_pending_messages(&self) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { - self.flow.release_pending_dns_messages() - } -} - impl< M: chain::Watch<SP::EcdsaSigner>, T: BroadcasterInterface, @@ -16584,30 +18242,30 @@ pub fn provided_init_features(config: &UserConfig) -> InitFeatures { const SERIALIZATION_VERSION: u8 = 1; const MIN_SERIALIZATION_VERSION: u8 = 1; -// We plan to start writing this version in 0.5. +// We plan to start writing this version a few versions after we start writing inbound committed +// payment onions in `Channel`, which is already done in tests but not yet switched on in prod. // -// LDK 0.5+ will reconstruct the set of pending HTLCs from `Channel{Monitor}` data that started -// being written in 0.3, ignoring legacy `ChannelManager` HTLC maps on read and not writing them. -// LDK 0.5+ will automatically fail to read if the pending HTLC set cannot be reconstructed, i.e. -// if we were last written with pending HTLCs on 0.2- or if the new 0.3+ fields are missing. +// If we see this version on read, we will use said onions when reconstructing the set of pending +// HTLCs, ignoring legacy `ChannelManager` HTLC maps on read and not writing them. We'll also +// automatically fail to read if the pending HTLC set cannot be reconstructed, i.e. if the new +// payment onion field is missing. // -// If 0.3 or 0.4 reads this manager version, it knows that the legacy maps were not written and -// acts accordingly. -const RECONSTRUCT_HTLCS_FROM_CHANS_VERSION: u8 = 5; +// Left as `None` for now until we are committed to writing inbound committed onions in `Channel`s. +const RECONSTRUCT_HTLCS_FROM_CHANS_VERSION: Option<u8> = None; -impl_writeable_tlv_based!(PhantomRouteHints, { +impl_ser_tlv_based!(PhantomRouteHints, { (2, channels, required_vec), (4, phantom_scid, required), (6, real_node_pubkey, required), }); -impl_writeable_tlv_based!(BlindedForward, { +impl_ser_tlv_based!(BlindedForward, { (0, inbound_blinding_point, required), (1, failure, (default_value, BlindedFailure::FromIntroductionNode)), (3, next_blinding_override, option), }); -impl_writeable_tlv_based_enum!(PendingHTLCRouting, +impl_ser_tlv_based_enum!(PendingHTLCRouting, (0, Forward) => { (0, onion_packet, required), (1, blinded, option), @@ -16637,15 +18295,18 @@ impl_writeable_tlv_based_enum!(PendingHTLCRouting, (11, invoice_request, option), }, (3, TrampolineForward) => { - (0, incoming_shared_secret, required), + (0, trampoline_shared_secret, required), (2, onion_packet, required), (4, blinded, option), (6, node_id, required), (8, incoming_cltv_expiry, required), + (10, incoming_multipath_data, option), + (12, next_trampoline_amt_msat, required), + (14, next_trampoline_cltv_expiry, required), } ); -impl_writeable_tlv_based!(PendingHTLCInfo, { +impl_ser_tlv_based!(PendingHTLCInfo, { (0, routing, required), (2, incoming_shared_secret, required), (4, payment_hash, required), @@ -16730,17 +18391,17 @@ impl Readable for HTLCFailureMsg { } } -impl_writeable_tlv_based_enum_legacy!(PendingHTLCStatus, ; +impl_ser_tlv_based_enum_legacy!(PendingHTLCStatus, ; (0, Forward), (1, Fail), ); -impl_writeable_tlv_based_enum!(BlindedFailure, +impl_ser_tlv_based_enum!(BlindedFailure, (0, FromIntroductionNode) => {}, (2, FromBlindedNode) => {}, ); -impl_writeable_tlv_based!(HTLCPreviousHopData, { +impl_ser_tlv_based!(HTLCPreviousHopData, { (0, prev_outbound_scid_alias, required), (1, phantom_shared_secret, option), (2, outpoint, required), @@ -16754,35 +18415,36 @@ impl_writeable_tlv_based!(HTLCPreviousHopData, { (9, channel_id, (default_value, ChannelId::v1_from_funding_outpoint(outpoint.0.unwrap()))), (11, counterparty_node_id, option), (13, trampoline_shared_secret, option), + (15, amount_msat, option), }); -impl Writeable for ClaimableHTLC { - fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { - let (payment_data, keysend_preimage) = match &self.onion_payload { - OnionPayload::Invoice { _legacy_hop_data } => (_legacy_hop_data.as_ref(), None), - OnionPayload::Spontaneous(preimage) => (None, Some(preimage)), - }; - write_tlv_fields!(writer, { - (0, self.prev_hop, required), - (1, self.total_msat, required), - (2, self.value, required), - (3, self.sender_intended_value, required), - (4, payment_data, option), - (5, self.total_value_received, option), - (6, self.cltv_expiry, required), - (8, keysend_preimage, option), - (10, self.counterparty_skimmed_fee_msat, option), - }); - Ok(()) - } +fn write_claimable_htlc<W: Writer>( + htlc: &ClaimableHTLC, total_mpp_value_msat: u64, writer: &mut W, +) -> Result<(), io::Error> { + let (payment_data, keysend_preimage) = match &htlc.onion_payload { + OnionPayload::Invoice { _legacy_hop_data } => (_legacy_hop_data.as_ref(), None), + OnionPayload::Spontaneous(preimage) => (None, Some(preimage)), + }; + write_tlv_fields!(writer, { + (0, htlc.mpp_part.prev_hop, required), + (1, total_mpp_value_msat, required), + (2, htlc.mpp_part.value, required), + (3, htlc.mpp_part.sender_intended_value, required), + (4, payment_data, option), + (5, htlc.mpp_part.total_value_received, option), + (6, htlc.mpp_part.cltv_expiry, required), + (8, keysend_preimage, option), + (10, htlc.counterparty_skimmed_fee_msat, option), + }); + Ok(()) } -impl Readable for ClaimableHTLC { +impl Readable for (ClaimableHTLC, u64) { #[rustfmt::skip] fn read<R: Read>(reader: &mut R) -> Result<Self, DecodeError> { _init_and_read_len_prefixed_tlv_fields!(reader, { (0, prev_hop, required), - (1, total_msat, option), + (1, total_msat, required), // Added and always written in 0.0.107 (2, value_ser, required), (3, sender_intended_value, option), (4, payment_data_opt, option), @@ -16798,32 +18460,22 @@ impl Readable for ClaimableHTLC { if payment_data.is_some() { return Err(DecodeError::InvalidValue) } - if total_msat.is_none() { - total_msat = Some(value); - } OnionPayload::Spontaneous(p) }, - None => { - if total_msat.is_none() { - if payment_data.is_none() { - return Err(DecodeError::InvalidValue) - } - total_msat = Some(payment_data.as_ref().unwrap().total_msat); - } - OnionPayload::Invoice { _legacy_hop_data: payment_data } - }, + None => OnionPayload::Invoice { _legacy_hop_data: payment_data }, }; - Ok(Self { - prev_hop: prev_hop.0.unwrap(), - timer_ticks: 0, - value, - sender_intended_value: sender_intended_value.unwrap_or(value), - total_value_received, - total_msat: total_msat.unwrap(), + Ok((ClaimableHTLC { + mpp_part: MppPart { + prev_hop: prev_hop.0.unwrap(), + timer_ticks: 0, + value, + sender_intended_value: sender_intended_value.unwrap_or(value), + total_value_received, + cltv_expiry: cltv_expiry.0.unwrap(), + }, onion_payload, - cltv_expiry: cltv_expiry.0.unwrap(), counterparty_skimmed_fee_msat, - }) + }, total_msat.0.expect("required field"))) } } @@ -16874,6 +18526,8 @@ impl Readable for HTLCSource { }) } 1 => Ok(HTLCSource::PreviousHopData(Readable::read(reader)?)), + // Note: we intentionally do not read HTLCSource::TrampolineForward because we do not + // want to allow downgrades with in-flight trampoline forwards. _ => Err(DecodeError::UnknownRequiredFeature), } } @@ -16906,12 +18560,19 @@ impl Writeable for HTLCSource { 1u8.write(writer)?; field.write(writer)?; }, + HTLCSource::TrampolineForward { ref previous_hop_data, ref outbound_payment } => { + 2u8.write(writer)?; + write_tlv_fields!(writer, { + (1, *previous_hop_data, required_vec), + (3, outbound_payment, option), + }); + }, } Ok(()) } } -impl_writeable_tlv_based!(PendingAddHTLCInfo, { +impl_ser_tlv_based!(PendingAddHTLCInfo, { (0, forward_info, required), (1, prev_user_channel_id, (default_value, 0)), (2, prev_outbound_scid_alias, required), @@ -16923,6 +18584,12 @@ impl_writeable_tlv_based!(PendingAddHTLCInfo, { (9, prev_counterparty_node_id, required), }); +impl_ser_tlv_based!(TrampolineDispatch, { + (1, payment_id, required), + (3, path, required), + (5, session_priv, required), +}); + impl Writeable for HTLCForwardInfo { fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { const FAIL_HTLC_VARIANT_ID: u8 = 1; @@ -16994,7 +18661,7 @@ impl Readable for HTLCForwardInfo { } } -impl_writeable_tlv_based!(PendingInboundPayment, { +impl_ser_tlv_based!(PendingInboundPayment, { (0, payment_secret, required), (2, expiry_time, required), (4, user_payment_id, required), @@ -17073,26 +18740,20 @@ impl< } } - let mut decode_update_add_htlcs_opt = None; - let decode_update_add_htlcs = self.decode_update_add_htlcs.lock().unwrap(); - if !decode_update_add_htlcs.is_empty() { - decode_update_add_htlcs_opt = Some(decode_update_add_htlcs); - } - let claimable_payments = self.claimable_payments.lock().unwrap(); let pending_outbound_payments = self.pending_outbound_payments.pending_outbound_payments.lock().unwrap(); let mut htlc_purposes: Vec<&events::PaymentPurpose> = Vec::new(); - let mut htlc_onion_fields: Vec<&_> = Vec::new(); + let mut htlc_onion_fields: Vec<Option<&_>> = Vec::new(); (claimable_payments.claimable_payments.len() as u64).write(writer)?; for (payment_hash, payment) in claimable_payments.claimable_payments.iter() { payment_hash.write(writer)?; (payment.htlcs.len() as u64).write(writer)?; for htlc in payment.htlcs.iter() { - htlc.write(writer)?; + write_claimable_htlc(&htlc, payment.onion_fields.total_mpp_amount_msat, writer)?; } htlc_purposes.push(&payment.purpose); - htlc_onion_fields.push(&payment.onion_fields); + htlc_onion_fields.push(Some(&payment.onion_fields)); } let mut monitor_update_blocked_actions_per_peer = None; @@ -17104,6 +18765,14 @@ impl< peer_states.push(peer_state_mutex.unsafe_well_ordered_double_lock_self()); } + let mut decode_update_add_htlcs_opt = None; + { + let decode_update_add_htlcs = self.decode_update_add_htlcs.lock().unwrap(); + if !decode_update_add_htlcs.is_empty() { + decode_update_add_htlcs_opt = Some(decode_update_add_htlcs); + } + } + let mut peer_storage_dir: Vec<(&PublicKey, &Vec<u8>)> = Vec::new(); (serializable_peer_count).write(writer)?; @@ -17128,23 +18797,32 @@ impl< let our_pending_intercepts = self.pending_intercepted_htlcs.lock().unwrap(); // Since some FundingNegotiation variants are not persisted, any splice in such state must - // be failed upon reload. However, as the necessary information for the SpliceFailed event - // is not persisted, the event itself needs to be persisted even though it hasn't been - // emitted yet. These are removed after the events are written. + // be failed upon reload. However, as the necessary information for the + // SpliceNegotiationFailed and DiscardFunding events is not persisted, the events need to + // be persisted even though they + // haven't been emitted yet. These are removed after the events are written. let mut events = self.pending_events.lock().unwrap(); let event_count = events.len(); for peer_state in peer_states.iter() { for chan in peer_state.channel_by_id.values().filter_map(Channel::as_funded) { if let Some(splice_funding_failed) = chan.maybe_splice_funding_failed() { + let (funding_info, contribution) = splice_funding_failed.into_parts(); + if let Some(funding_info) = funding_info { + events.push_back(( + events::Event::DiscardFunding { + channel_id: chan.context().channel_id(), + funding_info, + }, + None, + )); + } events.push_back(( - events::Event::SpliceFailed { + events::Event::SpliceNegotiationFailed { channel_id: chan.context.channel_id(), counterparty_node_id: chan.context.get_counterparty_node_id(), user_channel_id: chan.context.get_user_id(), - abandoned_funding_txo: splice_funding_failed.funding_txo, - channel_type: splice_funding_failed.channel_type, - contributed_inputs: splice_funding_failed.contributed_inputs, - contributed_outputs: splice_funding_failed.contributed_outputs, + reason: events::NegotiationFailureReason::PeerDisconnected, + contribution: Some(contribution), }, None, )); @@ -17265,9 +18943,10 @@ impl< (17, in_flight_monitor_updates, option), (19, peer_storage_dir, optional_vec), (21, WithoutLength(&self.flow.writeable_async_receive_offer_cache()), required), + (23, self.best_block.read().unwrap().previous_blocks, required), }); - // Remove the SpliceFailed events added earlier. + // Remove the SpliceNegotiationFailed and DiscardFunding events added earlier. events.truncate(event_count); Ok(()) @@ -17318,12 +18997,23 @@ impl Readable for VecDeque<(Event, Option<EventCompletionAction>)> { } } +/// We write the [`ClaimableHTLC`]'s [`RecipientOnionFields`] separately as they were added sometime +/// later. Because [`RecipientOnionFields`] only implements [`ReadableArgs`] we have to add a +/// wrapper which reads them without [`RecipientOnionFields::total_mpp_amount_msat`] and then fill +/// them in later. +struct AmountlessClaimablePaymentHTLCOnion(RecipientOnionFields); + +impl Readable for AmountlessClaimablePaymentHTLCOnion { + fn read<R: Read>(reader: &mut R) -> Result<Self, DecodeError> { + Ok(Self(ReadableArgs::read(reader, 0)?)) + } +} + // Raw deserialized data from a ChannelManager, before validation or reconstruction. // This is an internal DTO used in the two-stage deserialization process. pub(super) struct ChannelManagerData<SP: SignerProvider> { chain_hash: ChainHash, - best_block_height: u32, - best_block_hash: BlockHash, + best_block: BlockLocator, channels: Vec<FundedChannel<SP>>, claimable_payments: HashMap<PaymentHash, ClaimablePayment>, peer_init_features: Vec<(PublicKey, InitFeatures)>, @@ -17351,25 +19041,18 @@ pub(super) struct ChannelManagerData<SP: SignerProvider> { } /// Arguments for deserializing [`ChannelManagerData`]. -struct ChannelManagerDataReadArgs< - 'a, - ES: EntropySource, - NS: NodeSigner, - SP: SignerProvider, - L: Logger, -> { +struct ChannelManagerDataReadArgs<'a, ES: EntropySource, SP: SignerProvider, L: Logger> { entropy_source: &'a ES, - node_signer: &'a NS, signer_provider: &'a SP, config: UserConfig, logger: &'a L, } -impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> - ReadableArgs<ChannelManagerDataReadArgs<'a, ES, NS, SP, L>> for ChannelManagerData<SP> +impl<'a, ES: EntropySource, SP: SignerProvider, L: Logger> + ReadableArgs<ChannelManagerDataReadArgs<'a, ES, SP, L>> for ChannelManagerData<SP> { fn read<R: io::Read>( - reader: &mut R, args: ChannelManagerDataReadArgs<'a, ES, NS, SP, L>, + reader: &mut R, args: ChannelManagerDataReadArgs<'a, ES, SP, L>, ) -> Result<Self, DecodeError> { let version = read_ver_prefix!(reader, SERIALIZATION_VERSION); @@ -17394,7 +19077,7 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> } let forward_htlcs_legacy: HashMap<u64, Vec<HTLCForwardInfo>> = - if version < RECONSTRUCT_HTLCS_FROM_CHANS_VERSION { + if RECONSTRUCT_HTLCS_FROM_CHANS_VERSION.map_or(true, |v| version < v) { let forward_htlcs_count: u64 = Readable::read(reader)?; let mut fwds = hash_map_with_capacity(cmp::min(forward_htlcs_count as usize, 128)); for _ in 0..forward_htlcs_count { @@ -17424,10 +19107,20 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> previous_hops_len as usize, MAX_ALLOC_SIZE / mem::size_of::<ClaimableHTLC>(), )); + let mut total_mpp_value_msat = None; for _ in 0..previous_hops_len { - previous_hops.push(<ClaimableHTLC as Readable>::read(reader)?); + let (htlc, total_mpp_value_msat_read) = + <(ClaimableHTLC, u64) as Readable>::read(reader)?; + if total_mpp_value_msat.is_some() + && total_mpp_value_msat != Some(total_mpp_value_msat_read) + { + return Err(DecodeError::InvalidValue); + } + total_mpp_value_msat = Some(total_mpp_value_msat_read); + previous_hops.push(htlc); } - claimable_htlcs_list.push((payment_hash, previous_hops)); + let total_mpp_value_msat = total_mpp_value_msat.ok_or(DecodeError::InvalidValue)?; + claimable_htlcs_list.push((payment_hash, previous_hops, total_mpp_value_msat)); } let peer_count: u64 = Readable::read(reader)?; @@ -17510,8 +19203,10 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> let mut fake_scid_rand_bytes: Option<[u8; 32]> = None; let mut probing_cookie_secret: Option<[u8; 32]> = None; let mut claimable_htlc_purposes = None; - let mut claimable_htlc_onion_fields = None; - let mut pending_claiming_payments = None; + let mut amountless_claimable_htlc_onion_fields: Option< + Vec<Option<AmountlessClaimablePaymentHTLCOnion>>, + > = None; + let mut pending_claiming_payments = Some(new_hash_map()); let mut monitor_update_blocked_actions_per_peer: Option<Vec<(_, BTreeMap<_, Vec<_>>)>> = None; let mut events_override = None; @@ -17526,6 +19221,7 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> let mut inbound_payment_id_secret = None; let mut peer_storage_dir: Option<Vec<(PublicKey, Vec<u8>)>> = None; let mut async_receive_offer_cache: AsyncReceiveOfferCache = AsyncReceiveOfferCache::new(); + let mut best_block_previous_blocks = None; read_tlv_fields!(reader, { (1, pending_outbound_payments_no_retry, option), (2, pending_intercepted_htlcs_legacy, option), @@ -17538,12 +19234,13 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> (9, claimable_htlc_purposes, optional_vec), (10, legacy_in_flight_monitor_updates, option), (11, probing_cookie_secret, option), - (13, claimable_htlc_onion_fields, optional_vec), + (13, amountless_claimable_htlc_onion_fields, optional_vec), (14, decode_update_add_htlcs_legacy, option), (15, inbound_payment_id_secret, option), (17, in_flight_monitor_updates, option), (19, peer_storage_dir, optional_vec), (21, async_receive_offer_cache, (default_value, async_receive_offer_cache)), + (23, best_block_previous_blocks, option), }); // Merge legacy pending_outbound_payments fields into a single HashMap. @@ -17594,83 +19291,55 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> // Resolve events_override: if present, it replaces pending_events. let pending_events_read = events_override.unwrap_or(pending_events_read); - // Combine claimable_htlcs_list with their purposes and onion fields. For very old data - // (pre-0.0.107) that lacks purposes, reconstruct them from legacy hop data. - let expanded_inbound_key = args.node_signer.get_expanded_key(); - + // Combine claimable_htlcs_list with their purposes and onion fields. let mut claimable_payments = hash_map_with_capacity(claimable_htlcs_list.len()); if let Some(purposes) = claimable_htlc_purposes { if purposes.len() != claimable_htlcs_list.len() { return Err(DecodeError::InvalidValue); } - if let Some(onion_fields) = claimable_htlc_onion_fields { + if let Some(onion_fields) = amountless_claimable_htlc_onion_fields { if onion_fields.len() != claimable_htlcs_list.len() { return Err(DecodeError::InvalidValue); } - for (purpose, (onion, (payment_hash, htlcs))) in purposes + for (purpose, (onion, (payment_hash, htlcs, total_mpp_value_msat))) in purposes .into_iter() .zip(onion_fields.into_iter().zip(claimable_htlcs_list.into_iter())) { - let claimable = ClaimablePayment { purpose, htlcs, onion_fields: onion }; - let existing_payment = claimable_payments.insert(payment_hash, claimable); - if existing_payment.is_some() { + let onion_fields = if let Some(mut onion) = onion { + if onion.0.total_mpp_amount_msat != 0 + && onion.0.total_mpp_amount_msat != total_mpp_value_msat + { + return Err(DecodeError::InvalidValue); + } + onion.0.total_mpp_amount_msat = total_mpp_value_msat; + onion.0 + } else { return Err(DecodeError::InvalidValue); - } - } - } else { - for (purpose, (payment_hash, htlcs)) in - purposes.into_iter().zip(claimable_htlcs_list.into_iter()) - { - let claimable = ClaimablePayment { purpose, htlcs, onion_fields: None }; + }; + let claimable = ClaimablePayment { purpose, htlcs, onion_fields }; let existing_payment = claimable_payments.insert(payment_hash, claimable); if existing_payment.is_some() { return Err(DecodeError::InvalidValue); } } + } else if !purposes.is_empty() || !claimable_htlcs_list.is_empty() { + // `amountless_claimable_htlc_onion_fields` was first written in LDK 0.0.115. We + // haven't supported upgrade from 0.0.115 with pending HTLCs since 0.1. + return Err(DecodeError::InvalidValue); } } else { // LDK versions prior to 0.0.107 did not write a `pending_htlc_purposes`, but do // include a `_legacy_hop_data` in the `OnionPayload`. - for (payment_hash, htlcs) in claimable_htlcs_list.into_iter() { - if htlcs.is_empty() { - return Err(DecodeError::InvalidValue); - } - let purpose = match &htlcs[0].onion_payload { - OnionPayload::Invoice { _legacy_hop_data } => { - if let Some(hop_data) = _legacy_hop_data { - events::PaymentPurpose::Bolt11InvoicePayment { - payment_preimage: match inbound_payment::verify( - payment_hash, - &hop_data, - 0, - &expanded_inbound_key, - &args.logger, - ) { - Ok((payment_preimage, _)) => payment_preimage, - Err(()) => { - log_error!(args.logger, "Failed to read claimable payment data for HTLC with payment hash {} - was not a pending inbound payment and didn't match our payment key", &payment_hash); - return Err(DecodeError::InvalidValue); - }, - }, - payment_secret: hop_data.payment_secret, - } - } else { - return Err(DecodeError::InvalidValue); - } - }, - OnionPayload::Spontaneous(payment_preimage) => { - events::PaymentPurpose::SpontaneousPayment(*payment_preimage) - }, - }; - claimable_payments - .insert(payment_hash, ClaimablePayment { purpose, htlcs, onion_fields: None }); - } + return Err(DecodeError::InvalidValue); } Ok(ChannelManagerData { chain_hash, - best_block_height, - best_block_hash, + best_block: BlockLocator { + block_hash: best_block_hash, + height: best_block_height, + previous_blocks: best_block_previous_blocks.unwrap_or([None; 12]), + }, channels, forward_htlcs_legacy, claimable_payments, @@ -17703,7 +19372,7 @@ impl<'a, ES: EntropySource, NS: NodeSigner, SP: SignerProvider, L: Logger> /// is: /// 1) Deserialize all stored [`ChannelMonitor`]s. /// 2) Deserialize the [`ChannelManager`] by filling in this struct and calling: -/// `<(BlockHash, ChannelManager)>::read(reader, args)` +/// `<(BlockLocator, ChannelManager)>::read(reader, args)` /// This may result in closing some channels if the [`ChannelMonitor`] is newer than the stored /// [`ChannelManager`] state to ensure no loss of funds. Thus, transactions may be broadcasted. /// 3) If you are not fetching full blocks, register all relevant [`ChannelMonitor`] outpoints the @@ -17904,14 +19573,14 @@ impl< MR: MessageRouter, L: Logger + Clone, > ReadableArgs<ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, L>> - for (BlockHash, Arc<ChannelManager<M, T, ES, NS, SP, F, R, MR, L>>) + for (BlockLocator, Arc<ChannelManager<M, T, ES, NS, SP, F, R, MR, L>>) { fn read<Reader: io::Read>( reader: &mut Reader, args: ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, L>, ) -> Result<Self, DecodeError> { - let (blockhash, chan_manager) = - <(BlockHash, ChannelManager<M, T, ES, NS, SP, F, R, MR, L>)>::read(reader, args)?; - Ok((blockhash, Arc::new(chan_manager))) + let (best_block, chan_manager) = + <(BlockLocator, ChannelManager<M, T, ES, NS, SP, F, R, MR, L>)>::read(reader, args)?; + Ok((best_block, Arc::new(chan_manager))) } } @@ -17927,7 +19596,7 @@ impl< MR: MessageRouter, L: Logger + Clone, > ReadableArgs<ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, L>> - for (BlockHash, ChannelManager<M, T, ES, NS, SP, F, R, MR, L>) + for (BlockLocator, ChannelManager<M, T, ES, NS, SP, F, R, MR, L>) { fn read<Reader: io::Read>( reader: &mut Reader, args: ChannelManagerReadArgs<'a, M, T, ES, NS, SP, F, R, MR, L>, @@ -17937,7 +19606,6 @@ impl< reader, ChannelManagerDataReadArgs { entropy_source: &args.entropy_source, - node_signer: &args.node_signer, signer_provider: &args.signer_provider, config: args.config.clone(), logger: &args.logger, @@ -17972,11 +19640,10 @@ impl< pub(super) fn from_channel_manager_data( data: ChannelManagerData<SP>, mut args: ChannelManagerReadArgs<'_, M, T, ES, NS, SP, F, R, MR, L>, - ) -> Result<(BlockHash, Self), DecodeError> { + ) -> Result<(BlockLocator, Self), DecodeError> { let ChannelManagerData { chain_hash, - best_block_height, - best_block_hash, + best_block, channels, mut forward_htlcs_legacy, claimable_payments, @@ -18431,6 +20098,14 @@ impl< log_error!(logger, " Please ensure the chain::Watch API requirements are met and file a bug report at https://github.com/lightningdevkit/rust-lightning"); return Err(DecodeError::DangerousValue); } + if funded_chan.blocked_monitor_updates_pending() > 0 { + pending_background_events.push( + BackgroundEvent::AttemptUnblockMonitorUpdates { + counterparty_node_id: *counterparty_id, + channel_id: *chan_id, + }, + ); + } } else { // We shouldn't have persisted (or read) any unfunded channel types so none should have been // created in this `channel_by_id` map. @@ -18548,11 +20223,12 @@ impl< // `reconstruct_manager_from_monitors` is set below. Currently we set in tests randomly to // ensure the legacy codepaths also have test coverage. #[cfg(not(test))] - let reconstruct_manager_from_monitors = _version >= RECONSTRUCT_HTLCS_FROM_CHANS_VERSION; + let reconstruct_manager_from_monitors = + RECONSTRUCT_HTLCS_FROM_CHANS_VERSION.is_some_and(|v| _version >= v); #[cfg(test)] let reconstruct_manager_from_monitors = args.reconstruct_manager_from_monitors.unwrap_or_else(|| { - use core::hash::{BuildHasher, Hasher}; + use core::hash::BuildHasher; match std::env::var("LDK_TEST_REBUILD_MGR_FROM_MONITORS") { Ok(val) => match val.as_str() { @@ -18587,23 +20263,8 @@ impl< // that it is handled. let mut already_forwarded_htlcs: HashMap< (ChannelId, PaymentHash), - Vec<(HTLCPreviousHopData, u64)>, + Vec<(HTLCPreviousHopData, OutboundHop)>, > = new_hash_map(); - let prune_forwarded_htlc = |already_forwarded_htlcs: &mut HashMap< - (ChannelId, PaymentHash), - Vec<(HTLCPreviousHopData, u64)>, - >, - prev_hop: &HTLCPreviousHopData, - payment_hash: &PaymentHash| { - if let hash_map::Entry::Occupied(mut entry) = - already_forwarded_htlcs.entry((prev_hop.channel_id, *payment_hash)) - { - entry.get_mut().retain(|(htlc, _)| prev_hop.htlc_id != htlc.htlc_id); - if entry.get().is_empty() { - entry.remove(); - } - } - }; { // If we're tracking pending payments, ensure we haven't lost any by looking at the // ChannelMonitor data for any channels for which we do not have authorative state @@ -18626,33 +20287,26 @@ impl< if reconstruct_manager_from_monitors { if let Some(chan) = peer_state.channel_by_id.get(channel_id) { if let Some(funded_chan) = chan.as_funded() { + if funded_chan.has_legacy_inbound_htlcs() { + return Err(DecodeError::InvalidValue); + } + // Reconstruct `ChannelManager::decode_update_add_htlcs` from the serialized + // `Channel` as part of removing the requirement to regularly persist the + // `ChannelManager`. let scid_alias = funded_chan.context.outbound_scid_alias(); - let inbound_committed_update_adds = - funded_chan.inbound_committed_unresolved_htlcs(); - for (payment_hash, htlc) in inbound_committed_update_adds { - match htlc { - InboundUpdateAdd::WithOnion { update_add_htlc } => { - // Reconstruct `ChannelManager::decode_update_add_htlcs` from the serialized - // `Channel` as part of removing the requirement to regularly persist the - // `ChannelManager`. - decode_update_add_htlcs - .entry(scid_alias) - .or_insert_with(Vec::new) - .push(update_add_htlc); - }, - InboundUpdateAdd::Forwarded { - hop_data, - outbound_amt_msat, - } => { - already_forwarded_htlcs - .entry((hop_data.channel_id, payment_hash)) - .or_insert_with(Vec::new) - .push((hop_data, outbound_amt_msat)); - }, - InboundUpdateAdd::Legacy => { - return Err(DecodeError::InvalidValue) - }, - } + for update_add_htlc in funded_chan.inbound_htlcs_pending_decode() { + decode_update_add_htlcs + .entry(scid_alias) + .or_insert_with(Vec::new) + .push(update_add_htlc); + } + for (payment_hash, prev_hop, next_hop) in + funded_chan.inbound_forwarded_htlcs() + { + already_forwarded_htlcs + .entry((prev_hop.channel_id, payment_hash)) + .or_insert_with(Vec::new) + .push((prev_hop, next_hop)); } } } @@ -18682,7 +20336,7 @@ impl< htlc.payment_hash, session_priv_bytes, &path, - best_block_height, + best_block.height, &logger, ); } @@ -18690,14 +20344,16 @@ impl< } } for (channel_id, monitor) in args.channel_monitors.iter() { - let mut is_channel_closed = true; + let (mut is_channel_closed, mut user_channel_id_opt) = (true, None); let counterparty_node_id = monitor.get_counterparty_node_id(); if let Some(peer_state_mtx) = per_peer_state.get(&counterparty_node_id) { let mut peer_state_lock = peer_state_mtx.lock().unwrap(); let peer_state = &mut *peer_state_lock; - is_channel_closed = !peer_state.channel_by_id.contains_key(channel_id); - if reconstruct_manager_from_monitors && !is_channel_closed { - if let Some(chan) = peer_state.channel_by_id.get(channel_id) { + if let Some(chan) = peer_state.channel_by_id.get(channel_id) { + is_channel_closed = false; + user_channel_id_opt = Some(chan.context().get_user_id()); + + if reconstruct_manager_from_monitors { if let Some(funded_chan) = chan.as_funded() { for (payment_hash, prev_hop) in funded_chan.outbound_htlc_forwards() { @@ -18730,65 +20386,36 @@ impl< let htlc_id = SentHTLCId::from_source(&htlc_source); match htlc_source { HTLCSource::PreviousHopData(prev_hop_data) => { - let pending_forward_matches_htlc = |info: &PendingAddHTLCInfo| { - info.prev_funding_outpoint == prev_hop_data.outpoint - && info.prev_htlc_id == prev_hop_data.htlc_id - }; - - // If `reconstruct_manager_from_monitors` is set, we always add all inbound committed - // HTLCs to `decode_update_add_htlcs` in the above loop, but we need to prune from - // those added HTLCs if they were already forwarded to the outbound edge. Otherwise, - // we'll double-forward. - if reconstruct_manager_from_monitors { - dedup_decode_update_add_htlcs( - &mut decode_update_add_htlcs, - &prev_hop_data, - "HTLC already forwarded to the outbound edge", - &&logger, - ); - prune_forwarded_htlc( + reconcile_pending_htlcs_with_monitor( + reconstruct_manager_from_monitors, + &mut already_forwarded_htlcs, + &mut forward_htlcs_legacy, + &mut pending_events_read, + &mut pending_intercepted_htlcs_legacy, + &mut decode_update_add_htlcs, + &mut decode_update_add_htlcs_legacy, + prev_hop_data, + &logger, + htlc.payment_hash, + monitor.channel_id(), + ); + }, + HTLCSource::TrampolineForward { previous_hop_data, .. } => { + for prev_hop_data in previous_hop_data { + reconcile_pending_htlcs_with_monitor( + reconstruct_manager_from_monitors, &mut already_forwarded_htlcs, - &prev_hop_data, - &htlc.payment_hash, + &mut forward_htlcs_legacy, + &mut pending_events_read, + &mut pending_intercepted_htlcs_legacy, + &mut decode_update_add_htlcs, + &mut decode_update_add_htlcs_legacy, + prev_hop_data, + &logger, + htlc.payment_hash, + monitor.channel_id(), ); } - - // The ChannelMonitor is now responsible for this HTLC's - // failure/success and will let us know what its outcome is. If we - // still have an entry for this HTLC in `forward_htlcs_legacy`, - // `pending_intercepted_htlcs_legacy`, or - // `decode_update_add_htlcs_legacy`, we were apparently not persisted - // after the monitor was when forwarding the payment. - dedup_decode_update_add_htlcs( - &mut decode_update_add_htlcs_legacy, - &prev_hop_data, - "HTLC was forwarded to the closed channel", - &&logger, - ); - forward_htlcs_legacy.retain(|_, forwards| { - forwards.retain(|forward| { - if let HTLCForwardInfo::AddHTLC(htlc_info) = forward { - if pending_forward_matches_htlc(&htlc_info) { - log_info!(logger, "Removing pending to-forward HTLC with hash {} as it was forwarded to the closed channel {}", - &htlc.payment_hash, &monitor.channel_id()); - false - } else { true } - } else { true } - }); - !forwards.is_empty() - }); - pending_intercepted_htlcs_legacy.retain(|intercepted_id, htlc_info| { - if pending_forward_matches_htlc(&htlc_info) { - log_info!(logger, "Removing pending intercepted HTLC with hash {} as it was forwarded to the closed channel {}", - &htlc.payment_hash, &monitor.channel_id()); - pending_events_read.retain(|(event, _)| { - if let Event::HTLCIntercepted { intercept_id: ev_id, .. } = event { - intercepted_id != ev_id - } else { true } - }); - false - } else { true } - }); }, HTLCSource::OutboundRoute { payment_id, @@ -18900,112 +20527,65 @@ impl< // preimages from it which may be needed in upstream channels for forwarded // payments. let mut fail_read = false; - let outbound_claimed_htlcs_iter = monitor.get_all_current_outbound_htlcs() + let outbound_claimed_htlcs_iter = monitor + .get_all_current_outbound_htlcs() .into_iter() .filter_map(|(htlc_source, (htlc, preimage_opt))| { - if let HTLCSource::PreviousHopData(prev_hop) = &htlc_source { - if let Some(payment_preimage) = preimage_opt { - let inbound_edge_monitor = args.channel_monitors.get(&prev_hop.channel_id); - // Note that for channels which have gone to chain, - // `get_all_current_outbound_htlcs` is never pruned and always returns - // a constant set until the monitor is removed/archived. Thus, we - // want to skip replaying claims that have definitely been resolved - // on-chain. - - // If the inbound monitor is not present, we assume it was fully - // resolved and properly archived, implying this payment had plenty - // of time to get claimed and we can safely skip any further - // attempts to claim it (they wouldn't succeed anyway as we don't - // have a monitor against which to do so). - let inbound_edge_monitor = if let Some(monitor) = inbound_edge_monitor { - monitor - } else { - return None; - }; - // Second, if the inbound edge of the payment's monitor has been - // fully claimed we've had at least `ANTI_REORG_DELAY` blocks to - // get any PaymentForwarded event(s) to the user and assume that - // there's no need to try to replay the claim just for that. - let inbound_edge_balances = inbound_edge_monitor.get_claimable_balances(); - if inbound_edge_balances.is_empty() { - return None; - } - - if prev_hop.counterparty_node_id.is_none() { - // We no longer support claiming an HTLC where we don't have - // the counterparty_node_id available if the claim has to go to - // a closed channel. Its possible we can get away with it if - // the channel is not yet closed, but its by no means a - // guarantee. - - // Thus, in this case we are a bit more aggressive with our - // pruning - if we have no use for the claim (because the - // inbound edge of the payment's monitor has already claimed - // the HTLC) we skip trying to replay the claim. - let htlc_payment_hash: PaymentHash = payment_preimage.into(); - let logger = WithChannelMonitor::from( - &args.logger, - monitor, - Some(htlc_payment_hash), - ); - let balance_could_incl_htlc = |bal| match bal { - &Balance::ClaimableOnChannelClose { .. } => { - // The channel is still open, assume we can still - // claim against it - true - }, - &Balance::MaybePreimageClaimableHTLC { payment_hash, .. } => { - payment_hash == htlc_payment_hash - }, - _ => false, - }; - let htlc_may_be_in_balances = - inbound_edge_balances.iter().any(balance_could_incl_htlc); - if !htlc_may_be_in_balances { - return None; - } + let payment_preimage = preimage_opt?; + // If it was an outbound payment, we've handled it above - if a preimage + // came in and we persisted the `ChannelManager` we either handled it + // and are good to go or the channel force-closed - we don't have to + // handle the channel still live case here. + let prev_htlcs = htlc_source.previous_hop_data(); + let prev_htlcs_count = prev_htlcs.len(); + if prev_htlcs_count == 0 { + return None; + } - // First check if we're absolutely going to fail - if we need - // to replay this claim to get the preimage into the inbound - // edge monitor but the channel is closed (and thus we'll - // immediately panic if we call claim_funds_from_hop). - if short_to_chan_info.get(&prev_hop.prev_outbound_scid_alias).is_none() { - log_error!(logger, - "We need to replay the HTLC claim for payment_hash {} (preimage {}) but cannot do so as the HTLC was forwarded prior to LDK 0.0.124.\ - All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1", - htlc_payment_hash, - payment_preimage, - ); - fail_read = true; - } + for prev_hop in prev_htlcs { + // Note that for channels which have gone to chain, + // `get_all_current_outbound_htlcs` is never pruned and always returns + // a constant set until the monitor is removed/archived. Thus, we want + // to skip replaying claims that have definitely been resolved on-chain. + + // If the inbound monitor is not present, we assume it was fully + // resolved and properly archived, implying this payment had plenty of + // time to get claimed and we can safely skip any further attempts to + // claim it (they wouldn't succeed anyway as we don't have a monitor + // against which to do so). + let inbound_edge_monitor = + match args.channel_monitors.get(&prev_hop.channel_id) { + Some(monitor) => monitor, + None => continue, + }; - // At this point we're confident we need the claim, but the - // inbound edge channel is still live. As long as this remains - // the case, we can conceivably proceed, but we run some risk - // of panicking at runtime. The user ideally should have read - // the release notes and we wouldn't be here, but we go ahead - // and let things run in the hope that it'll all just work out. - log_error!(logger, - "We need to replay the HTLC claim for payment_hash {} (preimage {}) but don't have all the required information to do so reliably.\ - As long as the channel for the inbound edge of the forward remains open, this may work okay, but we may panic at runtime!\ - All HTLCs that were forwarded by LDK 0.0.123 and prior must be resolved prior to upgrading to LDK 0.1\ - Continuing anyway, though panics may occur!", - htlc_payment_hash, - payment_preimage, - ); - } + if inbound_edge_monitor.get_claimable_balances().is_empty() { + continue; + } - Some((htlc_source, payment_preimage, htlc.amount_msat, - is_channel_closed, monitor.get_counterparty_node_id(), - monitor.get_funding_txo(), monitor.channel_id())) - } else { None } - } else { - // If it was an outbound payment, we've handled it above - if a preimage - // came in and we persisted the `ChannelManager` we either handled it and - // are good to go or the channel force-closed - we don't have to handle the - // channel still live case here. - None + // We no longer support claiming an HTLC where we don't have the + // counterparty_node_id. This field has been populated since 0.0.124, + // so we expect it to be present for in flight claims in 0.3+. + if prev_hop.counterparty_node_id.is_none() { + fail_read = true; + return None; + } + return Some(( + // When we have multiple prev_htlcs we know that they are all from + // a single HTLCSource (see match above) which contains all previous + // hops, so we can exit on the first claimable prev_hop because this + // will result in all prev_hops being claimed. + htlc_source, + payment_preimage, + htlc.amount_msat, + is_channel_closed, + monitor.get_counterparty_node_id(), + monitor.get_funding_txo(), + monitor.channel_id(), + user_channel_id_opt, + )); } + None }); for tuple in outbound_claimed_htlcs_iter { pending_claims_to_replay.push(tuple); @@ -19021,10 +20601,13 @@ impl< // panic if we attempted to claim them at this point. for (payment_hash, payment) in claimable_payments.iter() { for htlc in payment.htlcs.iter() { - if htlc.prev_hop.counterparty_node_id.is_some() { + if htlc.mpp_part.prev_hop.counterparty_node_id.is_some() { continue; } - if short_to_chan_info.get(&htlc.prev_hop.prev_outbound_scid_alias).is_some() { + if short_to_chan_info + .get(&htlc.mpp_part.prev_hop.prev_outbound_scid_alias) + .is_some() + { log_error!(args.logger, "We do not have the required information to claim a pending payment with payment hash {} reliably.\ As long as the channel for the inbound edge of the forward remains open, this may work okay, but we may panic at runtime!\ @@ -19071,7 +20654,7 @@ impl< loop { outbound_scid_alias = fake_scid::Namespace::OutboundAlias .get_fake_scid( - best_block_height, + best_block.height, &chain_hash, fake_scid_rand_bytes.as_ref().unwrap(), &args.entropy_source, @@ -19124,14 +20707,14 @@ impl< let logger = WithContext::from(&args.logger, Some(node_id), Some(*channel_id), None); for action in actions.iter() { - if let MonitorUpdateCompletionAction::EmitEventAndFreeOtherChannel { + if let MonitorUpdateCompletionAction::EmitEventOptionAndFreeOtherChannel { downstream_counterparty_and_funding_outpoint: - Some(EventUnblockedChannel { + EventUnblockedChannel { counterparty_node_id: blocked_node_id, funding_txo: _, channel_id: blocked_channel_id, blocking_action, - }), + }, .. } = action { @@ -19154,7 +20737,7 @@ impl< // anymore. } } - if let MonitorUpdateCompletionAction::FreeOtherChannelImmediately { + if let MonitorUpdateCompletionAction::FreeDuplicateClaimImmediately { .. } = action { @@ -19212,10 +20795,10 @@ impl< // See above comment on `failed_htlcs`. for htlcs in claimable_payments.values().map(|pmt| &pmt.htlcs) { - for prev_hop_data in htlcs.iter().map(|h| &h.prev_hop) { + for htlc in htlcs.iter() { dedup_decode_update_add_htlcs( &mut decode_update_add_htlcs, - prev_hop_data, + &htlc.mpp_part.prev_hop, "HTLC was already decoded and marked as a claimable payment", &args.logger, ); @@ -19255,7 +20838,7 @@ impl< if let Some(signing_session) = chan.context().interactive_tx_signing_session.as_ref() { - if signing_session.holder_tx_signatures().is_none() + if !signing_session.has_holder_witnesses() && signing_session.has_local_contribution() { let unsigned_transaction = signing_session.unsigned_tx().tx().clone(); @@ -19273,7 +20856,6 @@ impl< } } - let best_block = BestBlock::new(best_block_hash, best_block_height); let flow = OffersMessageFlow::new( chain_hash, best_block, @@ -19307,6 +20889,7 @@ impl< claimable_payments, pending_claiming_payments, }), + awaiting_trampoline_forwards: Mutex::new(new_hash_map()), outbound_scid_aliases: Mutex::new(outbound_scid_aliases), short_to_chan_info: FairRwLock::new(short_to_chan_info), fake_scid_rand_bytes: fake_scid_rand_bytes.unwrap(), @@ -19321,8 +20904,8 @@ impl< per_peer_state: FairRwLock::new(per_peer_state), - #[cfg(not(any(test, feature = "_externalize_tests")))] - monitor_update_type: AtomicUsize::new(0), + #[cfg(test)] + skip_monitor_update_assertion: AtomicBool::new(false), pending_events: Mutex::new(pending_events_read), pending_events_processor: AtomicBool::new(false), @@ -19346,9 +20929,6 @@ impl< logger: args.logger, config: RwLock::new(args.config), - - #[cfg(feature = "_test_utils")] - testing_dnssec_proof_offer_resolution_override: Mutex::new(new_hash_map()), }; let mut processed_claims: HashSet<Vec<MPPClaimHTLCSource>> = new_hash_set(); @@ -19361,33 +20941,33 @@ impl< if let Some(forwarded_htlcs) = already_forwarded_htlcs.remove(&(*channel_id, payment_hash)) { - for (hop_data, outbound_amt_msat) in forwarded_htlcs { + for (prev_hop, next_hop) in forwarded_htlcs { let new_pending_claim = - !pending_claims_to_replay.iter().any(|(src, _, _, _, _, _, _)| { - matches!(src, HTLCSource::PreviousHopData(hop) if hop.htlc_id == hop_data.htlc_id && hop.channel_id == hop_data.channel_id) + !pending_claims_to_replay.iter().any(|(src, _, _, _, _, _, _, _)| { + matches!(src, HTLCSource::PreviousHopData(hop) if hop.htlc_id == prev_hop.htlc_id && hop.channel_id == prev_hop.channel_id) }); if new_pending_claim { - let counterparty_node_id = monitor.get_counterparty_node_id(); - let is_channel_closed = channel_manager + let is_downstream_closed = channel_manager .per_peer_state .read() .unwrap() - .get(&counterparty_node_id) + .get(&next_hop.node_id) .map_or(true, |peer_state_mtx| { !peer_state_mtx .lock() .unwrap() .channel_by_id - .contains_key(channel_id) + .contains_key(&next_hop.channel_id) }); pending_claims_to_replay.push(( - HTLCSource::PreviousHopData(hop_data), + HTLCSource::PreviousHopData(prev_hop), payment_preimage, - outbound_amt_msat, - is_channel_closed, - counterparty_node_id, - monitor.get_funding_txo(), - *channel_id, + next_hop.amt_msat, + is_downstream_closed, + next_hop.node_id, + next_hop.funding_txo, + next_hop.channel_id, + Some(next_hop.user_channel_id), )); } } @@ -19522,7 +21102,8 @@ impl< log_info!(channel_manager.logger, "Re-claiming HTLCs with payment hash {} as we've released the preimage to a ChannelMonitor!", &payment_hash); let mut claimable_amt_msat = 0; let mut receiver_node_id = Some(our_network_pubkey); - let phantom_shared_secret = payment.htlcs[0].prev_hop.phantom_shared_secret; + let phantom_shared_secret = + payment.htlcs[0].mpp_part.prev_hop.phantom_shared_secret; if phantom_shared_secret.is_some() { let phantom_pubkey = channel_manager .node_signer @@ -19531,7 +21112,7 @@ impl< receiver_node_id = Some(phantom_pubkey) } for claimable_htlc in &payment.htlcs { - claimable_amt_msat += claimable_htlc.value; + claimable_amt_msat += claimable_htlc.mpp_part.value; // Add a holding-cell claim of the payment to the Channel, which should be // applied ~immediately on peer reconnection. Because it won't generate a @@ -19548,7 +21129,7 @@ impl< // this channel as well. On the flip side, there's no harm in restarting // without the new monitor persisted - we'll end up right back here on // restart. - let previous_channel_id = claimable_htlc.prev_hop.channel_id; + let previous_channel_id = claimable_htlc.mpp_part.prev_hop.channel_id; let peer_node_id = monitor.get_counterparty_node_id(); { let peer_state_mutex = per_peer_state.get(&peer_node_id).unwrap(); @@ -19566,14 +21147,15 @@ impl< ); channel .claim_htlc_while_disconnected_dropping_mon_update_legacy( - claimable_htlc.prev_hop.htlc_id, + claimable_htlc.mpp_part.prev_hop.htlc_id, payment_preimage, &&logger, ); } } - if let Some(previous_hop_monitor) = - args.channel_monitors.get(&claimable_htlc.prev_hop.channel_id) + if let Some(previous_hop_monitor) = args + .channel_monitors + .get(&claimable_htlc.mpp_part.prev_hop.channel_id) { // Note that this is unsafe as we no longer require the // `ChannelMonitor`s to be re-persisted prior to this @@ -19600,8 +21182,7 @@ impl< let payment_id = payment.inbound_payment_id(&inbound_payment_id_secret.unwrap()); let htlcs = payment.htlcs.iter().map(events::ClaimedHTLC::from).collect(); - let sender_intended_total_msat = - payment.htlcs.first().map(|htlc| htlc.total_msat); + let sender_intended_total_msat = payment.onion_fields.total_mpp_amount_msat; pending_events.push_back(( events::Event::PaymentClaimed { receiver_node_id, @@ -19609,8 +21190,8 @@ impl< purpose: payment.purpose, amount_msat: claimable_amt_msat, htlcs, - sender_intended_total_msat, - onion_fields: payment.onion_fields, + sender_intended_total_msat: Some(sender_intended_total_msat), + onion_fields: Some(payment.onion_fields), payment_id: Some(payment_id), }, // Note that we don't bother adding a EventCompletionAction here to @@ -19627,11 +21208,15 @@ impl< for htlc_source in failed_htlcs { let (source, hash, counterparty_id, channel_id, failure_reason, ev_action) = htlc_source; - let receiver = - HTLCHandlingFailureType::Forward { node_id: Some(counterparty_id), channel_id }; + let failure_type = source.failure_type(counterparty_id, channel_id); let reason = HTLCFailReason::from_failure_code(failure_reason); - channel_manager - .fail_htlc_backwards_internal(&source, &hash, &reason, receiver, ev_action); + channel_manager.fail_htlc_backwards_internal( + &source, + &hash, + &reason, + failure_type, + ev_action, + ); } for ((_, hash), htlcs) in already_forwarded_htlcs.into_iter() { for (htlc, _) in htlcs { @@ -19657,6 +21242,7 @@ impl< downstream_node_id, downstream_funding, downstream_channel_id, + downstream_user_channel_id, ) in pending_claims_to_replay { // We use `downstream_closed` in place of `from_onchain` here just as a guess - we @@ -19666,13 +21252,13 @@ impl< channel_manager.claim_funds_internal( source, preimage, - Some(downstream_value), + downstream_value, None, downstream_closed, downstream_node_id, downstream_funding, downstream_channel_id, - None, + downstream_user_channel_id, None, None, ); @@ -19681,10 +21267,98 @@ impl< //TODO: Broadcast channel update for closed channels, but only after we've made a //connection or two. - Ok((best_block_hash, channel_manager)) + Ok((best_block, channel_manager)) + } +} + +fn prune_forwarded_htlc( + already_forwarded_htlcs: &mut HashMap< + (ChannelId, PaymentHash), + Vec<(HTLCPreviousHopData, OutboundHop)>, + >, + prev_hop: &HTLCPreviousHopData, payment_hash: &PaymentHash, +) { + if let hash_map::Entry::Occupied(mut entry) = + already_forwarded_htlcs.entry((prev_hop.channel_id, *payment_hash)) + { + entry.get_mut().retain(|(htlc, _)| prev_hop.htlc_id != htlc.htlc_id); + if entry.get().is_empty() { + entry.remove(); + } } } +/// Removes pending HTLC entries that the ChannelMonitor has already taken responsibility for, +/// cleaning up state mismatches that can occur during restart. +fn reconcile_pending_htlcs_with_monitor( + reconstruct_manager_from_monitors: bool, + already_forwarded_htlcs: &mut HashMap< + (ChannelId, PaymentHash), + Vec<(HTLCPreviousHopData, OutboundHop)>, + >, + forward_htlcs_legacy: &mut HashMap<u64, Vec<HTLCForwardInfo>>, + pending_events_read: &mut VecDeque<(Event, Option<EventCompletionAction>)>, + pending_intercepted_htlcs_legacy: &mut HashMap<InterceptId, PendingAddHTLCInfo>, + decode_update_add_htlcs: &mut HashMap<u64, Vec<msgs::UpdateAddHTLC>>, + decode_update_add_htlcs_legacy: &mut HashMap<u64, Vec<msgs::UpdateAddHTLC>>, + prev_hop_data: HTLCPreviousHopData, logger: &impl Logger, payment_hash: PaymentHash, + channel_id: ChannelId, +) { + let pending_forward_matches_htlc = |info: &PendingAddHTLCInfo| { + info.prev_funding_outpoint == prev_hop_data.outpoint + && info.prev_htlc_id == prev_hop_data.htlc_id + }; + + // If `reconstruct_manager_from_monitors` is set, we always add all inbound committed + // HTLCs to `decode_update_add_htlcs` in the above loop, but we need to prune from + // those added HTLCs if they were already forwarded to the outbound edge. Otherwise, + // we'll double-forward. + if reconstruct_manager_from_monitors { + dedup_decode_update_add_htlcs( + decode_update_add_htlcs, + &prev_hop_data, + "HTLC already forwarded to the outbound edge", + &&logger, + ); + prune_forwarded_htlc(already_forwarded_htlcs, &prev_hop_data, &payment_hash); + } + + // The ChannelMonitor is now responsible for this HTLC's failure/success and will let us know + // what its outcome is. If we still have an entry for this HTLC in `forward_htlcs_legacy`, + // `pending_intercepted_htlcs_legacy`, or `decode_update_add_htlcs_legacy`, we were apparently + // not persisted after the monitor was when forwarding the payment. + dedup_decode_update_add_htlcs( + decode_update_add_htlcs_legacy, + &prev_hop_data, + "HTLC was forwarded to the closed channel", + &&logger, + ); + forward_htlcs_legacy.retain(|_, forwards| { + forwards.retain(|forward| { + if let HTLCForwardInfo::AddHTLC(htlc_info) = forward { + if pending_forward_matches_htlc(&htlc_info) { + log_info!(logger, "Removing pending to-forward HTLC with hash {} as it was forwarded to the closed channel {}", + &payment_hash, channel_id); + false + } else { true } + } else { true } + }); + !forwards.is_empty() + }); + pending_intercepted_htlcs_legacy.retain(|intercepted_id, htlc_info| { + if pending_forward_matches_htlc(&htlc_info) { + log_info!(logger, "Removing pending intercepted HTLC with hash {} as it was forwarded to the closed channel {}", + payment_hash, channel_id); + pending_events_read.retain(|(event, _)| { + if let Event::HTLCIntercepted { intercept_id: ev_id, .. } = event { + intercepted_id != ev_id + } else { true } + }); + false + } else { true } + }); +} + #[cfg(test)] mod tests { use crate::events::{ClosureReason, Event, HTLCHandlingFailureType}; @@ -19810,9 +21484,9 @@ mod tests { // indicates there are more HTLCs coming. let cur_height = CHAN_CONFIRM_DEPTH + 1; // route_payment calls send_payment, which adds 1 to the current height. So we do the same here to match. let session_privs = nodes[0].node.test_add_new_pending_payment(our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), payment_id, &mpp_route).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), payment_id, &mpp_route).unwrap(); nodes[0].node.test_send_payment_along_path(&mpp_route.paths[0], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), 200_000, cur_height, payment_id, &None, session_privs[0]).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), cur_height, payment_id, &None, session_privs[0]).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -19820,8 +21494,8 @@ mod tests { // Next, send a keysend payment with the same payment_hash and make sure it fails. nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), - PaymentId(payment_preimage.0), route.route_params.clone().unwrap(), Retry::Attempts(0) + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), + PaymentId(payment_preimage.0), route.route_params.clone(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -19848,7 +21522,7 @@ mod tests { // Send the second half of the original MPP payment. nodes[0].node.test_send_payment_along_path(&mpp_route.paths[1], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), 200_000, cur_height, payment_id, &None, session_privs[1]).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 200_000), cur_height, payment_id, &None, session_privs[1]).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -19938,7 +21612,7 @@ mod tests { PaymentParameters::for_keysend(expected_route.last().unwrap().node.get_our_node_id(), TEST_FINAL_CLTV, false), 100_000); nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), PaymentId(payment_preimage.0), route_params.clone(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -19976,8 +21650,8 @@ mod tests { None, nodes[0].logger, &scorer, &Default::default(), &random_seed_bytes ).unwrap(); let payment_hash = nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), - PaymentId(payment_preimage.0), route.route_params.clone().unwrap(), Retry::Attempts(0) + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), + PaymentId(payment_preimage.0), route.route_params.clone(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -19989,7 +21663,7 @@ mod tests { // Next, attempt a regular payment and make sure it fails. let payment_secret = PaymentSecret([43; 32]); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 100_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); @@ -20019,8 +21693,8 @@ mod tests { // To start (3), send a keysend payment but don't claim it. let payment_id_1 = PaymentId([44; 32]); let payment_hash = nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), payment_id_1, - route.route_params.clone().unwrap(), Retry::Attempts(0) + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), payment_id_1, + route.route_params.clone(), Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -20036,7 +21710,7 @@ mod tests { ); let payment_id_2 = PaymentId([45; 32]); nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), payment_id_2, route_params, + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(100_000), payment_id_2, route_params, Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -20094,9 +21768,9 @@ mod tests { let test_preimage = PaymentPreimage([42; 32]); let mismatch_payment_hash = PaymentHash([43; 32]); let session_privs = nodes[0].node.test_add_new_pending_payment(mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), PaymentId(mismatch_payment_hash.0), &route).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), PaymentId(mismatch_payment_hash.0), &route).unwrap(); nodes[0].node.test_send_payment_internal(&route, mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), session_privs).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); @@ -20138,9 +21812,10 @@ mod tests { route.paths[1].hops[0].pubkey = nodes[2].node.get_our_node_id(); route.paths[1].hops[0].short_channel_id = chan_2_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.final_value_msat *= 2; nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::spontaneous_empty(), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::spontaneous_empty(200000), PaymentId(payment_hash.0)).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); match events[0] { @@ -20285,7 +21960,7 @@ mod tests { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let (_, payment_hash, payment_secret) = get_payment_preimage_hash!(&nodes[0]); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[0], None, None); let payment_data = msgs::FinalOnionHopData { payment_secret, total_msat: 100_000, @@ -20295,7 +21970,7 @@ mod tests { // payment verification fails as expected. let mut bad_payment_hash = payment_hash.clone(); bad_payment_hash.0[0] += 1; - match inbound_payment::verify(bad_payment_hash, &payment_data, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger) { + match inbound_payment::verify(bad_payment_hash, &payment_data, None, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger) { Ok(_) => panic!("Unexpected ok"), Err(()) => { nodes[0].logger.assert_log_contains("lightning::ln::inbound_payment", "Failing HTLC with user-generated payment_hash", 1); @@ -20303,7 +21978,7 @@ mod tests { } // Check that using the original payment hash succeeds. - assert!(inbound_payment::verify(payment_hash, &payment_data, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger).is_ok()); + assert!(inbound_payment::verify(payment_hash, &payment_data, None, nodes[0].node.highest_seen_timestamp.load(Ordering::Acquire) as u64, &nodes[0].node.inbound_payment_key, &nodes[0].logger).is_ok()); } fn check_not_connected_to_peer_error<T>( @@ -20315,13 +21990,13 @@ mod tests { #[rustfmt::skip] fn check_unkown_peer_error<T>(res_err: Result<T, APIError>, expected_public_key: PublicKey) { - let expected_message = format!("Can't find a peer matching the passed counterparty node_id {}", expected_public_key); + let expected_message = format!("No such peer for the passed counterparty_node_id {}", expected_public_key); check_api_error_message(expected_message, res_err) } #[rustfmt::skip] fn check_channel_unavailable_error<T>(res_err: Result<T, APIError>, expected_channel_id: ChannelId, peer_node_id: PublicKey) { - let expected_message = format!("Channel with id {} not found for the passed counterparty node_id {}", expected_channel_id, peer_node_id); + let expected_message = format!("No such channel_id {} for the passed counterparty_node_id {}", expected_channel_id, peer_node_id); check_api_error_message(expected_message, res_err) } @@ -20779,7 +22454,7 @@ pub mod bench { use crate::chain::Listen; use crate::events::Event; use crate::ln::channelmanager::{ - BestBlock, ChainParameters, ChannelManager, PaymentHash, PaymentId, PaymentPreimage, + BlockLocator, ChainParameters, ChannelManager, PaymentHash, PaymentId, PaymentPreimage, RecipientOnionFields, Retry, }; use crate::ln::functional_test_utils::*; @@ -20863,20 +22538,20 @@ pub mod bench { let seed_a = [1u8; 32]; let keys_manager_a = KeysManager::new(&seed_a, 42, 42, true); - let chain_monitor_a = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_a, &keys_manager_a, keys_manager_a.get_peer_storage_key()); + let chain_monitor_a = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_a, &keys_manager_a, keys_manager_a.get_peer_storage_key(), false); let node_a = ChannelManager::new(&fee_estimator, &chain_monitor_a, &tx_broadcaster, &router, &message_router, &logger_a, &keys_manager_a, &keys_manager_a, &keys_manager_a, config.clone(), ChainParameters { network, - best_block: BestBlock::from_network(network), + best_block: BlockLocator::from_network(network), }, genesis_block.header.time); let node_a_holder = ANodeHolder { node: &node_a }; let logger_b = test_utils::TestLogger::with_id("node a".to_owned()); let seed_b = [2u8; 32]; let keys_manager_b = KeysManager::new(&seed_b, 42, 42, true); - let chain_monitor_b = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_b, &keys_manager_b, keys_manager_b.get_peer_storage_key()); + let chain_monitor_b = ChainMonitor::new(None, &tx_broadcaster, &logger_a, &fee_estimator, &persister_b, &keys_manager_b, keys_manager_b.get_peer_storage_key(), false); let node_b = ChannelManager::new(&fee_estimator, &chain_monitor_b, &tx_broadcaster, &router, &message_router, &logger_b, &keys_manager_b, &keys_manager_b, &keys_manager_b, config.clone(), ChainParameters { network, - best_block: BestBlock::from_network(network), + best_block: BlockLocator::from_network(network), }, genesis_block.header.time); let node_b_holder = ANodeHolder { node: &node_b }; @@ -20930,7 +22605,7 @@ pub mod bench { assert_eq!(&tx_broadcaster.txn_broadcasted.lock().unwrap()[..], &[tx.clone()]); - let block = create_dummy_block(BestBlock::from_network(network).block_hash, 42, vec![tx]); + let block = create_dummy_block(BlockLocator::from_network(network).block_hash, 42, vec![tx]); Listen::block_connected(&node_a, &block, 1); Listen::block_connected(&node_b, &block, 1); @@ -20976,9 +22651,10 @@ pub mod bench { payment_preimage.0[0..8].copy_from_slice(&payment_count.to_le_bytes()); payment_count += 1; let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); - let payment_secret = $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None).unwrap(); + let (payment_secret, _no_payment_metadata) = + $node_b.create_inbound_payment_for_hash(payment_hash, None, 7200, None, None).unwrap(); - $node_a.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret), + $node_a.send_payment(payment_hash, RecipientOnionFields::secret_only(payment_secret, 10_000), PaymentId(payment_hash.0), RouteParameters::from_payment_params_and_value(payment_params, 10_000), Retry::Attempts(0)).unwrap(); diff --git a/lightning/src/ln/features.rs b/lightning/src/ln/features.rs index b568d5595a5..a303027b879 100644 --- a/lightning/src/ln/features.rs +++ b/lightning/src/ln/features.rs @@ -40,7 +40,10 @@ macro_rules! impl_feature_len_prefixed_write { } impl Readable for $features { fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> { - Ok(Self::from_be_bytes(Vec::<u8>::read(r)?)) + let len: u16 = Readable::read(r)?; + let mut bytes = vec![0u8; len as usize]; + r.read_exact(&mut bytes[..])?; + Ok(Self::from_be_bytes(bytes)) } } }; @@ -81,6 +84,12 @@ macro_rules! impl_feature_write_without_length { } } + impl Writeable for WithoutLength<&&$features> { + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + write_be(w, self.0.le_flags()) + } + } + impl Readable for WithoutLength<$features> { fn read<R: io::Read>(r: &mut R) -> Result<Self, DecodeError> { let v = io_extras::read_to_end(r)?; diff --git a/lightning/src/ln/functional_test_utils.rs b/lightning/src/ln/functional_test_utils.rs index a5461154a02..26fed9ee926 100644 --- a/lightning/src/ln/functional_test_utils.rs +++ b/lightning/src/ln/functional_test_utils.rs @@ -10,26 +10,26 @@ //! A bunch of useful utilities for building networks of nodes and exchanging messages between //! nodes for functional tests. -use crate::blinded_path::payment::DummyTlvs; -use crate::chain::channelmonitor::ChannelMonitor; -use crate::chain::transaction::OutPoint; -use crate::chain::{BestBlock, ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; -use crate::events::bump_transaction::sync::{ - BumpTransactionEventHandlerSync, WalletSourceSync, WalletSync, +use crate::blinded_path::payment::{ + BlindedPaymentPath, DummyTlvs, ForwardNode, ReceiveTlvs, TrampolineForwardTlvs, }; +use crate::chain::channelmonitor::{ChannelMonitor, HTLC_FAIL_BACK_BUFFER}; +use crate::chain::transaction::OutPoint; +use crate::chain::{BlockLocator, ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; +use crate::events::bump_transaction::sync::BumpTransactionEventHandlerSync; use crate::events::bump_transaction::BumpTransactionEvent; use crate::events::{ - ClaimedHTLC, ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PathFailure, - PaymentFailureReason, PaymentPurpose, + ClaimedHTLC, ClosureReason, Event, FundingInfo, HTLCHandlingFailureType, + NegotiationFailureReason, PathFailure, PaymentFailureReason, PaymentPurpose, }; use crate::ln::chan_utils::{ commitment_tx_base_weight, COMMITMENT_TX_WEIGHT_PER_HTLC, TRUC_MAX_WEIGHT, }; use crate::ln::channelmanager::{ AChannelManager, ChainParameters, ChannelManager, ChannelManagerReadArgs, PaymentId, - RAACommitmentOrder, MIN_CLTV_EXPIRY_DELTA, + RAACommitmentOrder, TrustedChannelFeatures, TxSignaturesOrder, MIN_CLTV_EXPIRY_DELTA, }; -use crate::ln::funding::FundingTxInput; +use crate::ln::funding::FundingContribution; use crate::ln::msgs::{self, OpenChannel}; use crate::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, MessageSendEvent, RoutingMessageHandler, @@ -39,10 +39,12 @@ use crate::ln::outbound_payment::RecipientOnionFields; use crate::ln::outbound_payment::Retry; use crate::ln::peer_handler::IgnoringMessageHandler; use crate::ln::types::ChannelId; +use crate::offers::payer_proof::PaidBolt12Invoice; use crate::onion_message::messenger::OnionMessenger; use crate::routing::gossip::{NetworkGraph, NetworkUpdate, P2PGossipSync}; use crate::routing::router::{self, PaymentParameters, Route, RouteParameters}; -use crate::sign::{EntropySource, RandomBytes}; +use crate::routing::router::{compute_fees, BlindedTail, TrampolineHop}; +use crate::sign::{EntropySource, RandomBytes, ReceiveAuthKey}; use crate::types::features::ChannelTypeFeatures; use crate::types::features::InitFeatures; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; @@ -54,6 +56,7 @@ use crate::util::test_channel_signer::SignerOp; use crate::util::test_channel_signer::TestChannelSigner; use crate::util::test_utils::{self, TestLogger}; use crate::util::test_utils::{TestChainMonitor, TestKeysInterface, TestScorer}; +use crate::util::wallet_utils::{ConfirmedUtxo, WalletSourceSync, WalletSync}; use bitcoin::amount::Amount; use bitcoin::block::{Block, Header, Version as BlockVersion}; @@ -397,18 +400,17 @@ fn do_connect_block_without_consistency_checks<'a, 'b, 'c, 'd>( let wallet_script = node.wallet_source.get_change_script().unwrap(); for (idx, output) in tx.output.iter().enumerate() { if output.script_pubkey == wallet_script { - let outpoint = bitcoin::OutPoint { txid: tx.compute_txid(), vout: idx as u32 }; - node.wallet_source.add_utxo(outpoint, output.value); + node.wallet_source.add_utxo(tx.clone(), idx as u32); } } } } pub fn provide_anchor_reserves<'a, 'b, 'c>(nodes: &[Node<'a, 'b, 'c>]) -> Transaction { - provide_anchor_utxo_reserves(nodes, 1, Amount::ONE_BTC) + provide_utxo_reserves(nodes, 1, Amount::ONE_BTC) } -pub fn provide_anchor_utxo_reserves<'a, 'b, 'c>( +pub fn provide_utxo_reserves<'a, 'b, 'c>( nodes: &[Node<'a, 'b, 'c>], utxos: usize, amount: Amount, ) -> Transaction { let mut output = Vec::with_capacity(nodes.len()); @@ -446,13 +448,13 @@ pub fn disconnect_blocks<'a, 'b, 'c, 'd>(node: &'a Node<'b, 'c, 'd>, count: u32) match *node.connect_style.borrow() { ConnectStyle::FullBlockViaListen => { - let best_block = BestBlock::new(orig.0.header.prev_blockhash, orig.1 - 1); + let best_block = BlockLocator::new(orig.0.header.prev_blockhash, orig.1 - 1); node.chain_monitor.chain_monitor.blocks_disconnected(best_block); Listen::blocks_disconnected(node.node, best_block); }, ConnectStyle::FullBlockDisconnectionsSkippingViaListen => { if i == count - 1 { - let best_block = BestBlock::new(orig.0.header.prev_blockhash, orig.1 - 1); + let best_block = BlockLocator::new(orig.0.header.prev_blockhash, orig.1 - 1); node.chain_monitor.chain_monitor.blocks_disconnected(best_block); Listen::blocks_disconnected(node.node, best_block); } @@ -523,7 +525,6 @@ pub type TestChannelManager<'node_cfg, 'chan_mon_cfg> = ChannelManager< &'chan_mon_cfg test_utils::TestLogger, >; -#[cfg(not(feature = "dnssec"))] type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< DedicatedEntropy, &'node_cfg test_utils::TestKeysInterface, @@ -536,19 +537,6 @@ type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< IgnoringMessageHandler, >; -#[cfg(feature = "dnssec")] -type TestOnionMessenger<'chan_man, 'node_cfg, 'chan_mon_cfg> = OnionMessenger< - DedicatedEntropy, - &'node_cfg test_utils::TestKeysInterface, - &'chan_mon_cfg test_utils::TestLogger, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, - &'node_cfg test_utils::TestMessageRouter<'chan_mon_cfg>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, - &'chan_man TestChannelManager<'node_cfg, 'chan_mon_cfg>, - IgnoringMessageHandler, ->; - /// For use with [`OnionMessenger`] otherwise `test_restored_packages_retry` will fail. This is /// because that test uses older serialized data produced by calling [`EntropySource`] in a specific /// manner. Using the same [`EntropySource`] with [`OnionMessenger`] would introduce another call, @@ -600,6 +588,14 @@ impl<'a, 'b, 'c> Node<'a, 'b, 'c> { self.node.init_features() | self.onion_messenger.provided_init_features(peer_node_id) }) } + + /// Disables the panic when `Watch::update_channel` returns `Completed` while prior updates + /// are still `InProgress`. Some legacy tests switch the persister between modes mid-flight, + /// which violates this contract but is otherwise harmless. + #[cfg(test)] + pub fn disable_monitor_completeness_assertion(&self) { + self.node.skip_monitor_update_assertion.store(true, core::sync::atomic::Ordering::Relaxed); + } } impl<'a, 'b, 'c> std::panic::UnwindSafe for Node<'a, 'b, 'c> {} @@ -615,6 +611,10 @@ impl<'a, 'b, 'c> Node<'a, 'b, 'c> { self.blocks.lock().unwrap()[height as usize].0.header } + pub fn provide_funding_utxos(&self, utxos: usize, amount: Amount) -> Transaction { + provide_utxo_reserves(core::slice::from_ref(self), utxos, amount) + } + /// Executes `enable_channel_signer_op` for every single signer operation for this channel. #[cfg(test)] pub fn enable_all_channel_signer_ops(&self, peer_id: &PublicKey, chan_id: &ChannelId) { @@ -675,7 +675,7 @@ impl<'a, 'b, 'c> Node<'a, 'b, 'c> { if let Some(context) = chan_lock.channel_by_id.get_mut(chan_id).map(|chan| chan.context_mut()) { - let signer = context.get_mut_signer().as_mut_ecdsa().unwrap(); + let signer = context.get_mut_signer(); if available { signer.enable_op(signer_op); } else { @@ -849,7 +849,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { let mon = self.chain_monitor.chain_monitor.get_monitor(channel_id).unwrap(); mon.write(&mut w).unwrap(); let (_, deserialized_monitor) = - <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read( + <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read( &mut io::Cursor::new(&w.0), (self.keys_manager, self.keys_manager), ) @@ -878,7 +878,7 @@ impl<'a, 'b, 'c> Drop for Node<'a, 'b, 'c> { let mut w = test_utils::TestVecWriter(Vec::new()); self.node.write(&mut w).unwrap(); <( - BlockHash, + BlockLocator, ChannelManager< &test_utils::TestChainMonitor, &test_utils::TestBroadcaster, @@ -1024,7 +1024,7 @@ pub fn get_updates_and_revoke<CM: AChannelManager, H: NodeHolder<CM = CM>>( macro_rules! get_event_msg { ($node: expr, $event_type: path, $node_id: expr) => {{ let events = $node.node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); + assert_eq!(events.len(), 1, "{events:?}"); match events[0] { $event_type { ref node_id, ref msg } => { assert_eq!(*node_id, $node_id); @@ -1086,7 +1086,8 @@ pub fn get_warning_msg(node: &Node, recipient: &PublicKey) -> msgs::WarningMessa macro_rules! get_event { ($node: expr, $event_type: path) => {{ let mut events = $node.node.get_and_clear_pending_events(); - assert_eq!(events.len(), 1); + assert!(!events.is_empty(), "Expected an event"); + assert_eq!(events.len(), 1, "Unexpected events {events:?}"); let ev = events.pop().unwrap(); match ev { $event_type { .. } => ev, @@ -1110,10 +1111,6 @@ pub fn get_htlc_update_msgs(node: &Node, recipient: &PublicKey) -> msgs::Commitm /// Fetches the first `msg_event` to the passed `node_id` in the passed `msg_events` vec. /// Returns the `msg_event`. -/// -/// Note that even though `BroadcastChannelAnnouncement` and `BroadcastChannelUpdate` -/// `msg_events` are stored under specific peers, this function does not fetch such `msg_events` as -/// such messages are intended to all peers. pub fn remove_first_msg_event_to_node( msg_node_id: &PublicKey, msg_events: &mut Vec<MessageSendEvent>, ) -> MessageSendEvent { @@ -1316,7 +1313,7 @@ pub fn _reload_node<'a, 'b, 'c>( let mut monitors_read = Vec::with_capacity(monitors_encoded.len()); for encoded in monitors_encoded { let mut monitor_read = &encoded[..]; - let (_, monitor) = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read( + let (_, monitor) = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read( &mut monitor_read, (node.keys_manager, node.keys_manager), ) @@ -1331,7 +1328,7 @@ pub fn _reload_node<'a, 'b, 'c>( for monitor in monitors_read.iter() { assert!(channel_monitors.insert(monitor.channel_id(), monitor).is_none()); } - <(BlockHash, TestChannelManager<'b, 'c>)>::read( + <(BlockLocator, TestChannelManager<'b, 'c>)>::read( &mut node_read, ChannelManagerReadArgs { config, @@ -1383,7 +1380,7 @@ macro_rules! _reload_node_inner { ); $node.chain_monitor = &$new_chain_monitor; - $new_channelmanager = _reload_node( + $new_channelmanager = $crate::ln::functional_test_utils::_reload_node( &$node, $new_config, &chanman_encoded, @@ -1401,7 +1398,7 @@ macro_rules! reload_node { // Reload the node using the node's current config ($node: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => { let config = $node.node.get_current_config(); - _reload_node_inner!( + $crate::_reload_node_inner!( $node, config, $chanman_encoded, @@ -1414,7 +1411,7 @@ macro_rules! reload_node { }; // Reload the node with the new provided config ($node: expr, $new_config: expr, $chanman_encoded: expr, $monitors_encoded: expr, $persister: ident, $new_chain_monitor: ident, $new_channelmanager: ident) => { - _reload_node_inner!( + $crate::_reload_node_inner!( $node, $new_config, $chanman_encoded, @@ -1431,7 +1428,7 @@ macro_rules! reload_node { ident, $new_chain_monitor: ident, $new_channelmanager: ident, $reconstruct_pending_htlcs: expr ) => { let config = $node.node.get_current_config(); - _reload_node_inner!( + $crate::_reload_node_inner!( $node, config, $chanman_encoded, @@ -1516,7 +1513,7 @@ fn internal_create_funding_transaction<'a, 'b, 'c>( /// Return the inputs (with prev tx), and the total witness weight for these inputs pub fn create_dual_funding_utxos_with_prev_txs( node: &Node<'_, '_, '_>, utxo_values_in_satoshis: &[u64], -) -> Vec<FundingTxInput> { +) -> Vec<ConfirmedUtxo> { // Ensure we have unique transactions per node by using the locktime. let tx = Transaction { version: TxVersion::TWO, @@ -1540,7 +1537,7 @@ pub fn create_dual_funding_utxos_with_prev_txs( .iter() .enumerate() .map(|(index, _)| index as u32) - .map(|vout| FundingTxInput::new_p2wpkh(tx.clone(), vout).unwrap()) + .map(|vout| ConfirmedUtxo::new_p2wpkh(tx.clone(), vout).unwrap()) .collect() } @@ -1635,10 +1632,11 @@ pub fn exchange_open_accept_zero_conf_chan<'a, 'b, 'c, 'd>( Event::OpenChannelRequest { temporary_channel_id, .. } => { receiver .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( &temporary_channel_id, &initiator_node_id, 0, + TrustedChannelFeatures::ZeroConf, None, ) .unwrap(); @@ -2207,31 +2205,31 @@ macro_rules! check_spends { } } -macro_rules! get_closing_signed_broadcast { - ($node: expr, $dest_pubkey: expr) => {{ - let events = $node.get_and_clear_pending_msg_events(); - assert!(events.len() == 1 || events.len() == 2); - ( - match events[events.len() - 1] { - MessageSendEvent::BroadcastChannelUpdate { ref msg, .. } => { - assert_eq!(msg.contents.channel_flags & 2, 2); - msg.clone() +pub fn get_closing_signed_broadcast( + node: &Node, dest_pubkey: PublicKey, +) -> (msgs::ChannelUpdate, Option<msgs::ClosingSigned>) { + let events = node.node.get_and_clear_pending_msg_events(); + assert!(events.len() == 1 || events.len() == 2); + ( + match events[events.len() - 1] { + MessageSendEvent::BroadcastChannelUpdate { ref msg, .. } => { + assert_eq!(msg.contents.channel_flags & 2, 2); + msg.clone() + }, + _ => panic!("Unexpected event"), + }, + if events.len() == 2 { + match events[0] { + MessageSendEvent::SendClosingSigned { ref node_id, ref msg } => { + assert_eq!(*node_id, dest_pubkey); + Some(msg.clone()) }, _ => panic!("Unexpected event"), - }, - if events.len() == 2 { - match events[0] { - MessageSendEvent::SendClosingSigned { ref node_id, ref msg } => { - assert_eq!(*node_id, $dest_pubkey); - Some(msg.clone()) - }, - _ => panic!("Unexpected event"), - } - } else { - None - }, - ) - }}; + } + } else { + None + }, + ) } #[cfg(test)] @@ -2310,17 +2308,6 @@ pub fn check_closed_broadcast( .collect() } -/// Check that a channel's closing channel update has been broadcasted, and optionally -/// check whether an error message event has occurred. -/// -/// Don't use this, use the identically-named function instead. -#[macro_export] -macro_rules! check_closed_broadcast { - ($node: expr, $with_error_msg: expr) => { - $crate::ln::functional_test_utils::check_closed_broadcast(&$node, 1, $with_error_msg).pop() - }; -} - #[derive(Default)] pub struct ExpectedCloseEvent { pub channel_capacity_sats: Option<u64>, @@ -2392,7 +2379,7 @@ pub fn check_closed_events(node: &Node, expected_close_events: &[ExpectedCloseEv discard_events_count ); assert_eq!( - events.iter().filter(|e| matches!(e, Event::SpliceFailed { .. },)).count(), + events.iter().filter(|e| matches!(e, Event::SpliceNegotiationFailed { .. },)).count(), splice_events_count ); } @@ -2527,10 +2514,10 @@ pub fn close_channel<'a, 'b, 'c>( assert_eq!(broadcaster_b.txn_broadcasted.lock().unwrap().len(), 1); tx_b = broadcaster_b.txn_broadcasted.lock().unwrap().remove(0); let (bs_update, closing_signed_b) = - get_closing_signed_broadcast!(node_b, node_a.get_our_node_id()); + get_closing_signed_broadcast(struct_b, node_a.get_our_node_id()); node_a.handle_closing_signed(node_b.get_our_node_id(), &closing_signed_b.unwrap()); - let (as_update, none_a) = get_closing_signed_broadcast!(node_a, node_b.get_our_node_id()); + let (as_update, none_a) = get_closing_signed_broadcast(struct_a, node_b.get_our_node_id()); assert!(none_a.is_none()); assert_eq!(broadcaster_a.txn_broadcasted.lock().unwrap().len(), 1); tx_a = broadcaster_a.txn_broadcasted.lock().unwrap().remove(0); @@ -2547,10 +2534,10 @@ pub fn close_channel<'a, 'b, 'c>( assert_eq!(broadcaster_a.txn_broadcasted.lock().unwrap().len(), 1); tx_a = broadcaster_a.txn_broadcasted.lock().unwrap().remove(0); let (as_update, closing_signed_a) = - get_closing_signed_broadcast!(node_a, node_b.get_our_node_id()); + get_closing_signed_broadcast(struct_a, node_b.get_our_node_id()); node_b.handle_closing_signed(node_a.get_our_node_id(), &closing_signed_a.unwrap()); - let (bs_update, none_b) = get_closing_signed_broadcast!(node_b, node_a.get_our_node_id()); + let (bs_update, none_b) = get_closing_signed_broadcast(struct_b, node_a.get_our_node_id()); assert!(none_b.is_none()); assert_eq!(broadcaster_b.txn_broadcasted.lock().unwrap().len(), 1); tx_b = broadcaster_b.txn_broadcasted.lock().unwrap().remove(0); @@ -2672,20 +2659,23 @@ pub fn commitment_signed_dance_through_cp_raa( node_a: &Node<'_, '_, '_>, node_b: &Node<'_, '_, '_>, fail_backwards: bool, includes_claim: bool, ) -> Option<MessageSendEvent> { - let (extra_msg_option, bs_revoke_and_ack) = + let (extra_msg_option, bs_revoke_and_ack, node_b_holding_cell_htlcs) = do_main_commitment_signed_dance(node_a, node_b, fail_backwards); + assert!(node_b_holding_cell_htlcs.is_empty()); node_a.node.handle_revoke_and_ack(node_b.node.get_our_node_id(), &bs_revoke_and_ack); check_added_monitors(node_a, if includes_claim { 0 } else { 1 }); extra_msg_option } /// Does the main logic in the commitment_signed dance. After the first `commitment_signed` has -/// been delivered, this method picks up and delivers the response `revoke_and_ack` and -/// `commitment_signed`, returning the recipient's `revoke_and_ack` and any extra message it may -/// have included. +/// been delivered, delivers the response `revoke_and_ack` and `commitment_signed`, and returns: +/// - The recipient's `revoke_and_ack` +/// - The recipient's extra message (if any) after handling the commitment_signed +/// - Any messages released from the initiator's holding cell after handling the `revoke_and_ack` +/// (e.g., a second HTLC on the same channel) pub fn do_main_commitment_signed_dance( node_a: &Node<'_, '_, '_>, node_b: &Node<'_, '_, '_>, fail_backwards: bool, -) -> (Option<MessageSendEvent>, msgs::RevokeAndACK) { +) -> (Option<MessageSendEvent>, msgs::RevokeAndACK, Vec<MessageSendEvent>) { let node_a_id = node_a.node.get_our_node_id(); let node_b_id = node_b.node.get_our_node_id(); @@ -2693,7 +2683,9 @@ pub fn do_main_commitment_signed_dance( check_added_monitors(&node_b, 0); assert!(node_b.node.get_and_clear_pending_msg_events().is_empty()); node_b.node.handle_revoke_and_ack(node_a_id, &as_revoke_and_ack); - assert!(node_b.node.get_and_clear_pending_msg_events().is_empty()); + // Handling the RAA may release HTLCs from node_b's holding cell (e.g., if multiple HTLCs + // were sent over the same channel and the second was queued behind the first). + let node_b_holding_cell_htlcs = node_b.node.get_and_clear_pending_msg_events(); check_added_monitors(&node_b, 1); node_b.node.handle_commitment_signed_batch_test(node_a_id, &as_commitment_signed); let (bs_revoke_and_ack, extra_msg_option) = { @@ -2716,7 +2708,7 @@ pub fn do_main_commitment_signed_dance( assert!(node_a.node.get_and_clear_pending_events().is_empty()); assert!(node_a.node.get_and_clear_pending_msg_events().is_empty()); } - (extra_msg_option, bs_revoke_and_ack) + (extra_msg_option, bs_revoke_and_ack, node_b_holding_cell_htlcs) } /// Runs the commitment_signed dance by delivering the commitment_signed and handling the @@ -2733,9 +2725,10 @@ pub fn commitment_signed_dance_return_raa( .node .handle_commitment_signed_batch_test(node_b.node.get_our_node_id(), commitment_signed); check_added_monitors(&node_a, 1); - let (extra_msg_option, bs_revoke_and_ack) = + let (extra_msg_option, bs_revoke_and_ack, node_b_holding_cell_htlcs) = do_main_commitment_signed_dance(&node_a, &node_b, fail_backwards); assert!(extra_msg_option.is_none()); + assert!(node_b_holding_cell_htlcs.is_empty()); bs_revoke_and_ack } @@ -2808,38 +2801,19 @@ pub fn get_payment_preimage_hash( let payment_preimage = PaymentPreimage([*payment_count; 32]); *payment_count += 1; let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0[..]).to_byte_array()); - let payment_secret = recipient + let (payment_secret, _) = recipient .node .create_inbound_payment_for_hash( payment_hash, min_value_msat, 7200, min_final_cltv_expiry_delta, + None, ) .unwrap(); (payment_preimage, payment_hash, payment_secret) } -/// Get a payment preimage and hash. -/// -/// Don't use this, use the identically-named function instead. -#[macro_export] -macro_rules! get_payment_preimage_hash { - ($dest_node: expr) => { - get_payment_preimage_hash!($dest_node, None) - }; - ($dest_node: expr, $min_value_msat: expr) => { - $crate::get_payment_preimage_hash!($dest_node, $min_value_msat, None) - }; - ($dest_node: expr, $min_value_msat: expr, $min_final_cltv_expiry_delta: expr) => { - $crate::ln::functional_test_utils::get_payment_preimage_hash( - &$dest_node, - $min_value_msat, - $min_final_cltv_expiry_delta, - ) - }; -} - /// Gets a route from the given sender to the node described in `payment_params`. pub fn get_route(send_node: &Node, route_params: &RouteParameters) -> Result<Route, &'static str> { let scorer = TestScorer::new(); @@ -2971,7 +2945,7 @@ pub fn check_payment_claimable( #[cfg(any(test, ldk_bench, feature = "_test_utils"))] macro_rules! expect_payment_claimable { ($node: expr, $expected_payment_hash: expr, $expected_payment_secret: expr, $expected_recv_value: expr) => { - expect_payment_claimable!( + $crate::expect_payment_claimable!( $node, $expected_payment_hash, $expected_payment_secret, @@ -3043,6 +3017,7 @@ pub fn expect_payment_sent<CM: AChannelManager, H: NodeHolder<CM = CM>>( ref amount_msat, ref fee_paid_msat, ref bolt12_invoice, + .. } => { assert_eq!(expected_payment_preimage, *payment_preimage); assert_eq!(expected_payment_hash, *payment_hash); @@ -3117,17 +3092,17 @@ pub fn expect_payment_forwarded<CM: AChannelManager, H: NodeHolder<CM = CM>>( ) -> Option<u64> { match event { Event::PaymentForwarded { - prev_channel_id, - next_channel_id, - prev_user_channel_id, - next_user_channel_id, - prev_node_id, - next_node_id, + prev_htlcs, + next_htlcs, total_fee_earned_msat, skimmed_fee_msat, claim_from_onchain_tx, + outbound_amount_forwarded_msat, .. } => { + assert_eq!(prev_htlcs.len(), 1); + assert_eq!(next_htlcs.len(), 1); + if allow_1_msat_fee_overpay { // Aggregating fees for blinded paths may result in a rounding error, causing slight // overpayment in fees. @@ -3141,34 +3116,50 @@ pub fn expect_payment_forwarded<CM: AChannelManager, H: NodeHolder<CM = CM>>( // Check that the (knowingly) withheld amount is always less or equal to the expected // overpaid amount. assert!(skimmed_fee_msat == expected_extra_fees_msat); + match expected_fee { + Some(_) => { + let actual_fee = total_fee_earned_msat.unwrap(); + assert_eq!(next_htlcs[0].amount_msat, Some(outbound_amount_forwarded_msat)); + assert_eq!( + prev_htlcs[0].amount_msat, + Some(next_htlcs[0].amount_msat.unwrap() + actual_fee) + ); + }, + None => { + assert_eq!(total_fee_earned_msat, None); + }, + } if !upstream_force_closed { - assert_eq!(prev_node.node().get_our_node_id(), prev_node_id.unwrap()); + let prev_node_id = prev_htlcs[0].node_id.unwrap(); + let prev_channel_id = prev_htlcs[0].channel_id; + let prev_user_channel_id = prev_htlcs[0].user_channel_id.unwrap(); + + assert_eq!(prev_node.node().get_our_node_id(), prev_node_id); // Is the event prev_channel_id in one of the channels between the two nodes? let node_chans = node.node().list_channels(); - assert!(node_chans.iter().any(|x| x.counterparty.node_id == prev_node_id.unwrap() - && x.channel_id == prev_channel_id.unwrap() - && x.user_channel_id == prev_user_channel_id.unwrap())); + assert!(node_chans.iter().any(|x| x.counterparty.node_id == prev_node_id + && x.channel_id == prev_channel_id + && x.user_channel_id == prev_user_channel_id)); } // We check for force closures since a force closed channel is removed from the // node's channel list if !downstream_force_closed { + let next_node_id = next_htlcs[0].node_id.unwrap(); + let next_channel_id = next_htlcs[0].channel_id; + let next_user_channel_id = next_htlcs[0].user_channel_id.unwrap(); // As documented, `next_user_channel_id` will only be `Some` if we didn't settle via an // onchain transaction, just as the `total_fee_earned_msat` field. Rather than // introducing yet another variable, we use the latter's state as a flag to detect // this and only check if it's `Some`. - assert_eq!(next_node.node().get_our_node_id(), next_node_id.unwrap()); + assert_eq!(next_node.node().get_our_node_id(), next_node_id); let node_chans = node.node().list_channels(); if total_fee_earned_msat.is_none() { - assert!(node_chans - .iter() - .any(|x| x.counterparty.node_id == next_node_id.unwrap() - && x.channel_id == next_channel_id.unwrap())); + assert!(node_chans.iter().any(|x| x.counterparty.node_id == next_node_id + && x.channel_id == next_channel_id)); } else { - assert!(node_chans - .iter() - .any(|x| x.counterparty.node_id == next_node_id.unwrap() - && x.channel_id == next_channel_id.unwrap() - && x.user_channel_id == next_user_channel_id.unwrap())); + assert!(node_chans.iter().any(|x| x.counterparty.node_id == next_node_id + && x.channel_id == next_channel_id + && x.user_channel_id == next_user_channel_id)); } } assert_eq!(claim_from_onchain_tx, downstream_force_closed); @@ -3247,7 +3238,7 @@ pub fn expect_splice_pending_event<'a, 'b, 'c, 'd>( let events = node.node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); match &events[0] { - crate::events::Event::SplicePending { channel_id, counterparty_node_id, .. } => { + crate::events::Event::SpliceNegotiated { channel_id, counterparty_node_id, .. } => { assert_eq!(*expected_counterparty_node_id, *counterparty_node_id); *channel_id }, @@ -3255,6 +3246,59 @@ pub fn expect_splice_pending_event<'a, 'b, 'c, 'd>( } } +#[cfg(any(test, ldk_bench, feature = "_test_utils"))] +pub fn expect_splice_failed_events<'a, 'b, 'c, 'd>( + node: &'a Node<'b, 'c, 'd>, expected_channel_id: &ChannelId, + funding_contribution: FundingContribution, expected_reason: NegotiationFailureReason, +) { + let events = node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + match &events[0] { + Event::DiscardFunding { funding_info, .. } => { + if let FundingInfo::Contribution { inputs, outputs } = &funding_info { + let (expected_inputs, expected_outputs) = + funding_contribution.clone().into_contributed_inputs_and_outputs(); + assert_eq!(*inputs, expected_inputs); + assert_eq!(*outputs, expected_outputs); + } else { + panic!("Expected FundingInfo::Contribution"); + } + }, + _ => panic!("Unexpected event"), + } + match &events[1] { + Event::SpliceNegotiationFailed { channel_id, reason, contribution, .. } => { + assert_eq!(*expected_channel_id, *channel_id); + assert_eq!(expected_reason, *reason); + assert_eq!(contribution.as_ref(), Some(&funding_contribution)); + }, + _ => panic!("Unexpected event"), + } +} + +#[cfg(any(test, ldk_bench, feature = "_test_utils"))] +pub fn expect_discard_funding_event<'a, 'b, 'c, 'd>( + node: &'a Node<'b, 'c, 'd>, expected_channel_id: &ChannelId, + funding_contribution: FundingContribution, +) { + let events = node.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::DiscardFunding { channel_id, funding_info } => { + assert_eq!(*expected_channel_id, *channel_id); + if let FundingInfo::Contribution { inputs, outputs } = &funding_info { + let (expected_inputs, expected_outputs) = + funding_contribution.into_contributed_inputs_and_outputs(); + assert_eq!(*inputs, expected_inputs); + assert_eq!(*outputs, expected_outputs); + } else { + panic!("Expected FundingInfo::Contribution"); + } + }, + _ => panic!("Unexpected event"), + } +} + pub fn expect_probe_successful_events( node: &Node, mut probe_results: Vec<(PaymentHash, PaymentId)>, ) { @@ -3464,14 +3508,14 @@ pub fn send_along_route_with_secret<'a, 'b, 'c>( recv_value: u64, our_payment_hash: PaymentHash, our_payment_secret: PaymentSecret, ) -> PaymentId { let payment_id = PaymentId(origin_node.keys_manager.backing.get_secure_random_bytes()); - origin_node.router.expect_find_route(route.route_params.clone().unwrap(), Ok(route.clone())); + origin_node.router.expect_find_route(route.route_params.clone(), Ok(route.clone())); origin_node .node .send_payment( our_payment_hash, - RecipientOnionFields::secret_only(our_payment_secret), + RecipientOnionFields::secret_only(our_payment_secret, recv_value), payment_id, - route.route_params.unwrap(), + route.route_params, Retry::Attempts(0), ) .unwrap(); @@ -3513,6 +3557,7 @@ pub struct PassAlongPathArgs<'a, 'b, 'c, 'd> { pub custom_tlvs: Vec<(u64, Vec<u8>)>, pub payment_metadata: Option<Vec<u8>>, pub expected_failure: Option<HTLCHandlingFailureType>, + pub payment_claimable_cltv: Option<u32>, } impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { @@ -3535,6 +3580,7 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { custom_tlvs: Vec::new(), payment_metadata: None, expected_failure: None, + payment_claimable_cltv: None, } } pub fn without_clearing_recipient_events(mut self) -> Self { @@ -3575,6 +3621,10 @@ impl<'a, 'b, 'c, 'd> PassAlongPathArgs<'a, 'b, 'c, 'd> { self.dummy_tlvs = dummy_tlvs.to_vec(); self } + pub fn with_payment_claimable_cltv(mut self, cltv: u32) -> Self { + self.payment_claimable_cltv = Some(cltv); + self + } } pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event> { @@ -3593,6 +3643,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event> custom_tlvs, payment_metadata, expected_failure, + payment_claimable_cltv, } = args; let mut payment_event = SendEvent::from_event(ev); @@ -3608,7 +3659,7 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event> if is_last_hop && is_probe { do_commitment_signed_dance(node, prev_node, &payment_event.commitment_msg, true, true); - node.node.process_pending_htlc_forwards(); + expect_and_process_pending_htlcs(node, true); check_added_monitors(node, 1); } else { let commitment = &payment_event.commitment_msg; @@ -3708,6 +3759,12 @@ pub fn do_pass_along_path<'a, 'b, 'c>(args: PassAlongPathArgs) -> Option<Event> assert_eq!(*user_chan_id, Some(chan.user_channel_id)); } assert!(claim_deadline.unwrap() > node.best_block_info().1); + if let Some(expected_cltv) = payment_claimable_cltv { + assert_eq!( + claim_deadline.unwrap(), + expected_cltv - HTLC_FAIL_BACK_BUFFER, + ); + } }, _ => panic!("Unexpected event"), } @@ -3811,7 +3868,7 @@ pub fn send_along_route<'a, 'b, 'c>( recv_value: u64, ) -> (PaymentPreimage, PaymentHash, PaymentSecret, PaymentId) { let (our_payment_preimage, our_payment_hash, our_payment_secret) = - get_payment_preimage_hash!(expected_route.last().unwrap()); + get_payment_preimage_hash(expected_route.last().unwrap(), None, None); let payment_id = send_along_route_with_secret( origin_node, route, @@ -4461,21 +4518,41 @@ pub fn create_chanmon_cfgs(node_count: usize) -> Vec<TestChanMonCfg> { pub fn create_chanmon_cfgs_with_legacy_keys( node_count: usize, predefined_keys_ids: Option<Vec<[u8; 32]>>, +) -> Vec<TestChanMonCfg> { + create_chanmon_cfgs_internal(node_count, predefined_keys_ids, false) +} + +pub fn create_phantom_chanmon_cfgs(node_count: usize) -> Vec<TestChanMonCfg> { + create_chanmon_cfgs_internal(node_count, None, true) +} + +pub fn create_chanmon_cfgs_internal( + node_count: usize, predefined_keys_ids: Option<Vec<[u8; 32]>>, phantom: bool, ) -> Vec<TestChanMonCfg> { let mut chan_mon_cfgs = Vec::new(); + let phantom_seed = if phantom { Some(&[42; 32]) } else { None }; for i in 0..node_count { let tx_broadcaster = test_utils::TestBroadcaster::new(Network::Testnet); let fee_estimator = test_utils::TestFeeEstimator::new(253); let chain_source = test_utils::TestChainSource::new(Network::Testnet); let logger = test_utils::TestLogger::with_id(format!("node {}", i)); let persister = test_utils::TestPersister::new(); - let seed = [i as u8; 32]; - let keys_manager = if predefined_keys_ids.is_some() { + let mut seed = [i as u8; 32]; + if phantom { + // We would ideally randomize keys on every test run, but some tests fail in that case. + // Instead, we only randomize in the phantom case. + use core::hash::{BuildHasher, Hasher}; + // Get a random value using the only std API to do so - the DefaultHasher + let rand_val = std::collections::hash_map::RandomState::new().build_hasher().finish(); + seed[..8].copy_from_slice(&rand_val.to_ne_bytes()); + } + let keys_manager = test_utils::TestKeysInterface::with_settings( + &seed, + Network::Testnet, // Use legacy (V1) remote_key derivation for tests using legacy key sets. - test_utils::TestKeysInterface::with_v1_remote_key_derivation(&seed, Network::Testnet) - } else { - test_utils::TestKeysInterface::new(&seed, Network::Testnet) - }; + predefined_keys_ids.is_some(), + phantom_seed, + ); let scorer = RwLock::new(test_utils::TestScorer::new()); // Set predefined keys_id if provided @@ -4502,6 +4579,7 @@ pub fn create_chanmon_cfgs_with_legacy_keys( fn create_node_cfgs_internal<'a, F>( node_count: usize, chanmon_cfgs: &'a Vec<TestChanMonCfg>, persisters: Vec<&'a impl test_utils::SyncPersist>, message_router_constructor: F, + deferred: bool, ) -> Vec<NodeCfg<'a>> where F: Fn( @@ -4514,14 +4592,25 @@ where for i in 0..node_count { let cfg = &chanmon_cfgs[i]; let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, &cfg.logger)); - let chain_monitor = test_utils::TestChainMonitor::new( - Some(&cfg.chain_source), - &cfg.tx_broadcaster, - &cfg.logger, - &cfg.fee_estimator, - persisters[i], - &cfg.keys_manager, - ); + let chain_monitor = if deferred { + test_utils::TestChainMonitor::new_deferred( + Some(&cfg.chain_source), + &cfg.tx_broadcaster, + &cfg.logger, + &cfg.fee_estimator, + persisters[i], + &cfg.keys_manager, + ) + } else { + test_utils::TestChainMonitor::new( + Some(&cfg.chain_source), + &cfg.tx_broadcaster, + &cfg.logger, + &cfg.fee_estimator, + persisters[i], + &cfg.keys_manager, + ) + }; let seed = [i as u8; 32]; nodes.push(NodeCfg { @@ -4558,6 +4647,20 @@ pub fn create_node_cfgs<'a>( chanmon_cfgs, persisters, test_utils::TestMessageRouter::new_default, + false, + ) +} + +pub fn create_node_cfgs_deferred<'a>( + node_count: usize, chanmon_cfgs: &'a Vec<TestChanMonCfg>, +) -> Vec<NodeCfg<'a>> { + let persisters = chanmon_cfgs.iter().map(|c| &c.persister).collect(); + create_node_cfgs_internal( + node_count, + chanmon_cfgs, + persisters, + test_utils::TestMessageRouter::new_default, + true, ) } @@ -4570,6 +4673,7 @@ pub fn create_node_cfgs_with_persisters<'a>( chanmon_cfgs, persisters, test_utils::TestMessageRouter::new_default, + false, ) } @@ -4582,6 +4686,7 @@ pub fn create_node_cfgs_with_node_id_message_router<'a>( chanmon_cfgs, persisters, test_utils::TestMessageRouter::new_node_id_router, + false, ) } @@ -4630,7 +4735,7 @@ pub fn create_node_chanmgrs<'a, 'b>( for i in 0..node_count { let network = Network::Testnet; let genesis_block = bitcoin::constants::genesis_block(network); - let params = ChainParameters { network, best_block: BestBlock::from_network(network) }; + let params = ChainParameters { network, best_block: BlockLocator::from_network(network) }; let node = ChannelManager::new( cfgs[i].fee_estimator, &cfgs[i].chain_monitor, @@ -4700,19 +4805,6 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>( for i in 0..node_count { let dedicated_entropy = DedicatedEntropy(RandomBytes::new([i as u8; 32])); - #[cfg(feature = "dnssec")] - let onion_messenger = OnionMessenger::new_with_offline_peer_interception( - dedicated_entropy, - cfgs[i].keys_manager, - cfgs[i].logger, - &chan_mgrs[i], - &cfgs[i].message_router, - &chan_mgrs[i], - &chan_mgrs[i], - &chan_mgrs[i], - IgnoringMessageHandler {}, - ); - #[cfg(not(feature = "dnssec"))] let onion_messenger = OnionMessenger::new_with_offline_peer_interception( dedicated_entropy, cfgs[i].keys_manager, @@ -4723,6 +4815,7 @@ pub fn create_network<'a, 'b: 'a, 'c: 'b>( &chan_mgrs[i], IgnoringMessageHandler {}, IgnoringMessageHandler {}, + true, ); let gossip_sync = P2PGossipSync::new(cfgs[i].network_graph.as_ref(), None, cfgs[i].logger); let wallet_source = Arc::new(test_utils::TestWalletSource::new( @@ -5121,6 +5214,18 @@ macro_rules! handle_chan_reestablish_msgs { stfu = Some(msg.clone()); } + let mut tx_signatures = None; + let mut tx_signatures_order = + $crate::ln::channelmanager::TxSignaturesOrder::CommitmentFirst; + if let Some(&MessageSendEvent::SendTxSignatures { ref node_id, ref msg }) = + msg_events.get(idx) + { + assert_eq!(*node_id, $dst_node.node.get_our_node_id()); + tx_signatures = Some(msg.clone()); + tx_signatures_order = $crate::ln::channelmanager::TxSignaturesOrder::SignaturesFirst; + idx += 1; + } + let mut revoke_and_ack = None; let mut commitment_update = None; let order = if let Some(ev) = msg_events.get(idx) { @@ -5169,13 +5274,14 @@ macro_rules! handle_chan_reestablish_msgs { } } - let mut tx_signatures = None; - if let Some(&MessageSendEvent::SendTxSignatures { ref node_id, ref msg }) = - msg_events.get(idx) - { - assert_eq!(*node_id, $dst_node.node.get_our_node_id()); - tx_signatures = Some(msg.clone()); - idx += 1; + if tx_signatures.is_none() { + if let Some(&MessageSendEvent::SendTxSignatures { ref node_id, ref msg }) = + msg_events.get(idx) + { + assert_eq!(*node_id, $dst_node.node.get_our_node_id()); + tx_signatures = Some(msg.clone()); + idx += 1; + } } if let Some(&MessageSendEvent::SendAnnouncementSignatures { ref node_id, ref msg }) = @@ -5205,6 +5311,7 @@ macro_rules! handle_chan_reestablish_msgs { tx_signatures, stfu, tx_abort, + tx_signatures_order, ) }}; } @@ -5360,8 +5467,25 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) { && pending_cell_htlc_claims.1 == 0 && pending_cell_htlc_fails.1 == 0) ); + let pending_commitment_update = ( + pending_htlc_adds.0 != 0 + || pending_htlc_claims.0 != 0 + || pending_htlc_fails.0 != 0 + || pending_cell_htlc_claims.0 != 0 + || pending_cell_htlc_fails.0 != 0 + || pending_responding_commitment_signed.0, + pending_htlc_adds.1 != 0 + || pending_htlc_claims.1 != 0 + || pending_htlc_fails.1 != 0 + || pending_cell_htlc_claims.1 != 0 + || pending_cell_htlc_fails.1 != 0 + || pending_responding_commitment_signed.1, + ); for mut chan_msgs in resp_1.drain(..) { + if send_interactive_tx_sigs.0 && pending_commitment_update.0 { + assert_eq!(chan_msgs.8, TxSignaturesOrder::SignaturesFirst); + } if send_channel_ready.0 { node_a.node.handle_channel_ready(node_b_id, &chan_msgs.0.unwrap()); let announcement_event = node_a.node.get_and_clear_pending_msg_events(); @@ -5423,13 +5547,7 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) { } else { assert!(chan_msgs.1.is_none()); } - if pending_htlc_adds.0 != 0 - || pending_htlc_claims.0 != 0 - || pending_htlc_fails.0 != 0 - || pending_cell_htlc_claims.0 != 0 - || pending_cell_htlc_fails.0 != 0 - || pending_responding_commitment_signed.0 - { + if pending_commitment_update.0 { let commitment_update = chan_msgs.2.unwrap(); assert_eq!(commitment_update.update_add_htlcs.len(), pending_htlc_adds.0); assert_eq!( @@ -5478,6 +5596,9 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) { } for mut chan_msgs in resp_2.drain(..) { + if send_interactive_tx_sigs.1 && pending_commitment_update.1 { + assert_eq!(chan_msgs.8, TxSignaturesOrder::SignaturesFirst); + } if send_channel_ready.1 { node_b.node.handle_channel_ready(node_a_id, &chan_msgs.0.unwrap()); let announcement_event = node_b.node.get_and_clear_pending_msg_events(); @@ -5539,13 +5660,7 @@ pub fn reconnect_nodes<'a, 'b, 'c, 'd>(args: ReconnectArgs<'a, 'b, 'c, 'd>) { } else { assert!(chan_msgs.1.is_none()); } - if pending_htlc_adds.1 != 0 - || pending_htlc_claims.1 != 0 - || pending_htlc_fails.1 != 0 - || pending_cell_htlc_claims.1 != 0 - || pending_cell_htlc_fails.1 != 0 - || pending_responding_commitment_signed.1 - { + if pending_commitment_update.1 { let commitment_update = chan_msgs.2.unwrap(); assert_eq!(commitment_update.update_add_htlcs.len(), pending_htlc_adds.1); assert_eq!( @@ -5698,3 +5813,52 @@ pub fn get_scid_from_channel_id<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, channel_id: .short_channel_id .unwrap() } + +/// Creates a [`BlindedTail`] for a trampoline forward through a single intermediate node. +/// +/// The resulting tail contains blinded hops built from `intermediate_nodes` plus a dummy receive +/// TLV, with the `TrampolineHop` fee and CLTV derived from the blinded path's aggregated payinfo. +/// The constructed [`BlindedPaymentPath`] is also returned so callers can register it in +/// [`PaymentParameters`]. +/// +/// [`PaymentParameters`]: crate::routing::router::PaymentParameters +pub fn create_trampoline_forward_blinded_tail<ES: EntropySource>( + secp_ctx: &bitcoin::secp256k1::Secp256k1<bitcoin::secp256k1::All>, entropy_source: ES, + intermediate_nodes: &[ForwardNode<TrampolineForwardTlvs>], payee_node_id: PublicKey, + payee_receive_key: ReceiveAuthKey, payee_tlvs: ReceiveTlvs, min_final_cltv_expiry_delta: u32, + excess_final_cltv_delta: u32, final_value_msat: u64, +) -> (BlindedTail, BlindedPaymentPath) { + let blinded_path = BlindedPaymentPath::new_for_trampoline( + intermediate_nodes, + payee_node_id, + payee_receive_key, + payee_tlvs, + u64::max_value(), + min_final_cltv_expiry_delta as u16, + entropy_source, + secp_ctx, + ) + .unwrap(); + + let tail = BlindedTail { + trampoline_hops: vec![TrampolineHop { + pubkey: intermediate_nodes.first().map(|n| n.node_id).unwrap_or(payee_node_id), + node_features: types::features::Features::empty(), + fee_msat: compute_fees( + final_value_msat, + lightning_types::routing::RoutingFees { + base_msat: blinded_path.payinfo.fee_base_msat, + proportional_millionths: blinded_path.payinfo.fee_proportional_millionths, + }, + ) + .unwrap(), + cltv_expiry_delta: blinded_path.payinfo.cltv_expiry_delta as u32 + + excess_final_cltv_delta, + }], + hops: blinded_path.blinded_hops().to_vec(), + blinding_point: blinded_path.blinding_point(), + excess_final_cltv_expiry_delta: excess_final_cltv_delta, + final_value_msat, + }; + (tail, blinded_path) +} diff --git a/lightning/src/ln/functional_tests.rs b/lightning/src/ln/functional_tests.rs index 6fe0c83dfe8..fdf092d8efe 100644 --- a/lightning/src/ln/functional_tests.rs +++ b/lightning/src/ln/functional_tests.rs @@ -19,6 +19,7 @@ use crate::chain::channelmonitor::{ LATENCY_GRACE_PERIOD_BLOCKS, }; use crate::chain::transaction::OutPoint; +use crate::chain::BlockLocator; use crate::chain::{ChannelMonitorUpdateStatus, Confirm, Listen, Watch}; use crate::events::{ ClosureReason, Event, HTLCHandlingFailureType, PathFailure, PaymentFailureReason, @@ -32,6 +33,7 @@ use crate::ln::channel::{ get_holder_selected_channel_reserve_satoshis, Channel, DISCONNECT_PEER_AWAITING_RESPONSE_TICKS, MIN_CHAN_DUST_LIMIT_SATOSHIS, UNFUNDED_CHANNEL_AGE_LIMIT_TICKS, }; +use crate::ln::channel_state::OutboundHTLCSource; use crate::ln::channelmanager::{ PaymentId, RAACommitmentOrder, BREAKDOWN_TIMEOUT, DISABLE_GOSSIP_TICKS, ENABLE_GOSSIP_TICKS, MIN_CLTV_EXPIRY_DELTA, @@ -48,6 +50,7 @@ use crate::routing::gossip::{NetworkGraph, NetworkUpdate}; use crate::routing::router::{ get_route, Path, PaymentParameters, Route, RouteHop, RouteParameters, }; +use crate::sign::ChannelSigner; use crate::sign::{EntropySource, OutputSpender, SignerProvider}; use crate::types::features::{ChannelFeatures, ChannelTypeFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentSecret}; @@ -162,7 +165,7 @@ pub fn fake_network_test() { let route_params = RouteParameters::from_payment_params_and_value(payment_params, 1000000); let route = Route { paths: vec![Path { hops, blinded_tail: None }], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; let path: &[_] = &[&nodes[2], &nodes[3], &nodes[1]]; let payment_preimage_1 = send_along_route(&nodes[1], route, path, 1000000).0; @@ -200,8 +203,7 @@ pub fn fake_network_test() { + chan_2.1.contents.fee_proportional_millionths as u64 * hops[2].fee_msat as u64 / 1000000; hops[0].fee_msat = chan_3.1.contents.fee_base_msat as u64 + chan_3.1.contents.fee_proportional_millionths as u64 * hops[1].fee_msat as u64 / 1000000; - let route = - Route { paths: vec![Path { hops, blinded_tail: None }], route_params: Some(route_params) }; + let route = Route { paths: vec![Path { hops, blinded_tail: None }], route_params }; let path: &[_] = &[&nodes[3], &nodes[2], &nodes[1]]; let payment_hash_2 = send_along_route(&nodes[1], route, path, 1000000).1; @@ -291,8 +293,10 @@ pub fn test_duplicate_htlc_different_direction_onchain() { let (payment_preimage, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], 900_000); let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[0], payment_value_msats); - let node_a_payment_secret = - nodes[0].node.create_inbound_payment_for_hash(payment_hash, None, 7200, None).unwrap(); + let (node_a_payment_secret, _) = nodes[0] + .node + .create_inbound_payment_for_hash(payment_hash, None, 7200, None, None) + .unwrap(); send_along_route_with_secret( &nodes[1], route, @@ -413,7 +417,8 @@ pub fn test_inbound_outbound_capacity_is_not_zero() { assert_eq!(channels0.len(), 1); assert_eq!(channels1.len(), 1); - let reserve = get_holder_selected_channel_reserve_satoshis(100_000, &default_config); + let reserve = + get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false).unwrap(); assert_eq!(channels0[0].inbound_capacity_msat, 95000000 - reserve * 1000); assert_eq!(channels1[0].outbound_capacity_msat, 95000000 - reserve * 1000); @@ -526,7 +531,7 @@ fn do_test_fail_back_before_backwards_timeout(post_fail_back_action: PostFailBac connect_blocks(&nodes[2], TEST_FINAL_CLTV - CLTV_CLAIM_BUFFER + 2); let node_2_txn = test_txn_broadcast(&nodes[2], &chan_2, None, HTLCType::SUCCESS); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) }; check_closed_event(&nodes[2], 1, reason, &[node_b_id], 100_000); check_added_monitors(&nodes[2], 1); @@ -618,7 +623,7 @@ pub fn channel_monitor_network_test() { .force_close_broadcasting_latest_txn(&chan_1.2, &node_a_id, message.clone()) .unwrap(); check_added_monitors(&nodes[1], 1); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); { @@ -650,7 +655,7 @@ pub fn channel_monitor_network_test() { .node .force_close_broadcasting_latest_txn(&chan_2.2, &node_c_id, message.clone()) .unwrap(); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); { let mut node_txn = test_txn_broadcast(&nodes[1], &chan_2, None, HTLCType::NONE); @@ -704,7 +709,7 @@ pub fn channel_monitor_network_test() { .force_close_broadcasting_latest_txn(&chan_3.2, &node_d_id, message.clone()) .unwrap(); check_added_monitors(&nodes[2], 1); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); let node2_commitment_txid; { let node_txn = test_txn_broadcast(&nodes[2], &chan_3, None, HTLCType::NONE); @@ -1247,7 +1252,7 @@ pub fn do_test_multiple_package_conflicts(p2a_anchor: bool) { mine_transaction(&nodes[1], node2_commit_tx); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_c_id], CHAN_CAPACITY); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); // Node 1 should immediately claim package 1 but has to wait a block to claim package 2. @@ -1288,7 +1293,7 @@ pub fn do_test_multiple_package_conflicts(p2a_anchor: bool) { mine_transaction(&nodes[2], node2_commit_tx); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[2], 1, reason, &[node_b_id], CHAN_CAPACITY); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); check_added_monitors(&nodes[2], 1); let process_bump_event = |node: &Node| { @@ -1463,7 +1468,7 @@ pub fn test_htlc_on_chain_success() { assert_eq!(updates.update_fulfill_htlcs.len(), 1); mine_transaction(&nodes[2], &commitment_tx[0]); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); check_added_monitors(&nodes[2], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[2], 1, reason, &[node_b_id], 100000); @@ -1490,38 +1495,42 @@ pub fn test_htlc_on_chain_success() { connect_blocks(&nodes[1], TEST_FINAL_CLTV); // Confirm blocks until the HTLC expires let forwarded_events = nodes[1].node.get_and_clear_pending_events(); assert_eq!(forwarded_events.len(), 3); - let chan_id = Some(chan_1.2); + let chan_id = chan_1.2; match forwarded_events[0] { Event::PaymentForwarded { + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, - prev_channel_id, claim_from_onchain_tx, - next_channel_id, outbound_amount_forwarded_msat, .. } => { assert_eq!(total_fee_earned_msat, Some(1000)); - assert_eq!(prev_channel_id, chan_id); + assert_eq!(prev_htlcs[0].channel_id, chan_id); + assert_eq!(prev_htlcs[0].amount_msat, Some(3001000)); assert_eq!(claim_from_onchain_tx, true); - assert_eq!(next_channel_id, Some(chan_2.2)); - assert_eq!(outbound_amount_forwarded_msat, Some(3000000)); + assert_eq!(next_htlcs[0].channel_id, chan_2.2); + assert_eq!(next_htlcs[0].amount_msat, Some(3000000)); + assert_eq!(outbound_amount_forwarded_msat, 3000000); }, _ => panic!(), } match forwarded_events[1] { Event::PaymentForwarded { + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, - prev_channel_id, claim_from_onchain_tx, - next_channel_id, outbound_amount_forwarded_msat, .. } => { assert_eq!(total_fee_earned_msat, Some(1000)); - assert_eq!(prev_channel_id, chan_id); + assert_eq!(prev_htlcs[0].channel_id, chan_id); + assert_eq!(prev_htlcs[0].amount_msat, Some(3001000)); assert_eq!(claim_from_onchain_tx, true); - assert_eq!(next_channel_id, Some(chan_2.2)); - assert_eq!(outbound_amount_forwarded_msat, Some(3000000)); + assert_eq!(next_htlcs[0].channel_id, chan_2.2); + assert_eq!(next_htlcs[0].amount_msat, Some(3000000)); + assert_eq!(outbound_amount_forwarded_msat, 3000000); }, _ => panic!(), } @@ -1585,7 +1594,7 @@ pub fn test_htlc_on_chain_success() { let node_a_commitment_tx = get_local_commitment_txn!(nodes[0], chan_1.2); check_spends!(node_a_commitment_tx[0], chan_1.3); mine_transaction(&nodes[1], &node_a_commitment_tx[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -1620,7 +1629,7 @@ pub fn test_htlc_on_chain_success() { let txn = vec![node_a_commitment_tx[0].clone(), commitment_spend.clone()]; connect_block(&nodes[0], &create_dummy_block(nodes[0].best_block_hash(), 42, txn)); connect_blocks(&nodes[0], TEST_FINAL_CLTV + MIN_CLTV_EXPIRY_DELTA as u32); // Confirm blocks until the HTLC expires - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let events = nodes[0].node.get_and_clear_pending_events(); check_added_monitors(&nodes[0], 2); @@ -1728,7 +1737,7 @@ fn do_test_htlc_on_chain_timeout(connect_style: ConnectStyle) { _ => panic!("Unexpected event"), }; mine_transaction(&nodes[2], &commitment_tx[0]); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); check_added_monitors(&nodes[2], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[2], 1, reason, &[node_b_id], 100000); @@ -1772,7 +1781,7 @@ fn do_test_htlc_on_chain_timeout(connect_style: ConnectStyle) { mine_transaction(&nodes[1], &timeout_tx); check_added_monitors(&nodes[1], 1); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); @@ -1812,7 +1821,7 @@ fn do_test_htlc_on_chain_timeout(connect_style: ConnectStyle) { mine_transaction(&nodes[0], &commitment_tx[0]); connect_blocks(&nodes[0], TEST_FINAL_CLTV + MIN_CLTV_EXPIRY_DELTA as u32); // Confirm blocks until the HTLC expires - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -1864,7 +1873,7 @@ pub fn test_simple_commitment_revoked_fail_backward() { check_closed_event(&nodes[1], 1, reason, &[node_c_id], 100000); connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); check_added_monitors(&nodes[1], 1); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); expect_and_process_pending_htlcs_and_htlc_handling_failed( &nodes[1], @@ -2026,7 +2035,7 @@ fn do_test_commitment_revoked_fail_backward_exhaustive( // on nodes[2]'s RAA. let (route, fourth_payment_hash, _, fourth_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[2], 1000000); - let onion = RecipientOnionFields::secret_only(fourth_payment_secret); + let onion = RecipientOnionFields::secret_only(fourth_payment_secret, 1000000); let id = PaymentId(fourth_payment_hash.0); nodes[1].node.send_payment_with_route(route, fourth_payment_hash, onion, id).unwrap(); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -2249,7 +2258,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 50_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 50_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2267,7 +2276,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { let (route, failed_payment_hash, _, failed_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 50_000); { - let onion = RecipientOnionFields::secret_only(failed_payment_secret); + let onion = RecipientOnionFields::secret_only(failed_payment_secret, 50_000); let id = PaymentId(failed_payment_hash.0); nodes[0].node.send_payment_with_route(route, failed_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -2283,10 +2292,9 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { let secp_ctx = Secp256k1::new(); let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); let current_height = nodes[1].node.best_block.read().unwrap().height + 1; - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 50_000); + let (onion_payloads, _amount_msat, cltv_expiry) = onion_utils::test_build_onion_payloads( &route.paths[0], - 50_000, &recipient_onion_fields, current_height, &None, @@ -2336,7 +2344,7 @@ pub fn fail_backward_pending_htlc_upon_channel_failure() { }, _ => panic!("Unexpected event {:?}", events[1]), } - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); } @@ -2373,7 +2381,7 @@ pub fn test_htlc_ignore_latest_remote_commitment() { .force_close_broadcasting_latest_txn(&chan_id, &node_b_id, message.clone()) .unwrap(); connect_blocks(&nodes[0], TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS + 1); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -2385,7 +2393,7 @@ pub fn test_htlc_ignore_latest_remote_commitment() { let block = create_dummy_block(nodes[1].best_block_hash(), 42, vec![node_txn[0].clone()]); connect_block(&nodes[1], &block); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -2419,7 +2427,7 @@ pub fn test_force_close_fail_back() { get_route_and_payment_hash!(nodes[0], nodes[2], 1000000); let mut payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2454,7 +2462,7 @@ pub fn test_force_close_fail_back() { .node .force_close_broadcasting_latest_txn(&channel_id, &node_b_id, message.clone()) .unwrap(); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); check_added_monitors(&nodes[2], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[2], 1, reason, &[node_b_id], 100000); @@ -2471,7 +2479,7 @@ pub fn test_force_close_fail_back() { mine_transaction(&nodes[1], &commitment_tx); // Note no UpdateHTLCs event here from nodes[1] to nodes[0]! - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_c_id], 100000); @@ -2705,7 +2713,7 @@ fn do_test_drop_messages_peer_disconnect(messages_delivered: u8, simulate_broken get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); let payment_event = { - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1_000_000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route, payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -3120,7 +3128,7 @@ pub fn test_drop_messages_peer_disconnect_dual_htlc() { // Now try to send a second payment which will fail to send let (route, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -3309,7 +3317,7 @@ fn do_test_htlc_timeout(send_partial_mpp: bool) { // indicates there are more HTLCs coming. let cur_height = CHAN_CONFIRM_DEPTH + 1; // route_payment calls send_payment, which adds 1 to the current height. So we do the same here to match. let payment_id = PaymentId([42; 32]); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100000); let session_privs = nodes[0] .node .test_add_new_pending_payment(our_payment_hash, onion, payment_id, &route) @@ -3320,8 +3328,7 @@ fn do_test_htlc_timeout(send_partial_mpp: bool) { .test_send_payment_along_path( &route.paths[0], &our_payment_hash, - RecipientOnionFields::secret_only(payment_secret), - 200_000, + RecipientOnionFields::secret_only(payment_secret, 200_000), cur_height, payment_id, &None, @@ -3409,7 +3416,7 @@ fn do_test_holding_cell_htlc_add_timeouts(forwarded_htlc: bool) { // Route a first payment to get the 1 -> 2 channel in awaiting_raa... let (route, first_payment_hash, _, first_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(first_payment_secret); + let onion = RecipientOnionFields::secret_only(first_payment_secret, 100000); let id = PaymentId(first_payment_hash.0); nodes[1].node.send_payment_with_route(route, first_payment_hash, onion, id).unwrap(); assert_eq!(nodes[1].node.get_and_clear_pending_msg_events().len(), 1); @@ -3419,7 +3426,8 @@ fn do_test_holding_cell_htlc_add_timeouts(forwarded_htlc: bool) { let sending_node = if forwarded_htlc { &nodes[0] } else { &nodes[1] }; let (route, second_payment_hash, _, second_payment_secret) = get_route_and_payment_hash!(sending_node, nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(second_payment_secret); + assert_ne!(second_payment_hash, first_payment_hash); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100000); let id = PaymentId(second_payment_hash.0); sending_node.node.send_payment_with_route(route, second_payment_hash, onion, id).unwrap(); @@ -3433,6 +3441,30 @@ fn do_test_holding_cell_htlc_add_timeouts(forwarded_htlc: bool) { expect_and_process_pending_htlcs(&nodes[1], false); } check_added_monitors(&nodes[1], 0); + if forwarded_htlc { + let channels = nodes[1].node.list_channels(); + let inbound_channel = + channels.iter().find(|details| details.counterparty.node_id == node_a_id).unwrap(); + let outbound_channel = + channels.iter().find(|details| details.counterparty.node_id == node_c_id).unwrap(); + let inbound_htlc = inbound_channel + .pending_inbound_htlcs + .iter() + .find(|details| details.payment_hash == second_payment_hash) + .unwrap(); + let outbound_htlc = outbound_channel + .pending_outbound_htlcs + .iter() + .find(|details| details.payment_hash == second_payment_hash) + .unwrap(); + assert_eq!(outbound_htlc.htlc_id, None); + let inbound_reference = match &outbound_htlc.source { + Some(OutboundHTLCSource::Forwarded { inbound_htlc }) => inbound_htlc, + _ => panic!("Unexpected outbound HTLC source"), + }; + assert_eq!(inbound_reference.channel_id, inbound_channel.channel_id); + assert_eq!(inbound_reference.htlc_id, inbound_htlc.htlc_id); + } connect_blocks(&nodes[1], TEST_FINAL_CLTV - LATENCY_GRACE_PERIOD_BLOCKS); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -3530,7 +3562,7 @@ pub fn test_claim_sizeable_push_msat() { .node .force_close_broadcasting_latest_txn(&chan.2, &node_a_id, message.clone()) .unwrap(); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -3571,7 +3603,7 @@ pub fn test_claim_on_remote_sizeable_push_msat() { .node .force_close_broadcasting_latest_txn(&chan.2, &node_b_id, message.clone()) .unwrap(); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -3582,7 +3614,7 @@ pub fn test_claim_on_remote_sizeable_push_msat() { assert_eq!(node_txn[0].output.len(), 2); // We can't force trimming of to_remote output as channel_reserve_satoshis block us to do so at channel opening mine_transaction(&nodes[1], &node_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -3615,7 +3647,7 @@ pub fn test_claim_on_remote_revoked_sizeable_push_msat() { claim_payment(&nodes[0], &[&nodes[1]], payment_preimage); mine_transaction(&nodes[1], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -3762,7 +3794,7 @@ fn do_test_static_spendable_outputs_justice_tx_revoked_commitment_tx(split_tx: b } mine_transaction(&nodes[1], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -3820,7 +3852,7 @@ pub fn test_static_spendable_outputs_justice_tx_revoked_htlc_timeout_tx() { // A will generate HTLC-Timeout from revoked commitment tx mine_transaction(&nodes[0], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -3843,7 +3875,7 @@ pub fn test_static_spendable_outputs_justice_tx_revoked_htlc_timeout_tx() { // B will generate justice tx from A's revoked commitment/HTLC tx let txn = vec![revoked_local_txn[0].clone(), revoked_htlc_txn[0].clone()]; connect_block(&nodes[1], &create_dummy_block(nodes[1].best_block_hash(), 42, txn)); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -3904,7 +3936,7 @@ pub fn test_static_spendable_outputs_justice_tx_revoked_htlc_success_tx() { // B will generate HTLC-Success from revoked commitment tx mine_transaction(&nodes[1], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -3925,7 +3957,7 @@ pub fn test_static_spendable_outputs_justice_tx_revoked_htlc_success_tx() { // A will generate justice tx from B's revoked commitment/HTLC tx let txn = vec![revoked_local_txn[0].clone(), revoked_htlc_txn[0].clone()]; connect_block(&nodes[0], &create_dummy_block(nodes[0].best_block_hash(), 42, txn)); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -4011,7 +4043,7 @@ pub fn test_onchain_to_onchain_claim() { assert!(updates.update_fail_malformed_htlcs.is_empty()); mine_transaction(&nodes[2], &commitment_tx[0]); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); check_added_monitors(&nodes[2], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[2], 1, reason, &[node_b_id], 100000); @@ -4033,18 +4065,18 @@ pub fn test_onchain_to_onchain_claim() { assert_eq!(events.len(), 2); match events[0] { Event::PaymentForwarded { + ref prev_htlcs, + ref next_htlcs, total_fee_earned_msat, - prev_channel_id, claim_from_onchain_tx, - next_channel_id, outbound_amount_forwarded_msat, .. } => { assert_eq!(total_fee_earned_msat, Some(1000)); - assert_eq!(prev_channel_id, Some(chan_1.2)); + assert_eq!(prev_htlcs[0].channel_id, chan_1.2); assert_eq!(claim_from_onchain_tx, true); - assert_eq!(next_channel_id, Some(chan_2.2)); - assert_eq!(outbound_amount_forwarded_msat, Some(3000000)); + assert_eq!(next_htlcs[0].channel_id, chan_2.2); + assert_eq!(outbound_amount_forwarded_msat, 3000000); }, _ => panic!("Unexpected event"), } @@ -4107,7 +4139,7 @@ pub fn test_onchain_to_onchain_claim() { assert!(b_txn[0].output[0].script_pubkey.is_p2wpkh()); // direct payment assert_eq!(b_txn[0].lock_time.to_consensus_u32(), nodes[1].best_block_info().1); // Success tx - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); } @@ -4156,8 +4188,10 @@ pub fn test_duplicate_payment_hash_one_failure_one_success() { let (our_payment_preimage, dup_payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2], &nodes[3]], 900_000); - let payment_secret = - nodes[4].node.create_inbound_payment_for_hash(dup_payment_hash, None, 7200, None).unwrap(); + let (payment_secret, _) = nodes[4] + .node + .create_inbound_payment_for_hash(dup_payment_hash, None, 7200, None, None) + .unwrap(); let payment_params = PaymentParameters::from_node_id(node_e_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[4].node.bolt11_invoice_features()) .unwrap(); @@ -4173,7 +4207,7 @@ pub fn test_duplicate_payment_hash_one_failure_one_success() { check_spends!(commitment_txn[0], chan_2.3); mine_transaction(&nodes[1], &commitment_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_c_id], 100000); @@ -4423,14 +4457,14 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 2nd HTLC (not added - smaller than dust limit + HTLC tx fee): let path_5: &[&[_]] = &[&[&nodes[2], &nodes[3], &nodes[5]]]; - let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_1, None, 7200, None).unwrap(); + let (payment_secret, _) = + nodes[5].node.create_inbound_payment_for_hash(hash_1, None, 7200, None, None).unwrap(); let route = route_to_5.clone(); send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_1, payment_secret); // 3rd HTLC (not added - smaller than dust limit + HTLC tx fee): - let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_2, None, 7200, None).unwrap(); + let (payment_secret, _) = + nodes[5].node.create_inbound_payment_for_hash(hash_2, None, 7200, None, None).unwrap(); let route = route_to_5; send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_2, payment_secret); @@ -4442,13 +4476,13 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000); // 6th HTLC: - let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_3, None, 7200, None).unwrap(); + let (payment_secret, _) = + nodes[5].node.create_inbound_payment_for_hash(hash_3, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route.clone(), path_5, 1000000, hash_3, payment_secret); // 7th HTLC: - let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_4, None, 7200, None).unwrap(); + let (payment_secret, _) = + nodes[5].node.create_inbound_payment_for_hash(hash_4, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, 1000000, hash_4, payment_secret); // 8th HTLC: @@ -4456,8 +4490,8 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 9th HTLC (not added - smaller than dust limit + HTLC tx fee): let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], dust_limit_msat); - let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_5, None, 7200, None).unwrap(); + let (payment_secret, _) = + nodes[5].node.create_inbound_payment_for_hash(hash_5, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, dust_limit_msat, hash_5, payment_secret); // 10th HTLC (not added - smaller than dust limit + HTLC tx fee): @@ -4465,8 +4499,8 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno // 11th HTLC: let (route, _, _, _) = get_route_and_payment_hash!(nodes[1], nodes[5], 1000000); - let payment_secret = - nodes[5].node.create_inbound_payment_for_hash(hash_6, None, 7200, None).unwrap(); + let (payment_secret, _) = + nodes[5].node.create_inbound_payment_for_hash(hash_6, None, 7200, None, None).unwrap(); send_along_route_with_secret(&nodes[1], route, path_5, 1000000, hash_6, payment_secret); // Double-check that six of the new HTLC were added @@ -4576,7 +4610,7 @@ fn do_test_fail_backwards_unrevoked_remote_announce(deliver_last_raa: bool, anno } connect_blocks(&nodes[2], ANTI_REORG_DELAY - 1); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); if deliver_last_raa { nodes[2].node.process_pending_htlc_forwards(); @@ -4808,7 +4842,7 @@ pub fn test_dynamic_spendable_outputs_local_htlc_timeout_tx() { // Timeout HTLC on A's chain and so it can generate a HTLC-Timeout tx mine_transaction(&nodes[0], &local_txn[0]); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -4936,7 +4970,7 @@ pub fn test_key_derivation_params() { // Timeout HTLC on A's chain and so it can generate a HTLC-Timeout tx mine_transaction(&nodes[0], &local_txn_1[0]); connect_blocks(&nodes[0], TEST_FINAL_CLTV); // Confirm blocks until the HTLC expires - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -5045,7 +5079,7 @@ fn do_htlc_claim_local_commitment_only(use_dust: bool) { } let htlc_type = if use_dust { HTLCType::NONE } else { HTLCType::SUCCESS }; test_txn_broadcast(&nodes[1], &chan, None, htlc_type); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -5065,7 +5099,8 @@ fn do_htlc_claim_current_remote_commitment_only(use_dust: bool) { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], if use_dust { 50000 } else { 3000000 }); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = + RecipientOnionFields::secret_only(payment_secret, if use_dust { 50000 } else { 3000000 }); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5086,7 +5121,7 @@ fn do_htlc_claim_current_remote_commitment_only(use_dust: bool) { block.header.prev_blockhash = block.block_hash(); } test_txn_broadcast(&nodes[0], &chan, None, HTLCType::NONE); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) }; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -5147,7 +5182,7 @@ fn do_htlc_claim_previous_remote_commitment_only(use_dust: bool, check_revoke_no } if !check_revoke_no_close { test_txn_broadcast(&nodes[0], &chan, None, HTLCType::NONE); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(our_payment_hash) }; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -5235,7 +5270,7 @@ pub fn test_fail_holding_cell_htlc_upon_free() { get_route_and_payment_hash!(nodes[0], nodes[1], max_can_send); // Send a payment which passes reserve checks but gets stuck in the holding cell. - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); chan_stat = get_channel_value_stat!(nodes[0], nodes[1], chan.2); @@ -5341,14 +5376,14 @@ pub fn test_free_and_fail_holding_cell_htlcs() { get_route_and_payment_hash!(nodes[0], nodes[1], amt_2); // Send 2 payments which pass reserve checks but get stuck in the holding cell. - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, amt_1); let id_1 = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route_1, payment_hash_1, onion, id_1).unwrap(); chan_stat = get_channel_value_stat!(nodes[0], nodes[1], chan.2); assert_eq!(chan_stat.holding_cell_outbound_amount_msat, amt_1); let id_2 = PaymentId(nodes[0].keys_manager.get_secure_random_bytes()); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, amt_2); nodes[0].node.send_payment_with_route(route_2.clone(), payment_hash_2, onion, id_2).unwrap(); chan_stat = get_channel_value_stat!(nodes[0], nodes[1], chan.2); assert_eq!(chan_stat.holding_cell_outbound_amount_msat, amt_1 + amt_2); @@ -5487,7 +5522,7 @@ pub fn test_fail_holding_cell_htlc_upon_free_multihop() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], max_can_send); let payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5595,7 +5630,7 @@ pub fn test_update_fulfill_htlc_bolt2_after_malformed_htlc_message_must_forward_ //First hop let mut payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5708,7 +5743,7 @@ pub fn test_channel_failed_after_message_with_badonion_node_perm_bits_set() { // First hop let mut payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5870,7 +5905,7 @@ fn do_test_failure_delay_dust_htlc_local_commitment(announce_latest: bool) { mine_transaction(&nodes[0], &as_prev_commitment_tx[0]); } - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -5954,7 +5989,7 @@ fn do_test_sweep_outbound_htlc_failure_update(revoked: bool, local: bool) { mine_transaction(&nodes[0], &as_commitment_tx[0]); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); let conditions = PaymentFailedConditions::new().from_mon_update(); @@ -5977,7 +6012,7 @@ fn do_test_sweep_outbound_htlc_failure_update(revoked: bool, local: bool) { } else { // We fail dust-HTLC 1 by broadcast of remote commitment tx. If revoked, fail also non-dust HTLC mine_transaction(&nodes[0], &bs_commitment_tx[0]); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -6057,12 +6092,12 @@ pub fn test_check_htlc_underpaying() { ) .unwrap(); - let (_, our_payment_hash, _) = get_payment_preimage_hash!(nodes[0]); - let our_payment_secret = nodes[1] + let (_, our_payment_hash, _) = get_payment_preimage_hash(&nodes[0], None, None); + let (our_payment_secret, _) = nodes[1] .node - .create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None) + .create_inbound_payment_for_hash(our_payment_hash, Some(100_000), 7200, None, None) .unwrap(); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -6682,16 +6717,15 @@ pub fn test_counterparty_raa_skip_no_crash() { const INITIAL_COMMITMENT_NUMBER: u64 = (1 << 48) - 1; // Make signer believe we got a counterparty signature, so that it allows the revocation - keys.as_ecdsa().unwrap().get_enforcement_state().last_holder_commitment -= 1; - per_commitment_secret = - keys.as_ref().release_commitment_secret(INITIAL_COMMITMENT_NUMBER).unwrap(); + keys.get_enforcement_state().last_holder_commitment -= 1; + per_commitment_secret = keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER).unwrap(); // Must revoke without gaps - keys.as_ecdsa().unwrap().get_enforcement_state().last_holder_commitment -= 1; - keys.as_ref().release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 1).unwrap(); + keys.get_enforcement_state().last_holder_commitment -= 1; + keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 1).unwrap(); - keys.as_ecdsa().unwrap().get_enforcement_state().last_holder_commitment -= 1; - let sec = keys.as_ref().release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 2).unwrap(); + keys.get_enforcement_state().last_holder_commitment -= 1; + let sec = keys.release_commitment_secret(INITIAL_COMMITMENT_NUMBER - 2).unwrap(); let key = SecretKey::from_slice(&sec).unwrap(); next_per_commitment_point = PublicKey::from_secret_key(&Secp256k1::new(), &key); } @@ -6700,13 +6734,11 @@ pub fn test_counterparty_raa_skip_no_crash() { channel_id, per_commitment_secret, next_per_commitment_point, - #[cfg(taproot)] - next_local_nonce: None, release_htlc_message_paths: Vec::new(), }; nodes[1].node.handle_revoke_and_ack(node_a_id, &raa); assert_eq!( - check_closed_broadcast!(nodes[1], true).unwrap().data, + check_closed_broadcast(&nodes[1], 1, true).pop().unwrap().data, "Received an unexpected revoke_and_ack" ); check_added_monitors(&nodes[1], 1); @@ -6748,7 +6780,7 @@ pub fn test_bump_txn_sanitize_tracking_maps() { assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 0); mine_transaction(&nodes[0], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 1000000); @@ -6905,22 +6937,22 @@ pub fn test_channel_update_has_correct_htlc_maximum_msat() { config_30_percent.channel_handshake_config.announce_for_forwarding = true; config_30_percent .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel = 30; + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 30; let mut config_50_percent = UserConfig::default(); config_50_percent.channel_handshake_config.announce_for_forwarding = true; config_50_percent .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel = 50; + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 50; let mut config_95_percent = UserConfig::default(); config_95_percent.channel_handshake_config.announce_for_forwarding = true; config_95_percent .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel = 95; + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 95; let mut config_100_percent = UserConfig::default(); config_100_percent.channel_handshake_config.announce_for_forwarding = true; config_100_percent .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel = 100; + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 100; let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); @@ -7007,13 +7039,12 @@ pub fn test_onion_value_mpp_set_calculation() { // Send payment let id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); let onion_session_privs = nodes[0].node.test_add_new_pending_payment(hash, onion.clone(), id, &route).unwrap(); - let amt = Some(total_msat); nodes[0] .node - .test_send_payment_internal(&route, hash, onion, None, id, amt, onion_session_privs) + .test_send_payment_internal(&route, hash, onion, None, id, onion_session_privs) .unwrap(); check_added_monitors(&nodes[0], expected_paths.len()); @@ -7040,10 +7071,9 @@ pub fn test_onion_value_mpp_set_calculation() { &route.paths[0], &session_priv, ); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (mut onion_payloads, _, _) = onion_utils::build_onion_payloads( + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, 100_000); + let (mut onion_payloads, _, _) = onion_utils::test_build_onion_payloads( &route.paths[0], - 100_000, &recipient_onion_fields, height + 1, &None, @@ -7145,14 +7175,13 @@ fn do_test_overshoot_mpp(msat_amounts: &[u64], total_msat: u64) { // Send payment with manually set total_msat let id = PaymentId(nodes[src_idx].keys_manager.backing.get_secure_random_bytes()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); let onion_session_privs = nodes[src_idx].node.test_add_new_pending_payment(hash, onion, id, &route).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); - let amt = Some(total_msat); + let onion = RecipientOnionFields::secret_only(payment_secret, total_msat); nodes[src_idx] .node - .test_send_payment_internal(&route, hash, onion, None, id, amt, onion_session_privs) + .test_send_payment_internal(&route, hash, onion, None, id, onion_session_privs) .unwrap(); check_added_monitors(&nodes[src_idx], expected_paths.len()); @@ -7205,7 +7234,7 @@ pub fn test_simple_mpp() { let chan_4_id = create_announced_chan_between_nodes(&nodes, 2, 3).0.contents.short_channel_id; let (mut route, payment_hash, payment_preimage, payment_secret) = - get_route_and_payment_hash!(&nodes[0], nodes[3], 100000); + get_route_and_payment_hash!(&nodes[0], nodes[3], 100_000); let path = route.paths[0].clone(); route.paths.push(path); route.paths[0].hops[0].pubkey = node_b_id; @@ -7214,8 +7243,52 @@ pub fn test_simple_mpp() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.final_value_msat = 200_000; let paths: &[&[_]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; - send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret); + let payment_id = send_along_route_with_secret( + &nodes[0], + route, + paths, + 200_000, + payment_hash, + payment_secret, + ); + + let locally_originated = nodes[0] + .node + .list_channels() + .into_iter() + .flat_map(|channel| channel.pending_outbound_htlcs) + .collect::<Vec<_>>(); + assert_eq!(locally_originated.len(), 2); + assert!(locally_originated + .iter() + .all(|details| { details.source == Some(OutboundHTLCSource::Local { payment_id }) })); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_d_id = nodes[3].node.get_our_node_id(); + let mut inbound_references = Vec::new(); + for forwarder in [&nodes[1], &nodes[2]] { + let channels = forwarder.node.list_channels(); + let inbound_channel = + channels.iter().find(|details| details.counterparty.node_id == node_a_id).unwrap(); + let outbound_channel = + channels.iter().find(|details| details.counterparty.node_id == node_d_id).unwrap(); + assert_eq!(inbound_channel.pending_inbound_htlcs.len(), 1); + assert_eq!(outbound_channel.pending_outbound_htlcs.len(), 1); + + let outbound_htlc = &outbound_channel.pending_outbound_htlcs[0]; + assert_eq!(outbound_htlc.payment_hash, payment_hash); + let inbound_reference = match &outbound_htlc.source { + Some(OutboundHTLCSource::Forwarded { inbound_htlc }) => inbound_htlc, + _ => panic!("Unexpected outbound HTLC source"), + }; + assert_eq!(inbound_reference.channel_id, inbound_channel.channel_id); + assert_eq!(inbound_reference.htlc_id, inbound_channel.pending_inbound_htlcs[0].htlc_id); + inbound_references.push(inbound_reference.clone()); + } + assert_ne!(inbound_references[0], inbound_references[1]); + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], paths, payment_preimage)); } @@ -7232,10 +7305,10 @@ pub fn test_preimage_storage() { create_announced_chan_between_nodes(&nodes, 0, 1); { - let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(Some(100_000), 7200, None).unwrap(); + let (payment_hash, payment_secret, _) = + nodes[1].node.create_inbound_payment(Some(100_000), 7200, None, None).unwrap(); let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); @@ -7277,8 +7350,8 @@ pub fn test_bad_secret_hash() { let random_hash = PaymentHash([42; 32]); let random_secret = PaymentSecret([43; 32]); - let (our_payment_hash, our_payment_secret) = - nodes[1].node.create_inbound_payment(Some(100_000), 2, None).unwrap(); + let (our_payment_hash, our_payment_secret, _) = + nodes[1].node.create_inbound_payment(Some(100_000), 2, None, None).unwrap(); let (route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[1], 100_000); // All the below cases should end up being handled exactly identically, so we macro the @@ -7327,20 +7400,20 @@ pub fn test_bad_secret_hash() { let expected_err_data = [0, 0, 0, 0, 0, 1, 0x86, 0xa0, 0, 0, 0, CHAN_CONFIRM_DEPTH as u8]; // Send a payment with the right payment hash but the wrong payment secret - let onion = RecipientOnionFields::secret_only(random_secret); + let onion = RecipientOnionFields::secret_only(random_secret, 100_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); handle_unknown_invalid_payment_data!(our_payment_hash); expect_payment_failed!(nodes[0], our_payment_hash, true, expected_err_code, expected_err_data); // Send a payment with a random payment hash, but the right payment secret - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100_000); nodes[0].node.send_payment_with_route(route.clone(), random_hash, onion, id).unwrap(); handle_unknown_invalid_payment_data!(random_hash); expect_payment_failed!(nodes[0], random_hash, true, expected_err_code, expected_err_data); // Send a payment with a random payment hash and random payment secret - let onion = RecipientOnionFields::secret_only(random_secret); + let onion = RecipientOnionFields::secret_only(random_secret, 100_000); nodes[0].node.send_payment_with_route(route, random_hash, onion, id).unwrap(); handle_unknown_invalid_payment_data!(random_hash); expect_payment_failed!(nodes[0], random_hash, true, expected_err_code, expected_err_data); @@ -7382,7 +7455,7 @@ pub fn test_update_err_monitor_lockdown() { let new_monitor = { let monitor = nodes[0].chain_monitor.chain_monitor.get_monitor(chan_1.2).unwrap(); let new_monitor = - <(BlockHash, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( + <(BlockLocator, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( &mut io::Cursor::new(&monitor.encode()), (nodes[0].keys_manager, nodes[0].keys_manager), ) @@ -7490,7 +7563,7 @@ pub fn test_concurrent_monitor_claim() { let new_monitor = { let monitor = nodes[0].chain_monitor.chain_monitor.get_monitor(chan_1.2).unwrap(); let new_monitor = - <(BlockHash, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( + <(BlockLocator, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( &mut io::Cursor::new(&monitor.encode()), (nodes[0].keys_manager, nodes[0].keys_manager), ) @@ -7540,7 +7613,7 @@ pub fn test_concurrent_monitor_claim() { let new_monitor = { let monitor = nodes[0].chain_monitor.chain_monitor.get_monitor(chan_1.2).unwrap(); let new_monitor = - <(BlockHash, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( + <(BlockLocator, channelmonitor::ChannelMonitor<TestChannelSigner>)>::read( &mut io::Cursor::new(&monitor.encode()), (nodes[0].keys_manager, nodes[0].keys_manager), ) @@ -7569,7 +7642,7 @@ pub fn test_concurrent_monitor_claim() { // Route another payment to generate another update with still previous HTLC pending let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 3000000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 3000000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -7733,7 +7806,7 @@ pub fn test_htlc_no_detection() { &block, nodes[0].best_block_info().1 + 1, ); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -7821,7 +7894,7 @@ fn do_test_onchain_htlc_settlement_after_close( .node .force_close_broadcasting_latest_txn(&chan_ab.2, &counterparty_node_id, message.clone()) .unwrap(); - check_closed_broadcast!(nodes[force_closing_node], true); + check_closed_broadcast(&nodes[force_closing_node], 1, true); check_added_monitors(&nodes[force_closing_node], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[force_closing_node], 1, reason, &[counterparty_node_id], 100000); @@ -7836,7 +7909,7 @@ fn do_test_onchain_htlc_settlement_after_close( &create_dummy_block(nodes[1].best_block_hash(), 42, vec![txn_to_broadcast[0].clone()]), ); if broadcast_alice { - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -7925,7 +7998,7 @@ fn do_test_onchain_htlc_settlement_after_close( ); // If Bob was the one to force-close, he will have already passed these checks earlier. if broadcast_alice { - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -8120,7 +8193,7 @@ pub fn test_error_chans_closed() { &msgs::ErrorMessage { channel_id: chan_2.2, data: "ERR".to_owned() }, ); check_added_monitors(&nodes[0], 1); - check_closed_broadcast!(nodes[0], false); + check_closed_broadcast(&nodes[0], 1, false); let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString("ERR".to_string()) }; @@ -8318,10 +8391,10 @@ fn do_test_dup_htlc_second_rejected(test_for_second_fail_panic: bool) { let route = get_route!(nodes[0], payment_params, 10_000).unwrap(); let (our_payment_preimage, our_payment_hash, our_payment_secret) = - get_payment_preimage_hash!(&nodes[1]); + get_payment_preimage_hash(&nodes[1], None, None); { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -8337,7 +8410,7 @@ fn do_test_dup_htlc_second_rejected(test_for_second_fail_panic: bool) { { // Note that we use a different PaymentId here to allow us to duplicativly pay - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_secret.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -8440,7 +8513,12 @@ pub fn test_inconsistent_mpp_params() { // such HTLC and allow the second to stay. let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; + let configs: [Option<UserConfig>; 4] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let nodes = create_network(4, &node_cfgs, &node_chanmgrs); let node_a_id = nodes[0].node.get_our_node_id(); @@ -8467,7 +8545,7 @@ pub fn test_inconsistent_mpp_params() { } }); - let (preimage, hash, payment_secret) = get_payment_preimage_hash!(&nodes[3]); + let (preimage, hash, payment_secret) = get_payment_preimage_hash(&nodes[3], None, None); let cur_height = nodes[0].best_block_info().1; let id = PaymentId([42; 32]); @@ -8477,16 +8555,16 @@ pub fn test_inconsistent_mpp_params() { // ultimately have, just not right away. let mut dup_route = route.clone(); dup_route.paths.push(route.paths[1].clone()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 15_000_000); nodes[0].node.test_add_new_pending_payment(hash, onion, id, &dup_route).unwrap() }; - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 15_000_000); let path_a = &route.paths[0]; let real_amt = 15_000_000; let priv_a = session_privs[0]; nodes[0] .node - .test_send_payment_along_path(path_a, &hash, onion, real_amt, cur_height, id, &None, priv_a) + .test_send_payment_along_path(path_a, &hash, onion, cur_height, id, &None, priv_a) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -8498,12 +8576,11 @@ pub fn test_inconsistent_mpp_params() { assert!(nodes[3].node.get_and_clear_pending_events().is_empty()); let path_b = &route.paths[1]; - let onion = RecipientOnionFields::secret_only(payment_secret); - let amt_b = 14_000_000; + let onion = RecipientOnionFields::secret_only(payment_secret, 14_000_000); let priv_b = session_privs[1]; nodes[0] .node - .test_send_payment_along_path(path_b, &hash, onion, amt_b, cur_height, id, &None, priv_b) + .test_send_payment_along_path(path_b, &hash, onion, cur_height, id, &None, priv_b) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -8558,12 +8635,12 @@ pub fn test_inconsistent_mpp_params() { let conditions = PaymentFailedConditions::new().mpp_parts_remain(); expect_payment_failed_conditions(&nodes[0], hash, true, conditions); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, real_amt); let path_b = &route.paths[1]; let priv_c = session_privs[2]; nodes[0] .node - .test_send_payment_along_path(path_b, &hash, onion, real_amt, cur_height, id, &None, priv_c) + .test_send_payment_along_path(path_b, &hash, onion, cur_height, id, &None, priv_c) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -8574,7 +8651,7 @@ pub fn test_inconsistent_mpp_params() { pass_along_path(&nodes[0], path_b, real_amt, hash, Some(payment_secret), event, true, None); do_claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path_a, path_b], preimage)); - expect_payment_sent(&nodes[0], preimage, Some(None), true, true); + expect_payment_sent(&nodes[0], preimage, Some(Some(2000)), true, true); } #[xtest(feature = "_externalize_tests")] @@ -8585,7 +8662,12 @@ pub fn test_double_partial_claim() { // amount. let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; + let configs: [Option<UserConfig>; 4] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let nodes = create_network(4, &node_cfgs, &node_chanmgrs); let node_b_id = nodes[1].node.get_our_node_id(); @@ -8626,7 +8708,7 @@ pub fn test_double_partial_claim() { pass_failed_payment_back(&nodes[0], paths, false, hash, reason); // nodes[1] now retries one of the two paths... - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 15_000_000); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route, hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 2); @@ -8858,12 +8940,18 @@ fn do_test_max_dust_htlc_exposure( }; // With default dust exposure: 5000 sats if on_holder_tx { - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only( + payment_secret, + dust_outbound_htlc_on_holder_tx_msat, + ); let id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); } else { - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only( + payment_secret, + dust_htlc_on_counterparty_tx_msat + 1, + ); let id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -8877,7 +8965,7 @@ fn do_test_max_dust_htlc_exposure( let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], amount_msats); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amount_msats); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -8916,7 +9004,7 @@ fn do_test_max_dust_htlc_exposure( // to cross the threshold. for _ in 0..AT_FEE_OUTBOUND_HTLCS { let (_, hash, payment_secret) = get_payment_preimage_hash(&nodes[1], Some(1_000), None); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route.clone(), hash, onion, id).unwrap(); } @@ -9066,7 +9154,8 @@ pub fn test_nondust_htlc_excess_fees_are_dust() { config.channel_handshake_limits.min_max_accepted_htlcs = chan_utils::max_htlcs(&chan_ty); config.channel_handshake_config.our_max_accepted_htlcs = chan_utils::max_htlcs(&chan_ty); config.channel_handshake_config.our_htlc_minimum_msat = 1; - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let node_chanmgrs = create_node_chanmgrs( 3, @@ -9146,7 +9235,7 @@ pub fn test_nondust_htlc_excess_fees_are_dust() { // Send an additional non-dust htlc from 1 to 0, and check the complaint let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], dust_limit * 2); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -9182,7 +9271,7 @@ pub fn test_nondust_htlc_excess_fees_are_dust() { assert_eq!(nodes[1].node.list_channels()[0].pending_outbound_htlcs.len(), 0); // Send an additional non-dust htlc from 0 to 1 using the pre-calculated route above, and check the immediate complaint - let onion = RecipientOnionFields::secret_only(payment_secret_0_1); + let onion = RecipientOnionFields::secret_only(payment_secret_0_1, route_0_1.get_total_amount()); let id = PaymentId(payment_hash_0_1.0); let res = nodes[0].node.send_payment_with_route(route_0_1, payment_hash_0_1, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -9200,7 +9289,7 @@ pub fn test_nondust_htlc_excess_fees_are_dust() { create_announced_chan_between_nodes(&nodes, 2, 0); let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[2], nodes[1], dust_limit * 2); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); nodes[2].node.send_payment_with_route(route, payment_hash, onion, PaymentId([0; 32])).unwrap(); check_added_monitors(&nodes[2], 1); let send = SendEvent::from_node(&nodes[2]); @@ -9321,7 +9410,7 @@ fn do_test_nondust_htlc_fees_dust_exposure_delta(features: ChannelTypeFeatures) // Send an additional non-dust htlc from 0 to 1, and check the complaint let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], NON_DUST_HTLC_MSAT); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, NON_DUST_HTLC_MSAT); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -9403,7 +9492,7 @@ fn do_test_nondust_htlc_fees_dust_exposure_delta(features: ChannelTypeFeatures) nodes[1].node.update_partial_channel_config(&node_a_id, &[chan_id], &update).unwrap(); // Send an additional non-dust htlc from 1 to 0 using the pre-calculated route above, and check the immediate complaint - let onion = RecipientOnionFields::secret_only(payment_secret_1_0); + let onion = RecipientOnionFields::secret_only(payment_secret_1_0, NON_DUST_HTLC_MSAT); let id = PaymentId(payment_hash_1_0.0); let res = nodes[1].node.send_payment_with_route(route_1_0, payment_hash_1_0, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -9476,17 +9565,24 @@ fn do_payment_with_custom_min_final_cltv_expiry(valid_delta: bool, use_user_hash PaymentParameters::from_node_id(node_b_id, final_cltv_expiry_delta as u32); let (hash, payment_preimage, payment_secret) = if use_user_hash { let (payment_preimage, hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_value), Some(min_cltv_expiry_delta)); + get_payment_preimage_hash(&nodes[1], Some(recv_value), Some(min_cltv_expiry_delta)); (hash, payment_preimage, payment_secret) } else { - let (hash, payment_secret) = nodes[1] + let (hash, payment_secret, _) = nodes[1] .node - .create_inbound_payment(Some(recv_value), 7200, Some(min_cltv_expiry_delta)) + .create_inbound_payment(Some(recv_value), 7200, Some(min_cltv_expiry_delta), None) .unwrap(); - (hash, nodes[1].node.get_payment_preimage(hash, payment_secret).unwrap(), payment_secret) + ( + hash, + nodes[1] + .node + .get_payment_preimage_decrypt_metadata(hash, payment_secret, None) + .unwrap(), + payment_secret, + ) }; let route = get_route!(nodes[0], payment_parameters, recv_value).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, recv_value); nodes[0].node.send_payment_with_route(route, hash, onion, PaymentId(hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); @@ -9960,7 +10056,7 @@ fn do_test_multi_post_event_actions(do_reload: bool) { let (route, payment_hash_3, _, payment_secret_3) = get_route_and_payment_hash!(nodes[1], nodes[0], 100_000); let payment_id = PaymentId(payment_hash_3.0); - let onion = RecipientOnionFields::secret_only(payment_secret_3); + let onion = RecipientOnionFields::secret_only(payment_secret_3, 100_000); nodes[1].node.send_payment_with_route(route, payment_hash_3, onion, payment_id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -10038,7 +10134,8 @@ pub fn test_dust_exposure_holding_cell_assertion() { // Use a fixed dust exposure limit to make the test simpler const DUST_HTLC_VALUE_MSAT: u64 = 500_000; config.channel_config.max_dust_htlc_exposure = MaxDustHTLCExposure::FixedLimitMsat(5_000_000); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let configs = [Some(config.clone()), Some(config.clone()), Some(config.clone())]; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &configs); @@ -10071,7 +10168,7 @@ pub fn test_dust_exposure_holding_cell_assertion() { // messages (leaving B waiting on C's RAA) the next HTLC will go into B's holding cell. let (route_bc, payment_hash_bc, _payment_preimage_bc, payment_secret_bc) = get_route_and_payment_hash!(nodes[1], nodes[2], DUST_HTLC_VALUE_MSAT); - let onion_bc = RecipientOnionFields::secret_only(payment_secret_bc); + let onion_bc = RecipientOnionFields::secret_only(payment_secret_bc, DUST_HTLC_VALUE_MSAT); let id = PaymentId(payment_hash_bc.0); nodes[1].node.send_payment_with_route(route_bc, payment_hash_bc, onion_bc, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -10091,7 +10188,7 @@ pub fn test_dust_exposure_holding_cell_assertion() { .unwrap(); let (route_ac, payment_hash_cell, _, payment_secret_ac) = get_route_and_payment_hash!(nodes[0], nodes[2], payment_params_ac, DUST_HTLC_VALUE_MSAT); - let onion_ac = RecipientOnionFields::secret_only(payment_secret_ac); + let onion_ac = RecipientOnionFields::secret_only(payment_secret_ac, DUST_HTLC_VALUE_MSAT); let id = PaymentId(payment_hash_cell.0); nodes[0].node.send_payment_with_route(route_ac, payment_hash_cell, onion_ac, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -10114,7 +10211,7 @@ pub fn test_dust_exposure_holding_cell_assertion() { // its holding cell as it would be over-exposed to dust. let (route_cb, payment_hash_cb, payment_preimage_cb, payment_secret_cb) = get_route_and_payment_hash!(nodes[2], nodes[1], DUST_HTLC_VALUE_MSAT); - let onion_cb = RecipientOnionFields::secret_only(payment_secret_cb); + let onion_cb = RecipientOnionFields::secret_only(payment_secret_cb, DUST_HTLC_VALUE_MSAT); let id = PaymentId(payment_hash_cb.0); nodes[2].node.send_payment_with_route(route_cb, payment_hash_cb, onion_cb, id).unwrap(); check_added_monitors(&nodes[2], 1); @@ -10167,3 +10264,69 @@ pub fn test_dust_exposure_holding_cell_assertion() { // Now that everything has settled, make sure the channels still work with a simple claim. claim_payment(&nodes[2], &[&nodes[1]], payment_preimage_cb); } + +#[test] +fn test_dup_htlc_claim_onchain_and_offchain() { + // Tests what happens if we receive a claim first offchain, then see a counterparty broadcast + // their commitment transaction and re-claim the same HTLC on-chain. This was never broken, but + // the very specific ordering in this test did hit a debug assertion failure. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let legacy_cfg = test_legacy_channel_config(); + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(legacy_cfg.clone()), Some(legacy_cfg.clone()), Some(legacy_cfg)], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + + create_announced_chan_between_nodes(&nodes, 0, 1); + let chan_bc = create_announced_chan_between_nodes(&nodes, 1, 2); + + // Route payment A -> B -> C. + let (payment_preimage, payment_hash, _, _) = + route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 1_000_000); + + // C claims the payment. + nodes[2].node.claim_funds(payment_preimage); + expect_payment_claimed!(nodes[2], payment_hash, 1_000_000); + check_added_monitors(&nodes[2], 1); + + // Deliver only C's update_fulfill_htlc to B (NOT the commitment_signed). B learns + // the preimage and claims from A (adding an RAA blocker on B-C via + // internal_update_fulfill_htlc, then removing it when the A-B monitor update completes + // and the EmitEventOptionAndFreeOtherChannel action runs). + let cs_updates = get_htlc_update_msgs(&nodes[2], &node_b_id); + nodes[1].node.handle_update_fulfill_htlc(node_c_id, cs_updates.update_fulfill_htlcs[0].clone()); + check_added_monitors(&nodes[1], 1); + + // Ignore B's attempts to claim the HTLC from A. + nodes[1].node.get_and_clear_pending_msg_events(); + + // Get C's commitment transactions. C's commitment includes the HTLC and C has + // an HTLC-success transaction (claiming with preimage). Mine both on B. + let cs_txn = get_local_commitment_txn!(nodes[2], chan_bc.2); + assert!(cs_txn.len() >= 2, "Expected commitment + HTLC-success tx, got {}", cs_txn.len()); + + // Mine C's commitment on B. B sees the counterparty commitment on-chain. + mine_transaction(&nodes[1], &cs_txn[0]); + check_closed_broadcast(&nodes[1], 1, true); + check_added_monitors(&nodes[1], 1); + let events = nodes[1].node.get_and_clear_pending_events(); + assert!( + events.iter().any(|e| matches!(e, Event::ChannelClosed { .. })), + "Expected ChannelClosed event" + ); + + // Mine C's HTLC-success transaction. B's monitor sees the preimage being used on-chain + // and generates an HTLCEvent with the preimage. + mine_transaction(&nodes[1], &cs_txn[1]); + + // Advance past ANTI_REORG_DELAY so the on-chain HTLC resolution matures. This triggers + // the monitor to generate an HTLCEvent with the preimage via process_pending_monitor_events, + // which calls claim_funds_internal a second time. + connect_blocks(&nodes[1], ANTI_REORG_DELAY); +} diff --git a/lightning/src/ln/funding.rs b/lightning/src/ln/funding.rs index 8092a0e4451..bcc5c665a86 100644 --- a/lightning/src/ln/funding.rs +++ b/lightning/src/ln/funding.rs @@ -9,243 +9,3841 @@ //! Types pertaining to funding channels. -use alloc::vec::Vec; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Amount, FeeRate, OutPoint, ScriptBuf, SignedAmount, TxOut, WScriptHash, Weight}; -use bitcoin::{Amount, ScriptBuf, SignedAmount, TxOut}; -use bitcoin::{Script, Sequence, Transaction, Weight}; +use crate::ln::chan_utils::{ + make_funding_redeemscript, BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, + FUNDING_TRANSACTION_WITNESS_WEIGHT, +}; +use crate::ln::interactivetxs::{get_output_weight, TX_COMMON_FIELDS_WEIGHT}; +use crate::ln::msgs; +use crate::ln::types::ChannelId; +use crate::ln::LN_MAX_MSG_LEN; +use crate::prelude::*; +use crate::util::native_async::MaybeSend; +use crate::util::wallet_utils::{ + CoinSelection, CoinSelectionSource, CoinSelectionSourceSync, ConfirmedUtxo, Input, +}; -use crate::events::bump_transaction::Utxo; -use crate::ln::chan_utils::EMPTY_SCRIPT_SIG_WEIGHT; -use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; +/// Error returned when a [`FundingContribution`] cannot be adjusted to a target feerate. +/// +/// This is used when re-estimating an already-built contribution at a different feerate than the +/// one used during coin selection. That includes, for example, acceptor-side adjustment to the +/// initiator's chosen feerate during splice tie-break resolution, as well as initiator-side +/// adjustment to a minimum RBF feerate for later attempts. +/// +/// Callers decide how to handle the failure. Depending on the context, they may drop the +/// contribution, wait and retry later, or abort the splice negotiation. +/// +/// See [`ChannelManager::splice_channel`] for further details. +/// +/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel +#[derive(Debug)] +pub(super) enum FeeRateAdjustmentError { + /// The counterparty's proposed feerate is below `min_feerate`, which was used as the feerate + /// during coin selection. We'll retry via RBF at our preferred feerate. + FeeRateTooLow { target_feerate: FeeRate, min_feerate: FeeRate }, + /// The counterparty's proposed feerate is above `max_feerate` and the re-estimated fee for + /// our contributed inputs and outputs exceeds the original fee estimate (computed at + /// `min_feerate` assuming initiator responsibility). If the re-estimated fee were within the + /// original estimate, a feerate above `max_feerate` would be tolerable since the acceptor + /// doesn't pay for common fields or the shared input/output. + FeeRateTooHigh { + target_feerate: FeeRate, + max_feerate: FeeRate, + target_fee: Amount, + original_fee: Amount, + }, + /// Arithmetic overflow when computing the fee buffer. + FeeBufferOverflow, + /// The re-estimated fee exceeds the available fee buffer regardless of `max_feerate`. The fee + /// buffer is the maximum fee that can be accommodated: + /// - **input-backed contributions**: the original fee plus any change output value + /// - **input-less contributions**: the channel balance minus the withdrawal outputs + FeeBufferInsufficient { source: &'static str, available: Amount, required: Amount }, +} + +impl core::fmt::Display for FeeRateAdjustmentError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FeeRateAdjustmentError::FeeRateTooLow { target_feerate, min_feerate } => { + write!( + f, + "Target feerate {} is below our minimum {}; \ + proceeding without contribution, will RBF later", + target_feerate, min_feerate, + ) + }, + FeeRateAdjustmentError::FeeRateTooHigh { + target_feerate, + max_feerate, + target_fee, + original_fee, + } => { + write!( + f, + "Target feerate {} exceeds our maximum {} and target fee {} exceeds original fee estimate {}", + target_feerate, max_feerate, target_fee, original_fee, + ) + }, + FeeRateAdjustmentError::FeeBufferOverflow => { + write!( + f, + "Arithmetic overflow when computing available fee buffer; \ + proceeding without contribution", + ) + }, + FeeRateAdjustmentError::FeeBufferInsufficient { source, available, required } => { + write!( + f, + "Fee buffer {} ({}) is insufficient for required fee {}; \ + proceeding without contribution", + available, source, required, + ) + }, + } + } +} -/// The components of a splice's funding transaction that are contributed by one party. -#[derive(Debug, Clone)] -pub struct SpliceContribution { - /// The amount from [`inputs`] to contribute to the splice. +/// Error returned when building a [`FundingContribution`] from a [`FundingTemplate`]. +#[derive(Debug)] +pub enum FundingContributionError { + /// The feerate exceeds the maximum allowed feerate. + FeeRateExceedsMaximum { + /// The requested feerate. + feerate: FeeRate, + /// The maximum allowed feerate. + max_feerate: FeeRate, + }, + /// The feerate is below the minimum RBF feerate. /// - /// [`inputs`]: Self::inputs - value_added: Amount, + /// Note: [`FundingTemplate::min_rbf_feerate`] may be derived from an in-progress + /// negotiation that later aborts, leaving a stale (higher than necessary) minimum. If + /// this error occurs after receiving [`Event::SpliceNegotiationFailed`], call + /// [`ChannelManager::splice_channel`] again to get a fresh template. + /// + /// [`Event::SpliceNegotiationFailed`]: crate::events::Event::SpliceNegotiationFailed + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + FeeRateBelowRbfMinimum { + /// The requested feerate. + feerate: FeeRate, + /// The minimum RBF feerate. + min_rbf_feerate: FeeRate, + }, + /// The splice value is invalid (zero, empty outputs, duplicate inputs or outputs, exceeds the + /// maximum money supply, or splices out more than the available channel balance). + InvalidSpliceValue, + /// An input's `prevtx` is too large to fit in a `tx_add_input` message. + PrevTxTooLarge, + /// Coin selection failed to find suitable inputs. + CoinSelectionFailed, + /// Coin selection is required but no coin selection source was provided. + /// + /// This can also be returned when reusing a prior contribution would otherwise satisfy the + /// request, but that prior contribution cannot be adjusted in-place to the requested feerate. + /// For example, an input-backed prior contribution may no longer have enough fee buffer in its + /// change output to absorb the higher fee. In that case, providing a coin selection source lets + /// the builder fall back to fresh coin selection, which may replace the prior input set instead + /// of preserving it. + MissingCoinSelectionSource, + /// The request cannot be satisfied using the manually selected inputs. + ManuallySelectedInputsInsufficient, + /// This template cannot build an RBF contribution. + NotRbfScenario, +} + +impl core::fmt::Display for FundingContributionError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FundingContributionError::FeeRateExceedsMaximum { feerate, max_feerate } => { + write!(f, "Feerate {} exceeds maximum {}", feerate, max_feerate) + }, + FundingContributionError::FeeRateBelowRbfMinimum { feerate, min_rbf_feerate } => { + write!(f, "Feerate {} is below minimum RBF feerate {}", feerate, min_rbf_feerate) + }, + FundingContributionError::InvalidSpliceValue => { + write!( + f, + "Invalid splice value (zero, empty, duplicate, exceeds limit, or overdraws balance)" + ) + }, + FundingContributionError::PrevTxTooLarge => { + write!(f, "Input prevtx is too large to fit in a tx_add_input message") + }, + FundingContributionError::CoinSelectionFailed => { + write!(f, "Coin selection failed to find suitable inputs") + }, + FundingContributionError::MissingCoinSelectionSource => { + write!(f, "Coin selection source required to build this contribution") + }, + FundingContributionError::ManuallySelectedInputsInsufficient => { + write!(f, "The request cannot be satisfied using the manually selected inputs") + }, + FundingContributionError::NotRbfScenario => { + write!(f, "This template cannot build an RBF contribution") + }, + } + } +} - /// The inputs included in the splice's funding transaction to meet the contributed amount - /// plus fees. Any excess amount will be sent to a change output. - inputs: Vec<FundingTxInput>, +/// A template for contributing to a channel's splice funding transaction. +/// +/// This is returned from [`ChannelManager::splice_channel`] when a channel is ready to be +/// spliced. A [`FundingContribution`] must be obtained from it and passed to +/// [`ChannelManager::funding_contributed`] in order to resume the splicing process. +/// +/// # Building a Contribution +/// +/// For a fresh splice (no pending splice to replace), either use the convenience methods +/// [`FundingTemplate::splice_in_sync`] and [`FundingTemplate::splice_out`] or start with +/// [`FundingTemplate::without_prior_contribution`] to compose a request manually. +/// +/// The builder API supports adding value, adding withdrawal outputs, or both. Attach a wallet +/// when the request may need new wallet inputs; pure splice-out requests can be built without one +/// and pay fees from the channel balance. +/// +/// # Replace By Fee (RBF) +/// +/// When a pending splice exists that hasn't been locked yet, use +/// [`FundingTemplate::rbf_prior_contribution_sync`] (or +/// [`FundingTemplate::rbf_prior_contribution`] for async) to retry the stored prior contribution +/// at an RBF-compatible feerate. To amend that prior request before building, start from +/// [`FundingTemplate::with_prior_contribution`] instead. +/// +/// Check [`FundingTemplate::min_rbf_feerate`] for the minimum feerate required (the greater of +/// the previous feerate + 25 sat/kwu and the spec's 25/24 rule). Use +/// [`FundingTemplate::prior_contribution`] to inspect the stored contribution before deciding +/// whether to reuse it or replace it with a fresh request via +/// [`FundingTemplate::without_prior_contribution`]. +/// +/// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel +/// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FundingTemplate { + /// The shared input, which, if present indicates the funding template is for a splice funding + /// transaction. + shared_input: Option<Input>, - /// The outputs to include in the splice's funding transaction. The total value of all - /// outputs plus fees will be the amount that is removed. - outputs: Vec<TxOut>, + /// The minimum RBF feerate (the greater of previous feerate + 25 sat/kwu and the spec's + /// 25/24 rule), if this template is for an RBF attempt. `None` for fresh splices with no + /// pending splice candidates. + min_rbf_feerate: Option<FeeRate>, + + /// The user's prior contribution from a previous splice negotiation on this channel. + prior_contribution: Option<FundingContribution>, - /// An optional change output script. This will be used if needed or, when not set, - /// generated using [`SignerProvider::get_destination_script`]. + /// The portion of the user's balance that can be spliced out. /// - /// [`SignerProvider::get_destination_script`]: crate::sign::SignerProvider::get_destination_script - change_script: Option<ScriptBuf>, + /// This value is captured at [`ChannelManager::splice_channel`] time and may become stale + /// if balances change before the contribution is used. Staleness is acceptable here because + /// this is only used as an optimization to determine if the prior contribution can be + /// reused with adjusted fees — the contribution is re-validated at + /// [`ChannelManager::funding_contributed`] time and again at quiescence time against the + /// current balances. + /// + /// [`ChannelManager::splice_channel`]: crate::ln::channelmanager::ChannelManager::splice_channel + /// [`ChannelManager::funding_contributed`]: crate::ln::channelmanager::ChannelManager::funding_contributed + spliceable_balance: Amount, } -impl SpliceContribution { - /// Creates a contribution for when funds are only added to a channel. - pub fn splice_in( - value_added: Amount, inputs: Vec<FundingTxInput>, change_script: Option<ScriptBuf>, +impl FundingTemplate { + /// Constructs a [`FundingTemplate`] for a splice using the provided shared input. + pub(super) fn new( + shared_input: Option<Input>, min_rbf_feerate: Option<FeeRate>, + prior_contribution: Option<FundingContribution>, spliceable_balance: Amount, ) -> Self { - Self { value_added, inputs, outputs: vec![], change_script } + Self { shared_input, min_rbf_feerate, prior_contribution, spliceable_balance } } - /// Creates a contribution for when funds are only removed from a channel. - pub fn splice_out(outputs: Vec<TxOut>) -> Self { - Self { value_added: Amount::ZERO, inputs: vec![], outputs, change_script: None } + /// Returns the minimum RBF feerate, if this template is for an RBF attempt. + /// + /// When set, the `min_feerate` passed to the splice/builder methods must be at least this + /// value. + pub fn min_rbf_feerate(&self) -> Option<FeeRate> { + self.min_rbf_feerate } - /// Creates a contribution for when funds are both added to and removed from a channel. + /// Returns a reference to the prior contribution from a previous splice negotiation, if + /// available. /// - /// Note that `value_added` represents the value added by `inputs` but should not account for - /// value removed by `outputs`. The net value contributed can be obtained by calling - /// [`SpliceContribution::net_value`]. - pub fn splice_in_and_out( - value_added: Amount, inputs: Vec<FundingTxInput>, outputs: Vec<TxOut>, - change_script: Option<ScriptBuf>, - ) -> Self { - Self { value_added, inputs, outputs, change_script } + /// Use this to inspect the prior contribution's current parameters (for example, + /// [`FundingContribution::outputs`], [`FundingContribution::change_output`], and + /// [`FundingContribution::net_value`]) before deciding + /// whether to reuse it via [`FundingTemplate::rbf_prior_contribution`] or build a fresh + /// contribution with different parameters using + /// [`FundingTemplate::without_prior_contribution`]. + /// + /// Note: the returned contribution may reflect a different feerate than originally provided, + /// as it may have been adjusted for RBF or for the counterparty's feerate when acting as + /// the acceptor. This can change other parameters too; for example, the amount added to the + /// channel may increase if the change output was removed to cover a higher fee. + pub fn prior_contribution(&self) -> Option<&FundingContribution> { + self.prior_contribution.as_ref() } - /// The net value contributed to a channel by the splice. If negative, more value will be - /// spliced out than spliced in. - pub fn net_value(&self) -> SignedAmount { - let value_added = self.value_added.to_signed().unwrap_or(SignedAmount::MAX); - let value_removed = self - .outputs + /// Creates a [`FundingBuilder`] for constructing a contribution. + /// + /// If a prior contribution is available, the builder starts from it automatically and builder + /// mutations amend that prior request. Use [`FundingTemplate::without_prior_contribution`] to + /// start empty instead. + /// + /// `feerate` is the feerate used for fee estimation and, if wallet inputs are needed, coin + /// selection. When [`FundingTemplate::min_rbf_feerate`] is set, it must be at least that value. + /// `max_feerate` is the highest feerate we are willing to tolerate if we end up as the + /// acceptor, and must be at least `feerate`. + pub fn with_prior_contribution(self, feerate: FeeRate, max_feerate: FeeRate) -> FundingBuilder { + FundingBuilder::new(self, feerate, max_feerate) + } + + /// Creates a [`FundingBuilder`] for constructing a contribution without using any prior + /// contribution. + /// + /// `feerate` and `max_feerate` have the same meaning as in + /// [`FundingTemplate::with_prior_contribution`]. This is useful when an RBF template carries a + /// prior contribution but the caller wants to replace, rather than amend, that request. + pub fn without_prior_contribution( + mut self, feerate: FeeRate, max_feerate: FeeRate, + ) -> FundingBuilder { + self.prior_contribution.take(); + FundingBuilder::new(self, feerate, max_feerate) + } + + /// Creates a [`FundingContribution`] for adding funds to a channel. + /// + /// This is a convenience wrapper around [`FundingTemplate::with_prior_contribution`]. As a + /// result, if this template carries a prior contribution, `value_added` is added on top of the + /// amount that prior request was already adding to the channel instead of replacing it. Use + /// [`FundingTemplate::without_prior_contribution`] if you want to replace the prior request + /// instead. + /// + /// `value_added` is the amount of additional value to add to the channel. `min_feerate` is the + /// feerate used for fee estimation and, if needed, coin selection; when + /// [`FundingTemplate::min_rbf_feerate`] is set, it must be at least that value. `max_feerate` is + /// the highest feerate we are willing to tolerate if we end up as the acceptor, and must be at + /// least `min_feerate`. `wallet` is only consulted if the request cannot be satisfied by + /// reusing/amending the prior contribution. When this template carries a prior contribution, + /// increasing its value may therefore re-run coin selection and yield a different input set than + /// the prior contribution used. This is not supported when the prior contribution used manually + /// selected inputs; use [`FundingTemplate::splice_in_inputs`] or + /// [`FundingTemplate::without_prior_contribution`] in that case. + pub async fn splice_in<W: CoinSelectionSource + MaybeSend>( + self, value_added: Amount, min_feerate: FeeRate, max_feerate: FeeRate, wallet: W, + ) -> Result<FundingContribution, FundingContributionError> { + self.with_prior_contribution(min_feerate, max_feerate) + .with_coin_selection_source(wallet) + .add_value(value_added)? + .build() + .await + } + + /// Creates a [`FundingContribution`] for adding funds to a channel. + /// + /// This is the synchronous variant of [`FundingTemplate::splice_in`]; `value_added`, + /// `min_feerate`, `max_feerate`, and `wallet` have the same meaning, including the restriction + /// on prior contributions with manually selected inputs. + pub fn splice_in_sync<W: CoinSelectionSourceSync>( + self, value_added: Amount, min_feerate: FeeRate, max_feerate: FeeRate, wallet: W, + ) -> Result<FundingContribution, FundingContributionError> { + self.with_prior_contribution(min_feerate, max_feerate) + .with_coin_selection_source_sync(wallet) + .add_value(value_added)? + .build() + } + + /// Creates a [`FundingContribution`] for adding funds to a channel using manually selected + /// inputs. + /// + /// This is a convenience wrapper around [`FundingTemplate::with_prior_contribution`] with no + /// wallet attached. Each input is fully consumed with no change output, so the amount added to + /// the channel is derived from the total input value minus the estimated fee. + /// + /// When a prior contribution with manually selected inputs is present, `inputs` are appended to + /// the prior [`FundingContribution::inputs`] instead of replacing them. Use + /// [`FundingTemplate::without_prior_contribution`] if you want to replace the prior request + /// instead. If the template carries a coin-selected prior contribution, manual inputs are + /// incompatible and this method returns [`FundingContributionError::InvalidSpliceValue`]. + /// + /// `inputs` are the additional manually selected inputs to fully consume. `min_feerate` is the + /// feerate used for fee estimation and must be at least [`FundingTemplate::min_rbf_feerate`] + /// when that is set. `max_feerate` is the highest feerate we are willing to tolerate if we end + /// up as the acceptor, and must be at least `min_feerate`. + pub fn splice_in_inputs( + self, inputs: Vec<ConfirmedUtxo>, min_feerate: FeeRate, max_feerate: FeeRate, + ) -> Result<FundingContribution, FundingContributionError> { + self.with_prior_contribution(min_feerate, max_feerate).add_inputs(inputs)?.build() + } + + /// Creates a [`FundingContribution`] for removing funds from a channel. + /// + /// This is a convenience wrapper around [`FundingTemplate::with_prior_contribution`] with no + /// wallet attached. For a fresh splice, fees are paid from the channel balance, so this does + /// not perform coin selection or spend wallet inputs. When a prior contribution is present, + /// `outputs` are appended to the prior [`FundingContribution::outputs`] instead of replacing + /// them. Use [`FundingTemplate::without_prior_contribution`] if you want to replace the prior + /// outputs instead. + /// + /// `outputs` are the additional withdrawal outputs to include. `min_feerate` is the feerate + /// used for fee estimation and must be at least [`FundingTemplate::min_rbf_feerate`] when that + /// is set. `max_feerate` is the highest feerate we are willing to tolerate if we end up as the + /// acceptor, and must be at least `min_feerate`. + /// + /// If amending a prior contribution would require selecting new wallet inputs, this method + /// returns [`FundingContributionError::MissingCoinSelectionSource`]. This can happen, for + /// example, when the prior contribution was input-backed and its existing change output cannot + /// absorb the additional withdrawal outputs or the higher fee implied by `min_feerate`. In + /// that case, use the builder APIs with a coin selection source instead. + pub fn splice_out( + self, outputs: Vec<TxOut>, min_feerate: FeeRate, max_feerate: FeeRate, + ) -> Result<FundingContribution, FundingContributionError> { + self.with_prior_contribution(min_feerate, max_feerate).add_outputs(outputs).build() + } + + /// Creates a [`FundingContribution`] for an RBF (Replace-By-Fee) attempt on a pending splice. + /// + /// This requires [`FundingTemplate::prior_contribution`] to be available. `feerate` overrides + /// the template's minimum RBF feerate; passing `None` uses + /// [`FundingTemplate::min_rbf_feerate`]. `max_feerate` is the highest feerate we are willing to + /// tolerate if we end up as the acceptor, and must be at least the effective feerate. `wallet` + /// is only consulted if the prior contribution cannot be reused or adjusted directly. The + /// chosen `max_feerate` is stored on the returned contribution so that any later acceptor-side + /// fee adjustment for that contribution remains capped at the caller's chosen maximum, even if + /// this RBF attempt had to fall back to a fresh coin selection. + /// + /// This handles the prior contribution logic internally: + /// - If the prior contribution's feerate can be adjusted to the effective target feerate, the + /// adjusted contribution is returned directly. For splice-in, the change output absorbs + /// the fee difference. For splice-out (no wallet inputs), the holder's channel balance + /// covers the higher fees. + /// - If adjustment fails, coin selection is re-run using the prior contribution's + /// parameters and the caller's `max_feerate`. For prior contributions without inputs, + /// this changes the funding source: wallet inputs are selected to cover the outputs and + /// fees instead of deducting them from the channel balance. + /// - If no prior contribution exists, coin selection is run for a fee-bump-only contribution + /// (`value_added = 0`), covering fees for the common fields and shared input/output via + /// a newly selected input. Check [`FundingTemplate::prior_contribution`] to see if this + /// is intended. + /// + /// # Errors + /// + /// Returns a [`FundingContributionError`] if there is no reusable prior contribution, if no + /// effective RBF feerate is available, if the effective feerate violates the template's fee + /// constraints, or if coin selection fails. + pub async fn rbf_prior_contribution<W: CoinSelectionSource + MaybeSend>( + self, feerate: Option<FeeRate>, max_feerate: FeeRate, wallet: W, + ) -> Result<FundingContribution, FundingContributionError> { + if self.prior_contribution().is_none() { + return Err(FundingContributionError::NotRbfScenario); + } + let feerate = feerate + .or_else(|| self.min_rbf_feerate()) + .ok_or(FundingContributionError::NotRbfScenario)?; + self.with_prior_contribution(feerate, max_feerate) + .with_coin_selection_source(wallet) + .build() + .await + } + + /// Creates a [`FundingContribution`] for an RBF (Replace-By-Fee) attempt on a pending splice. + /// + /// This is the synchronous variant of [`FundingTemplate::rbf_prior_contribution`]; `feerate`, + /// `max_feerate`, and `wallet` have the same meaning. + pub fn rbf_prior_contribution_sync<W: CoinSelectionSourceSync>( + self, feerate: Option<FeeRate>, max_feerate: FeeRate, wallet: W, + ) -> Result<FundingContribution, FundingContributionError> { + if self.prior_contribution().is_none() { + return Err(FundingContributionError::NotRbfScenario); + } + let feerate = feerate + .or_else(|| self.min_rbf_feerate()) + .ok_or(FundingContributionError::NotRbfScenario)?; + + self.with_prior_contribution(feerate, max_feerate) + .with_coin_selection_source_sync(wallet) + .build() + } +} + +fn estimate_transaction_fee( + inputs: &[ConfirmedUtxo], outputs: &[TxOut], change_output: Option<&TxOut>, is_initiator: bool, + is_splice: bool, feerate: FeeRate, +) -> Amount { + let input_weight: u64 = inputs + .iter() + .map(|input| BASE_INPUT_WEIGHT.saturating_add(input.utxo.satisfaction_weight)) + .fold(0, |total_weight, input_weight| total_weight.saturating_add(input_weight)); + + let output_weight: u64 = outputs + .iter() + .chain(change_output.into_iter()) + .map(|txout| txout.weight().to_wu()) + .fold(0, |total_weight, output_weight| total_weight.saturating_add(output_weight)); + + let mut weight = input_weight.saturating_add(output_weight); + + // The initiator pays for all common fields and the shared output in the funding transaction. + if is_initiator { + weight = weight + .saturating_add(TX_COMMON_FIELDS_WEIGHT) + // The weight of the funding output, a P2WSH output + // NOTE: The witness script hash given here is irrelevant as it's a fixed size and we just want + // to calculate the contributed weight, so we use an all-zero hash. + // + // TODO(taproot): Needs to consider different weights based on channel type + .saturating_add( + get_output_weight(&ScriptBuf::new_p2wsh(&WScriptHash::from_raw_hash( + Hash::all_zeros(), + ))) + .to_wu(), + ); + + // The splice initiator pays for the input spending the previous funding output. + if is_splice { + weight = weight + .saturating_add(BASE_INPUT_WEIGHT) + .saturating_add(EMPTY_SCRIPT_SIG_WEIGHT) + .saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); + #[cfg(feature = "grind_signatures")] + { + // Guarantees a low R signature + weight -= 1; + } + } + } + + Weight::from_wu(weight) * feerate +} + +fn validate_inputs(inputs: &[ConfirmedUtxo]) -> Result<(), FundingContributionError> { + let mut total_value = Amount::ZERO; + for (idx, input) in inputs.iter().enumerate() { + if inputs[..idx] .iter() - .map(|txout| txout.value) - .sum::<Amount>() - .to_signed() - .unwrap_or(SignedAmount::MAX); + .any(|existing_input| existing_input.utxo.outpoint == input.utxo.outpoint) + { + return Err(FundingContributionError::InvalidSpliceValue); + } + + use crate::util::ser::Writeable; + const MESSAGE_TEMPLATE: msgs::TxAddInput = msgs::TxAddInput { + channel_id: ChannelId([0; 32]), + serial_id: 0, + prevtx: None, + prevtx_out: 0, + sequence: 0, + // Mutually exclusive with prevtx, which is accounted for below. + shared_input_txid: None, + }; + let message_len = MESSAGE_TEMPLATE.serialized_length() + input.prevtx.serialized_length(); + (message_len <= LN_MAX_MSG_LEN) + .then(|| ()) + .ok_or(FundingContributionError::PrevTxTooLarge)?; - value_added - value_removed + total_value = match total_value.checked_add(input.utxo.output.value) { + Some(sum) if sum <= Amount::MAX_MONEY => sum, + _ => return Err(FundingContributionError::InvalidSpliceValue), + }; } - pub(super) fn value_added(&self) -> Amount { - self.value_added + Ok(()) +} + +/// Describes how a contribution request should source its wallet-backed inputs. +#[derive(Debug, Clone, PartialEq, Eq)] +enum FundingInputs { + /// Reuses the contribution's existing inputs while targeting at least `value_added` added to + /// the channel after fees. If dropping the change output leaves surplus value, it remains in + /// the channel contribution. + CoinSelected { value_added: Amount }, + /// Replaces the contribution's inputs with the provided set and fully consumes them without a + /// change output. The amount added to the channel is recomputed from the input total minus fees, + /// while explicit withdrawal outputs still reduce the splice's net value. + ManuallySelected { inputs: Vec<ConfirmedUtxo> }, +} + +impl FundingInputs { + fn mode(&self) -> FundingInputMode { + match self { + FundingInputs::CoinSelected { .. } => FundingInputMode::CoinSelected, + FundingInputs::ManuallySelected { .. } => FundingInputMode::ManuallySelected, + } } - pub(super) fn inputs(&self) -> &[FundingTxInput] { - &self.inputs[..] + fn is_empty(&self) -> bool { + match self { + FundingInputs::CoinSelected { value_added } => *value_added == Amount::ZERO, + FundingInputs::ManuallySelected { inputs } => inputs.is_empty(), + } } - pub(super) fn outputs(&self) -> &[TxOut] { - &self.outputs[..] + fn value_added(&self) -> Amount { + match self { + FundingInputs::CoinSelected { value_added } => *value_added, + FundingInputs::ManuallySelected { .. } => Amount::ZERO, + } } - pub(super) fn into_tx_parts(self) -> (Vec<FundingTxInput>, Vec<TxOut>, Option<ScriptBuf>) { - let SpliceContribution { value_added: _, inputs, outputs, change_script } = self; - (inputs, outputs, change_script) + fn manually_selected_inputs(&self) -> &[ConfirmedUtxo] { + match self { + FundingInputs::ManuallySelected { inputs } => inputs, + FundingInputs::CoinSelected { .. } => &[], + } } } -/// An input to contribute to a channel's funding transaction either when using the v2 channel -/// establishment protocol or when splicing. -#[derive(Debug, Clone)] -pub struct FundingTxInput { - /// The unspent [`TxOut`] that the input spends. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +enum FundingInputMode { + CoinSelected, + ManuallySelected, +} + +impl_ser_tlv_based_enum!(FundingInputMode, + (1, CoinSelected) => {}, + (3, ManuallySelected) => {} +); + +/// The components of a funding transaction contributed by one party. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct FundingContribution { + /// The estimate fees responsible to be paid for the contribution. + estimated_fee: Amount, + + /// The inputs included in the funding transaction. /// - /// [`TxOut`]: bitcoin::TxOut - pub(super) utxo: Utxo, + /// For coin-selected contributions, excess value is returned via [`Self::change_output`]. For + /// manually selected inputs, the full input value is consumed and no change output is created. + inputs: Vec<ConfirmedUtxo>, - /// The sequence number to use in the [`TxIn`]. + /// The outputs to include in the funding transaction. /// - /// [`TxIn`]: bitcoin::TxIn - pub(super) sequence: Sequence, + /// When no wallet inputs are contributed, these outputs are paid from the channel balance. + /// Otherwise, they are paid by the contributed inputs. + outputs: Vec<TxOut>, + + /// The output where any change will be sent. + change_output: Option<TxOut>, - /// The transaction containing the unspent [`TxOut`] referenced by [`utxo`]. + /// The fee rate used to select `inputs` (the minimum feerate). + feerate: FeeRate, + + /// The maximum fee rate to accept as acceptor before rejecting the splice. + max_feerate: FeeRate, + + /// Whether the contribution is for funding a splice. + is_splice: bool, + + /// Whether this contribution currently uses coin-selected or manual-input semantics. /// - /// [`TxOut`]: bitcoin::TxOut - /// [`utxo`]: Self::utxo - pub(super) prevtx: Transaction, + /// This is `None` when the contribution has no inputs and is set accordingly based on the first + /// `add_value` or `add_input` call on the builder. + input_mode: Option<FundingInputMode>, } -impl_writeable_tlv_based!(FundingTxInput, { - (1, utxo, required), - (3, sequence, required), - (5, prevtx, required), +impl_ser_tlv_based!(FundingContribution, { + (1, estimated_fee, required), + (3, inputs, optional_vec), + (5, outputs, optional_vec), + (7, change_output, option), + (9, feerate, required), + (11, max_feerate, required), + (13, is_splice, required), + (15, input_mode, option), }); -impl FundingTxInput { - fn new<F: FnOnce(&bitcoin::Script) -> bool>( - prevtx: Transaction, vout: u32, witness_weight: Weight, script_filter: F, - ) -> Result<Self, ()> { - Ok(FundingTxInput { - utxo: Utxo { - outpoint: bitcoin::OutPoint { txid: prevtx.compute_txid(), vout }, - output: prevtx - .output - .get(vout as usize) - .filter(|output| script_filter(&output.script_pubkey)) - .ok_or(())? - .clone(), - satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(), - }, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - prevtx, - }) +impl FundingContribution { + pub(super) fn is_splice(&self) -> bool { + self.is_splice + } + + pub(crate) fn contributed_inputs(&self) -> impl Iterator<Item = OutPoint> + '_ { + self.inputs.iter().map(|input| input.utxo.outpoint) + } + + pub(crate) fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.outputs + .iter() + .chain(self.change_output.iter()) + .map(|output| output.script_pubkey.as_script()) } - /// Creates an input spending a P2WPKH output from the given `prevtx` at index `vout`. + /// The positive value added to the channel after explicit outputs and fees. /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. + /// This saturates at zero for net-negative contributions. See [`Self::net_value`] for the full + /// signed contribution to the channel. + pub fn value_added(&self) -> Amount { + let total_input_value = self.inputs.iter().map(|i| i.utxo.output.value).sum::<Amount>(); + let total_output_value = self.outputs.iter().map(|output| output.value).sum(); + total_input_value + .checked_sub(total_output_value) + .and_then(|v| v.checked_sub(self.estimated_fee)) + .and_then(|v| { + v.checked_sub( + self.change_output.as_ref().map_or(Amount::ZERO, |output| output.value), + ) + }) + .unwrap_or(Amount::ZERO) + } + + /// Returns the estimated on-chain fee this contribution is responsible for paying. + pub fn estimated_fee(&self) -> Amount { + self.estimated_fee + } + + /// Returns the inputs included in this contribution. + pub fn inputs(&self) -> &[ConfirmedUtxo] { + &self.inputs + } + + /// Returns the outputs (e.g., withdrawal destinations) included in this contribution. /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// This does not include the change output; see [`FundingContribution::change_output`]. + pub fn outputs(&self) -> &[TxOut] { + &self.outputs + } + + /// Returns the change output included in this contribution, if any. /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2wpkh(prevtx: Transaction, vout: u32) -> Result<Self, ()> { - let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT) - - if cfg!(feature = "grind_signatures") { - // Guarantees a low R signature - Weight::from_wu(1) - } else { - Weight::ZERO + /// When coin selection provides more value than needed for the funding contribution and fees, + /// the surplus is returned to the wallet via this change output. + pub fn change_output(&self) -> Option<&TxOut> { + self.change_output.as_ref() + } + + /// Returns the fee rate used to select `inputs` (the minimum feerate). + pub fn feerate(&self) -> FeeRate { + self.feerate + } + + /// Returns the maximum fee rate this contribution will accept as acceptor before rejecting + /// the splice. + pub fn max_feerate(&self) -> FeeRate { + self.max_feerate + } + + /// Tries to satisfy a new request using only this contribution's existing inputs. + /// + /// For input-backed contributions, this reuses the current inputs, adjusts the explicit + /// outputs, and shrinks or drops the change output as needed before applying + /// `target_feerate`. If dropping change leaves surplus value, that surplus remains in the + /// channel contribution. + /// + /// For input-less contributions, `spliceable_balance` must be provided to cover the outputs and + /// fees from the channel balance. + /// + /// Returns `None` if the request would require new wallet inputs or cannot accommodate the + /// requested feerate. + fn amend_without_coin_selection( + self, funding_inputs: Option<FundingInputs>, outputs: &[TxOut], target_feerate: FeeRate, + max_feerate: FeeRate, spliceable_balance: Amount, + ) -> Option<Self> { + // NOTE: The contribution returned is not guaranteed to be valid. We defer doing so until + // `compute_feerate_adjustment`. + let adjust_for_inputs_and_outputs = |contribution: Self, + inputs: Option<FundingInputs>, + outputs: &[TxOut]| + -> Option<Self> { + let input_mode = inputs.as_ref().map(FundingInputs::mode); + let (target_value_added, inputs) = match inputs { + None => (None, Vec::new()), + Some(FundingInputs::CoinSelected { value_added }) => { + // We track the prior contribution's inputs here to see if they can cover the + // new `value_added` without running coin selection. + (Some(value_added), contribution.inputs) + }, + Some(FundingInputs::ManuallySelected { inputs }) => (None, inputs), }; - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2wpkh) + + if inputs.is_empty() && target_value_added.unwrap_or(Amount::ZERO) != Amount::ZERO { + // Prior contribution didn't have any inputs, but now we need some. + return None; + } + + // When inputs are coin-selected, adjust the existing change output, if any, to account + // for the requested value added and any explicit outputs that must also be funded by + // the inputs. + if let Some(value_added) = target_value_added { + let estimated_fee = estimate_transaction_fee( + &inputs, + &outputs, + contribution.change_output.as_ref(), + true, + contribution.is_splice, + contribution.feerate, + ); + let total_output_value: Amount = outputs.iter().map(|output| output.value).sum(); + let required_value = + value_added.checked_add(total_output_value)?.checked_add(estimated_fee)?; + + if let Some(change_output) = contribution.change_output.as_ref() { + let dust_limit = change_output.script_pubkey.minimal_non_dust(); + let total_input_value: Amount = + inputs.iter().map(|input| input.utxo.output.value).sum(); + match total_input_value.checked_sub(required_value) { + Some(new_change_value) if new_change_value >= dust_limit => { + let new_change_output = TxOut { + value: new_change_value, + script_pubkey: change_output.script_pubkey.clone(), + }; + return Some(FundingContribution { + estimated_fee, + inputs, + outputs: outputs.to_vec(), + change_output: Some(new_change_output), + input_mode, + ..contribution + }); + }, + _ => {}, + } + } + } + + let estimated_fee_no_change = estimate_transaction_fee( + &inputs, + &outputs, + None, + true, + contribution.is_splice, + contribution.feerate, + ); + Some(FundingContribution { + estimated_fee: estimated_fee_no_change, + outputs: outputs.to_vec(), + inputs, + change_output: None, + input_mode, + ..contribution + }) + }; + + let new_contribution_at_current_feerate = + adjust_for_inputs_and_outputs(self, funding_inputs, outputs)?; + let mut new_contribution_at_target_feerate = new_contribution_at_current_feerate + .at_feerate(target_feerate, spliceable_balance, true) + .ok()?; + new_contribution_at_target_feerate.max_feerate = max_feerate; + + Some(new_contribution_at_target_feerate) + } + + pub(super) fn into_tx_parts(self) -> (Vec<ConfirmedUtxo>, Vec<TxOut>) { + let FundingContribution { inputs, mut outputs, change_output, .. } = self; + + if let Some(change_output) = change_output { + outputs.push(change_output); + } + + (inputs, outputs) + } + + pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec<OutPoint>, Vec<ScriptBuf>) { + let FundingContribution { inputs, outputs, change_output, .. } = self; + let contributed_inputs = inputs.into_iter().map(|input| input.utxo.outpoint).collect(); + let contributed_outputs = outputs.into_iter().chain(change_output.into_iter()); + (contributed_inputs, contributed_outputs.map(|output| output.script_pubkey).collect()) } - /// Creates an input spending a P2WSH output from the given `prevtx` at index `vout`. + /// Returns this contribution's inputs and outputs after removing any that overlap + /// with the provided `existing_inputs`/`existing_outputs`. /// - /// Requires passing the weight of witness needed to satisfy the output's script. + /// Multiple contribution outputs sharing a `script_pubkey` are all dropped when any + /// existing output uses the same script. /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. + /// Returns `None` if every input and output was filtered as overlapping. + pub(crate) fn into_unique_contributions<'a>( + self, existing_inputs: impl Iterator<Item = OutPoint>, + existing_outputs: impl Iterator<Item = &'a bitcoin::Script>, + ) -> Option<(Vec<OutPoint>, Vec<ScriptBuf>)> { + let FundingContribution { mut inputs, mut outputs, mut change_output, .. } = self; + for existing in existing_inputs { + inputs.retain(|input| input.outpoint() != existing); + } + for existing in existing_outputs { + outputs.retain(|output| output.script_pubkey.as_script() != existing); + // TODO: Replace with `take_if` once our MSRV is >= 1.80. + if change_output + .as_ref() + .filter(|output| output.script_pubkey.as_script() == existing) + .is_some() + { + change_output.take(); + } + } + if inputs.is_empty() && outputs.is_empty() && change_output.as_ref().is_none() { + None + } else { + let inputs = inputs.into_iter().map(|input| input.outpoint()).collect(); + let outputs = outputs + .into_iter() + .chain(change_output.into_iter()) + .map(|output| output.script_pubkey) + .collect(); + Some((inputs, outputs)) + } + } + + /// Computes the adjusted fee and change output value at the given target feerate, which may + /// differ from the feerate used during coin selection. + /// + /// The `is_initiator` parameter determines fee responsibility: the initiator pays for common + /// transaction fields, the shared input, and the shared output, while the acceptor only pays + /// for their own contributed inputs and outputs. + /// + /// On success, returns the new estimated fee and, if applicable, the new change output value: + /// - `Some(change)` — the adjusted change output value + /// - `None` — no change output (no inputs or change fell below dust) + /// + /// Returns `Err` if the contribution cannot accommodate the target feerate. + fn compute_feerate_adjustment( + &self, target_feerate: FeeRate, spliceable_balance: Amount, is_initiator: bool, + ) -> Result<(Amount, Option<Amount>), FeeRateAdjustmentError> { + if target_feerate < self.feerate { + return Err(FeeRateAdjustmentError::FeeRateTooLow { + target_feerate, + min_feerate: self.feerate, + }); + } + + // If the target fee rate exceeds our max fee rate, we may still add our contribution + // if we pay less in fees at the target feerate than at the original feerate. This can + // happen when adjusting as acceptor, since the acceptor doesn't pay for common fields + // and the shared input / output. + if target_feerate > self.max_feerate { + let target_fee = estimate_transaction_fee( + &self.inputs, + &self.outputs, + self.change_output.as_ref(), + is_initiator, + self.is_splice, + target_feerate, + ); + if target_fee > self.estimated_fee { + return Err(FeeRateAdjustmentError::FeeRateTooHigh { + target_feerate, + max_feerate: self.max_feerate, + target_fee, + original_fee: self.estimated_fee, + }); + } + } + + let target_fee = estimate_transaction_fee( + &self.inputs, + &self.outputs, + self.change_output.as_ref(), + is_initiator, + self.is_splice, + target_feerate, + ); + + if !self.inputs.is_empty() && self.input_mode == Some(FundingInputMode::CoinSelected) { + // Any withdrawal outputs and fees always come from the coin-selected inputs, as we want + // to guarantee the net contribution adds the desired value. + let fee_buffer = self + .estimated_fee + .checked_add( + self.change_output.as_ref().map_or(Amount::ZERO, |output| output.value), + ) + .ok_or(FeeRateAdjustmentError::FeeBufferOverflow)?; + + if let Some(change_output) = self.change_output.as_ref() { + let dust_limit = change_output.script_pubkey.minimal_non_dust(); + if let Some(new_change_value) = fee_buffer.checked_sub(target_fee) { + if new_change_value >= dust_limit { + return Ok((target_fee, Some(new_change_value))); + } + + // Our remaining change was not enough to be a valid output, fallthrough to the + // no remaining change case. + } + + let target_fee_no_change = estimate_transaction_fee( + &self.inputs, + &self.outputs, + None, + is_initiator, + self.is_splice, + target_feerate, + ); + if target_fee_no_change > fee_buffer { + Err(FeeRateAdjustmentError::FeeBufferInsufficient { + source: "estimated fee + change value", + available: fee_buffer, + required: target_fee_no_change, + }) + } else { + Ok((target_fee_no_change, None)) + } + } else if let Some(_surplus) = fee_buffer.checked_sub(target_fee) { + Ok((target_fee, None)) + } else { + Err(FeeRateAdjustmentError::FeeBufferInsufficient { + source: "estimated fee", + available: fee_buffer, + required: target_fee, + }) + } + } else { + // Manually selected inputs may either add value to the channel or offset some of the + // withdrawal outputs. Any remaining fee cost must come from the channel balance. + let net_value_without_fee = self.net_value_without_fee(); + let fee_buffer = if net_value_without_fee.is_negative() { + spliceable_balance + .checked_sub(net_value_without_fee.unsigned_abs()) + .unwrap_or(Amount::ZERO) + } else { + spliceable_balance + .checked_add(net_value_without_fee.unsigned_abs()) + .ok_or(FeeRateAdjustmentError::FeeBufferOverflow)? + }; + if fee_buffer < target_fee { + return Err(FeeRateAdjustmentError::FeeBufferInsufficient { + source: "channel balance", + available: fee_buffer, + required: target_fee, + }); + } + Ok((target_fee, None)) + } + } + + /// Adjusts the contribution for a different feerate, updating the change output, fee + /// estimate, and feerate. Returns the adjusted contribution, or an error if the feerate + /// can't be accommodated. + fn at_feerate( + mut self, feerate: FeeRate, spliceable_balance: Amount, is_initiator: bool, + ) -> Result<Self, FeeRateAdjustmentError> { + let (new_estimated_fee, new_change) = + self.compute_feerate_adjustment(feerate, spliceable_balance, is_initiator)?; + match new_change { + Some(value) => self.change_output.as_mut().unwrap().value = value, + None => self.change_output = None, + } + self.estimated_fee = new_estimated_fee; + self.feerate = feerate; + Ok(self) + } + + /// Adjusts the contribution's change output for the initiator's feerate. /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// When the acceptor has a pending contribution (from the quiescence tie-breaker scenario), + /// the initiator's proposed feerate may differ from the feerate used during coin selection. + /// This adjusts the change output so the acceptor pays their target fee at the target + /// feerate. + pub(super) fn for_acceptor_at_feerate( + self, feerate: FeeRate, spliceable_balance: Amount, + ) -> Result<Self, FeeRateAdjustmentError> { + self.at_feerate(feerate, spliceable_balance, false) + } + + /// Adjusts the contribution's change output for the minimum RBF feerate. /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2wsh(prevtx: Transaction, vout: u32, witness_weight: Weight) -> Result<Self, ()> { - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2wsh) + /// When a pending splice exists with negotiated candidates and the contribution's feerate is + /// below the minimum RBF feerate, this adjusts the change output so the initiator pays fees + /// at the minimum RBF feerate. + pub(super) fn for_initiator_at_feerate( + self, feerate: FeeRate, spliceable_balance: Amount, + ) -> Result<Self, FeeRateAdjustmentError> { + self.at_feerate(feerate, spliceable_balance, true) } - /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. + /// Returns the net value at the given target feerate without mutating `self`. /// - /// This is meant for inputs spending a taproot output using the key path. See - /// [`new_p2tr_script_spend`] for when spending using a script path. + /// This serves double duty: it checks feerate compatibility (returning `Err` if the feerate + /// can't be accommodated) and computes the adjusted net value (returning `Ok` with the value + /// accounting for the target feerate). + fn net_value_at_feerate( + &self, target_feerate: FeeRate, spliceable_balance: Amount, is_initiator: bool, + ) -> Result<SignedAmount, FeeRateAdjustmentError> { + let (new_estimated_fee, new_change) = + self.compute_feerate_adjustment(target_feerate, spliceable_balance, is_initiator)?; + + let prev_fee = self + .estimated_fee + .to_signed() + .expect("total input amount cannot exceed Amount::MAX_MONEY"); + let prev_change = self + .change_output + .as_ref() + .map_or(Amount::ZERO, |output| output.value) + .to_signed() + .expect("total input amount cannot exceed Amount::MAX_MONEY"); + + let new_fee = new_estimated_fee + .to_signed() + .expect("total input amount cannot exceed Amount::MAX_MONEY"); + let new_change = new_change + .unwrap_or(Amount::ZERO) + .to_signed() + .expect("total input amount cannot exceed Amount::MAX_MONEY"); + + let prev_net_value = self.net_value(); + Ok(prev_net_value + prev_fee + prev_change - new_fee - new_change) + } + + /// Returns the net value at the given target feerate without mutating `self`, + /// assuming acceptor fee responsibility. + pub(super) fn net_value_for_acceptor_at_feerate( + &self, target_feerate: FeeRate, spliceable_balance: Amount, + ) -> Result<SignedAmount, FeeRateAdjustmentError> { + self.net_value_at_feerate(target_feerate, spliceable_balance, false) + } + + /// Returns the net value at the given target feerate without mutating `self`, + /// assuming initiator fee responsibility. + pub(super) fn net_value_for_initiator_at_feerate( + &self, target_feerate: FeeRate, spliceable_balance: Amount, + ) -> Result<SignedAmount, FeeRateAdjustmentError> { + self.net_value_at_feerate(target_feerate, spliceable_balance, true) + } + + /// The net value contributed to a channel by the splice. + pub fn net_value(&self) -> SignedAmount { + let estimated_fee = self + .estimated_fee + .to_signed() + .expect("total_input_value is validated to not exceed Amount::MAX_MONEY"); + self.net_value_without_fee() + .checked_sub(estimated_fee) + .expect("all amounts are validated to not exceed Amount::MAX_MONEY") + } + + fn net_value_without_fee(&self) -> SignedAmount { + let total_input_value = self + .inputs + .iter() + .map(|input| input.utxo.output.value) + .sum::<Amount>() + .to_signed() + .expect("total_input_value is validated to not exceed Amount::MAX_MONEY"); + let total_output_value = self + .outputs + .iter() + .chain(self.change_output.iter()) + .map(|txout| txout.value) + .sum::<Amount>() + .to_signed() + .expect("total_output_value is validated to not exceed Amount::MAX_MONEY"); + total_input_value + .checked_sub(total_output_value) + .expect("all amounts are validated to not exceed Amount::MAX_MONEY") + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct NoCoinSelectionSource; +#[derive(Debug, Clone, PartialEq, Eq)] +struct AsyncCoinSelectionSource<W>(W); +#[derive(Debug, Clone, PartialEq, Eq)] +struct SyncCoinSelectionSource<W>(W); + +#[derive(Debug, Clone, PartialEq, Eq)] +struct FundingBuilderInner<State> { + shared_input: Option<Input>, + min_rbf_feerate: Option<FeeRate>, + prior_contribution: Option<FundingContribution>, + spliceable_balance: Amount, + funding_inputs: Option<FundingInputs>, + outputs: Vec<TxOut>, + feerate: FeeRate, + max_feerate: FeeRate, + state: State, +} + +/// A builder for composing or amending a [`FundingContribution`]. +/// +/// The builder tracks either a requested amount to add to the channel or a fixed set of manually +/// selected inputs, together with any explicit withdrawal outputs. Building without an attached +/// wallet only succeeds when the request can be satisfied by reusing or amending a prior +/// contribution, by using only manually selected inputs, or by constructing a splice-out that +/// pays fees from the channel balance. +/// +/// Attach a wallet via [`FundingBuilder::with_coin_selection_source`] or +/// [`FundingBuilder::with_coin_selection_source_sync`] when the request may need new wallet +/// inputs. Manually selected inputs are not supplemented with coin selection. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FundingBuilder(FundingBuilderInner<NoCoinSelectionSource>); + +/// A [`FundingBuilder`] with an attached asynchronous [`CoinSelectionSource`]. +/// +/// Created by [`FundingBuilder::with_coin_selection_source`]. The attached wallet is only used +/// if the request cannot be satisfied by reusing a prior contribution, by using only manually +/// selected inputs, or by building a pure splice-out directly. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AsyncFundingBuilder<W>(FundingBuilderInner<AsyncCoinSelectionSource<W>>); + +/// A [`FundingBuilder`] with an attached synchronous [`CoinSelectionSourceSync`]. +/// +/// Created by [`FundingBuilder::with_coin_selection_source_sync`]. The attached wallet is only +/// used if the request cannot be satisfied by reusing a prior contribution, by using only +/// manually selected inputs, or by building a pure splice-out directly. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SyncFundingBuilder<W>(FundingBuilderInner<SyncCoinSelectionSource<W>>); + +impl<State> FundingBuilderInner<State> { + fn request_matches_prior(&self, prior_contribution: &FundingContribution) -> bool { + let request_matches_prior_inputs = + match (self.funding_inputs.as_ref(), prior_contribution.input_mode) { + ( + Some(FundingInputs::ManuallySelected { inputs }), + Some(FundingInputMode::ManuallySelected), + ) => { + let request_inputs = inputs.iter().map(|input| input.utxo.outpoint); + let prior_inputs = + prior_contribution.inputs.iter().map(|input| input.utxo.outpoint); + request_inputs.eq(prior_inputs) + }, + ( + Some(FundingInputs::CoinSelected { value_added }), + Some(FundingInputMode::CoinSelected), + ) => *value_added == prior_contribution.value_added(), + (None, None) => true, + _ => false, + }; + request_matches_prior_inputs && self.outputs == prior_contribution.outputs + } + + fn build_from_prior_contribution( + &self, contribution: FundingContribution, + ) -> Result<FundingContribution, FundingContributionError> { + let input_mode = self.funding_inputs.as_ref().map(FundingInputs::mode); + + if self.request_matches_prior(&contribution) { + // Same request, but the feerate may have changed. Adjust the prior contribution + // to the new feerate if possible. + return contribution + .for_initiator_at_feerate(self.feerate, self.spliceable_balance) + .map(|mut adjusted| { + adjusted.max_feerate = self.max_feerate; + adjusted + }) + .map_err(|_| { + if input_mode == Some(FundingInputMode::ManuallySelected) { + FundingContributionError::ManuallySelectedInputsInsufficient + } else { + FundingContributionError::MissingCoinSelectionSource + } + }); + } + + return contribution + .amend_without_coin_selection( + self.funding_inputs.clone(), + &self.outputs, + self.feerate, + self.max_feerate, + self.spliceable_balance, + ) + .ok_or_else(|| { + if input_mode == Some(FundingInputMode::ManuallySelected) { + FundingContributionError::ManuallySelectedInputsInsufficient + } else { + FundingContributionError::MissingCoinSelectionSource + } + }); + } + + /// Tries to build the current request without selecting any new wallet inputs. /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. + /// This first attempts to reuse or amend any prior contribution. If there is no prior + /// contribution, it also supports manually selected inputs and pure splice-out requests by + /// building a contribution without coin selection. /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// Returns [`FundingContributionError::MissingCoinSelectionSource`] if the request is + /// otherwise valid but needs wallet inputs, or + /// [`FundingContributionError::ManuallySelectedInputsInsufficient`] if the manually selected + /// inputs cannot satisfy the request. + fn try_build_without_coin_selection( + &self, + ) -> Result<FundingContribution, FundingContributionError> { + if let Some(contribution) = self.prior_contribution.as_ref() { + return self.build_from_prior_contribution(contribution.clone()); + } + + let value_added = + self.funding_inputs.as_ref().map_or(Amount::ZERO, FundingInputs::value_added); + if value_added == Amount::ZERO { + let inputs = self + .funding_inputs + .as_ref() + .map_or(&[][..], FundingInputs::manually_selected_inputs); + let input_mode = + if inputs.is_empty() { None } else { Some(FundingInputMode::ManuallySelected) }; + + let estimated_fee = estimate_transaction_fee( + inputs, + &self.outputs, + None, + true, + self.shared_input.is_some(), + self.feerate, + ); + + let contribution = FundingContribution { + estimated_fee, + inputs: match self.funding_inputs { + Some(FundingInputs::ManuallySelected { ref inputs }) => inputs.clone(), + None | Some(FundingInputs::CoinSelected { .. }) => Vec::new(), + }, + outputs: self.outputs.clone(), + change_output: None, + feerate: self.feerate, + max_feerate: self.max_feerate, + is_splice: self.shared_input.is_some(), + input_mode, + }; + let net_value = contribution.net_value(); + if net_value.is_negative() { + self.spliceable_balance.checked_sub(net_value.unsigned_abs()).ok_or_else(|| { + if contribution.inputs.is_empty() { + FundingContributionError::InvalidSpliceValue + } else { + FundingContributionError::ManuallySelectedInputsInsufficient + } + })?; + } + + return Ok(contribution); + } + + Err(FundingContributionError::MissingCoinSelectionSource) + } + + fn prepare_coin_selection_request( + &self, + ) -> Result<(Vec<Input>, Vec<TxOut>), FundingContributionError> { + let value_added = + self.funding_inputs.as_ref().map_or(Amount::ZERO, FundingInputs::value_added); + let dummy_pubkey = PublicKey::from_slice(&[2; 33]).unwrap(); + let shared_output = bitcoin::TxOut { + value: self + .shared_input + .as_ref() + .map(|shared_input| shared_input.previous_utxo.value) + .unwrap_or(Amount::ZERO) + .checked_add(value_added) + .ok_or(FundingContributionError::InvalidSpliceValue)?, + script_pubkey: make_funding_redeemscript(&dummy_pubkey, &dummy_pubkey).to_p2wsh(), + }; + + let must_spend = self.shared_input.clone().map(|input| vec![input]).unwrap_or_default(); + let must_pay_to = if self.outputs.is_empty() { + vec![shared_output] + } else { + self.outputs.iter().cloned().chain(core::iter::once(shared_output)).collect() + }; + + Ok((must_spend, must_pay_to)) + } + + fn validate_contribution_parameters(&self) -> Result<(), FundingContributionError> { + if self.feerate > self.max_feerate { + return Err(FundingContributionError::FeeRateExceedsMaximum { + feerate: self.feerate, + max_feerate: self.max_feerate, + }); + } + + if let Some(min_rbf_feerate) = self.min_rbf_feerate.as_ref() { + if self.feerate < *min_rbf_feerate { + return Err(FundingContributionError::FeeRateBelowRbfMinimum { + feerate: self.feerate, + min_rbf_feerate: *min_rbf_feerate, + }); + } + } + + if self.funding_inputs.as_ref().map_or(true, FundingInputs::is_empty) + && self.outputs.is_empty() + { + return Err(FundingContributionError::InvalidSpliceValue); + } + + // Validate user-provided amounts are within MAX_MONEY before coin selection to + // ensure FundingContribution::net_value() arithmetic cannot overflow. With all + // amounts bounded by MAX_MONEY (~2.1e15 sat), the worst-case net_value() + // computation is -2 * MAX_MONEY (~-4.2e15), well within i64::MIN (~-9.2e18). + if self.funding_inputs.as_ref().map_or(Amount::ZERO, FundingInputs::value_added) + > Amount::MAX_MONEY + { + return Err(FundingContributionError::InvalidSpliceValue); + } + + validate_inputs( + self.funding_inputs.as_ref().map_or(&[][..], FundingInputs::manually_selected_inputs), + )?; + + let mut value_removed = Amount::ZERO; + for (idx, output) in self.outputs.iter().enumerate() { + if self.outputs[..idx] + .iter() + .any(|existing_output| existing_output.script_pubkey == output.script_pubkey) + { + return Err(FundingContributionError::InvalidSpliceValue); + } + + value_removed = match value_removed.checked_add(output.value) { + Some(sum) if sum <= Amount::MAX_MONEY => sum, + _ => return Err(FundingContributionError::InvalidSpliceValue), + }; + } + + Ok(()) + } +} + +impl FundingBuilder { + fn new(template: FundingTemplate, feerate: FeeRate, max_feerate: FeeRate) -> FundingBuilder { + let FundingTemplate { + shared_input, + min_rbf_feerate, + prior_contribution, + spliceable_balance, + } = template; + let (funding_inputs, outputs) = match prior_contribution.as_ref() { + Some(prior_contribution) => { + let funding_inputs = match prior_contribution.input_mode { + Some(FundingInputMode::ManuallySelected) => { + Some(FundingInputs::ManuallySelected { + inputs: prior_contribution.inputs.clone(), + }) + }, + Some(FundingInputMode::CoinSelected) => Some(FundingInputs::CoinSelected { + value_added: prior_contribution.value_added(), + }), + None => None, + }; + (funding_inputs, prior_contribution.outputs.clone()) + }, + None => (None, Vec::new()), + }; + + FundingBuilder(FundingBuilderInner { + shared_input, + min_rbf_feerate, + prior_contribution, + spliceable_balance, + funding_inputs, + outputs, + feerate, + max_feerate, + state: NoCoinSelectionSource, + }) + } + + /// Attaches an asynchronous [`CoinSelectionSource`] for later use. /// - /// [`new_p2tr_script_spend`]: Self::new_p2tr_script_spend + /// The wallet is only consulted if [`AsyncFundingBuilder::build`] cannot satisfy the request by + /// reusing a prior contribution, by using only manually selected inputs, or by constructing a + /// pure splice-out directly. + pub fn with_coin_selection_source<W: CoinSelectionSource + MaybeSend>( + self, wallet: W, + ) -> AsyncFundingBuilder<W> { + AsyncFundingBuilder(self.0.with_state(AsyncCoinSelectionSource(wallet))) + } + + /// Attaches a synchronous [`CoinSelectionSourceSync`] for later use. /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2tr_key_spend(prevtx: Transaction, vout: u32) -> Result<Self, ()> { - let witness_weight = Weight::from_wu(P2TR_KEY_PATH_WITNESS_WEIGHT); - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2tr) + /// The wallet is only consulted if [`SyncFundingBuilder::build`] cannot satisfy the request by + /// reusing a prior contribution, by using only manually selected inputs, or by constructing a + /// pure splice-out directly. + pub fn with_coin_selection_source_sync<W: CoinSelectionSourceSync>( + self, wallet: W, + ) -> SyncFundingBuilder<W> { + SyncFundingBuilder(self.0.with_state(SyncCoinSelectionSource(wallet))) } - /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. + /// Adds a manually selected input to the request. /// - /// Requires passing the weight of witness needed to satisfy a script path of the taproot - /// output. See [`new_p2tr_key_spend`] for when spending using the key path. + /// Each input is fully consumed with no change output. When built without additional coin + /// selection, the inputs and explicit outputs are modeled by their net effect on the channel: + /// the contribution may be net-positive or net-negative before fees. + /// + /// Manually selected inputs are a separate request mode and cannot be combined with requesting + /// additional coin-selected value. If the manually selected inputs cannot satisfy the request, + /// [`FundingBuilder::build`] returns + /// [`FundingContributionError::ManuallySelectedInputsInsufficient`] instead of falling back to + /// coin selection. + /// + /// Returns [`FundingContributionError::InvalidSpliceValue`] if the builder already has a + /// coin-selected value request. + pub fn add_input(self, input: ConfirmedUtxo) -> Result<Self, FundingContributionError> { + self.0.add_input_inner(input).map(FundingBuilder) + } + + /// Adds manually selected inputs to the request. /// - /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden - /// by [`set_sequence`]. + /// Each input is fully consumed with no change output. When built without additional coin + /// selection, the inputs and explicit outputs are modeled by their net effect on the channel: + /// the contribution may be net-positive or net-negative before fees. /// - /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// Manually selected inputs are a separate request mode and cannot be combined with requesting + /// additional coin-selected value. If the manually selected inputs cannot satisfy the request, + /// [`FundingBuilder::build`] returns + /// [`FundingContributionError::ManuallySelectedInputsInsufficient`] instead of falling back to + /// coin selection. /// - /// [`new_p2tr_key_spend`]: Self::new_p2tr_key_spend + /// Returns [`FundingContributionError::InvalidSpliceValue`] if the builder already has a + /// coin-selected value request. + pub fn add_inputs(self, inputs: Vec<ConfirmedUtxo>) -> Result<Self, FundingContributionError> { + self.0.add_inputs_inner(inputs).map(FundingBuilder) + } + + /// Removes all manually selected inputs whose outpoint matches `outpoint`. /// - /// [`TxIn::sequence`]: bitcoin::TxIn::sequence - /// [`set_sequence`]: Self::set_sequence - pub fn new_p2tr_script_spend( - prevtx: Transaction, vout: u32, witness_weight: Weight, - ) -> Result<Self, ()> { - FundingTxInput::new(prevtx, vout, witness_weight, Script::is_p2tr) + /// Returns [`FundingContributionError::InvalidSpliceValue`] if the builder already has a + /// coin-selected value request. + pub fn remove_input(self, outpoint: &OutPoint) -> Result<Self, FundingContributionError> { + self.0.remove_input_inner(outpoint).map(FundingBuilder) } - #[cfg(test)] - pub(crate) fn new_p2pkh(prevtx: Transaction, vout: u32) -> Result<Self, ()> { - FundingTxInput::new(prevtx, vout, Weight::ZERO, Script::is_p2pkh) + /// Adds a withdrawal output to the request. + /// + /// `output` is appended to the current set of explicit outputs. If the builder was seeded from + /// a prior contribution, this adds an additional withdrawal on top of the prior outputs. This + /// does not affect any change output derived when the contribution is built. + pub fn add_output(self, output: TxOut) -> Self { + FundingBuilder(self.0.add_output_inner(output)) } - /// The outpoint of the UTXO being spent. - pub fn outpoint(&self) -> bitcoin::OutPoint { - self.utxo.outpoint + /// Adds withdrawal outputs to the request. + /// + /// `outputs` are appended to the current set of explicit outputs. If the builder was seeded + /// from a prior contribution, this adds additional withdrawals on top of the prior outputs. + /// This does not affect any change output derived when the contribution is built. + pub fn add_outputs(self, outputs: Vec<TxOut>) -> Self { + FundingBuilder(self.0.add_outputs_inner(outputs)) } - /// The sequence number to use in the [`TxIn`]. + /// Removes all explicit withdrawal outputs whose script pubkey matches `script_pubkey`. /// - /// [`TxIn`]: bitcoin::TxIn - pub fn sequence(&self) -> Sequence { - self.sequence + /// This only affects outputs returned by [`FundingContribution::outputs`]; it never removes the + /// change output returned by [`FundingContribution::change_output`]. + pub fn remove_outputs(self, script_pubkey: &ScriptBuf) -> Self { + FundingBuilder(self.0.remove_outputs_inner(script_pubkey)) } - /// Sets the sequence number to use in the [`TxIn`]. + /// Builds a [`FundingContribution`] without coin selection. /// - /// [`TxIn`]: bitcoin::TxIn - pub fn set_sequence(&mut self, sequence: Sequence) { - self.sequence = sequence; + /// This succeeds when the request can be satisfied by reusing or amending a prior + /// contribution, by using only manually selected inputs, or by building a splice-out + /// contribution that pays fees from the channel balance. + /// + /// Returns [`FundingContributionError::MissingCoinSelectionSource`] if additional wallet + /// inputs are needed, or [`FundingContributionError::ManuallySelectedInputsInsufficient`] if + /// the manually selected inputs cannot satisfy the request. + pub fn build(self) -> Result<FundingContribution, FundingContributionError> { + self.0.build_without_coin_selection() + } +} + +impl<State> FundingBuilderInner<State> { + fn with_state<NewState>(self, state: NewState) -> FundingBuilderInner<NewState> { + FundingBuilderInner { + shared_input: self.shared_input, + min_rbf_feerate: self.min_rbf_feerate, + prior_contribution: self.prior_contribution, + spliceable_balance: self.spliceable_balance, + funding_inputs: self.funding_inputs, + outputs: self.outputs, + feerate: self.feerate, + max_feerate: self.max_feerate, + state, + } + } + + fn add_value_inner(mut self, value: Amount) -> Result<Self, FundingContributionError> { + match &mut self.funding_inputs { + None => self.funding_inputs = Some(FundingInputs::CoinSelected { value_added: value }), + Some(FundingInputs::CoinSelected { value_added }) => { + *value_added = + Amount::from_sat(value_added.to_sat().saturating_add(value.to_sat())); + }, + Some(FundingInputs::ManuallySelected { .. }) => { + return Err(FundingContributionError::InvalidSpliceValue); + }, + } + Ok(self) + } + + fn remove_value_inner(mut self, value: Amount) -> Result<Self, FundingContributionError> { + match &mut self.funding_inputs { + None => {}, + Some(FundingInputs::CoinSelected { value_added }) => { + *value_added = + Amount::from_sat(value_added.to_sat().saturating_sub(value.to_sat())); + }, + Some(FundingInputs::ManuallySelected { .. }) => { + return Err(FundingContributionError::InvalidSpliceValue); + }, + } + Ok(self) + } + + fn add_input_inner(mut self, input: ConfirmedUtxo) -> Result<Self, FundingContributionError> { + match &mut self.funding_inputs { + None => { + self.funding_inputs = Some(FundingInputs::ManuallySelected { inputs: vec![input] }) + }, + Some(FundingInputs::ManuallySelected { inputs }) => inputs.push(input), + Some(FundingInputs::CoinSelected { .. }) => { + return Err(FundingContributionError::InvalidSpliceValue); + }, + } + Ok(self) + } + + fn add_inputs_inner( + mut self, inputs: Vec<ConfirmedUtxo>, + ) -> Result<Self, FundingContributionError> { + match &mut self.funding_inputs { + None => self.funding_inputs = Some(FundingInputs::ManuallySelected { inputs }), + Some(FundingInputs::ManuallySelected { inputs: existing_inputs }) => { + existing_inputs.extend(inputs) + }, + Some(FundingInputs::CoinSelected { .. }) => { + return Err(FundingContributionError::InvalidSpliceValue); + }, + } + Ok(self) + } + + fn remove_input_inner(mut self, outpoint: &OutPoint) -> Result<Self, FundingContributionError> { + match &mut self.funding_inputs { + None => {}, + Some(FundingInputs::ManuallySelected { inputs }) => { + inputs.retain(|input| input.utxo.outpoint != *outpoint); + }, + Some(FundingInputs::CoinSelected { .. }) => { + return Err(FundingContributionError::InvalidSpliceValue); + }, + } + Ok(self) + } + + fn add_output_inner(mut self, output: TxOut) -> Self { + self.outputs.push(output); + self + } + + fn add_outputs_inner(mut self, outputs: Vec<TxOut>) -> Self { + self.outputs.extend(outputs); + self } - /// Converts the [`FundingTxInput`] into a [`Utxo`] for coin selection. - pub fn into_utxo(self) -> Utxo { - self.utxo + fn remove_outputs_inner(mut self, script_pubkey: &ScriptBuf) -> Self { + self.outputs.retain(|output| output.script_pubkey != *script_pubkey); + self + } + + /// Validates the current request and then tries to build it without selecting new wallet + /// inputs. + /// + /// Returns [`FundingContributionError::MissingCoinSelectionSource`] if the request is valid but + /// cannot be satisfied without wallet inputs, or + /// [`FundingContributionError::ManuallySelectedInputsInsufficient`] if the manually selected + /// inputs cannot satisfy the request. + fn build_without_coin_selection( + &self, + ) -> Result<FundingContribution, FundingContributionError> { + self.validate_contribution_parameters()?; + self.try_build_without_coin_selection() + } +} + +impl<W> AsyncFundingBuilder<W> { + /// Adds a withdrawal output to the request. + /// + /// `output` is appended to the current set of explicit outputs. If the builder was seeded from + /// a prior contribution, this adds an additional withdrawal on top of the prior outputs. This + /// does not affect any change output derived when the contribution is built. + pub fn add_output(self, output: TxOut) -> Self { + AsyncFundingBuilder(self.0.add_output_inner(output)) + } + + /// Adds withdrawal outputs to the request. + /// + /// `outputs` are appended to the current set of explicit outputs. If the builder was seeded + /// from a prior contribution, this adds additional withdrawals on top of the prior outputs. + /// This does not affect any change output derived when the contribution is built. + pub fn add_outputs(self, outputs: Vec<TxOut>) -> Self { + AsyncFundingBuilder(self.0.add_outputs_inner(outputs)) + } + + /// Removes all explicit withdrawal outputs whose script pubkey matches `script_pubkey`. + /// + /// This only affects outputs returned by [`FundingContribution::outputs`]; it never removes the + /// change output returned by [`FundingContribution::change_output`]. + pub fn remove_outputs(self, script_pubkey: &ScriptBuf) -> Self { + AsyncFundingBuilder(self.0.remove_outputs_inner(script_pubkey)) + } + + /// Increases the requested amount to add to the channel. + /// + /// `value` is added on top of the builder's current request. If the builder was seeded from a + /// prior contribution, this increases that prior contribution's current amount added to the + /// channel. If the updated request cannot be satisfied in-place, [`AsyncFundingBuilder::build`] + /// may re-run coin selection and return a contribution with a different input set. + /// + /// Returns [`FundingContributionError::InvalidSpliceValue`] if the builder already has manually + /// selected inputs. + pub fn add_value(self, value: Amount) -> Result<Self, FundingContributionError> { + self.0.add_value_inner(value).map(AsyncFundingBuilder) + } + + /// Decreases the requested amount to add to the channel. + /// + /// `value` is subtracted from the builder's current request, saturating at zero. If the builder + /// was seeded from a prior contribution, this decreases that prior contribution's current + /// amount added to the channel. If the updated request cannot be satisfied in-place, + /// [`AsyncFundingBuilder::build`] may re-run coin selection and return a contribution with a + /// different input set. + /// + /// Returns [`FundingContributionError::InvalidSpliceValue`] if the builder already has manually + /// selected inputs. + pub fn remove_value(self, value: Amount) -> Result<Self, FundingContributionError> { + self.0.remove_value_inner(value).map(AsyncFundingBuilder) + } +} + +impl<W: CoinSelectionSource + MaybeSend> AsyncFundingBuilder<W> { + /// Builds a [`FundingContribution`], using the attached asynchronous wallet only when needed. + /// + /// If the request can be satisfied by reusing or amending a prior contribution, or by building + /// a pure splice-out directly, or by using only manually selected inputs, the attached wallet is + /// ignored. + pub async fn build(self) -> Result<FundingContribution, FundingContributionError> { + let inner = self.0; + match inner.build_without_coin_selection() { + Err(FundingContributionError::MissingCoinSelectionSource) => {}, + other => return other, + } + + let (must_spend, must_pay_to) = inner.prepare_coin_selection_request()?; + let AsyncCoinSelectionSource(wallet) = inner.state; + let coin_selection = wallet + .select_confirmed_utxos( + None, + must_spend, + &must_pay_to, + inner.feerate.to_sat_per_kwu() as u32, + u64::MAX, + ) + .await + .map_err(|_| FundingContributionError::CoinSelectionFailed)?; + + let CoinSelection { confirmed_utxos: inputs, change_output } = coin_selection; + validate_inputs(&inputs)?; + + let outputs = inner.outputs; + let is_splice = inner.shared_input.is_some(); + let estimated_fee = estimate_transaction_fee( + &inputs, + &outputs, + change_output.as_ref(), + true, + is_splice, + inner.feerate, + ); + + return Ok(FundingContribution { + estimated_fee, + inputs, + outputs, + change_output, + feerate: inner.feerate, + max_feerate: inner.max_feerate, + is_splice, + input_mode: Some(FundingInputMode::CoinSelected), + }); + } +} + +impl<W> SyncFundingBuilder<W> { + /// Adds a withdrawal output to the request. + /// + /// `output` is appended to the current set of explicit outputs. If the builder was seeded from + /// a prior contribution, this adds an additional withdrawal on top of the prior outputs. This + /// does not affect any change output derived when the contribution is built. + pub fn add_output(self, output: TxOut) -> Self { + SyncFundingBuilder(self.0.add_output_inner(output)) + } + + /// Adds withdrawal outputs to the request. + /// + /// `outputs` are appended to the current set of explicit outputs. If the builder was seeded + /// from a prior contribution, this adds additional withdrawals on top of the prior outputs. + /// This does not affect any change output derived when the contribution is built. + pub fn add_outputs(self, outputs: Vec<TxOut>) -> Self { + SyncFundingBuilder(self.0.add_outputs_inner(outputs)) + } + + /// Removes all explicit withdrawal outputs whose script pubkey matches `script_pubkey`. + /// + /// This only affects outputs returned by [`FundingContribution::outputs`]; it never removes the + /// change output returned by [`FundingContribution::change_output`]. + pub fn remove_outputs(self, script_pubkey: &ScriptBuf) -> Self { + SyncFundingBuilder(self.0.remove_outputs_inner(script_pubkey)) + } + + /// Increases the requested amount to add to the channel. + /// + /// `value` is added on top of the builder's current request. If the builder was seeded from a + /// prior contribution, this increases that prior contribution's current amount added to the + /// channel. If the updated request cannot be satisfied in-place, [`SyncFundingBuilder::build`] + /// may re-run coin selection and return a contribution with a different input set. + /// + /// Returns [`FundingContributionError::InvalidSpliceValue`] if the builder already has manually + /// selected inputs. + pub fn add_value(self, value: Amount) -> Result<Self, FundingContributionError> { + self.0.add_value_inner(value).map(SyncFundingBuilder) + } + + /// Decreases the requested amount to add to the channel. + /// + /// `value` is subtracted from the builder's current request, saturating at zero. If the builder + /// was seeded from a prior contribution, this decreases that prior contribution's current + /// amount added to the channel. If the updated request cannot be satisfied in-place, + /// [`SyncFundingBuilder::build`] may re-run coin selection and return a contribution with a + /// different input set. + /// + /// Returns [`FundingContributionError::InvalidSpliceValue`] if the builder already has manually + /// selected inputs. + pub fn remove_value(self, value: Amount) -> Result<Self, FundingContributionError> { + self.0.remove_value_inner(value).map(SyncFundingBuilder) + } +} + +impl<W: CoinSelectionSourceSync> SyncFundingBuilder<W> { + /// Builds a [`FundingContribution`], using the attached synchronous wallet only when needed. + /// + /// If the request can be satisfied by reusing or amending a prior contribution, or by building + /// a pure splice-out directly, or by using only manually selected inputs, the attached wallet is + /// ignored. + pub fn build(self) -> Result<FundingContribution, FundingContributionError> { + let inner = self.0; + match inner.build_without_coin_selection() { + Err(FundingContributionError::MissingCoinSelectionSource) => {}, + other => return other, + } + + let (must_spend, must_pay_to) = inner.prepare_coin_selection_request()?; + let SyncCoinSelectionSource(wallet) = inner.state; + let coin_selection = wallet + .select_confirmed_utxos( + None, + must_spend, + &must_pay_to, + inner.feerate.to_sat_per_kwu() as u32, + u64::MAX, + ) + .map_err(|_| FundingContributionError::CoinSelectionFailed)?; + + let CoinSelection { confirmed_utxos: inputs, change_output } = coin_selection; + validate_inputs(&inputs)?; + + let outputs = inner.outputs; + let is_splice = inner.shared_input.is_some(); + let estimated_fee = estimate_transaction_fee( + &inputs, + &outputs, + change_output.as_ref(), + true, + is_splice, + inner.feerate, + ); + + return Ok(FundingContribution { + estimated_fee, + inputs, + outputs, + change_output, + feerate: inner.feerate, + max_feerate: inner.max_feerate, + is_splice, + input_mode: Some(FundingInputMode::CoinSelected), + }); + } +} + +#[cfg(test)] +mod tests { + use super::{ + estimate_transaction_fee, FeeRateAdjustmentError, FundingBuilder, FundingContribution, + FundingContributionError, FundingInputMode, FundingTemplate, SyncCoinSelectionSource, + SyncFundingBuilder, + }; + use crate::chain::ClaimId; + use crate::util::wallet_utils::{CoinSelection, CoinSelectionSourceSync, ConfirmedUtxo, Input}; + use bitcoin::hashes::Hash; + use bitcoin::transaction::{Transaction, TxOut, Version}; + use bitcoin::{Amount, FeeRate, Psbt, ScriptBuf, SignedAmount, WPubkeyHash, WScriptHash}; + + #[test] + #[rustfmt::skip] + fn test_estimate_transaction_fee() { + let one_input = [funding_input_sats(1_000)]; + let two_inputs = [funding_input_sats(1_000), funding_input_sats(1_000)]; + + // 2 inputs, initiator, 2000 sat/kw feerate + assert_eq!( + estimate_transaction_fee(&two_inputs, &[], None, true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1512 } else { 1516 }), + ); + + // higher feerate + assert_eq!( + estimate_transaction_fee(&two_inputs, &[], None, true, false, FeeRate::from_sat_per_kwu(3000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 2268 } else { 2274 }), + ); + + // only 1 input + assert_eq!( + estimate_transaction_fee(&one_input, &[], None, true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 970 } else { 972 }), + ); + + // 0 inputs + assert_eq!( + estimate_transaction_fee(&[], &[], None, true, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(428), + ); + + // not initiator + assert_eq!( + estimate_transaction_fee(&[], &[], None, false, false, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(0), + ); + + // splice initiator + assert_eq!( + estimate_transaction_fee(&one_input, &[], None, true, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1736 } else { 1740 }), + ); + + // splice acceptor + assert_eq!( + estimate_transaction_fee(&one_input, &[], None, false, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 542 } else { 544 }), + ); + + // splice initiator, 1 input, 1 output + let outputs = [funding_output_sats(500)]; + assert_eq!( + estimate_transaction_fee(&one_input, &outputs, None, true, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1984 } else { 1988 }), + ); + + // splice acceptor, 1 input, 1 output + assert_eq!( + estimate_transaction_fee(&one_input, &outputs, None, false, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 790 } else { 792 }), + ); + + // splice initiator, 1 input, 1 output, 1 change via change_output parameter + let change = funding_output_sats(1_000); + assert_eq!( + estimate_transaction_fee(&one_input, &outputs, Some(&change), true, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 2232 } else { 2236 }), + ); + + // splice acceptor, 1 input, 1 output, 1 change via change_output parameter + assert_eq!( + estimate_transaction_fee(&one_input, &outputs, Some(&change), false, true, FeeRate::from_sat_per_kwu(2000)), + Amount::from_sat(if cfg!(feature = "grind_signatures") { 1038 } else { 1040 }), + ); + } + + #[rustfmt::skip] + fn funding_input_sats(input_value_sats: u64) -> ConfirmedUtxo { + let prevout = TxOut { + value: Amount::from_sat(input_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }; + let prevtx = Transaction { + input: vec![], output: vec![prevout], + version: Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, + }; + + ConfirmedUtxo::new_p2wpkh(prevtx, 0).unwrap() + } + + fn funding_output_sats(output_value_sats: u64) -> TxOut { + TxOut { + value: Amount::from_sat(output_value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + } + } + + struct UnreachableWallet; + + impl CoinSelectionSourceSync for UnreachableWallet { + fn select_confirmed_utxos( + &self, _claim_id: Option<ClaimId>, _must_spend: Vec<Input>, _must_pay_to: &[TxOut], + _target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64, + ) -> Result<CoinSelection, ()> { + unreachable!("should not reach coin selection") + } + fn sign_psbt(&self, _psbt: Psbt) -> Result<Transaction, ()> { + unreachable!("should not reach signing") + } + } + + struct MustPayToWallet { + utxo: ConfirmedUtxo, + change_output: Option<TxOut>, + expected_must_pay_to_values: Vec<Amount>, + } + + impl CoinSelectionSourceSync for MustPayToWallet { + fn select_confirmed_utxos( + &self, _claim_id: Option<ClaimId>, _must_spend: Vec<Input>, must_pay_to: &[TxOut], + _target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64, + ) -> Result<CoinSelection, ()> { + assert_eq!( + must_pay_to.iter().map(|output| output.value).collect::<Vec<_>>(), + self.expected_must_pay_to_values, + ); + Ok(CoinSelection { + confirmed_utxos: vec![self.utxo.clone()], + change_output: self.change_output.clone(), + }) + } + + fn sign_psbt(&self, _psbt: Psbt) -> Result<Transaction, ()> { + unreachable!("should not reach signing") + } + } + + #[test] + fn test_funding_builder_builds_splice_out_without_wallet() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let output = funding_output_sats(25_000); + + let contribution = FundingBuilder::new( + FundingTemplate::new(None, None, None, Amount::MAX_MONEY), + feerate, + FeeRate::MAX, + ) + .add_output(output.clone()) + .build() + .unwrap(); + + let expected_fee = estimate_transaction_fee( + &[], + std::slice::from_ref(&output), + None, + true, + false, + feerate, + ); + assert!(contribution.inputs.is_empty()); + assert_eq!(contribution.outputs, vec![output.clone()]); + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.estimated_fee, expected_fee); + assert_eq!( + contribution.net_value(), + -output.value.to_signed().unwrap() - expected_fee.to_signed().unwrap(), + ); + } + + #[test] + fn test_funding_builder_rejects_splice_out_over_balance() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let output = funding_output_sats(25_000); + let expected_fee = estimate_transaction_fee( + &[], + std::slice::from_ref(&output), + None, + true, + false, + feerate, + ); + let exact_balance = output.value + expected_fee; + + let contribution = FundingTemplate::new(None, None, None, exact_balance) + .splice_out(vec![output.clone()], feerate, FeeRate::MAX) + .unwrap(); + assert_eq!(contribution.net_value(), -exact_balance.to_signed().unwrap()); + + let result = FundingTemplate::new(None, None, None, exact_balance - Amount::from_sat(1)) + .splice_out(vec![output], feerate, FeeRate::MAX); + assert!(matches!(result, Err(FundingContributionError::InvalidSpliceValue))); + } + + #[test] + fn test_funding_builder_requires_wallet_for_splice_in() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let builder = FundingBuilder::new( + FundingTemplate::new(None, None, None, Amount::ZERO), + feerate, + FeeRate::MAX, + ); + let builder = FundingBuilder(builder.0.add_value_inner(Amount::from_sat(25_000)).unwrap()); + + assert!(matches!( + builder.build(), + Err(FundingContributionError::MissingCoinSelectionSource), + )); + } + + #[test] + fn test_funding_builder_amends_prior_by_dropping_subdust_change() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(500); + let dust_limit = change.script_pubkey.minimal_non_dust(); + assert!(change.value >= dust_limit); + + let estimated_fee_with_change = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, feerate); + let estimated_fee_no_change = + estimate_transaction_fee(&inputs, &[], None, true, true, feerate); + let prior = FundingContribution { + estimated_fee: estimated_fee_with_change, + inputs: inputs.clone(), + outputs: vec![], + change_output: Some(change.clone()), + feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let delta = Amount::from_sat(change.value.to_sat() - dust_limit.to_sat() + 1); + let target_value_added = prior.value_added().checked_add(delta).unwrap(); + let total_input_value: Amount = inputs.iter().map(|input| input.utxo.output.value).sum(); + let remaining_change = total_input_value + .checked_sub(target_value_added.checked_add(estimated_fee_with_change).unwrap()) + .unwrap(); + assert_eq!(remaining_change.to_sat(), dust_limit.to_sat() - 1); + assert!( + total_input_value >= target_value_added.checked_add(estimated_fee_no_change).unwrap() + ); + + let builder = FundingTemplate::new(None, None, Some(prior), Amount::MAX_MONEY) + .with_prior_contribution(feerate, FeeRate::MAX); + let contribution = + FundingBuilder(builder.0.add_value_inner(delta).unwrap()).build().unwrap(); + + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.inputs, inputs); + assert!(contribution.outputs.is_empty()); + assert_eq!(contribution.estimated_fee, estimated_fee_no_change); + assert_eq!( + contribution.value_added(), + total_input_value.checked_sub(estimated_fee_no_change).unwrap() + ); + assert!(contribution.value_added() > target_value_added); + } + + #[test] + fn test_funding_builder_remove_outputs_removes_all_matching_scripts() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let removed_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + let kept_script = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros()); + let removed_output_1 = + TxOut { value: Amount::from_sat(10_000), script_pubkey: removed_script.clone() }; + let removed_output_2 = + TxOut { value: Amount::from_sat(12_000), script_pubkey: removed_script.clone() }; + let kept_output = TxOut { value: Amount::from_sat(15_000), script_pubkey: kept_script }; + + let contribution = FundingBuilder::new( + FundingTemplate::new(None, None, None, Amount::MAX_MONEY), + feerate, + FeeRate::MAX, + ) + .add_output(removed_output_1) + .add_output(kept_output.clone()) + .add_output(removed_output_2) + .remove_outputs(&removed_script) + .build() + .unwrap(); + + assert_eq!(contribution.outputs, vec![kept_output]); + } + + #[test] + fn test_funding_builder_add_and_remove_value_update_request() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let value_added = Amount::from_sat(15_000); + let input_template = funding_input_sats(1); + let estimated_fee = estimate_transaction_fee( + std::slice::from_ref(&input_template), + &[], + None, + true, + false, + feerate, + ); + let selected_amount = value_added + estimated_fee; + let input = funding_input_sats(selected_amount.to_sat()); + let wallet = MustPayToWallet { + utxo: input.clone(), + change_output: None, + expected_must_pay_to_values: vec![value_added], + }; + + let contribution = FundingBuilder::new( + FundingTemplate::new(None, None, None, Amount::ZERO), + feerate, + FeeRate::MAX, + ) + .with_coin_selection_source_sync(wallet) + .add_value(Amount::from_sat(20_000)) + .unwrap() + .add_value(Amount::from_sat(5_000)) + .unwrap() + .remove_value(Amount::from_sat(10_000)) + .unwrap() + .build() + .unwrap(); + + assert_eq!(contribution.inputs, vec![input]); + assert!(contribution.outputs.is_empty()); + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.value_added(), value_added); + } + + #[test] + fn test_coin_selection_request_funds_outputs_from_inputs() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let value_added = Amount::from_sat(15_000); + let output = funding_output_sats(8_000); + let input = funding_input_sats(50_000); + let change_template = funding_output_sats(1_000); + let estimated_fee = estimate_transaction_fee( + std::slice::from_ref(&input), + std::slice::from_ref(&output), + Some(&change_template), + true, + false, + feerate, + ); + let change_value = input.utxo.output.value - value_added - output.value - estimated_fee; + let wallet = MustPayToWallet { + utxo: input, + change_output: Some(TxOut { + value: change_value, + script_pubkey: change_template.script_pubkey, + }), + expected_must_pay_to_values: vec![output.value, value_added], + }; + + let contribution = FundingBuilder::new( + FundingTemplate::new(None, None, None, Amount::MAX_MONEY), + feerate, + FeeRate::MAX, + ) + .with_coin_selection_source_sync(wallet) + .add_value(value_added) + .unwrap() + .add_output(output.clone()) + .build() + .unwrap(); + + assert_eq!(contribution.value_added(), value_added); + assert_eq!(contribution.outputs, vec![output]); + assert_eq!(contribution.change_output.as_ref().unwrap().value, change_value); + } + + #[test] + fn test_funding_builder_remove_value_saturates_at_zero() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let output = funding_output_sats(8_000); + let contribution = FundingBuilder::new( + FundingTemplate::new(None, None, None, Amount::MAX_MONEY), + feerate, + FeeRate::MAX, + ) + .with_coin_selection_source_sync(UnreachableWallet) + .add_value(Amount::from_sat(10_000)) + .unwrap() + .remove_value(Amount::from_sat(15_000)) + .unwrap() + .add_output(output.clone()) + .build() + .unwrap(); + + assert!(contribution.inputs.is_empty()); + assert_eq!(contribution.outputs, vec![output]); + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.value_added(), Amount::ZERO); + } + + #[test] + fn test_funding_builder_builds_manual_input_contribution_without_change() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let input = funding_input_sats(100_000); + let output = funding_output_sats(25_000); + + let contribution = FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_input(input.clone()) + .unwrap() + .add_output(output.clone()) + .build() + .unwrap(); + + let expected_fee = estimate_transaction_fee( + std::slice::from_ref(&input), + std::slice::from_ref(&output), + None, + true, + false, + feerate, + ); + assert_eq!(contribution.inputs, vec![input]); + assert_eq!(contribution.outputs, vec![output.clone()]); + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.input_mode, Some(FundingInputMode::ManuallySelected)); + assert_eq!(contribution.estimated_fee, expected_fee); + assert_eq!( + contribution.value_added(), + Amount::from_sat(100_000) - output.value - expected_fee, + ); + assert_eq!( + contribution.net_value(), + Amount::from_sat(100_000).to_signed().unwrap() + - output.value.to_signed().unwrap() + - expected_fee.to_signed().unwrap(), + ); + } + + #[test] + fn test_funding_builder_add_inputs_builds_manual_input_contribution() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let first_input = funding_input_sats(40_000); + let second_input = funding_input_sats(60_000); + let output = funding_output_sats(25_000); + + let contribution = FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_inputs(vec![first_input.clone(), second_input.clone()]) + .unwrap() + .add_output(output.clone()) + .build() + .unwrap(); + + let expected_fee = estimate_transaction_fee( + &[first_input.clone(), second_input.clone()], + std::slice::from_ref(&output), + None, + true, + false, + feerate, + ); + assert_eq!(contribution.inputs, vec![first_input, second_input]); + assert_eq!(contribution.outputs, vec![output.clone()]); + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.input_mode, Some(FundingInputMode::ManuallySelected)); + assert_eq!(contribution.estimated_fee, expected_fee); + assert_eq!( + contribution.value_added(), + Amount::from_sat(100_000) - output.value - expected_fee, + ); + } + + #[test] + fn test_funding_builder_rejects_duplicate_inputs() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let input = funding_input_sats(100_000); + + let result = FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_inputs(vec![input.clone(), input]) + .unwrap() + .build(); + + assert!(matches!(result, Err(FundingContributionError::InvalidSpliceValue),)); + } + + #[test] + fn test_funding_builder_rejects_duplicate_outputs() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let first_output = funding_output_sats(25_000); + let second_output = funding_output_sats(30_000); + assert_ne!(first_output, second_output); + assert_eq!(first_output.script_pubkey, second_output.script_pubkey); + + let result = FundingTemplate::new(None, None, None, Amount::MAX_MONEY) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_outputs(vec![first_output, second_output]) + .build(); + + assert!(matches!(result, Err(FundingContributionError::InvalidSpliceValue),)); + } + + #[test] + fn test_funding_builder_remove_input_updates_manual_input_request() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let first_input = funding_input_sats(40_000); + let second_input = funding_input_sats(60_000); + let output = funding_output_sats(25_000); + + let contribution = FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_inputs(vec![first_input.clone(), second_input.clone()]) + .unwrap() + .remove_input(&first_input.utxo.outpoint) + .unwrap() + .add_output(output.clone()) + .build() + .unwrap(); + + let expected_fee = estimate_transaction_fee( + std::slice::from_ref(&second_input), + std::slice::from_ref(&output), + None, + true, + false, + feerate, + ); + assert_eq!(contribution.inputs, vec![second_input]); + assert_eq!(contribution.outputs, vec![output.clone()]); + assert_eq!(contribution.input_mode, Some(FundingInputMode::ManuallySelected)); + assert_eq!( + contribution.value_added(), + Amount::from_sat(60_000) - output.value - expected_fee, + ); + } + + #[test] + fn test_splice_in_inputs_builds_manual_input_contribution() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let first_input = funding_input_sats(40_000); + let second_input = funding_input_sats(60_000); + + let contribution = FundingTemplate::new(None, None, None, Amount::ZERO) + .splice_in_inputs( + vec![first_input.clone(), second_input.clone()], + feerate, + FeeRate::MAX, + ) + .unwrap(); + + let expected_fee = estimate_transaction_fee( + &[first_input.clone(), second_input.clone()], + &[], + None, + true, + false, + feerate, + ); + assert_eq!(contribution.inputs, vec![first_input, second_input]); + assert!(contribution.outputs.is_empty()); + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.input_mode, Some(FundingInputMode::ManuallySelected)); + assert_eq!(contribution.value_added(), Amount::from_sat(100_000) - expected_fee); + } + + #[test] + fn test_splice_in_inputs_appends_to_prior_manual_inputs() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let prior_input = funding_input_sats(40_000); + let additional_input = funding_input_sats(60_000); + let prior_fee = estimate_transaction_fee( + std::slice::from_ref(&prior_input), + &[], + None, + true, + false, + feerate, + ); + let prior = FundingContribution { + estimated_fee: prior_fee, + inputs: vec![prior_input.clone()], + outputs: vec![], + change_output: None, + feerate, + max_feerate: FeeRate::MAX, + is_splice: false, + input_mode: Some(FundingInputMode::ManuallySelected), + }; + + let contribution = FundingTemplate::new(None, None, Some(prior), Amount::MAX_MONEY) + .splice_in_inputs(vec![additional_input.clone()], feerate, FeeRate::MAX) + .unwrap(); + + assert_eq!(contribution.inputs, vec![prior_input, additional_input]); + assert!(contribution.outputs.is_empty()); + assert_eq!(contribution.input_mode, Some(FundingInputMode::ManuallySelected)); + } + + #[test] + fn test_sync_funding_builder_manual_inputs_insufficient_do_not_fallback_to_coin_selection() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let builder = FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_input(funding_input_sats(1)) + .unwrap(); + let builder = + SyncFundingBuilder(builder.0.with_state(SyncCoinSelectionSource(UnreachableWallet))); + + assert!(matches!( + builder.build(), + Err(FundingContributionError::ManuallySelectedInputsInsufficient), + )); + } + + #[test] + fn test_funding_builder_rejects_manual_inputs_with_value_request() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let builder = FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_input(funding_input_sats(100_000)) + .unwrap(); + let result = builder.clone().0.add_value_inner(Amount::from_sat(1_000)); + assert!(matches!(result, Err(FundingContributionError::InvalidSpliceValue),)); + + let builder = + SyncFundingBuilder(builder.0.with_state(SyncCoinSelectionSource(UnreachableWallet))); + let result = builder.remove_value(Amount::from_sat(1_000)); + assert!(matches!(result, Err(FundingContributionError::InvalidSpliceValue),)); + } + + #[test] + fn test_funding_builder_rejects_manual_inputs_on_coin_selected_prior() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let prior_input = funding_input_sats(100_000); + let prior_outpoint = prior_input.utxo.outpoint; + let prior = FundingContribution { + estimated_fee: Amount::from_sat(1_000), + inputs: vec![prior_input], + outputs: vec![], + change_output: Some(funding_output_sats(10_000)), + feerate, + max_feerate: FeeRate::MAX, + is_splice: false, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let builder = FundingTemplate::new(None, None, Some(prior), Amount::MAX_MONEY) + .with_prior_contribution(feerate, FeeRate::MAX); + + assert!(matches!( + builder.clone().add_input(funding_input_sats(50_000)), + Err(FundingContributionError::InvalidSpliceValue), + )); + assert!(matches!( + builder.remove_input(&prior_outpoint), + Err(FundingContributionError::InvalidSpliceValue), + )); + } + + #[test] + fn test_funding_builder_validates_manual_input_max_money() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let inputs = vec![funding_input_sats(Amount::MAX_MONEY.to_sat()), funding_input_sats(1)]; + + let builder = FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, FeeRate::MAX) + .add_inputs(inputs) + .unwrap(); + + assert!(matches!(builder.build(), Err(FundingContributionError::InvalidSpliceValue),)); + } + + #[test] + fn test_build_from_prior_manual_inputs_exact_match_reuses_and_adjusts() { + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let input = funding_input_sats(100_000); + let output = funding_output_sats(20_000); + let estimated_fee = estimate_transaction_fee( + std::slice::from_ref(&input), + std::slice::from_ref(&output), + None, + true, + false, + original_feerate, + ); + let prior = FundingContribution { + estimated_fee, + inputs: vec![input.clone()], + outputs: vec![output.clone()], + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: false, + input_mode: Some(FundingInputMode::ManuallySelected), + }; + + let contribution = FundingTemplate::new(None, None, Some(prior), Amount::MAX_MONEY) + .with_prior_contribution(target_feerate, FeeRate::MAX) + .build() + .unwrap(); + + assert_eq!(contribution.inputs, vec![input]); + assert_eq!(contribution.outputs, vec![output]); + assert_eq!(contribution.feerate, target_feerate); + assert_eq!(contribution.input_mode, Some(FundingInputMode::ManuallySelected)); + } + + #[test] + fn test_build_from_prior_manual_inputs_changed_request_insufficient_maps_error() { + let feerate = FeeRate::from_sat_per_kwu(2000); + let input = funding_input_sats(50_000); + let estimated_fee = + estimate_transaction_fee(std::slice::from_ref(&input), &[], None, true, false, feerate); + let prior = FundingContribution { + estimated_fee, + inputs: vec![input], + outputs: vec![], + change_output: None, + feerate, + max_feerate: FeeRate::MAX, + is_splice: false, + input_mode: Some(FundingInputMode::ManuallySelected), + }; + + let result = FundingTemplate::new(None, None, Some(prior), Amount::ZERO) + .with_prior_contribution(feerate, FeeRate::MAX) + .add_output(funding_output_sats(60_000)) + .build(); + + assert!(matches!( + result, + Err(FundingContributionError::ManuallySelectedInputsInsufficient), + )); + } + + #[test] + fn test_for_acceptor_at_feerate_manual_inputs_balance_insufficient() { + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(100_000); + let inputs = vec![funding_input_sats(100_000)]; + let outputs = vec![funding_output_sats(80_000)]; + let net_value_without_fee = Amount::from_sat(20_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &outputs, None, true, true, original_feerate); + let target_fee = + estimate_transaction_fee(&inputs, &outputs, None, false, true, target_feerate); + assert!(target_fee > net_value_without_fee); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs, + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::ManuallySelected), + }; + + let holder_balance = target_fee + .checked_sub(net_value_without_fee) + .and_then(|shortfall| shortfall.checked_sub(Amount::from_sat(1))) + .unwrap(); + match contribution.for_acceptor_at_feerate(target_feerate, holder_balance) { + Err(FeeRateAdjustmentError::FeeBufferInsufficient { source, available, required }) => { + assert_eq!(source, "channel balance"); + assert_eq!(available, target_fee - Amount::from_sat(1)); + assert_eq!(required, target_fee); + }, + other => panic!("Expected channel-balance shortfall, got {other:?}"), + } + } + + #[test] + fn test_for_acceptor_at_feerate_manual_inputs_balance_sufficient() { + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(100_000); + let inputs = vec![funding_input_sats(100_000)]; + let outputs = vec![funding_output_sats(80_000)]; + let net_value_without_fee = Amount::from_sat(20_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &outputs, None, true, true, original_feerate); + let target_fee = + estimate_transaction_fee(&inputs, &outputs, None, false, true, target_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: inputs.clone(), + outputs: outputs.clone(), + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::ManuallySelected), + }; + + let holder_balance = target_fee.checked_sub(net_value_without_fee).unwrap(); + let adjusted = + contribution.for_acceptor_at_feerate(target_feerate, holder_balance).unwrap(); + + assert_eq!(adjusted.inputs, inputs); + assert_eq!(adjusted.outputs, outputs); + assert_eq!(adjusted.estimated_fee, target_fee); + assert_eq!( + adjusted.net_value(), + net_value_without_fee.to_signed().unwrap() - target_fee.to_signed().unwrap(), + ); + } + + #[test] + fn test_build_funding_contribution_validates_max_money() { + let over_max = Amount::MAX_MONEY + Amount::from_sat(1); + let feerate = FeeRate::from_sat_per_kwu(2000); + + // splice_in_sync with value_added > MAX_MONEY + { + let template = FundingTemplate::new(None, None, None, Amount::ZERO); + assert!(matches!( + template.splice_in_sync(over_max, feerate, feerate, UnreachableWallet), + Err(FundingContributionError::InvalidSpliceValue), + )); + } + + // splice_out with single output value > MAX_MONEY + { + let template = FundingTemplate::new(None, None, None, Amount::ZERO); + let outputs = vec![funding_output_sats(over_max.to_sat())]; + assert!(matches!( + template.splice_out(outputs, feerate, feerate), + Err(FundingContributionError::InvalidSpliceValue), + )); + } + + // splice_out with multiple outputs summing > MAX_MONEY + { + let template = FundingTemplate::new(None, None, None, Amount::ZERO); + let half_over = Amount::MAX_MONEY / 2 + Amount::from_sat(1); + let outputs = vec![ + funding_output_sats(half_over.to_sat()), + funding_output_sats(half_over.to_sat()), + ]; + assert!(matches!( + template.splice_out(outputs, feerate, feerate), + Err(FundingContributionError::InvalidSpliceValue), + )); + } + } + + #[test] + fn test_funding_builder_validates_mixed_request_max_money() { + let over_max = Amount::MAX_MONEY + Amount::from_sat(1); + let feerate = FeeRate::from_sat_per_kwu(2000); + + // Mixed add/remove request with value_added > MAX_MONEY. + assert!(matches!( + FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, feerate) + .with_coin_selection_source_sync(UnreachableWallet) + .add_value(over_max) + .unwrap() + .add_outputs(vec![funding_output_sats(1_000)]) + .build(), + Err(FundingContributionError::InvalidSpliceValue), + )); + + // Mixed add/remove request with outputs summing > MAX_MONEY. + let half_over = Amount::MAX_MONEY / 2 + Amount::from_sat(1); + assert!(matches!( + FundingTemplate::new(None, None, None, Amount::ZERO) + .without_prior_contribution(feerate, feerate) + .with_coin_selection_source_sync(UnreachableWallet) + .add_value(Amount::from_sat(1_000)) + .unwrap() + .add_outputs(vec![ + funding_output_sats(half_over.to_sat()), + funding_output_sats(half_over.to_sat()), + ]) + .build(), + Err(FundingContributionError::InvalidSpliceValue), + )); + } + + #[test] + fn test_build_funding_contribution_validates_feerate_range() { + let low = FeeRate::from_sat_per_kwu(1000); + let high = FeeRate::from_sat_per_kwu(2000); + + // min_feerate > max_feerate is rejected + { + let template = FundingTemplate::new(None, None, None, Amount::ZERO); + assert!(matches!( + template.splice_in_sync(Amount::from_sat(10_000), high, low, UnreachableWallet), + Err(FundingContributionError::FeeRateExceedsMaximum { .. }), + )); + } + + // min_feerate < min_rbf_feerate is rejected + { + let template = FundingTemplate::new(None, Some(high), None, Amount::ZERO); + assert!(matches!( + template.splice_in_sync( + Amount::from_sat(10_000), + low, + FeeRate::MAX, + UnreachableWallet + ), + Err(FundingContributionError::FeeRateBelowRbfMinimum { .. }), + )); + } + } + + #[test] + fn test_build_funding_contribution_rejects_oversized_prevtx() { + use crate::util::ser::Writeable; + + let feerate = FeeRate::from_sat_per_kwu(2000); + let prevtx = Transaction { + input: vec![], + output: vec![funding_output_sats(50_000); 2_200], + version: Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + }; + assert!(prevtx.serialized_length() > crate::ln::LN_MAX_MSG_LEN); + + let wallet = SingleUtxoWallet { + utxo: ConfirmedUtxo::new_p2wpkh(prevtx, 0).unwrap(), + change_output: None, + }; + assert!(matches!( + FundingTemplate::new(None, None, None, Amount::ZERO) + .with_prior_contribution(feerate, feerate) + .with_coin_selection_source_sync(wallet) + .add_value(Amount::from_sat(10_000)) + .unwrap() + .build(), + Err(FundingContributionError::PrevTxTooLarge), + )); + } + + #[test] + fn test_for_acceptor_at_feerate_higher_change_adjusted() { + // Splice-in: higher target feerate reduces the change output. + // The fee overestimates (with is_initiator=true) by including common TX fields, shared + // output, and shared input weight. So we need a sufficiently high target feerate for the + // acceptor's target fee to exceed the original fee estimate, causing the change to decrease. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(6000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + + // Fee estimate computed as initiator (overestimate), including change output weight. + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: inputs.clone(), + outputs: vec![], + change_output: Some(change.clone()), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let net_value_before = contribution.net_value(); + let contribution = + contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY).unwrap(); + + // Target fee at target feerate for acceptor (is_initiator=false), including change weight. + let expected_target_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), false, true, target_feerate); + let expected_change = estimated_fee + Amount::from_sat(10_000) - expected_target_fee; + + assert_eq!(contribution.estimated_fee, expected_target_fee); + assert!(contribution.change_output.is_some()); + assert_eq!(contribution.change_output.as_ref().unwrap().value, expected_change); + assert!(expected_change < Amount::from_sat(10_000)); // Change reduced + assert_eq!(contribution.net_value(), net_value_before); + } + + #[test] + fn test_for_acceptor_at_feerate_lower_rejected_too_low() { + // Splice-in: target feerate below our minimum is rejected as FeeRateTooLow. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(1000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeRateTooLow { .. }))); + } + + #[test] + fn test_for_acceptor_at_feerate_change_removed() { + // Splice-in: feerate high enough that change drops below dust and is removed, + // but the fee buffer (estimated_fee + change) still covers the fee without the change output. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(7000); + let value_added = Amount::from_sat(50_000); + let change_value = Amount::from_sat(500); + + // Compute estimated_fee first (weight-based, independent of input value). + let dummy_inputs = vec![funding_input_sats(1)]; + let change = funding_output_sats(change_value.to_sat()); + let estimated_fee = estimate_transaction_fee( + &dummy_inputs, + &[], + Some(&change), + true, + true, + original_feerate, + ); + + // Realistic input: value_added + estimated_fee + change (what coin selection produces). + let input_value = value_added + estimated_fee + change_value; + let inputs = vec![funding_input_sats(input_value.to_sat())]; + let change = funding_output_sats(change_value.to_sat()); + + let contribution = FundingContribution { + estimated_fee, + inputs: inputs.clone(), + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let net_value_before = contribution.net_value(); + let contribution = + contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY).unwrap(); + + // Change should be removed; estimated_fee updated to no-change target fee. + assert!(contribution.change_output.is_none()); + let expected_fee_no_change = + estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate); + assert_eq!(contribution.estimated_fee, expected_fee_no_change); + // The surplus (old fee buffer - new fee) goes to value_added, increasing net_value. + let surplus = estimated_fee + change_value - expected_fee_no_change; + assert_eq!(contribution.net_value(), net_value_before + surplus.to_signed().unwrap()); + } + + #[test] + fn test_for_acceptor_at_feerate_too_high_rejected() { + // Splice-in: feerate so high that even without change, the fee can't be covered. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(100_000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(500); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. }))); + } + + #[test] + fn test_for_acceptor_at_feerate_splice_out_sufficient() { + // Splice-out (no inputs): the fee estimate from the is_initiator=true overestimate covers + // the acceptor's target fee at a moderately higher target feerate. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let outputs = vec![funding_output_sats(50_000)]; + + let estimated_fee = + estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: vec![], + outputs: outputs.clone(), + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let contribution = + contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY).unwrap(); + // estimated_fee is updated to the target fee; surplus goes back to channel balance. + let expected_target_fee = + estimate_transaction_fee(&[], &outputs, None, false, true, target_feerate); + assert_eq!(contribution.estimated_fee, expected_target_fee); + assert!(expected_target_fee <= estimated_fee); + } + + #[test] + fn test_for_acceptor_at_feerate_splice_out_insufficient() { + // Splice-out: channel balance too small for outputs + target fee at high target feerate. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(50_000); + let outputs = vec![funding_output_sats(50_000)]; + + let estimated_fee = + estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: vec![], + outputs, + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + // Balance of 55,000 sats can't cover outputs (50,000) + target_fee at 50k sat/kwu. + let spliceable_balance = Amount::from_sat(55_000); + let result = contribution.for_acceptor_at_feerate(target_feerate, spliceable_balance); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. }))); + } + + #[test] + fn test_net_value_for_acceptor_at_feerate_splice_in() { + // Splice-in: net_value_for_acceptor_at_feerate returns the same value as net_value() since + // splice-in fees are paid by inputs, not from channel balance. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + let change_value = change.value; + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + // For splice-in with change that stays above dust, the surplus is absorbed by the change + // output so net_value_for_acceptor_at_feerate equals net_value. + let net_at_feerate = contribution + .net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY) + .unwrap(); + assert_eq!(net_at_feerate, contribution.net_value()); + assert_eq!( + net_at_feerate, + (Amount::from_sat(100_000) - estimated_fee - change_value).to_signed().unwrap(), + ); + } + + #[test] + fn test_net_value_for_acceptor_at_feerate_splice_out() { + // Splice-out: net_value_for_acceptor_at_feerate returns the adjusted value using the target fee + // at the target feerate. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let outputs = vec![funding_output_sats(50_000)]; + + let estimated_fee = + estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: vec![], + outputs: outputs.clone(), + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let net_at_feerate = contribution + .net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY) + .unwrap(); + + // The target fee at target feerate should be less than the initiator's fee estimate. + let target_fee = estimate_transaction_fee(&[], &outputs, None, false, true, target_feerate); + let expected_net = SignedAmount::ZERO + - Amount::from_sat(50_000).to_signed().unwrap() + - target_fee.to_signed().unwrap(); + assert_eq!(net_at_feerate, expected_net); + + // Should be less negative than net_value() which uses the higher fee estimate. + assert!(net_at_feerate > contribution.net_value()); + } + + #[test] + fn test_net_value_for_acceptor_at_feerate_does_not_mutate() { + // Verify net_value_for_acceptor_at_feerate does not modify the contribution. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(5000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let net_before = contribution.net_value(); + let fee_before = contribution.estimated_fee; + let change_before = contribution.change_output.as_ref().unwrap().value; + + let _ = contribution.net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + + // Nothing should have changed. + assert_eq!(contribution.net_value(), net_before); + assert_eq!(contribution.estimated_fee, fee_before); + assert_eq!(contribution.change_output.as_ref().unwrap().value, change_before); + } + + #[test] + fn test_net_value_for_acceptor_at_feerate_too_high() { + // net_value_for_acceptor_at_feerate returns Err when feerate can't be accommodated. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(100_000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(500); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = + contribution.net_value_for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. }))); + } + + #[test] + fn test_for_acceptor_at_feerate_exceeds_max_rejected() { + // Splice-in: target feerate exceeds max_feerate and target fee exceeds the fee buffer, + // so the adjustment is rejected as FeeRateTooHigh. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let max_feerate = FeeRate::from_sat_per_kwu(3000); + let target_feerate = FeeRate::from_sat_per_kwu(100_000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeRateTooHigh { .. }))); + } + + #[test] + fn test_for_acceptor_at_feerate_exceeds_max_allowed() { + // Splice-in: target feerate exceeds max_feerate but the acceptor's target fee + // (is_initiator=false at target) is less than the fee buffer (is_initiator=true at + // original feerate). This works because the initiator fee estimate includes ~598 WU of + // extra weight (common TX fields, funding output, shared input) that the acceptor + // doesn't pay for, so the fee buffer is ~2.5x larger than the acceptor's target fee at + // the same feerate. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let max_feerate = FeeRate::from_sat_per_kwu(3000); + let target_feerate = FeeRate::from_sat_per_kwu(4000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change.clone()), + feerate: original_feerate, + max_feerate, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(result.is_ok()); + let adjusted = result.unwrap(); + + // The acceptor's target fee at target (4000, is_initiator=false) is less than the + // fee estimate at original (2000, is_initiator=true) due to the ~2.5x weight ratio, + // so change increases despite the higher feerate. + assert!(adjusted.change_output.is_some()); + assert!(adjusted.change_output.as_ref().unwrap().value > Amount::from_sat(10_000)); + } + + #[test] + fn test_for_acceptor_at_feerate_within_range() { + // Splice-in: target feerate is between min and max, so the min/max checks + // don't interfere and the normal adjustment logic applies. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let max_feerate = FeeRate::from_sat_per_kwu(5000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(result.is_ok()); + let adjusted = result.unwrap(); + + // At a higher target feerate, the target fee increases so change should decrease + // (or stay the same if the fee estimate absorbs the difference). + // The key assertion is that the adjustment succeeds with a valid change output. + assert!(adjusted.change_output.is_some()); + } + + #[test] + fn test_for_acceptor_at_feerate_no_change_shortfall_from_value_added() { + // Inputs present, no change output. Higher target feerate makes target_fee > estimated_fee. + // With realistic inputs (no coin selection surplus), the fee buffer is just estimated_fee, + // so the shortfall cannot be absorbed and the contribution is dropped. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(20_000); + let value_added = Amount::from_sat(50_000); + + // Compute estimated_fee first (weight-based, independent of input value). + let dummy_inputs = vec![funding_input_sats(1)]; + let estimated_fee = + estimate_transaction_fee(&dummy_inputs, &[], None, true, true, original_feerate); + + // Realistic input: value_added + estimated_fee (what coin selection produces, no surplus). + let inputs = vec![funding_input_sats((value_added + estimated_fee).to_sat())]; + let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate); + + // Verify our setup: target_fee > estimated_fee (shortfall exists) and the fee buffer + // (estimated_fee, with no coin selection surplus) cannot cover it. + assert!(target_fee > estimated_fee); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. }))); + } + + #[test] + fn test_for_acceptor_at_feerate_no_change_insufficient() { + // Inputs present, no change output. The target feerate is so high that the fee buffer + // (total input value minus value_added) cannot cover the target fee. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(20_000); + let value_added = Amount::from_sat(1); + + // Compute estimated_fee first (weight-based, independent of input value). + let dummy_inputs = vec![funding_input_sats(1)]; + let estimated_fee = + estimate_transaction_fee(&dummy_inputs, &[], None, true, true, original_feerate); + + // Realistic input: value_added + estimated_fee (no surplus). + let inputs = vec![funding_input_sats((value_added + estimated_fee).to_sat())]; + let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate); + assert!(target_fee > estimated_fee); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. }))); + } + + #[test] + fn test_for_acceptor_at_feerate_no_change_surplus_below_dust() { + // Inputs present, no change output. The acceptor built their contribution at a low + // feerate as if they were the initiator (including common TX fields in estimated_fee). + // The initiator proposes a ~3x higher feerate. At that rate, the acceptor's target fee + // (only their personal input weight) nearly matches the original fee estimate, leaving a + // small surplus below the dust limit. + let original_feerate = FeeRate::from_sat_per_kwu(1000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let inputs = vec![funding_input_sats(100_000)]; + + // estimated_fee includes common TX fields (is_initiator=true) at the original feerate. + let estimated_fee = + estimate_transaction_fee(&inputs, &[], None, true, true, original_feerate); + + // target_fee only includes the acceptor's contributed weight (is_initiator=false) at the + // higher target feerate. + let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, target_feerate); + + // Verify our setup: surplus is positive and below the P2WPKH dust limit (294 sats). + assert!(estimated_fee > target_fee); + let dust_limit = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()).minimal_non_dust(); + assert!(estimated_fee - target_fee < dust_limit); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY); + assert!(result.is_ok()); + let adjusted = result.unwrap(); + assert!(adjusted.change_output.is_none()); + assert_eq!(adjusted.estimated_fee, target_fee); + } + + #[test] + fn test_for_acceptor_at_feerate_no_change_surplus_absorbed() { + // Inputs, no change. The estimated_fee (is_initiator=true) far exceeds the acceptor's + // target fee (is_initiator=false). The surplus stays in the channel contribution rather + // than being burned as excess fees. + let feerate = FeeRate::from_sat_per_kwu(2000); + let value_added = Amount::from_sat(50_000); + + // Compute estimated_fee first (weight-based, independent of input value). + let dummy_inputs = vec![funding_input_sats(1)]; + let estimated_fee = estimate_transaction_fee(&dummy_inputs, &[], None, true, true, feerate); + + // Realistic input: value_added + estimated_fee (no surplus). + let inputs = vec![funding_input_sats((value_added + estimated_fee).to_sat())]; + + // Initiator fee estimate includes common TX fields + shared output + shared input weight, + // making it ~3x the acceptor's target fee at the same feerate. + let target_fee = estimate_transaction_fee(&inputs, &[], None, false, true, feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: None, + feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + // target == min feerate, so FeeRateTooLow check passes. + // The surplus (estimated_fee - target_fee) goes to value_added (shared output). + let net_value_before = contribution.net_value(); + let result = contribution.for_acceptor_at_feerate(feerate, Amount::MAX_MONEY); + assert!(result.is_ok()); + let adjusted = result.unwrap(); + assert!(adjusted.change_output.is_none()); + assert_eq!(adjusted.estimated_fee, target_fee); + let surplus = estimated_fee - target_fee; + assert_eq!(adjusted.value_added(), value_added + surplus); + assert_eq!(adjusted.net_value(), net_value_before + surplus.to_signed().unwrap()); + } + + #[test] + fn test_for_acceptor_at_feerate_fee_buffer_overflow_with_change() { + // Overflow in estimated_fee + change value should surface as FeeBufferOverflow. + let feerate = FeeRate::from_sat_per_kwu(2000); + let contribution = FundingContribution { + estimated_fee: Amount::MAX, + inputs: vec![funding_input_sats(100_000)], + outputs: vec![], + change_output: Some(funding_output_sats(1)), + feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let result = contribution.for_acceptor_at_feerate(feerate, Amount::MAX_MONEY); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferOverflow))); + } + + #[test] + fn test_for_acceptor_at_feerate_splice_out_balance_insufficient() { + // Splice-out: channel balance too small to cover outputs + target fee. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let outputs = vec![funding_output_sats(50_000)]; + + let estimated_fee = + estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: vec![], + outputs: outputs.clone(), + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + // Balance of 40,000 sats is less than outputs (50,000) + target_fee. + let spliceable_balance = Amount::from_sat(40_000); + let result = contribution.for_acceptor_at_feerate(target_feerate, spliceable_balance); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. }))); + } + + #[test] + fn test_for_acceptor_at_feerate_splice_out_balance_sufficient() { + // Splice-out: channel balance large enough to cover outputs + target fee. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let outputs = vec![funding_output_sats(50_000)]; + + let estimated_fee = + estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: vec![], + outputs: outputs.clone(), + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + // Balance of 100,000 sats is more than outputs (50,000) + target_fee. + let spliceable_balance = Amount::from_sat(100_000); + let contribution = + contribution.for_acceptor_at_feerate(target_feerate, spliceable_balance).unwrap(); + let expected_target_fee = + estimate_transaction_fee(&[], &outputs, None, false, true, target_feerate); + assert_eq!(contribution.estimated_fee, expected_target_fee); + } + + #[test] + fn test_net_value_for_acceptor_at_feerate_splice_out_balance_insufficient() { + // Splice-out: net_value_for_acceptor_at_feerate returns Err when channel balance + // is too small to cover outputs + target fee. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let outputs = vec![funding_output_sats(50_000)]; + + let estimated_fee = + estimate_transaction_fee(&[], &outputs, None, true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs: vec![], + outputs, + change_output: None, + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + // Balance of 40,000 sats is less than outputs (50,000) + target_fee. + let spliceable_balance = Amount::from_sat(40_000); + let result = + contribution.net_value_for_acceptor_at_feerate(target_feerate, spliceable_balance); + assert!(matches!(result, Err(FeeRateAdjustmentError::FeeBufferInsufficient { .. }))); + } + + #[test] + fn test_for_initiator_at_feerate_higher_fee_than_acceptor() { + // Verify that the initiator fee estimate is higher than the acceptor estimate at the + // same feerate, since the initiator pays for common fields + shared input/output. + let original_feerate = FeeRate::from_sat_per_kwu(2000); + let target_feerate = FeeRate::from_sat_per_kwu(3000); + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, original_feerate); + + let contribution = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: original_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let acceptor = contribution + .clone() + .for_acceptor_at_feerate(target_feerate, Amount::MAX_MONEY) + .unwrap(); + let initiator = + contribution.for_initiator_at_feerate(target_feerate, Amount::MAX_MONEY).unwrap(); + + // Initiator pays more in fees (common fields + shared input/output weight). + assert!(initiator.estimated_fee > acceptor.estimated_fee); + // Initiator has less change remaining. + assert!( + initiator.change_output.as_ref().unwrap().value + < acceptor.change_output.as_ref().unwrap().value + ); + // Both have the adjusted feerate. + assert_eq!(initiator.feerate, target_feerate); + assert_eq!(acceptor.feerate, target_feerate); + } + + #[test] + fn test_rbf_rejects_max_feerate_below_min_rbf_feerate() { + // When the caller's max_feerate is below the minimum RBF feerate, + // rbf_prior_contribution_sync should return an error. + let prior_feerate = FeeRate::from_sat_per_kwu(2000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let max_feerate = FeeRate::from_sat_per_kwu(2020); + + let prior = FundingContribution { + estimated_fee: Amount::from_sat(1_000), + inputs: vec![funding_input_sats(100_000)], + outputs: vec![], + change_output: None, + feerate: prior_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + // max_feerate (2020) < min_rbf_feerate (2025). + let template = + FundingTemplate::new(None, Some(min_rbf_feerate), Some(prior), Amount::MAX_MONEY); + assert!(matches!( + template.rbf_prior_contribution_sync(None, max_feerate, UnreachableWallet), + Err(FundingContributionError::FeeRateExceedsMaximum { .. }), + )); + } + + #[test] + fn test_rbf_adjusts_prior_to_rbf_feerate() { + // When the prior contribution's feerate is below the minimum RBF feerate and holder + // balance is available, rbf_prior_contribution_sync should adjust the prior to the + // RBF feerate. + let prior_feerate = FeeRate::from_sat_per_kwu(2000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let max_feerate = FeeRate::from_sat_per_kwu(5000); + + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, prior_feerate); + + let prior = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: prior_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let template = + FundingTemplate::new(None, Some(min_rbf_feerate), Some(prior), Amount::MAX_MONEY); + let contribution = + template.rbf_prior_contribution_sync(None, max_feerate, UnreachableWallet).unwrap(); + assert_eq!(contribution.feerate, min_rbf_feerate); + assert_eq!(contribution.max_feerate, max_feerate); + } + + #[test] + fn test_rbf_uses_explicit_override_feerate() { + let prior_feerate = FeeRate::from_sat_per_kwu(2000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let override_feerate = FeeRate::from_sat_per_kwu(2100); + let max_feerate = FeeRate::from_sat_per_kwu(5000); + + let inputs = vec![funding_input_sats(100_000)]; + let change = funding_output_sats(10_000); + let estimated_fee = + estimate_transaction_fee(&inputs, &[], Some(&change), true, true, prior_feerate); + + let prior = FundingContribution { + estimated_fee, + inputs, + outputs: vec![], + change_output: Some(change), + feerate: prior_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let template = + FundingTemplate::new(None, Some(min_rbf_feerate), Some(prior), Amount::MAX_MONEY); + let contribution = template + .rbf_prior_contribution_sync(Some(override_feerate), max_feerate, UnreachableWallet) + .unwrap(); + assert_eq!(contribution.feerate, override_feerate); + assert_eq!(contribution.max_feerate, max_feerate); + } + + #[test] + fn test_rbf_rejects_explicit_override_below_min_rbf_feerate() { + let prior_feerate = FeeRate::from_sat_per_kwu(2000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let override_feerate = FeeRate::from_sat_per_kwu(2024); + + let prior = FundingContribution { + estimated_fee: Amount::from_sat(1_000), + inputs: vec![funding_input_sats(100_000)], + outputs: vec![], + change_output: None, + feerate: prior_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let template = + FundingTemplate::new(None, Some(min_rbf_feerate), Some(prior), Amount::MAX_MONEY); + assert!(matches!( + template.rbf_prior_contribution_sync( + Some(override_feerate), + FeeRate::MAX, + UnreachableWallet, + ), + Err(FundingContributionError::FeeRateBelowRbfMinimum { .. }), + )); + } + + #[test] + fn test_rbf_rejects_explicit_override_above_max_feerate() { + let prior_feerate = FeeRate::from_sat_per_kwu(2000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let override_feerate = FeeRate::from_sat_per_kwu(2100); + let max_feerate = FeeRate::from_sat_per_kwu(2099); + + let prior = FundingContribution { + estimated_fee: Amount::from_sat(1_000), + inputs: vec![funding_input_sats(100_000)], + outputs: vec![], + change_output: None, + feerate: prior_feerate, + max_feerate: FeeRate::MAX, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let template = + FundingTemplate::new(None, Some(min_rbf_feerate), Some(prior), Amount::MAX_MONEY); + assert!(matches!( + template.rbf_prior_contribution_sync( + Some(override_feerate), + max_feerate, + UnreachableWallet, + ), + Err(FundingContributionError::FeeRateExceedsMaximum { .. }), + )); + } + + /// A mock wallet that returns a single UTXO for coin selection. + struct SingleUtxoWallet { + utxo: ConfirmedUtxo, + change_output: Option<TxOut>, + } + + impl CoinSelectionSourceSync for SingleUtxoWallet { + fn select_confirmed_utxos( + &self, _claim_id: Option<ClaimId>, _must_spend: Vec<Input>, _must_pay_to: &[TxOut], + _target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64, + ) -> Result<CoinSelection, ()> { + Ok(CoinSelection { + confirmed_utxos: vec![self.utxo.clone()], + change_output: self.change_output.clone(), + }) + } + fn sign_psbt(&self, _psbt: Psbt) -> Result<Transaction, ()> { + unreachable!("should not reach signing") + } + } + + fn shared_input(value_sats: u64) -> Input { + Input { + outpoint: bitcoin::OutPoint::null(), + previous_utxo: TxOut { + value: Amount::from_sat(value_sats), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }, + satisfaction_weight: 107, + } + } + + #[test] + fn test_rbf_unadjusted_splice_out_runs_coin_selection() { + // When the prior contribution's feerate is below the minimum RBF feerate and no + // holder balance is available, rbf_prior_contribution_sync should run coin selection to + // add inputs that cover the higher RBF fee. + let prior_feerate = FeeRate::from_sat_per_kwu(2000); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let withdrawal = funding_output_sats(20_000); + + let prior = FundingContribution { + estimated_fee: Amount::from_sat(500), + inputs: vec![], + outputs: vec![withdrawal.clone()], + change_output: None, + feerate: prior_feerate, + max_feerate: prior_feerate, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let template = FundingTemplate::new( + Some(shared_input(100_000)), + Some(min_rbf_feerate), + Some(prior), + Amount::ZERO, + ); + + let wallet = SingleUtxoWallet { + utxo: funding_input_sats(50_000), + change_output: Some(funding_output_sats(25_000)), + }; + + // rbf_prior_contribution_sync should succeed and the contribution should have inputs from + // coin selection. + let contribution = + template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet).unwrap(); + assert!(!contribution.inputs.is_empty(), "coin selection should have added inputs"); + assert!(contribution.value_added() > Amount::ZERO); + assert_eq!(contribution.outputs, vec![withdrawal]); + assert_eq!(contribution.feerate, min_rbf_feerate); + } + + #[test] + fn test_rbf_unadjusted_uses_callers_max_feerate() { + // When the prior contribution's feerate is below the minimum RBF feerate and no + // holder balance is available, rbf_prior_contribution_sync should use the caller's + // max_feerate (not the prior's) for the resulting contribution. + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let prior_max_feerate = FeeRate::from_sat_per_kwu(50_000); + let callers_max_feerate = FeeRate::from_sat_per_kwu(10_000); + let withdrawal = funding_output_sats(20_000); + + let prior = FundingContribution { + estimated_fee: Amount::from_sat(500), + inputs: vec![], + outputs: vec![withdrawal.clone()], + change_output: None, + feerate: FeeRate::from_sat_per_kwu(2000), + max_feerate: prior_max_feerate, + is_splice: true, + input_mode: Some(FundingInputMode::CoinSelected), + }; + + let template = FundingTemplate::new( + Some(shared_input(100_000)), + Some(min_rbf_feerate), + Some(prior), + Amount::MAX_MONEY, + ); + + let wallet = SingleUtxoWallet { + utxo: funding_input_sats(50_000), + change_output: Some(funding_output_sats(25_000)), + }; + + let contribution = + template.rbf_prior_contribution_sync(None, callers_max_feerate, &wallet).unwrap(); + assert_eq!( + contribution.max_feerate, callers_max_feerate, + "should use caller's max_feerate, not prior's" + ); + } + + #[test] + fn test_splice_out_skips_coin_selection_during_rbf() { + // When splice_out is called on a template with min_rbf_feerate set (user choosing a + // fresh splice-out instead of rbf_prior_contribution_sync), coin selection should NOT + // run. + // Fees come from the channel balance. + let min_rbf_feerate = FeeRate::from_sat_per_kwu(2025); + let feerate = FeeRate::from_sat_per_kwu(2025); + let withdrawal = funding_output_sats(20_000); + + let template = FundingTemplate::new( + Some(shared_input(100_000)), + Some(min_rbf_feerate), + None, + Amount::MAX_MONEY, + ); + + let contribution = + template.splice_out(vec![withdrawal.clone()], feerate, FeeRate::MAX).unwrap(); + assert_eq!(contribution.value_added(), Amount::ZERO); + assert!(contribution.inputs.is_empty()); + assert!(contribution.change_output.is_none()); + assert_eq!(contribution.outputs, vec![withdrawal]); } } diff --git a/lightning/src/ln/htlc_reserve_unit_tests.rs b/lightning/src/ln/htlc_reserve_unit_tests.rs index 5b2ffca5fd4..290ae18f6fc 100644 --- a/lightning/src/ln/htlc_reserve_unit_tests.rs +++ b/lightning/src/ln/htlc_reserve_unit_tests.rs @@ -2,29 +2,35 @@ use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaymentPurpose}; use crate::ln::chan_utils::{ - self, commitment_tx_base_weight, second_stage_tx_fees_sat, CommitmentTransaction, - COMMITMENT_TX_WEIGHT_PER_HTLC, + self, commit_tx_fee_sat, commitment_tx_base_weight, second_stage_tx_fees_sat, + shared_anchor_script_pubkey, CommitmentTransaction, HTLCOutputInCommitment, + COMMITMENT_TX_WEIGHT_PER_HTLC, TRUC_CHILD_MAX_WEIGHT, }; use crate::ln::channel::{ - get_holder_selected_channel_reserve_satoshis, Channel, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, - MIN_AFFORDABLE_HTLC_COUNT, MIN_CHAN_DUST_LIMIT_SATOSHIS, + get_holder_selected_channel_reserve_satoshis, Channel, ANCHOR_OUTPUT_VALUE_SATOSHI, + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_AFFORDABLE_HTLC_COUNT, + MIN_CHAN_DUST_LIMIT_SATOSHIS, }; -use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder}; +use crate::ln::channel_state::ChannelDetails; +use crate::ln::channelmanager::{PaymentId, RAACommitmentOrder, TrustedChannelFeatures}; use crate::ln::functional_test_utils::*; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::ln::onion_utils::{self, AttributionData}; use crate::ln::outbound_payment::RecipientOnionFields; +use crate::ln::types::ChannelId; use crate::routing::router::PaymentParameters; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::tx_builder::{SpecTxBuilder, TxBuilder}; +use crate::sign::ChannelSigner; use crate::types::features::ChannelTypeFeatures; -use crate::types::payment::PaymentPreimage; +use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::config::UserConfig; use crate::util::errors::APIError; use lightning_macros::xtest; use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use bitcoin::{Amount, Transaction}; fn do_test_counterparty_no_reserve(send_from_initiator: bool) { // A peer providing a channel_reserve_satoshis of 0 (or less than our dust limit) is insecure, @@ -33,9 +39,7 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) { // in normal testing, we test it explicitly here. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let legacy_cfg = test_legacy_channel_config(); - let node_chanmgrs = - create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg.clone()), Some(legacy_cfg)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_a_id = nodes[0].node.get_our_node_id(); @@ -46,11 +50,14 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) { // Have node0 initiate a channel to node1 with aforementioned parameters let mut push_amt = 100_000_000; let feerate_per_kw = 253; - let channel_type_features = ChannelTypeFeatures::only_static_remote_key(); + let channel_type_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); push_amt -= feerate_per_kw as u64 * (commitment_tx_base_weight(&channel_type_features) + 4 * COMMITMENT_TX_WEIGHT_PER_HTLC) / 1000 * 1000; - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) + .unwrap() + * 1000; + push_amt -= 2 * 330_000; let push = if send_from_initiator { 0 } else { push_amt }; let temp_channel_id = @@ -101,10 +108,8 @@ fn do_test_counterparty_no_reserve(send_from_initiator: bool) { &nodes[0], &[&nodes[1]], 100_000_000 - // Note that for outbound channels we have to consider the commitment tx fee and the - // "fee spike buffer", which is currently a multiple of the total commitment tx fee as - // well as an additional HTLC. - - FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE * commit_tx_fee_msat(feerate_per_kw, 2, &channel_type_features), + - commit_tx_fee_msat(feerate_per_kw, 2, &channel_type_features) + - 2 * 330_000, ); } else { send_payment(&nodes[1], &[&nodes[0]], push_amt); @@ -172,7 +177,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { route.paths[0].hops.last_mut().unwrap().fee_msat += 1; assert!(route.paths[0].hops.iter().rev().skip(1).all(|h| h.fee_msat == feemsat)); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -248,7 +253,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_1); let payment_event_1 = { let route = route_1.clone(); - let onion = RecipientOnionFields::secret_only(our_payment_secret_1); + let onion = RecipientOnionFields::secret_only(our_payment_secret_1, recv_value_1); let id = PaymentId(our_payment_hash_1.0); nodes[0].node.send_payment_with_route(route, our_payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -268,8 +273,9 @@ pub fn test_channel_reserve_holding_cell_htlcs() { { let mut route = route_1.clone(); route.paths[0].hops.last_mut().unwrap().fee_msat = recv_value_2 + 1; - let (_, our_payment_hash, our_payment_secret) = get_payment_preimage_hash!(nodes[2]); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let (_, our_payment_hash, our_payment_secret) = + get_payment_preimage_hash(&nodes[2], None, None); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -297,7 +303,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { let (route_21, our_payment_hash_21, our_payment_preimage_21, our_payment_secret_21) = get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_21); // but this will stuck in the holding cell - let onion = RecipientOnionFields::secret_only(our_payment_secret_21); + let onion = RecipientOnionFields::secret_only(our_payment_secret_21, recv_value_21); let id = PaymentId(our_payment_hash_21.0); nodes[0].node.send_payment_with_route(route_21, our_payment_hash_21, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -309,7 +315,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_22); route.paths[0].hops.last_mut().unwrap().fee_msat += 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, route.get_total_amount()); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -319,7 +325,7 @@ pub fn test_channel_reserve_holding_cell_htlcs() { let (route_22, our_payment_hash_22, our_payment_preimage_22, our_payment_secret_22) = get_route_and_payment_hash!(nodes[0], nodes[2], recv_value_22); // this will also stuck in the holding cell - let onion = RecipientOnionFields::secret_only(our_payment_secret_22); + let onion = RecipientOnionFields::secret_only(our_payment_secret_22, recv_value_22); let id = PaymentId(our_payment_hash_22.0); nodes[0].node.send_payment_with_route(route_22, our_payment_hash_22, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -495,7 +501,7 @@ pub fn channel_reserve_in_flight_removes() { let (route, payment_hash_3, payment_preimage_3, payment_secret_3) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); let send_1 = { - let onion = RecipientOnionFields::secret_only(payment_secret_3); + let onion = RecipientOnionFields::secret_only(payment_secret_3, 100000); let id = PaymentId(payment_hash_3.0); nodes[0].node.send_payment_with_route(route, payment_hash_3, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -572,7 +578,7 @@ pub fn channel_reserve_in_flight_removes() { let (route, payment_hash_4, payment_preimage_4, payment_secret_4) = get_route_and_payment_hash!(nodes[1], nodes[0], 10000); let send_2 = { - let onion = RecipientOnionFields::secret_only(payment_secret_4); + let onion = RecipientOnionFields::secret_only(payment_secret_4, 10000); let id = PaymentId(payment_hash_4.0); nodes[1].node.send_payment_with_route(route, payment_hash_4, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -639,7 +645,7 @@ pub fn holding_cell_htlc_counting() { for _ in 0..50 { let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); payments.push((payment_preimage, payment_hash)); @@ -655,7 +661,7 @@ pub fn holding_cell_htlc_counting() { // the holding cell waiting on B's RAA to send. At this point we should not be able to add // another HTLC. { - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 100000); let id = PaymentId(payment_hash_1.0); let res = nodes[1].node.send_payment_with_route(route, payment_hash_1, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -665,7 +671,7 @@ pub fn holding_cell_htlc_counting() { // This should also be true if we try to forward a payment. let (route, payment_hash_2, _, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[2], 100000); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 100000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -771,7 +777,7 @@ pub fn test_basic_channel_reserve() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], max_can_send); route.paths[0].hops.last_mut().unwrap().fee_msat += 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send + 1); let id = PaymentId(our_payment_hash.0); let err = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], err, true, APIError::ChannelUnavailable { .. }, {}); @@ -819,10 +825,10 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) { let payment_amt_msat = 3460001; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, payment_amt_msat); + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( &route.paths[0], - payment_amt_msat, &recipient_onion_fields, cur_height, &None, @@ -862,14 +868,11 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) { let local_chan = chan_lock.channel_by_id.get(&chan.2).and_then(Channel::as_funded).unwrap(); let chan_signer = local_chan.get_signer(); // Make the signer believe we validated another commitment, so we can release the secret - chan_signer.as_ecdsa().unwrap().get_enforcement_state().last_holder_commitment -= 1; + chan_signer.get_enforcement_state().last_holder_commitment -= 1; ( - chan_signer.as_ref().release_commitment_secret(INITIAL_COMMITMENT_NUMBER).unwrap(), - chan_signer - .as_ref() - .get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 2, &secp_ctx) - .unwrap(), + chan_signer.release_commitment_secret(INITIAL_COMMITMENT_NUMBER).unwrap(), + chan_signer.get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 2, &secp_ctx).unwrap(), ) }; let remote_point = { @@ -878,15 +881,12 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) { let channel = get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, chan.2); let chan_signer = channel.as_funded().unwrap().get_signer(); - chan_signer - .as_ref() - .get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 1, &secp_ctx) - .unwrap() + chan_signer.get_per_commitment_point(INITIAL_COMMITMENT_NUMBER - 1, &secp_ctx).unwrap() }; // Build the remote commitment transaction so we can sign it, and then later use the // signature for the commitment_signed message. - let accepted_htlc_info = chan_utils::HTLCOutputInCommitment { + let accepted_htlc_info = HTLCOutputInCommitment { offered: false, amount_msat: payment_amt_msat, cltv_expiry: htlc_cltv, @@ -918,8 +918,6 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) { ); let params = &channel.funding().channel_transaction_parameters; chan_signer - .as_ecdsa() - .unwrap() .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) .unwrap() }; @@ -929,8 +927,6 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) { signature: res.0, htlc_signatures: res.1, funding_txid: None, - #[cfg(taproot)] - partial_signature_with_nonce: None, }; // Send the commitment_signed message to the nodes[1]. @@ -942,8 +938,6 @@ pub fn do_test_fee_spike_buffer(cfg: Option<UserConfig>, htlc_fails: bool) { channel_id: chan.2, per_commitment_secret: local_secret, next_per_commitment_point: next_local_point, - #[cfg(taproot)] - next_local_nonce: None, release_htlc_message_paths: Vec::new(), }; nodes[1].node.handle_revoke_and_ack(node_a_id, &raa_msg); @@ -1007,7 +1001,9 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() { &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) + .unwrap() + * 1000; let _ = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt); @@ -1020,91 +1016,13 @@ pub fn test_chan_reserve_violation_outbound_htlc_inbound_chan() { } // However one more HTLC should be significantly over the reserve amount and fail. - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1_000_000); let id = PaymentId(our_payment_hash.0); let res = nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); } -#[xtest(feature = "_externalize_tests")] -pub fn test_chan_reserve_violation_inbound_htlc_outbound_channel() { - let mut chanmon_cfgs = create_chanmon_cfgs(2); - let feerate_per_kw = *chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap(); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let legacy_cfg = test_legacy_channel_config(); - let node_chanmgrs = - create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg.clone()), Some(legacy_cfg)]); - let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); - - let node_b_id = nodes[1].node.get_our_node_id(); - - let default_config = UserConfig::default(); - let channel_type_features = ChannelTypeFeatures::only_static_remote_key(); - - // Set nodes[0]'s balance such that they will consider any above-dust received HTLC to be a - // channel reserve violation (so their balance is channel reserve (1000 sats) + commitment - // transaction fee with 0 HTLCs (183 sats)). - let mut push_amt = 100_000_000; - push_amt -= commit_tx_fee_msat( - feerate_per_kw, - MIN_AFFORDABLE_HTLC_COUNT as u64, - &channel_type_features, - ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; - let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, push_amt); - - // Send four HTLCs to cover the initial push_msat buffer we're required to include - for _ in 0..MIN_AFFORDABLE_HTLC_COUNT { - route_payment(&nodes[1], &[&nodes[0]], 1_000_000); - } - - let (mut route, payment_hash, _, payment_secret) = - get_route_and_payment_hash!(nodes[1], nodes[0], 1000); - route.paths[0].hops[0].fee_msat = 700_000; - // Need to manually create the update_add_htlc message to go around the channel reserve check in send_htlc() - let secp_ctx = Secp256k1::new(); - let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); - let cur_height = nodes[1].node.best_block.read().unwrap().height + 1; - let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( - &route.paths[0], - 700_000, - &recipient_onion_fields, - cur_height, - &None, - None, - None, - ) - .unwrap(); - let onion_packet = - onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash) - .unwrap(); - let msg = msgs::UpdateAddHTLC { - channel_id: chan.2, - htlc_id: MIN_AFFORDABLE_HTLC_COUNT as u64, - amount_msat: htlc_msat, - payment_hash, - cltv_expiry: htlc_cltv, - onion_routing_packet: onion_packet, - skimmed_fee_msat: None, - blinding_point: None, - hold_htlc: None, - accountable: None, - }; - - nodes[0].node.handle_update_add_htlc(node_b_id, &msg); - // Check that the payment failed and the channel is closed in response to the malicious UpdateAdd. - nodes[0].logger.assert_log_contains("lightning::ln::channelmanager", "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value", 3); - assert_eq!(nodes[0].node.list_channels().len(), 0); - let err_msg = check_closed_broadcast!(nodes[0], true).unwrap(); - assert_eq!(err_msg.data, "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value"); - let reason = ClosureReason::ProcessingError { err: "Cannot accept HTLC that would put our balance under counterparty-announced channel reserve value".to_string() }; - check_added_monitors(&nodes[0], 1); - check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); -} - #[xtest(feature = "_externalize_tests")] pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() { // Test that if we receive many dust HTLCs over an outbound channel, they don't count when @@ -1130,13 +1048,15 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() { MIN_AFFORDABLE_HTLC_COUNT as u64, &channel_type_features, ); - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) + .unwrap() + * 1000; create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, push_amt); - let (htlc_success_tx_fee_sat, _) = + let (_htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = second_stage_tx_fees_sat(&channel_type_features, feerate_per_kw); let dust_amt = crate::ln::channel::MIN_CHAN_DUST_LIMIT_SATOSHIS * 1000 - + htlc_success_tx_fee_sat * 1000 + + htlc_timeout_tx_fee_sat * 1000 - 1; // In the previous code, routing this dust payment would cause nodes[0] to perceive a channel // reserve violation even though it's a dust HTLC and therefore shouldn't count towards the @@ -1152,7 +1072,7 @@ pub fn test_chan_reserve_dust_inbound_htlcs_outbound_chan() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], dust_amt); route.paths[0].hops[0].fee_msat += 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, dust_amt + 1); let id = PaymentId(our_payment_hash.0); let res = nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1223,7 +1143,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { let (route_1, our_payment_hash_1, _, our_payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[2], amt_msat_1); let payment_event_1 = { - let onion = RecipientOnionFields::secret_only(our_payment_secret_1); + let onion = RecipientOnionFields::secret_only(our_payment_secret_1, amt_msat_1); let id = PaymentId(our_payment_hash_1.0); let route = route_1.clone(); nodes[0].node.send_payment_with_route(route, our_payment_hash_1, onion, id).unwrap(); @@ -1252,10 +1172,9 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route_2.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); - let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(recv_value_2); + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( &route_2.paths[0], - recv_value_2, &recipient_onion_fields, cur_height, &None, @@ -1291,7 +1210,7 @@ pub fn test_chan_reserve_violation_inbound_htlc_inbound_chan() { 3, ); assert_eq!(nodes[1].node.list_channels().len(), 1); - let err_msg = check_closed_broadcast!(nodes[1], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); assert_eq!(err_msg.data, "Remote HTLC add would put them under remote reserve value"); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::ProcessingError { err: err_msg.data.clone() }; @@ -1322,7 +1241,7 @@ pub fn test_payment_route_reaching_same_channel_twice() { route.paths[0].hops.extend_from_slice(&cloned_hops); unwrap_send_err!(nodes[0], nodes[0].node.send_payment_with_route(route, our_payment_hash, - RecipientOnionFields::secret_only(our_payment_secret), PaymentId(our_payment_hash.0) + RecipientOnionFields::secret_only(our_payment_secret, 100000000), PaymentId(our_payment_hash.0) ), false, APIError::InvalidRoute { ref err }, assert_eq!(err, &"Path went through the same channel twice")); assert!(nodes[0].node.list_recent_payments().is_empty()); @@ -1346,7 +1265,7 @@ pub fn test_update_add_htlc_bolt2_sender_value_below_minimum_msat() { get_route_and_payment_hash!(nodes[0], nodes[1], 100000); route.paths[0].hops[0].fee_msat = 100; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1366,7 +1285,7 @@ pub fn test_update_add_htlc_bolt2_sender_zero_value_msat() { let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); route.paths[0].hops[0].fee_msat = 0; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 0); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, @@ -1396,7 +1315,7 @@ pub fn test_update_add_htlc_bolt2_receiver_zero_value_msat() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1409,7 +1328,7 @@ pub fn test_update_add_htlc_bolt2_receiver_zero_value_msat() { "Remote side tried to send a 0-msat HTLC", 3, ); - check_closed_broadcast!(nodes[1], true).unwrap(); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::ProcessingError { err: "Remote side tried to send a 0-msat HTLC".to_string(), @@ -1430,14 +1349,15 @@ pub fn test_update_add_htlc_bolt2_sender_cltv_expiry_too_high() { let _chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 0); - let payment_params = PaymentParameters::from_node_id(node_b_id, 0) + let mut payment_params = PaymentParameters::from_node_id(node_b_id, 0) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) .unwrap(); + payment_params.max_total_cltv_expiry_delta = 500000001; let (mut route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, 100000000); route.paths[0].hops.last_mut().unwrap().cltv_expiry_delta = 500000001; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000000); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::InvalidRoute { ref err }, @@ -1472,7 +1392,7 @@ pub fn test_update_add_htlc_bolt2_sender_exceed_max_htlc_num_and_htlc_id_increme let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 100000); let payment_event = { - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1498,7 +1418,7 @@ pub fn test_update_add_htlc_bolt2_sender_exceed_max_htlc_num_and_htlc_id_increme expect_and_process_pending_htlcs(&nodes[1], false); expect_payment_claimable!(nodes[1], our_payment_hash, our_payment_secret, 100000); } - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 100000); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1526,7 +1446,7 @@ pub fn test_update_add_htlc_bolt2_sender_exceed_max_htlc_value_in_flight() { // Manually create a route over our max in flight (which our router normally automatically // limits us to. route.paths[0].hops[0].fee_msat = max_in_flight + 1; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_in_flight + 1); let id = PaymentId(our_payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -1558,7 +1478,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_amount_received_more_than_min() let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], htlc_minimum_msat); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, htlc_minimum_msat); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1566,7 +1486,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_amount_received_more_than_min() updates.update_add_htlcs[0].amount_msat = htlc_minimum_msat - 1; nodes[1].node.handle_update_add_htlc(node_a_id, &updates.update_add_htlcs[0]); assert!(nodes[1].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[1], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); assert!(regex::Regex::new(r"Remote side tried to send less than our minimum HTLC value\. Lower limit: \(\d+\)\. Actual: \(\d+\)").unwrap().is_match(err_msg.data.as_str())); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::ProcessingError { err: err_msg.data }; @@ -1598,7 +1518,7 @@ pub fn test_update_add_htlc_bolt2_receiver_sender_can_afford_amount_sent() { let max_can_send = 5000000 - channel_reserve - commit_tx_fee_outbound; let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], max_can_send); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, max_can_send); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1611,7 +1531,7 @@ pub fn test_update_add_htlc_bolt2_receiver_sender_can_afford_amount_sent() { nodes[1].node.handle_update_add_htlc(node_a_id, &updates.update_add_htlcs[0]); assert!(nodes[1].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[1], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); assert_eq!(err_msg.data, "Remote HTLC add would put them under remote reserve value"); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::ProcessingError { err: err_msg.data }; @@ -1642,10 +1562,9 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { &route.paths[0], &session_priv, ); - let recipient_onion_fields = RecipientOnionFields::secret_only(our_payment_secret); - let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::build_onion_payloads( + let recipient_onion_fields = RecipientOnionFields::secret_only(our_payment_secret, send_amt); + let (onion_payloads, _htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( &route.paths[0], - send_amt, &recipient_onion_fields, cur_height, &None, @@ -1678,7 +1597,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_htlc_limit() { nodes[1].node.handle_update_add_htlc(node_a_id, &msg); assert!(nodes[1].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[1], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); assert!(regex::Regex::new(r"Remote tried to push more than our max accepted HTLCs \(\d+\)") .unwrap() .is_match(err_msg.data.as_str())); @@ -1702,7 +1621,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_in_flight_msat() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1713,7 +1632,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_max_in_flight_msat() { nodes[1].node.handle_update_add_htlc(node_a_id, &updates.update_add_htlcs[0]); assert!(nodes[1].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[1], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); assert!(regex::Regex::new("Remote HTLC add would put them over our max HTLC value") .unwrap() .is_match(err_msg.data.as_str())); @@ -1736,7 +1655,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_cltv_expiry() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 95000000); let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let reason = RecipientOnionFields::secret_only(our_payment_secret); + let reason = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, reason, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1745,7 +1664,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_cltv_expiry() { nodes[1].node.handle_update_add_htlc(node_a_id, &updates.update_add_htlcs[0]); assert!(nodes[1].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[1], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); assert_eq!(err_msg.data, "Remote provided CLTV expiry in seconds instead of block height"); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::ProcessingError { err: err_msg.data }; @@ -1768,7 +1687,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_repeated_id_ignore() { create_announced_chan_between_nodes(&nodes, 0, 1); let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); @@ -1809,7 +1728,7 @@ pub fn test_update_add_htlc_bolt2_receiver_check_repeated_id_ignore() { nodes[1].node.handle_update_add_htlc(node_a_id, &updates.update_add_htlcs[0]); assert!(nodes[1].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[1], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); assert!(regex::Regex::new(r"Remote skipped HTLC ID \(skipped ID: \d+\)") .unwrap() .is_match(err_msg.data.as_str())); @@ -1833,7 +1752,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fulfill_htlc_before_commitment() { let chan = create_announced_chan_between_nodes(&nodes, 0, 1); let (route, our_payment_hash, our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); @@ -1851,7 +1770,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fulfill_htlc_before_commitment() { nodes[0].node.handle_update_fulfill_htlc(node_b_id, update_msg); assert!(nodes[0].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[0], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[0], 1, true).pop().unwrap(); assert!(regex::Regex::new( r"Remote tried to fulfill/fail HTLC \(\d+\) before it had been committed" ) @@ -1878,7 +1797,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fail_htlc_before_commitment() { let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1895,7 +1814,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fail_htlc_before_commitment() { nodes[0].node.handle_update_fail_htlc(node_b_id, &update_msg); assert!(nodes[0].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[0], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[0], 1, true).pop().unwrap(); assert!(regex::Regex::new( r"Remote tried to fulfill/fail HTLC \(\d+\) before it had been committed" ) @@ -1922,7 +1841,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fail_malformed_htlc_before_commitme let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1938,7 +1857,7 @@ pub fn test_update_fulfill_htlc_bolt2_update_fail_malformed_htlc_before_commitme nodes[0].node.handle_update_fail_malformed_htlc(node_b_id, &update_msg); assert!(nodes[0].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[0], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[0], 1, true).pop().unwrap(); assert!(regex::Regex::new( r"Remote tried to fulfill/fail HTLC \(\d+\) before it had been committed" ) @@ -2001,7 +1920,7 @@ pub fn test_update_fulfill_htlc_bolt2_incorrect_htlc_id() { nodes[0].node.handle_update_fulfill_htlc(node_b_id, update_fulfill_msg); assert!(nodes[0].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[0], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[0], 1, true).pop().unwrap(); assert_eq!(err_msg.data, "Remote tried to fulfill/fail an HTLC we couldn't find"); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::ProcessingError { err: err_msg.data }; @@ -2060,7 +1979,7 @@ pub fn test_update_fulfill_htlc_bolt2_wrong_preimage() { nodes[0].node.handle_update_fulfill_htlc(node_b_id, update_fulfill_msg); assert!(nodes[0].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[0], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[0], 1, true).pop().unwrap(); assert!(regex::Regex::new(r"Remote tried to fulfill HTLC \(\d+\) with an incorrect preimage") .unwrap() .is_match(err_msg.data.as_str())); @@ -2085,7 +2004,7 @@ pub fn test_update_fulfill_htlc_bolt2_missing_badonion_bit_for_malformed_htlc_me let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 1000000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2133,7 +2052,7 @@ pub fn test_update_fulfill_htlc_bolt2_missing_badonion_bit_for_malformed_htlc_me nodes[0].node.handle_update_fail_malformed_htlc(node_b_id, &update_msg); assert!(nodes[0].node.list_channels().is_empty()); - let err_msg = check_closed_broadcast!(nodes[0], true).unwrap(); + let err_msg = check_closed_broadcast(&nodes[0], 1, true).pop().unwrap(); assert_eq!(err_msg.data, "Got update_fail_malformed_htlc with BADONION not set"); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::ProcessingError { err: err_msg.data }; @@ -2224,7 +2143,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { let (_payment_preimage, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], HTLC_AMT_SAT * 1000); // Grab a snapshot of these HTLCs to manually build the commitment transaction later... - let accepted_htlc = chan_utils::HTLCOutputInCommitment { + let accepted_htlc = HTLCOutputInCommitment { offered: false, amount_msat: HTLC_AMT_SAT * 1000, // Hard-coded to match the expected value @@ -2243,10 +2162,10 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route_0_1.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret_0_1); - let (onion_payloads, amount_msat, cltv_expiry) = onion_utils::build_onion_payloads( + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret_0_1, HTLC_AMT_SAT * 1000); + let (onion_payloads, amount_msat, cltv_expiry) = onion_utils::test_build_onion_payloads( &route_0_1.paths[0], - HTLC_AMT_SAT * 1000, &recipient_onion_fields, cur_height, &None, @@ -2296,17 +2215,15 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { chan_lock.channel_by_id.get(&chan_id).and_then(Channel::as_funded).unwrap(); let chan_signer = local_chan.get_signer(); // Make the signer believe we validated another commitment, so we can release the secret - chan_signer.as_ecdsa().unwrap().get_enforcement_state().last_holder_commitment -= 1; + chan_signer.get_enforcement_state().last_holder_commitment -= 1; ( chan_signer - .as_ref() .release_commitment_secret( INITIAL_COMMITMENT_NUMBER - MIN_AFFORDABLE_HTLC_COUNT as u64 + 1, ) .unwrap(), chan_signer - .as_ref() .get_per_commitment_point( INITIAL_COMMITMENT_NUMBER - MIN_AFFORDABLE_HTLC_COUNT as u64, &secp_ctx, @@ -2322,7 +2239,6 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, chan_id); let chan_signer = channel.as_funded().unwrap().get_signer(); chan_signer - .as_ref() .get_per_commitment_point( INITIAL_COMMITMENT_NUMBER - MIN_AFFORDABLE_HTLC_COUNT as u64, &secp_ctx, @@ -2341,7 +2257,7 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { &channel_type, ); - let accepted_htlc_info = chan_utils::HTLCOutputInCommitment { + let accepted_htlc_info = HTLCOutputInCommitment { offered: false, amount_msat: HTLC_AMT_SAT * 1000, cltv_expiry, @@ -2372,8 +2288,6 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { ); let params = &channel.funding().channel_transaction_parameters; chan_signer - .as_ecdsa() - .unwrap() .sign_counterparty_commitment( params, &commitment_tx, @@ -2389,8 +2303,6 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { signature: res.0, htlc_signatures: res.1, funding_txid: None, - #[cfg(taproot)] - partial_signature_with_nonce: None, }; // Send the commitment_signed message to the nodes[1]. @@ -2402,8 +2314,6 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { channel_id: chan_id, per_commitment_secret: local_secret, next_per_commitment_point: next_local_point, - #[cfg(taproot)] - next_local_nonce: None, release_htlc_message_paths: Vec::new(), }; nodes[1].node.handle_revoke_and_ack(node_a_id, &raa_msg); @@ -2440,3 +2350,1446 @@ pub fn do_test_dust_limit_fee_accounting(can_afford: bool) { check_added_monitors(&nodes[1], 3); } } + +#[xtest(feature = "_externalize_tests")] +fn test_create_channel_to_trusted_peer_0reserve() { + let mut config = test_default_channel_config(); + + // Anchor channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + // 0FC channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + let channel_type = do_test_create_channel_to_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_fee_commitments()); +} + +fn do_test_create_channel_to_trusted_peer_0reserve(mut config: UserConfig) -> ChannelTypeFeatures { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + + let temp_channel_id = nodes[0] + .node + .create_channel_to_trusted_peer_0reserve(node_b_id, channel_value_sat, 0, 42, None, None) + .unwrap(); + let mut open_channel_message = + get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + handle_and_accept_open_channel(&nodes[1], node_a_id, &open_channel_message); + let mut accept_channel_message = + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_message); + let funding_tx = sign_funding_transaction(&nodes[0], &nodes[1], 100_000, temp_channel_id); + let funding_msgs = + create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &funding_tx); + create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &funding_msgs.0); + + let details = &nodes[0].node.list_channels()[0]; + let reserve_sat = details.unspendable_punishment_reserve.unwrap(); + assert_ne!(reserve_sat, 0); + let channel_type = details.channel_type.clone().unwrap(); + let feerate_per_kw = details.feerate_sat_per_1000_weight.unwrap(); + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + 2 * 330 + } else { + 0 + }; + let reserved_commit_tx_fee_sat = chan_utils::commit_tx_fee_sat( + feerate_per_kw, + 2, // We reserve space for two HTLCs, the next outbound non-dust HTLC, and the fee spike buffer HTLC + &channel_type, + ); + + let max_outbound_htlc_sat = + channel_value_sat - anchors_sat - reserved_commit_tx_fee_sat - reserve_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[0], &[&nodes[1]], max_outbound_htlc_sat * 1000); + + let details = &nodes[1].node.list_channels()[0]; + assert_eq!(details.unspendable_punishment_reserve.unwrap(), 0); + // Assert that the fundee can send back the full amount they just received, since they have 0-reserve. + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[1], &[&nodes[0]], max_outbound_htlc_sat * 1000); + + channel_type +} + +#[xtest(feature = "_externalize_tests")] +fn test_accept_inbound_channel_from_trusted_peer_0reserve() { + let mut config = test_default_channel_config(); + + // Anchor channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + // 0FC channels + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + let channel_type = do_test_accept_inbound_channel_from_trusted_peer_0reserve(config.clone()); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_fee_commitments()); +} + +fn do_test_accept_inbound_channel_from_trusted_peer_0reserve( + mut config: UserConfig, +) -> ChannelTypeFeatures { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + + nodes[0].node.create_channel(node_b_id, channel_value_sat, 0, 42, None, None).unwrap(); + + let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + nodes[1].node.handle_open_channel(node_a_id, &open_channel); + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => { + nodes[1] + .node + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroReserve, + None, + ) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + + let mut accept_channel_msg = + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg); + + let (chan_id, tx, _) = create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42); + + nodes[0].node.funding_transaction_generated(chan_id, node_b_id, tx.clone()).unwrap(); + nodes[1].node.handle_funding_created( + node_a_id, + &get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id), + ); + check_added_monitors(&nodes[1], 1); + expect_channel_pending_event(&nodes[1], &node_a_id); + + nodes[0].node.handle_funding_signed( + node_b_id, + &get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id), + ); + check_added_monitors(&nodes[0], 1); + expect_channel_pending_event(&nodes[0], &node_b_id); + + let (channel_ready, _channel_id) = + create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &tx); + let (announcement, as_update, bs_update) = + create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready); + update_nodes_with_chan_announce(&nodes, 0, 1, &announcement, &as_update, &bs_update); + + let details = &nodes[0].node.list_channels()[0]; + assert_eq!(details.unspendable_punishment_reserve.unwrap(), 0); + let channel_type = details.channel_type.clone().unwrap(); + let feerate_per_kw = details.feerate_sat_per_1000_weight.unwrap(); + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + 2 * 330 + } else { + 0 + }; + let reserved_commit_tx_fee_sat = chan_utils::commit_tx_fee_sat( + feerate_per_kw, + 2, // We reserve space for two HTLCs, the next outbound non-dust HTLC, and the fee spike buffer HTLC + &channel_type, + ); + + let max_outbound_htlc_sat = channel_value_sat - reserved_commit_tx_fee_sat - anchors_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[0], &[&nodes[1]], max_outbound_htlc_sat * 1000); + + let details = &nodes[1].node.list_channels()[0]; + let reserve_sat = details.unspendable_punishment_reserve.unwrap(); + assert_ne!(reserve_sat, 0); + let max_outbound_htlc_sat = max_outbound_htlc_sat - reserve_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, max_outbound_htlc_sat * 1000); + send_payment(&nodes[1], &[&nodes[0]], max_outbound_htlc_sat * 1000); + + channel_type +} + +#[xtest(feature = "_externalize_tests")] +fn test_0reserve_no_outputs() { + do_test_0reserve_no_outputs_keyed_anchors(true); + do_test_0reserve_no_outputs_keyed_anchors(false); + + do_test_0reserve_no_outputs_p2a_anchor(); +} + +pub(crate) fn setup_0reserve_no_outputs_channels<'a, 'b, 'c, 'd>( + nodes: &'a Vec<Node<'b, 'c, 'd>>, channel_value_sat: u64, dust_limit_satoshis: u64, +) -> (ChannelId, Transaction) { + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + // Create a channel with an identical, high dust limit and zero-reserve on both sides to make our lives easier + + nodes[0] + .node + .create_channel_to_trusted_peer_0reserve(node_b_id, channel_value_sat, 0, 42, None, None) + .unwrap(); + + let mut open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + open_channel.common_fields.dust_limit_satoshis = dust_limit_satoshis; + nodes[1].node.handle_open_channel(node_a_id, &open_channel); + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => { + nodes[1] + .node + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroReserve, + None, + ) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + + let mut accept_channel_msg = + get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + accept_channel_msg.common_fields.dust_limit_satoshis = dust_limit_satoshis; + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel_msg); + + let (chan_id, tx, _) = create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42); + + nodes[0].node.funding_transaction_generated(chan_id, node_b_id, tx.clone()).unwrap(); + nodes[1].node.handle_funding_created( + node_a_id, + &get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id), + ); + check_added_monitors(&nodes[1], 1); + expect_channel_pending_event(&nodes[1], &node_a_id); + + nodes[0].node.handle_funding_signed( + node_b_id, + &get_event_msg!(nodes[1], MessageSendEvent::SendFundingSigned, node_a_id), + ); + check_added_monitors(&nodes[0], 1); + expect_channel_pending_event(&nodes[0], &node_b_id); + + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); + nodes[0].tx_broadcaster.clear(); + + let (channel_ready, channel_id) = + create_chan_between_nodes_with_value_confirm(&nodes[0], &nodes[1], &tx); + let (announcement, as_update, bs_update) = + create_chan_between_nodes_with_value_b(&nodes[0], &nodes[1], &channel_ready); + update_nodes_with_chan_announce(nodes, 0, 1, &announcement, &as_update, &bs_update); + + { + let mut per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id); + if let Some(mut chan) = channel.as_funded_mut() { + chan.context.holder_dust_limit_satoshis = dust_limit_satoshis; + } else { + panic!("Unexpected Channel phase"); + } + } + + { + let mut per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id); + if let Some(mut chan) = channel.as_funded_mut() { + chan.context.holder_dust_limit_satoshis = dust_limit_satoshis; + } else { + panic!("Unexpected Channel phase"); + } + } + + (channel_id, tx) +} + +fn manually_trigger_update_fail_htlc<'a, 'b, 'c, 'd>( + nodes: &'a Vec<Node<'b, 'c, 'd>>, channel_id: ChannelId, value_to_self_msat: u64, + dust_limit_satoshis: u64, payment_hash: PaymentHash, + htlcs_in_commitment: Vec<HTLCOutputInCommitment>, can_afford_but_reserve_is_breached: bool, +) { + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let secp_ctx = Secp256k1::new(); + + // Now manually create the commitment_signed message corresponding to the update_add + // nodes[0] just sent. In the code for construction of this message, "local" refers + // to the sender of the message, and "remote" refers to the receiver. + + let feerate_per_kw = get_feerate!(nodes[0], nodes[1], channel_id); + + let (local_secret, next_local_point) = { + let per_peer_state = nodes[0].node.per_peer_state.read().unwrap(); + let chan_lock = per_peer_state.get(&node_b_id).unwrap().lock().unwrap(); + let local_chan = + chan_lock.channel_by_id.get(&channel_id).and_then(Channel::as_funded).unwrap(); + let chan_signer = local_chan.get_signer(); + // Make the signer believe we validated another commitment, so we can release the secret + let commit_number = chan_signer.get_enforcement_state().last_holder_commitment; + chan_signer.get_enforcement_state().last_holder_commitment -= 1; + + ( + chan_signer.release_commitment_secret(commit_number).unwrap(), + chan_signer.get_per_commitment_point(commit_number - 2, &secp_ctx).unwrap(), + ) + }; + let (remote_commit_number, remote_point) = { + let per_peer_lock; + let mut peer_state_lock; + + let channel = + get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id); + let chan_signer = channel.as_funded().unwrap().get_signer(); + let commit_number = chan_signer.get_enforcement_state().last_holder_commitment; + let remote_point = + chan_signer.get_per_commitment_point(commit_number - 1, &secp_ctx).unwrap(); + (commit_number - 1, remote_point) + }; + + // Build the remote commitment transaction so we can sign it, and then later use the + // signature for the commitment_signed message. + let res = { + let per_peer_lock; + let mut peer_state_lock; + + let channel = + get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id); + let chan_signer = channel.as_funded().unwrap().get_signer(); + + let (commitment_tx, _stats) = SpecTxBuilder {}.build_commitment_transaction( + false, + remote_commit_number, + &remote_point, + &channel.funding().channel_transaction_parameters, + &secp_ctx, + value_to_self_msat, + htlcs_in_commitment, + feerate_per_kw, + dust_limit_satoshis, + &nodes[0].logger, + ); + let params = &channel.funding().channel_transaction_parameters; + chan_signer + .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) + .unwrap() + }; + + let commit_signed_msg = msgs::CommitmentSigned { + channel_id, + signature: res.0, + htlc_signatures: res.1, + funding_txid: None, + }; + + // Send the commitment_signed message to the nodes[1]. + nodes[1].node.handle_commitment_signed(node_a_id, &commit_signed_msg); + let _ = nodes[1].node.get_and_clear_pending_msg_events(); + + // Send the RAA to nodes[1]. + let raa_msg = msgs::RevokeAndACK { + channel_id, + per_commitment_secret: local_secret, + next_per_commitment_point: next_local_point, + release_htlc_message_paths: Vec::new(), + }; + nodes[1].node.handle_revoke_and_ack(node_a_id, &raa_msg); + expect_and_process_pending_htlcs(&nodes[1], false); + + expect_htlc_handling_failed_destinations!( + nodes[1].node.get_and_clear_pending_events(), + &[HTLCHandlingFailureType::Receive { payment_hash }] + ); + + let events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + + // Make sure the HTLC failed in the way we expect. + match events[0] { + MessageSendEvent::UpdateHTLCs { + updates: msgs::CommitmentUpdate { ref update_fail_htlcs, .. }, + .. + } => { + assert_eq!(update_fail_htlcs.len(), 1); + update_fail_htlcs[0].clone() + }, + _ => panic!("Unexpected event"), + }; + let log_string = + if can_afford_but_reserve_is_breached { + String::from("Attempting to fail HTLC due to fee spike buffer violation. Rebalancing is required.") + } else { + String::from("Attempting to fail HTLC due to balance exhausted on remote commitment") + }; + nodes[1].logger.assert_log("lightning::ln::channel", log_string, 1); + + check_added_monitors(&nodes[1], 3); +} + +fn do_test_0reserve_no_outputs_keyed_anchors(payment_success: bool) { + let mut config = test_default_channel_config(); + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let _node_b_id = nodes[1].node.get_our_node_id(); + + let feerate_per_kw = 253; + let anchors_sat = 2 * ANCHOR_OUTPUT_VALUE_SATOSHI; + let dust_limit_satoshis: u64 = 546; + let channel_value_sat = { + // min opener balance is the fee for 4 HTLCs, the anchors, and the dust limit + let min_channel_size = + commit_tx_fee_sat(feerate_per_kw, MIN_AFFORDABLE_HTLC_COUNT, &channel_type) + + anchors_sat + dust_limit_satoshis; + assert!(min_channel_size > 1002); + min_channel_size + }; + + let (channel_id, _funding_tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Sending the biggest dust HTLC possible trims our balance output! + let max_dust_htlc_sat = dust_limit_satoshis - 1; + assert!( + channel_value_sat + .saturating_sub(anchors_sat) + .saturating_sub(commit_tx_fee_sat(feerate_per_kw, 0, &channel_type)) + .saturating_sub(max_dust_htlc_sat) + < dust_limit_satoshis + ); + + // We can afford the fee for an additional non-dust HTLC plus the fee spike HTLC, so we can send + // non-dust HTLCs + let capacity_minus_max_commitment_fee_sat = + channel_value_sat - anchors_sat - commit_tx_fee_sat(feerate_per_kw, 2, &channel_type); + assert!(capacity_minus_max_commitment_fee_sat > dust_limit_satoshis); + // And since the biggest dust HTLC results in no outputs on the commitment, + // we can *only* send non-dust HTLCs + let details_0 = &nodes[0].node.list_channels()[0]; + assert_eq!(details_0.next_outbound_htlc_minimum_msat, dust_limit_satoshis * 1000); + assert_eq!( + details_0.next_outbound_htlc_limit_msat, + capacity_minus_max_commitment_fee_sat * 1000 + ); + + // Send the smallest non-dust HTLC possible, this will pass both holder and counterparty validation + // + // One msat below the non-dust HTLC value will break counterparty validation at + // `validate_update_add_htlc`. This is why we don't bother taking a look at the range between the + // failure of `can_accept_incoming_htlc` and the failure of `validate_update_add_htlc`. + let sender_amount_msat = dust_limit_satoshis * 1000; + + let (sender_amount_msat, receiver_amount_msat) = if payment_success { + (sender_amount_msat, sender_amount_msat) + } else { + (sender_amount_msat, sender_amount_msat - 1) + }; + + if payment_success { + send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat); + // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back! + // Node 0 should *always* have the funds to cover the fee of a single non-dust HTLC from node 1. + assert_eq!( + nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat, + sender_amount_msat + ); + send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat); + } else { + let (route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], sender_amount_msat); + let secp_ctx = Secp256k1::new(); + let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); + let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; + let onion_keys = + onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, sender_amount_msat); + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( + &route.paths[0], + &recipient_onion_fields, + cur_height, + &None, + None, + None, + ) + .unwrap(); + assert_eq!(htlc_msat, sender_amount_msat); + let onion_packet = + onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash) + .unwrap(); + let msg = msgs::UpdateAddHTLC { + channel_id, + htlc_id: 0, + amount_msat: receiver_amount_msat, + payment_hash, + cltv_expiry: htlc_cltv, + onion_routing_packet: onion_packet, + skimmed_fee_msat: None, + blinding_point: None, + hold_htlc: None, + accountable: None, + }; + + nodes[1].node.handle_update_add_htlc(node_a_id, &msg); + + nodes[1].logger.assert_log_contains( + "lightning::ln::channelmanager", + "Remote HTLC add would overdraw remaining funds", + 3, + ); + assert_eq!(nodes[1].node.list_channels().len(), 0); + let err_msg = check_closed_broadcast(&nodes[1], 1, true).pop().unwrap(); + assert_eq!(err_msg.data, "Remote HTLC add would overdraw remaining funds"); + let reason = ClosureReason::ProcessingError { + err: "Remote HTLC add would overdraw remaining funds".to_string(), + }; + check_added_monitors(&nodes[1], 1); + check_closed_event(&nodes[1], 1, reason, &[node_a_id], channel_value_sat); + } +} + +fn do_test_0reserve_no_outputs_p2a_anchor() { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_fee_commitments(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_a_id = nodes[0].node.get_our_node_id(); + let _node_b_id = nodes[1].node.get_our_node_id(); + + let dust_limit_satoshis: u64 = 546; + let channel_value_sat = 1000; + + let _channel_id = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Sending the biggest dust HTLC possible trims our balance output! + let max_dust_htlc_sat = dust_limit_satoshis - 1; + assert!(channel_value_sat.saturating_sub(max_dust_htlc_sat) < dust_limit_satoshis); + + // We'll always have the P2A output on the commitment, so we are free to send any size HTLC, + // including those that result in only a single output on the commitment, the P2A output. + let details_0 = &nodes[0].node.list_channels()[0]; + assert_eq!(details_0.next_outbound_htlc_minimum_msat, 1000); + // 0FC + 0-reserve baby! + assert_eq!(details_0.next_outbound_htlc_limit_msat, channel_value_sat * 1000); + + // Send the max size dust HTLC; this results in a commitment with only the P2A output present + let sender_amount_msat = max_dust_htlc_sat * 1000; + + send_payment(&nodes[0], &[&nodes[1]], sender_amount_msat); + // Node 1 the fundee has 0-reserve too, so whatever they receive, they can send right back! + assert_eq!(nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat, sender_amount_msat); + send_payment(&nodes[1], &[&nodes[0]], sender_amount_msat); +} + +#[xtest(feature = "_externalize_tests")] +pub fn test_0reserve_force_close_with_single_p2a_output() { + do_test_0reserve_force_close_with_single_p2a_output(false); + do_test_0reserve_force_close_with_single_p2a_output(true); +} + +fn do_test_0reserve_force_close_with_single_p2a_output(high_feerate: bool) { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + if high_feerate { + let mut feerate_lock = chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap(); + *feerate_lock = 2500; + } + if high_feerate { + let mut feerate_lock = chanmon_cfgs[1].fee_estimator.sat_per_kw.lock().unwrap(); + *feerate_lock = 2500; + } + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_fee_commitments(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let coinbase_tx = provide_anchor_reserves(&nodes); + + let _node_a_id = nodes[0].node.get_our_node_id(); + let _node_b_id = nodes[1].node.get_our_node_id(); + + let dust_limit_satoshis: u64 = 546; + // This is the fundee 1000sat reserve + 2 min HTLCs + let channel_value_sat = 1002; + + let (channel_id, funding_tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Send the smallest HTLC possible that trims our own balance output, this will be a dust HTLC + let htlc_sat = channel_value_sat - dust_limit_satoshis + 1; + assert!(htlc_sat < dust_limit_satoshis); + route_payment(&nodes[0], &[&nodes[1]], htlc_sat * 1000); + + let commitment_tx = get_local_commitment_txn!(nodes[0], channel_id).pop().unwrap(); + let commitment_txid = commitment_tx.compute_txid(); + + let message = "Channel force-closed".to_owned(); + nodes[0] + .node + .force_close_broadcasting_latest_txn( + &channel_id, + &nodes[1].node.get_our_node_id(), + message.clone(), + ) + .unwrap(); + check_closed_broadcast(&nodes[0], 1, true); + check_added_monitors(&nodes[0], 1); + let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; + check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], channel_value_sat); + + let mut events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events.pop().unwrap() { + Event::BumpTransaction(bump_event) => { + nodes[0].bump_tx_handler.handle_event(&bump_event); + }, + _ => panic!("Unexpected event"), + } + let txns = nodes[0].tx_broadcaster.txn_broadcast(); + + if high_feerate { + assert_eq!(txns.len(), 2); + check_spends!(txns[1], txns[0], coinbase_tx); + assert!(txns[1].weight().to_wu() < TRUC_CHILD_MAX_WEIGHT); + assert_eq!(txns[1].input.len(), 2); + assert_eq!(txns[1].output.len(), 1); + + assert_eq!(txns[0].compute_txid(), commitment_txid); + assert_eq!(txns[0].input.len(), 1); + assert_eq!(txns[0].output.len(), 1); + assert_eq!(txns[0].output[0].value, Amount::from_sat(240)); + assert_eq!(txns[0].output[0].script_pubkey, shared_anchor_script_pubkey()); + check_spends!(txns[0], funding_tx); + + nodes[0].logger.assert_log( + "lightning::events::bump_transaction", + format!( + "Broadcasting anchor transaction {} to bump channel close with txid {}", + txns[1].compute_txid(), + txns[0].compute_txid() + ), + 1, + ); + } else { + assert_eq!(txns.len(), 1); + assert_eq!(txns[0].compute_txid(), commitment_txid); + assert_eq!(txns[0].input.len(), 1); + assert_eq!(txns[0].output.len(), 1); + assert_eq!(txns[0].output[0].value, Amount::from_sat(240)); + assert_eq!(txns[0].output[0].script_pubkey, shared_anchor_script_pubkey()); + check_spends!(txns[0], funding_tx); + + let weight = txns[0].weight(); + let feerate = (channel_value_sat - 240) * 1000 / weight.to_wu(); + + nodes[0].logger.assert_log( + "lightning::events::bump_transaction", + format!( + "Pre-signed commitment {} already has feerate {} sat/kW above required 253 sat/kW, broadcasting.", + txns[0].compute_txid(), + feerate, + ), + 1, + ); + } +} + +#[xtest(feature = "_externalize_tests")] +fn test_0reserve_zero_conf_combined() { + // Test that zero-reserve and zero-conf features work together: a channel that + // is immediately usable (no confirmations needed) and has zero reserve for the opener. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + + // Node 0 creates a channel to node 1. + nodes[0].node.create_channel(node_b_id, channel_value_sat, 0, 42, None, None).unwrap(); + let open_channel = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); + + // Node 1 accepts with both zero-conf AND zero-reserve. + nodes[1].node.handle_open_channel(node_a_id, &open_channel); + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match events[0] { + Event::OpenChannelRequest { temporary_channel_id: chan_id, .. } => { + nodes[1] + .node + .accept_inbound_channel_from_trusted_peer( + &chan_id, + &node_a_id, + 0, + TrustedChannelFeatures::ZeroConfZeroReserve, + None, + ) + .unwrap(); + }, + _ => panic!("Unexpected event"), + }; + + // Verify zero-conf: minimum_depth should be 0. + let accept_channel = get_event_msg!(nodes[1], MessageSendEvent::SendAcceptChannel, node_a_id); + assert_eq!(accept_channel.common_fields.minimum_depth, 0); + nodes[0].node.handle_accept_channel(node_b_id, &accept_channel); + + // Create the funding transaction (no block confirmations needed for zero-conf). + let (temporary_channel_id, tx, _) = + create_funding_transaction(&nodes[0], &node_b_id, channel_value_sat, 42); + nodes[0] + .node + .funding_transaction_generated(temporary_channel_id, node_b_id, tx.clone()) + .unwrap(); + let funding_created = get_event_msg!(nodes[0], MessageSendEvent::SendFundingCreated, node_b_id); + + // Node 1 handles funding_created and immediately sends both FundingSigned and ChannelReady. + nodes[1].node.handle_funding_created(node_a_id, &funding_created); + check_added_monitors(&nodes[1], 1); + let bs_signed_locked = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(bs_signed_locked.len(), 2); + + let as_channel_ready; + match &bs_signed_locked[0] { + MessageSendEvent::SendFundingSigned { node_id, msg } => { + assert_eq!(*node_id, node_a_id); + nodes[0].node.handle_funding_signed(node_b_id, &msg); + expect_channel_pending_event(&nodes[0], &node_b_id); + expect_channel_pending_event(&nodes[1], &node_a_id); + check_added_monitors(&nodes[0], 1); + + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); + assert_eq!(nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap()[0], tx); + nodes[0].tx_broadcaster.clear(); + + as_channel_ready = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelReady, node_b_id); + }, + _ => panic!("Unexpected event"), + } + match &bs_signed_locked[1] { + MessageSendEvent::SendChannelReady { node_id, msg } => { + assert_eq!(*node_id, node_a_id); + nodes[0].node.handle_channel_ready(node_b_id, &msg); + expect_channel_ready_event(&nodes[0], &node_b_id); + }, + _ => panic!("Unexpected event"), + } + + nodes[1].node.handle_channel_ready(node_a_id, &as_channel_ready); + expect_channel_ready_event(&nodes[1], &node_a_id); + + let as_channel_update = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, node_b_id); + let bs_channel_update = + get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_a_id); + nodes[0].node.handle_channel_update(node_b_id, &bs_channel_update); + nodes[1].node.handle_channel_update(node_a_id, &as_channel_update); + + // Channel should be immediately usable without any block confirmations. + assert_eq!(nodes[0].node.list_usable_channels().len(), 1); + assert_eq!(nodes[1].node.list_usable_channels().len(), 1); + + // Verify zero-reserve: opener (node 0) should have 0 reserve. + let details_a = &nodes[0].node.list_channels()[0]; + let node_0_reserve = details_a.unspendable_punishment_reserve.unwrap(); + let node_0_max_htlc = details_a.next_outbound_htlc_limit_msat; + let channel_type = details_a.channel_type.clone().unwrap(); + assert_eq!(node_0_reserve, 0); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + assert!(details_a.is_usable); + assert_eq!(details_a.confirmations.unwrap(), 0); + assert_eq!( + node_0_max_htlc, + (channel_value_sat - commit_tx_fee_sat(253, 2, &channel_type) - 2 * 330) * 1000 + ); + + // Verify acceptor (node 1) has a non-zero reserve. + let details_b = &nodes[1].node.list_channels()[0]; + assert_ne!(details_b.unspendable_punishment_reserve.unwrap(), 0); + assert!(details_b.is_usable); + + // Send payments in both directions to verify the combined feature works end-to-end. + send_payment(&nodes[0], &[&nodes[1]], node_0_max_htlc); + + let details_b = &nodes[1].node.list_channels()[0]; + let node_1_reserve = details_b.unspendable_punishment_reserve.unwrap(); + let node_1_max_htlc = details_b.next_outbound_htlc_limit_msat; + assert_eq!(node_1_reserve, 1000); + assert_eq!(node_1_max_htlc, node_0_max_htlc - node_1_reserve * 1000); + send_payment(&nodes[1], &[&nodes[0]], node_1_max_htlc); +} + +#[xtest(feature = "_externalize_tests")] +fn test_outbound_vs_available_capacity_outbound_htlc_limit_spiked_feerate() { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + + let channel_type = ChannelTypeFeatures::only_static_remote_key(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_a_id = nodes[0].node.get_our_node_id(); + let _node_b_id = nodes[1].node.get_our_node_id(); + + const FEERATE: u32 = 253; + const MULTIPLE: u32 = FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; + const SPIKED_FEERATE: u32 = FEERATE * MULTIPLE; + const DUST_LIMIT_MSAT: u64 = 354 * 1000; + const CHANNEL_VALUE_MSAT: u64 = 10_000 * 1000; + const NODE_0_VALUE_TO_SELF_MSAT: u64 = 5000 * 1000; + const NODE_1_VALUE_TO_SELF_MSAT: u64 = 5000 * 1000; + const CHANNEL_RESERVE_MSAT: u64 = 1000 * 1000; + + // Find the HTLC amount that will be non-dust at the current feerate, but dust at the spiked feerate + const SPIKED_DUST_HTLC_MSAT: u64 = 688 * 1000; + const HTLC_SPIKE_DUST_LIMIT_MSAT: u64 = 689 * 1000; + let htlc_timeout_spike_tx_fee_msat = + second_stage_tx_fees_sat(&channel_type, SPIKED_FEERATE).1 * 1000; + assert_eq!(HTLC_SPIKE_DUST_LIMIT_MSAT, DUST_LIMIT_MSAT + htlc_timeout_spike_tx_fee_msat); + + let channel_id = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, CHANNEL_VALUE_MSAT / 1000, 0) + .2; + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + { + // Quick double-check on the dust limit to make sure HTLCs would be dust at 2x the + // feerate... + let mut per_peer_lock; + let mut peer_state_lock; + + let channel = + get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id); + assert_eq!(channel.context().holder_dust_limit_satoshis * 1000, DUST_LIMIT_MSAT); + } + + // Balance the channel so each side has 5_000 sats + send_payment(&nodes[0], &[&nodes[1]], NODE_1_VALUE_TO_SELF_MSAT); + + let count_total_htlcs = |details: &ChannelDetails| { + details.pending_outbound_htlcs.len() + details.pending_inbound_htlcs.len() + }; + let count_node_0_nondust_htlcs = || { + let mut txs = get_local_commitment_txn!(nodes[0], channel_id); + let commitment_tx = &txs[0]; + commitment_tx + .output + .iter() + .filter(|output| output.value.to_sat() * 1000 == SPIKED_DUST_HTLC_MSAT) + .count() + }; + let count_node_1_nondust_htlcs = || { + let mut txs = get_local_commitment_txn!(nodes[1], channel_id); + let commitment_tx = &txs[0]; + commitment_tx + .output + .iter() + .filter(|output| output.value.to_sat() * 1000 == SPIKED_DUST_HTLC_MSAT) + .count() + }; + + // Sanity check + { + let reserved_fee_sat = commit_tx_fee_sat(SPIKED_FEERATE, 2, &channel_type); + let node_0_outbound_capacity_msat = NODE_0_VALUE_TO_SELF_MSAT - CHANNEL_RESERVE_MSAT; + let node_0_available_capacity_msat = + node_0_outbound_capacity_msat - reserved_fee_sat * 1000; + let node_0_details = &nodes[0].node.list_channels()[0]; + assert_eq!(node_0_details.outbound_capacity_msat, node_0_outbound_capacity_msat); + assert_eq!(node_0_details.next_outbound_htlc_limit_msat, node_0_available_capacity_msat); + assert_eq!(count_total_htlcs(&node_0_details), 0); + assert_eq!(count_node_0_nondust_htlcs(), 0); + } + + // Route 2 688sat HTLCs from node 0 to node 1 + for i in 1..3 { + route_payment(&nodes[0], &[&nodes[1]], SPIKED_DUST_HTLC_MSAT); + + let max_reserved_fee_msat = commit_tx_fee_sat(SPIKED_FEERATE, 2 + i, &channel_type) * 1000; + let node_0_outbound_capacity_msat = + NODE_0_VALUE_TO_SELF_MSAT - SPIKED_DUST_HTLC_MSAT * i as u64 - CHANNEL_RESERVE_MSAT; + let node_0_available_capacity_msat = node_0_outbound_capacity_msat - max_reserved_fee_msat; + // Node 0 can send non-dust HTLCs throughout + assert!(node_0_available_capacity_msat >= HTLC_SPIKE_DUST_LIMIT_MSAT); + let node_0_details = &nodes[0].node.list_channels()[0]; + assert_eq!(node_0_details.outbound_capacity_msat, node_0_outbound_capacity_msat); + assert_eq!(node_0_details.next_outbound_htlc_limit_msat, node_0_available_capacity_msat); + assert_eq!(count_total_htlcs(&node_0_details), i); + assert_eq!(count_node_0_nondust_htlcs(), i); + } + + let node_0_details = &nodes[0].node.list_channels()[0]; + let local_nondust_htlc_count = 2; + assert_eq!(count_total_htlcs(&node_0_details), local_nondust_htlc_count); + assert_eq!(count_node_0_nondust_htlcs(), local_nondust_htlc_count); + assert_eq!(count_node_1_nondust_htlcs(), local_nondust_htlc_count); + + let node_0_outbound_capacity_msat = node_0_details.outbound_capacity_msat; + + // Route 2 688sat HTLCs from node 1 to node 0 + for i in 1..3 { + route_payment(&nodes[1], &[&nodes[0]], SPIKED_DUST_HTLC_MSAT); + + let node_1_outbound_capacity_msat = + NODE_1_VALUE_TO_SELF_MSAT - SPIKED_DUST_HTLC_MSAT * i as u64 - CHANNEL_RESERVE_MSAT; + assert!(node_1_outbound_capacity_msat >= HTLC_SPIKE_DUST_LIMIT_MSAT); + let node_1_details = &nodes[1].node.list_channels()[0]; + assert_eq!(node_1_details.outbound_capacity_msat, node_1_outbound_capacity_msat); + assert_eq!(node_1_details.next_outbound_htlc_limit_msat, node_1_outbound_capacity_msat); + + let nondust_htlc_count = 2 + i; + // At the current feerate, 688sat HTLCs are present on both commitments + assert_eq!(count_node_0_nondust_htlcs(), nondust_htlc_count); + assert_eq!(count_node_1_nondust_htlcs(), nondust_htlc_count); + + assert_eq!( + nodes[0].node.list_channels()[0].outbound_capacity_msat, + node_0_outbound_capacity_msat + ); + let max_reserved_fee_msat = + commit_tx_fee_sat(SPIKED_FEERATE, nondust_htlc_count + 2, &channel_type) * 1000; + assert_eq!( + nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat, + node_0_outbound_capacity_msat - max_reserved_fee_msat + ); + } +} + +/// Make sure that we do not account for HTLCs going from non-dust to dust at the spiked feerate +/// when checking the fee spike buffer in `can_accept_incoming_htlc`. This is required to make sure +/// that we can afford *any* increase in the feerate between 1x to 2x, instead of checking whether +/// we can afford only the 2x increase in the feerate. +#[xtest(feature = "_externalize_tests")] +fn test_fail_cannot_afford_dust_htlcs_at_spike_multiple_if_nondust_at_base_feerate() { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + + let channel_type = ChannelTypeFeatures::only_static_remote_key(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let _node_b_id = nodes[1].node.get_our_node_id(); + + const FEERATE: u32 = 253; + const MULTIPLE: u32 = FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32; + const SPIKED_FEERATE: u32 = FEERATE * MULTIPLE; + const DUST_LIMIT_MSAT: u64 = 354 * 1000; + const CHANNEL_VALUE_MSAT: u64 = 10_000 * 1000; + const NODE_0_VALUE_TO_SELF_MSAT: u64 = 5_000 * 1000; + const NODE_1_VALUE_TO_SELF_MSAT: u64 = 5_000 * 1000; + const CHANNEL_RESERVE_MSAT: u64 = 1_000 * 1_000; + + let channel_id = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + CHANNEL_VALUE_MSAT / 1000, + NODE_1_VALUE_TO_SELF_MSAT, + ) + .2; + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + // Find the HTLC amount that will be non-dust at the current feerate, + // but dust at the spiked feerate. + const SPIKED_DUST_HTLC_MSAT: u64 = 688 * 1000; + const HTLC_SPIKE_DUST_LIMIT_MSAT: u64 = 689 * 1000; + // When checking the fee spike buffer in `can_accept_incoming_htlc`, we check the remote + // commitment, hence inbound HTLCs will be offered HTLCs, and use the timeout dust limit. + let htlc_timeout_spike_tx_fee_msat = + second_stage_tx_fees_sat(&channel_type, SPIKED_FEERATE).1 * 1000; + assert_eq!(HTLC_SPIKE_DUST_LIMIT_MSAT, DUST_LIMIT_MSAT + htlc_timeout_spike_tx_fee_msat); + + // Calculate here the dust limit at the current feerate so we know when node 0 cannot send + // any further non-dust HTLCs at the current feerate. + let htlc_timeout_tx_fee_msat = second_stage_tx_fees_sat(&channel_type, FEERATE).1 * 1000; + let htlc_dust_limit_msat = DUST_LIMIT_MSAT + htlc_timeout_tx_fee_msat; + // Make sure the HTLC will be non-dust at the current feerate + assert!(SPIKED_DUST_HTLC_MSAT > htlc_dust_limit_msat); + + // Place a few non-dust HTLCs on the commitment, these HTLCs would get trimmed upon a 2x + // increase in the feerate. + let mut sent_htlcs_count: usize = 0; + let mut payment_hashes = Vec::new(); + while nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat >= htlc_dust_limit_msat { + let (_preimage, hash, _secret, _id) = + route_payment(&nodes[0], &[&nodes[1]], SPIKED_DUST_HTLC_MSAT); + payment_hashes.push(hash); + sent_htlcs_count += 1; + } + assert_eq!(sent_htlcs_count, 4); + + // Check the outbound and available capacities + let node_0_outbound_capacity_msat = NODE_0_VALUE_TO_SELF_MSAT + - sent_htlcs_count as u64 * SPIKED_DUST_HTLC_MSAT + - CHANNEL_RESERVE_MSAT; + let node_0_details = &nodes[0].node.list_channels()[0]; + assert_eq!(node_0_details.outbound_capacity_msat, node_0_outbound_capacity_msat); + // Node 0 can now only send dust HTLCs, so we reserve the fees for a single additional + // inbound non-dust HTLC. + let min_reserved_fee_msat = + commit_tx_fee_sat(SPIKED_FEERATE, sent_htlcs_count + 1, &channel_type) * 1000; + let node_0_available_capacity_msat = node_0_outbound_capacity_msat - min_reserved_fee_msat; + assert_eq!(node_0_details.next_outbound_htlc_limit_msat, node_0_available_capacity_msat); + + // Then send an identical, 5th non-dust HTLC, bypass the validation from the holder, and + // check that the counterparty fails it due to a fee spike buffer violation. + + // First check the maths + + // Node 0 can afford an exact 2x increase in the feerate + let spiked_commit_tx_fee_msat = commit_tx_fee_sat(SPIKED_FEERATE, 0, &channel_type) * 1000; + assert!((node_0_outbound_capacity_msat - SPIKED_DUST_HTLC_MSAT) + .checked_sub(spiked_commit_tx_fee_msat) + .is_some()); + // Node 0 can afford a 5th non-dust HTLC at the current feerate, so `update_add_htlc` + // validation will pass. + let real_commit_tx_fee_msat = commit_tx_fee_sat(FEERATE, 5, &channel_type) * 1000; + assert!((node_0_outbound_capacity_msat - SPIKED_DUST_HTLC_MSAT) + .checked_sub(real_commit_tx_fee_msat) + .is_some()); + // But we don't account for the HTLC trimming effect of the spike multiple feerate increase, + // so the 5th HTLC should be rejected at `can_accept_incoming_htlc`! + let expected_commit_tx_fee_msat = commit_tx_fee_sat(SPIKED_FEERATE, 5, &channel_type) * 1000; + assert!((node_0_outbound_capacity_msat - SPIKED_DUST_HTLC_MSAT) + .checked_sub(expected_commit_tx_fee_msat) + .is_none()); + + // Then run the experiment + + let sender_amount_msat = node_0_available_capacity_msat; + let receiver_amount_msat = SPIKED_DUST_HTLC_MSAT; + let (route, payment_hash, _, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], sender_amount_msat); + let secp_ctx = Secp256k1::new(); + let session_priv = SecretKey::from_slice(&[42; 32]).unwrap(); + let cur_height = nodes[0].node.best_block.read().unwrap().height + 1; + let onion_keys = onion_utils::construct_onion_keys(&secp_ctx, &route.paths[0], &session_priv); + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, sender_amount_msat); + let (onion_payloads, htlc_msat, htlc_cltv) = onion_utils::test_build_onion_payloads( + &route.paths[0], + &recipient_onion_fields, + cur_height, + &None, + None, + None, + ) + .unwrap(); + assert_eq!(htlc_msat, sender_amount_msat); + let onion_packet = + onion_utils::construct_onion_packet(onion_payloads, onion_keys, [0; 32], &payment_hash) + .unwrap(); + let msg = msgs::UpdateAddHTLC { + channel_id, + htlc_id: sent_htlcs_count as u64, + amount_msat: receiver_amount_msat, + payment_hash, + cltv_expiry: htlc_cltv, + onion_routing_packet: onion_packet, + skimmed_fee_msat: None, + blinding_point: None, + hold_htlc: None, + accountable: None, + }; + + nodes[1].node.handle_update_add_htlc(node_a_id, &msg); + + let htlcs_in_tx = vec![ + HTLCOutputInCommitment { + offered: false, + cltv_expiry: 81, + payment_hash: payment_hashes.iter().find(|hash| hash.0[0] == 0x75).unwrap().clone(), + amount_msat: 688_000, + transaction_output_index: Some(0), + }, + HTLCOutputInCommitment { + offered: false, + cltv_expiry: 81, + payment_hash: payment_hashes.iter().find(|hash| hash.0[0] == 0x64).unwrap().clone(), + amount_msat: 688_000, + transaction_output_index: Some(1), + }, + HTLCOutputInCommitment { + offered: false, + cltv_expiry: 81, + payment_hash, + amount_msat: 688_000, + transaction_output_index: Some(2), + }, + HTLCOutputInCommitment { + offered: false, + cltv_expiry: 81, + payment_hash: payment_hashes.iter().find(|hash| hash.0[0] == 0x72).unwrap().clone(), + amount_msat: 688_000, + transaction_output_index: Some(3), + }, + HTLCOutputInCommitment { + offered: false, + cltv_expiry: 81, + payment_hash: payment_hashes.iter().find(|hash| hash.0[0] == 0x66).unwrap().clone(), + amount_msat: 688_000, + transaction_output_index: Some(4), + }, + ]; + + manually_trigger_update_fail_htlc( + &nodes, + channel_id, + NODE_0_VALUE_TO_SELF_MSAT, + DUST_LIMIT_MSAT / 1000, + payment_hash, + htlcs_in_tx, + true, + ); +} + +#[xtest(feature = "_externalize_tests")] +fn test_available_balances_both_commitments_dust_on_funder_commitment() { + let mut config = test_default_channel_config(); + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + const FEERATE: u32 = 253; + const TOTAL_ANCHORS_MSAT: u64 = 2 * 330_000; + const NODE_0_DUST_LIMIT_MSAT: u64 = 10_000 * 1000; + const NODE_1_DUST_LIMIT_MSAT: u64 = 354 * 1000; + const CHANNEL_VALUE_MSAT: u64 = 50_000 * 1000; + const NODE_0_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000; + const NODE_1_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000; + const NODE_0_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 1_000 * 1_000; + const NODE_1_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 10_000 * 1_000; + + let channel_id = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + CHANNEL_VALUE_MSAT / 1000, + NODE_1_VALUE_TO_SELF_MSAT, + ) + .2; + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().holder_dust_limit_satoshis = NODE_0_DUST_LIMIT_MSAT / 1000; + chan.funding_mut().counterparty_selected_channel_reserve_satoshis = + Some(NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000); + assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_1_DUST_LIMIT_MSAT / 1000); + assert_eq!( + chan.funding().holder_selected_channel_reserve_satoshis, + NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000 + ); + } + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().counterparty_dust_limit_satoshis = NODE_0_DUST_LIMIT_MSAT / 1000; + chan.funding_mut().holder_selected_channel_reserve_satoshis = + NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000; + assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_1_DUST_LIMIT_MSAT / 1000); + assert_eq!( + chan.funding().counterparty_selected_channel_reserve_satoshis, + Some(NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000) + ); + } + + // This HTLC is only present on node 1's commitment + const SNEAKY_HTLC_MSAT: u64 = 5_000_000; + + route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT); + + let node_1_details = &nodes[1].node.list_channels()[0]; + let expected_outbound_capacity_msat = + NODE_1_VALUE_TO_SELF_MSAT - SNEAKY_HTLC_MSAT - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT; + assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat); + let expected_available_capacity_msat = expected_outbound_capacity_msat; + assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat); + let expected_splice_out_max = + NODE_1_VALUE_TO_SELF_MSAT / 1000 - SNEAKY_HTLC_MSAT / 1000 - NODE_1_DUST_LIMIT_MSAT / 1000; + assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max); + + let node_0_details = &nodes[0].node.list_channels()[0]; + let expected_outbound_capacity_msat = + NODE_0_VALUE_TO_SELF_MSAT - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT - TOTAL_ANCHORS_MSAT; + assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat); + let expected_available_capacity_msat = + expected_outbound_capacity_msat - commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000; + assert_eq!(node_0_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat); + let expected_splice_out_max = NODE_0_VALUE_TO_SELF_MSAT / 1000 + - TOTAL_ANCHORS_MSAT / 1000 + - commit_tx_fee_sat(FEERATE, 2, &channel_type) + - NODE_0_DUST_LIMIT_MSAT / 1000; + assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_splice_out_max); + + let node_0_payment_msat = expected_available_capacity_msat; + send_payment(&nodes[0], &[&nodes[1]], node_0_payment_msat); + + route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT); + route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT); + + let node_0_details = &nodes[0].node.list_channels()[0]; + let expected_outbound_capacity_msat = NODE_0_VALUE_TO_SELF_MSAT + - node_0_payment_msat + - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT + - TOTAL_ANCHORS_MSAT; + assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat); + assert_eq!( + node_0_details.outbound_capacity_msat, + commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000 + ); + assert_eq!(node_0_details.next_outbound_htlc_limit_msat, 0); + assert_eq!(node_0_details.next_splice_out_maximum_sat, 0); + + let node_1_details = &nodes[1].node.list_channels()[0]; + let expected_outbound_capacity_msat = NODE_1_VALUE_TO_SELF_MSAT + node_0_payment_msat + - 3 * SNEAKY_HTLC_MSAT + - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT; + assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat); + let (_htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(&channel_type, FEERATE); + let expected_available_capacity_msat = + (NODE_1_DUST_LIMIT_MSAT / 1000 + htlc_timeout_tx_fee_sat) * 1000 - 1; + assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat); + let expected_splice_out_max = NODE_1_VALUE_TO_SELF_MSAT / 1000 + node_0_payment_msat / 1000 + - 3 * SNEAKY_HTLC_MSAT / 1000 + - NODE_1_DUST_LIMIT_MSAT / 1000; + assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max); +} + +#[xtest(feature = "_externalize_tests")] +fn test_available_balances_both_commitments_dust_on_fundee_commitment() { + let mut config = test_default_channel_config(); + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + + let channel_type = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + const FEERATE: u32 = 253; + const TOTAL_ANCHORS_MSAT: u64 = 2 * 330_000; + const NODE_0_DUST_LIMIT_MSAT: u64 = 354 * 1000; + const NODE_1_DUST_LIMIT_MSAT: u64 = 10_000 * 1000; + const CHANNEL_VALUE_MSAT: u64 = 50_000 * 1000; + const NODE_0_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000; + const NODE_1_VALUE_TO_SELF_MSAT: u64 = 25_000 * 1000; + const NODE_0_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 10_000 * 1_000; + const NODE_1_SELECTED_CHANNEL_RESERVE_MSAT: u64 = 1_000 * 1_000; + + let channel_id = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + CHANNEL_VALUE_MSAT / 1000, + NODE_1_VALUE_TO_SELF_MSAT, + ) + .2; + assert_eq!(nodes[0].node.list_channels()[0].channel_type.as_ref().unwrap(), &channel_type); + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().counterparty_dust_limit_satoshis = NODE_1_DUST_LIMIT_MSAT / 1000; + chan.funding_mut().holder_selected_channel_reserve_satoshis = + NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000; + assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_0_DUST_LIMIT_MSAT / 1000); + assert_eq!( + chan.funding().counterparty_selected_channel_reserve_satoshis, + Some(NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000) + ); + } + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().holder_dust_limit_satoshis = NODE_1_DUST_LIMIT_MSAT / 1000; + chan.funding_mut().counterparty_selected_channel_reserve_satoshis = + Some(NODE_0_SELECTED_CHANNEL_RESERVE_MSAT / 1000); + assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_0_DUST_LIMIT_MSAT / 1000); + assert_eq!( + chan.funding().holder_selected_channel_reserve_satoshis, + NODE_1_SELECTED_CHANNEL_RESERVE_MSAT / 1000 + ); + } + + // This HTLC is only present on node 0's commitment + const SNEAKY_HTLC_MSAT: u64 = 5_000_000; + + route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT); + + let node_1_details = &nodes[1].node.list_channels()[0]; + let expected_outbound_capacity_msat = + NODE_1_VALUE_TO_SELF_MSAT - SNEAKY_HTLC_MSAT - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT; + assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat); + let expected_available_capacity_msat = expected_outbound_capacity_msat; + assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat); + let expected_splice_out_max = + NODE_1_VALUE_TO_SELF_MSAT / 1000 - SNEAKY_HTLC_MSAT / 1000 - NODE_1_DUST_LIMIT_MSAT / 1000; + assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max); + + let node_0_details = &nodes[0].node.list_channels()[0]; + + let expected_outbound_capacity_msat = + NODE_0_VALUE_TO_SELF_MSAT - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT - TOTAL_ANCHORS_MSAT; + assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat); + + let expected_splice_out_max = NODE_0_VALUE_TO_SELF_MSAT / 1000 + - TOTAL_ANCHORS_MSAT / 1000 + - commit_tx_fee_sat(FEERATE, 2, &channel_type) + - NODE_0_DUST_LIMIT_MSAT / 1000; + assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_splice_out_max); + + let expected_available_capacity_msat = + expected_outbound_capacity_msat - commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000; + assert_eq!(node_0_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat); + + let node_0_payment_msat = expected_available_capacity_msat; + send_payment(&nodes[0], &[&nodes[1]], node_0_payment_msat); + + route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT); + route_payment(&nodes[1], &[&nodes[0]], SNEAKY_HTLC_MSAT); + + let node_0_details = &nodes[0].node.list_channels()[0]; + let expected_outbound_capacity_msat = NODE_0_VALUE_TO_SELF_MSAT + - node_0_payment_msat + - NODE_1_SELECTED_CHANNEL_RESERVE_MSAT + - TOTAL_ANCHORS_MSAT; + assert_eq!(node_0_details.outbound_capacity_msat, expected_outbound_capacity_msat); + assert_eq!( + node_0_details.outbound_capacity_msat, + commit_tx_fee_sat(FEERATE, 3, &channel_type) * 1000 + ); + assert_eq!(node_0_details.next_outbound_htlc_limit_msat, 0); + + let local_balance_before_fee_sat = + NODE_0_VALUE_TO_SELF_MSAT / 1000 - node_0_payment_msat / 1000 - TOTAL_ANCHORS_MSAT / 1000; + let post_splice_delta_above_reserve = commit_tx_fee_sat(FEERATE, 4, &channel_type); + let divident_sat = local_balance_before_fee_sat * 100 + 100 + - (post_splice_delta_above_reserve * 100) + - CHANNEL_VALUE_MSAT / 1000; + let expected_splice_out_max = (divident_sat - 1) / 99; + assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_splice_out_max); + + let node_1_details = &nodes[1].node.list_channels()[0]; + let expected_outbound_capacity_msat = NODE_1_VALUE_TO_SELF_MSAT + node_0_payment_msat + - 3 * SNEAKY_HTLC_MSAT + - NODE_0_SELECTED_CHANNEL_RESERVE_MSAT; + assert_eq!(node_1_details.outbound_capacity_msat, expected_outbound_capacity_msat); + let (htlc_success_tx_fee_sat, _htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(&channel_type, FEERATE); + let expected_available_capacity_msat = + (NODE_0_DUST_LIMIT_MSAT / 1000 + htlc_success_tx_fee_sat) * 1000 - 1; + assert_eq!(node_1_details.next_outbound_htlc_limit_msat, expected_available_capacity_msat); + let expected_splice_out_max = NODE_1_VALUE_TO_SELF_MSAT / 1000 + node_0_payment_msat / 1000 + - 3 * SNEAKY_HTLC_MSAT / 1000 + - NODE_1_DUST_LIMIT_MSAT / 1000; + assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max); +} diff --git a/lightning/src/ln/inbound_payment.rs b/lightning/src/ln/inbound_payment.rs index 51f8b7bfce9..077d2df60e5 100644 --- a/lightning/src/ln/inbound_payment.rs +++ b/lightning/src/ln/inbound_payment.rs @@ -14,11 +14,10 @@ use bitcoin::hashes::hmac::{Hmac, HmacEngine}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; -use crate::crypto::chacha20::ChaCha20; -use crate::crypto::utils::hkdf_extract_expand_6x; +use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_8x}; use crate::ln::msgs; use crate::ln::msgs::MAX_VALUE_MSAT; -use crate::offers::nonce::Nonce; +use crate::offers::nonce::Nonce as LocalNonce; use crate::sign::EntropySource; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::util::errors::APIError; @@ -28,10 +27,10 @@ use crate::util::logger::Logger; use crate::prelude::*; pub(crate) const IV_LEN: usize = 16; -const METADATA_LEN: usize = 16; -const METADATA_KEY_LEN: usize = 32; +const INFO_LEN: usize = 16; +const INFO_KEY_LEN: usize = 32; const AMT_MSAT_LEN: usize = 8; -// Used to shift the payment type bits to take up the top 3 bits of the metadata bytes, or to +// Used to shift the payment type bits to take up the top 3 bits of the info bytes, or to // retrieve said payment type bits. const METHOD_TYPE_OFFSET: usize = 5; @@ -40,22 +39,28 @@ const METHOD_TYPE_OFFSET: usize = 5; /// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key #[derive(Hash, Copy, Clone, PartialEq, Eq, Debug)] pub struct ExpandedKey { - /// The key used to encrypt the bytes containing the payment metadata (i.e. the amount and + /// The key used to encrypt the bytes containing the payment info (i.e. the amount and /// expiry, included for payment verification on decryption). - metadata_key: [u8; 32], - /// The key used to authenticate an LDK-provided payment hash and metadata as previously + info_key: [u8; 32], + /// The key used to authenticate an LDK-provided payment hash and info as previously /// registered with LDK. ldk_pmt_hash_key: [u8; 32], - /// The key used to authenticate a user-provided payment hash and metadata as previously + /// The key used to authenticate a user-provided payment hash and info as previously /// registered with LDK. user_pmt_hash_key: [u8; 32], /// The base key used to derive signing keys and authenticate messages for BOLT 12 Offers. offers_base_key: [u8; 32], /// The key used to encrypt message metadata for BOLT 12 Offers. offers_encryption_key: [u8; 32], - /// The key used to authenticate spontaneous payments' metadata as previously registered with LDK + /// The key used to authenticate spontaneous payments' info as previously registered with LDK /// for inclusion in a blinded path. spontaneous_pmt_key: [u8; 32], + /// The key used to authenticate phantom-node-shared blinded paths as generated by us. Note + /// that this is not used for blinded paths that are not expected to be shared across nodes + /// participating in a "phantom node". + pub(crate) phantom_node_blinded_path_key: [u8; 32], + /// The key used to encrypt payment metadata. + metadata_enc_key: [u8; 32], } impl ExpandedKey { @@ -64,20 +69,24 @@ impl ExpandedKey { /// It is recommended to cache this value and not regenerate it for each new inbound payment. pub fn new(key_material: [u8; 32]) -> ExpandedKey { let ( - metadata_key, + info_key, ldk_pmt_hash_key, user_pmt_hash_key, offers_base_key, offers_encryption_key, spontaneous_pmt_key, - ) = hkdf_extract_expand_6x(b"LDK Inbound Payment Key Expansion", &key_material); + phantom_node_blinded_path_key, + metadata_enc_key, + ) = hkdf_extract_expand_8x(b"LDK Inbound Payment Key Expansion", &key_material); Self { - metadata_key, + info_key, ldk_pmt_hash_key, user_pmt_hash_key, offers_base_key, offers_encryption_key, spontaneous_pmt_key, + phantom_node_blinded_path_key, + metadata_enc_key, } } @@ -90,8 +99,8 @@ impl ExpandedKey { /// Encrypts or decrypts the given `bytes`. Used for data included in an offer message's /// metadata (e.g., payment id). - pub(crate) fn crypt_for_offer(&self, mut bytes: [u8; 32], nonce: Nonce) -> [u8; 32] { - ChaCha20::encrypt_single_block_in_place(&self.offers_encryption_key, &nonce.0, &mut bytes); + pub(crate) fn crypt_for_offer(&self, mut bytes: [u8; 32], nonce: LocalNonce) -> [u8; 32] { + apply_chacha20(self.offers_encryption_key, nonce.0, &mut bytes); bytes } } @@ -122,7 +131,7 @@ impl Method { } } -fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 { +fn min_final_cltv_expiry_delta_from_info(bytes: [u8; INFO_LEN]) -> u16 { let expiry_bytes = &bytes[AMT_MSAT_LEN..]; u16::from_be_bytes([expiry_bytes[0], expiry_bytes[1]]) } @@ -139,13 +148,17 @@ fn min_final_cltv_expiry_delta_from_metadata(bytes: [u8; METADATA_LEN]) -> u16 { /// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable /// on versions of LDK prior to 0.0.114. /// +/// Returns an encrypted copy of the `payment_metadata` (if any) which must be included as a part of +/// validation. +/// /// [phantom node payments]: crate::sign::PhantomKeysManager /// [`NodeSigner::get_expanded_key`]: crate::sign::NodeSigner::get_expanded_key pub fn create<ES: EntropySource>( keys: &ExpandedKey, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, min_final_cltv_expiry_delta: Option<u16>, -) -> Result<(PaymentHash, PaymentSecret), ()> { - let metadata_bytes = construct_metadata_bytes( + mut payment_metadata: Option<Vec<u8>>, +) -> Result<(PaymentHash, PaymentSecret, Option<Vec<u8>>), ()> { + let info_bytes = construct_info_bytes( min_value_msat, if min_final_cltv_expiry_delta.is_some() { Method::LdkPaymentHashCustomFinalCltv @@ -161,14 +174,22 @@ pub fn create<ES: EntropySource>( let rand_bytes = entropy_source.get_secure_random_bytes(); iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]); + if let Some(metadata) = payment_metadata.as_mut() { + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata.as_mut_slice()); + } + let mut hmac = HmacEngine::<Sha256>::new(&keys.ldk_pmt_hash_key); hmac.input(&iv_bytes); - hmac.input(&metadata_bytes); + hmac.input(&info_bytes); + if let Some(metadata) = payment_metadata.as_ref() { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } let payment_preimage_bytes = Hmac::from_engine(hmac).to_byte_array(); let ldk_pmt_hash = PaymentHash(Sha256::hash(&payment_preimage_bytes).to_byte_array()); - let payment_secret = construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key); - Ok((ldk_pmt_hash, payment_secret)) + let payment_secret = construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key); + Ok((ldk_pmt_hash, payment_secret, payment_metadata)) } /// Equivalent to [`crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash`], @@ -180,12 +201,16 @@ pub fn create<ES: EntropySource>( /// Note that if `min_final_cltv_expiry_delta` is set to some value, then the payment will not be receivable /// on versions of LDK prior to 0.0.114. /// +/// Returns an encrypted copy of the `payment_metadata` (if any) which must be included as a part of +/// validation. +/// /// [phantom node payments]: crate::sign::PhantomKeysManager -pub fn create_from_hash( +pub fn create_from_hash<ES: EntropySource>( keys: &ExpandedKey, min_value_msat: Option<u64>, payment_hash: PaymentHash, - invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option<u16>, -) -> Result<PaymentSecret, ()> { - let metadata_bytes = construct_metadata_bytes( + invoice_expiry_delta_secs: u32, entropy_source: &ES, current_time: u64, + min_final_cltv_expiry_delta: Option<u16>, mut payment_metadata: Option<Vec<u8>>, +) -> Result<(PaymentSecret, Option<Vec<u8>>), ()> { + let info_bytes = construct_info_bytes( min_value_msat, if min_final_cltv_expiry_delta.is_some() { Method::UserPaymentHashCustomFinalCltv @@ -197,22 +222,35 @@ pub fn create_from_hash( min_final_cltv_expiry_delta, )?; + if let Some(metadata) = payment_metadata.as_mut() { + let mut iv_bytes = [0 as u8; IV_LEN]; + let rand_bytes = entropy_source.get_secure_random_bytes(); + iv_bytes.copy_from_slice(&rand_bytes[..IV_LEN]); + + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata.as_mut_slice()); + metadata.extend_from_slice(&iv_bytes); + } + let mut hmac = HmacEngine::<Sha256>::new(&keys.user_pmt_hash_key); - hmac.input(&metadata_bytes); + hmac.input(&info_bytes); hmac.input(&payment_hash.0); + if let Some(metadata) = payment_metadata.as_ref() { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } let hmac_bytes = Hmac::from_engine(hmac).to_byte_array(); let mut iv_bytes = [0 as u8; IV_LEN]; iv_bytes.copy_from_slice(&hmac_bytes[..IV_LEN]); - Ok(construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key)) + Ok((construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key), payment_metadata)) } pub(crate) fn create_for_spontaneous_payment( keys: &ExpandedKey, min_value_msat: Option<u64>, invoice_expiry_delta_secs: u32, current_time: u64, min_final_cltv_expiry_delta: Option<u16>, ) -> Result<PaymentSecret, ()> { - let metadata_bytes = construct_metadata_bytes( + let info_bytes = construct_info_bytes( min_value_msat, Method::SpontaneousPayment, invoice_expiry_delta_secs, @@ -221,13 +259,13 @@ pub(crate) fn create_for_spontaneous_payment( )?; let mut hmac = HmacEngine::<Sha256>::new(&keys.spontaneous_pmt_key); - hmac.input(&metadata_bytes); + hmac.input(&info_bytes); let hmac_bytes = Hmac::from_engine(hmac).to_byte_array(); let mut iv_bytes = [0 as u8; IV_LEN]; iv_bytes.copy_from_slice(&hmac_bytes[..IV_LEN]); - Ok(construct_payment_secret(&iv_bytes, &metadata_bytes, &keys.metadata_key)) + Ok(construct_payment_secret(&iv_bytes, &info_bytes, &keys.info_key)) } pub(crate) fn calculate_absolute_expiry( @@ -241,10 +279,10 @@ pub(crate) fn calculate_absolute_expiry( highest_seen_timestamp + invoice_expiry_delta_secs as u64 + 7200 } -fn construct_metadata_bytes( +fn construct_info_bytes( min_value_msat: Option<u64>, payment_type: Method, invoice_expiry_delta_secs: u32, highest_seen_timestamp: u64, min_final_cltv_expiry_delta: Option<u16>, -) -> Result<[u8; METADATA_LEN], ()> { +) -> Result<[u8; INFO_LEN], ()> { if min_value_msat.is_some() && min_value_msat.unwrap() > MAX_VALUE_MSAT { return Err(()); } @@ -279,41 +317,37 @@ fn construct_metadata_bytes( expiry_bytes[1] |= bytes[1]; } - let mut metadata_bytes: [u8; METADATA_LEN] = [0; METADATA_LEN]; + let mut info_bytes: [u8; INFO_LEN] = [0; INFO_LEN]; - metadata_bytes[..AMT_MSAT_LEN].copy_from_slice(&min_amt_msat_bytes); - metadata_bytes[AMT_MSAT_LEN..].copy_from_slice(&expiry_bytes); + info_bytes[..AMT_MSAT_LEN].copy_from_slice(&min_amt_msat_bytes); + info_bytes[AMT_MSAT_LEN..].copy_from_slice(&expiry_bytes); - Ok(metadata_bytes) + Ok(info_bytes) } fn construct_payment_secret( - iv_bytes: &[u8; IV_LEN], metadata_bytes: &[u8; METADATA_LEN], - metadata_key: &[u8; METADATA_KEY_LEN], + iv_bytes: &[u8; IV_LEN], info_bytes: &[u8; INFO_LEN], info_key: &[u8; INFO_KEY_LEN], ) -> PaymentSecret { let mut payment_secret_bytes: [u8; 32] = [0; 32]; - let (iv_slice, encrypted_metadata_slice) = payment_secret_bytes.split_at_mut(IV_LEN); + let (iv_slice, encrypted_info_slice) = payment_secret_bytes.split_at_mut(IV_LEN); iv_slice.copy_from_slice(iv_bytes); - ChaCha20::encrypt_single_block( - metadata_key, - iv_bytes, - encrypted_metadata_slice, - metadata_bytes, - ); + encrypted_info_slice.copy_from_slice(info_bytes); + apply_chacha20(*info_key, *iv_bytes, encrypted_info_slice); + PaymentSecret(payment_secret_bytes) } /// Check that an inbound payment's `payment_data` field is sane. /// /// LDK does not store any data for pending inbound payments. Instead, we construct our payment -/// secret (and, if supplied by LDK, our payment preimage) to include encrypted metadata about the -/// payment. +/// secret (and, if supplied by LDK, our payment preimage) to include encrypted information about +/// the payment. /// -/// For payments without a custom `min_final_cltv_expiry_delta`, the metadata is constructed as: +/// For payments without a custom `min_final_cltv_expiry_delta`, the payment info is: /// payment method (3 bits) || payment amount (8 bytes - 3 bits) || expiry (8 bytes) /// -/// For payments including a custom `min_final_cltv_expiry_delta`, the metadata is constructed as: +/// For payments including a custom `min_final_cltv_expiry_delta`, the payment info is: /// payment method (3 bits) || payment amount (8 bytes - 3 bits) || min_final_cltv_expiry_delta (2 bytes) || expiry (6 bytes) /// /// In both cases the result is then encrypted using a key derived from [`NodeSigner::get_expanded_key`]. @@ -326,14 +360,14 @@ fn construct_payment_secret( /// method is called, then the payment method bits mentioned above are represented internally as /// [`Method::LdkPaymentHash`]. If the latter, [`Method::UserPaymentHash`]. /// -/// For the former method, the payment preimage is constructed as an HMAC of payment metadata and -/// random bytes. Because the payment secret is also encoded with these random bytes and metadata -/// (with the metadata encrypted with a block cipher), we're able to authenticate the preimage on +/// For the former method, the payment preimage is constructed as an HMAC of payment info and +/// random bytes. Because the payment secret is also encoded with these random bytes and info +/// (with the info encrypted with a block cipher), we're able to authenticate the preimage on /// payment receipt. /// /// For the latter, the payment secret instead contains an HMAC of the user-provided payment hash -/// and payment metadata (encrypted with a block cipher), allowing us to authenticate the payment -/// hash and metadata on payment receipt. +/// and payment info (encrypted with a block cipher), allowing us to authenticate the payment +/// hash and info on payment receipt. /// /// See [`ExpandedKey`] docs for more info on the individual keys used. /// @@ -341,17 +375,17 @@ fn construct_payment_secret( /// [`create_inbound_payment`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment /// [`create_inbound_payment_for_hash`]: crate::ln::channelmanager::ChannelManager::create_inbound_payment_for_hash pub(super) fn verify<L: Logger>( - payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData, highest_seen_timestamp: u64, - keys: &ExpandedKey, logger: &L, + payment_hash: PaymentHash, payment_data: &msgs::FinalOnionHopData, + mut payment_metadata: Option<&mut Vec<u8>>, highest_seen_timestamp: u64, keys: &ExpandedKey, + logger: &L, ) -> Result<(Option<PaymentPreimage>, Option<u16>), ()> { - let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_data.payment_secret, keys); + let (iv_bytes, info_bytes) = decrypt_info(payment_data.payment_secret, keys); - let payment_type_res = - Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET); + let payment_type_res = Method::from_bits((info_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET); let mut amt_msat_bytes = [0; AMT_MSAT_LEN]; - let mut expiry_bytes = [0; METADATA_LEN - AMT_MSAT_LEN]; - amt_msat_bytes.copy_from_slice(&metadata_bytes[..AMT_MSAT_LEN]); - expiry_bytes.copy_from_slice(&metadata_bytes[AMT_MSAT_LEN..]); + let mut expiry_bytes = [0; INFO_LEN - AMT_MSAT_LEN]; + amt_msat_bytes.copy_from_slice(&info_bytes[..AMT_MSAT_LEN]); + expiry_bytes.copy_from_slice(&info_bytes[AMT_MSAT_LEN..]); // Zero out the bits reserved to indicate the payment type. amt_msat_bytes[0] &= 0b00011111; let mut min_final_cltv_expiry_delta = None; @@ -362,8 +396,12 @@ pub(super) fn verify<L: Logger>( match payment_type_res { Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => { let mut hmac = HmacEngine::<Sha256>::new(&keys.user_pmt_hash_key); - hmac.input(&metadata_bytes[..]); + hmac.input(&info_bytes[..]); hmac.input(&payment_hash.0); + if let Some(metadata) = payment_metadata.as_deref() { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } if !fixed_time_eq( &iv_bytes, &Hmac::from_engine(hmac).to_byte_array().split_at_mut(IV_LEN).0, @@ -374,10 +412,29 @@ pub(super) fn verify<L: Logger>( &payment_hash ); return Err(()); + }; + + if let Some(metadata) = payment_metadata.as_mut() { + if metadata.len() < IV_LEN { + log_trace!(logger, "payment_metadata was shorter than expected IV. Failing HTLC with payment_hash {payment_hash}"); + return Err(()); + } + let new_len = metadata.len() - IV_LEN; + let (metadata_enc, metadata_iv) = metadata.split_at_mut(new_len); + let metadata_iv: [u8; IV_LEN] = metadata_iv.try_into().expect("len checked"); + + apply_chacha20(keys.metadata_enc_key, metadata_iv, metadata_enc); + metadata.truncate(new_len); } }, Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - match derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys) { + match derive_ldk_payment_preimage( + payment_hash, + &iv_bytes, + &info_bytes, + payment_metadata.as_deref().map(Vec::as_slice), + keys, + ) { Ok(preimage) => payment_preimage = Some(preimage), Err(bad_preimage_bytes) => { log_trace!( @@ -389,10 +446,18 @@ pub(super) fn verify<L: Logger>( return Err(()); }, } + + if let Some(metadata) = payment_metadata { + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata); + } }, Ok(Method::SpontaneousPayment) => { + if payment_metadata.is_some() { + log_trace!(logger, "Shouldn't have a payment_metadata for a spontaneous payment, failing payment with hash {payment_hash}"); + return Err(()); + } let mut hmac = HmacEngine::<Sha256>::new(&keys.spontaneous_pmt_key); - hmac.input(&metadata_bytes[..]); + hmac.input(&info_bytes[..]); if !fixed_time_eq( &iv_bytes, &Hmac::from_engine(hmac).to_byte_array().split_at_mut(IV_LEN).0, @@ -414,8 +479,7 @@ pub(super) fn verify<L: Logger>( match payment_type_res { Ok(Method::UserPaymentHashCustomFinalCltv) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - min_final_cltv_expiry_delta = - Some(min_final_cltv_expiry_delta_from_metadata(metadata_bytes)); + min_final_cltv_expiry_delta = Some(min_final_cltv_expiry_delta_from_info(info_bytes)); // Zero out first two bytes of expiry reserved for `min_final_cltv_expiry_delta`. expiry_bytes[0] &= 0; expiry_bytes[1] &= 0; @@ -440,21 +504,32 @@ pub(super) fn verify<L: Logger>( } pub(super) fn get_payment_preimage( - payment_hash: PaymentHash, payment_secret: PaymentSecret, keys: &ExpandedKey, + payment_hash: PaymentHash, payment_secret: PaymentSecret, payment_metadata: Option<&mut [u8]>, + keys: &ExpandedKey, ) -> Result<PaymentPreimage, APIError> { - let (iv_bytes, metadata_bytes) = decrypt_metadata(payment_secret, keys); + let (iv_bytes, info_bytes) = decrypt_info(payment_secret, keys); - match Method::from_bits((metadata_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) { + match Method::from_bits((info_bytes[0] & 0b1110_0000) >> METHOD_TYPE_OFFSET) { Ok(Method::LdkPaymentHash) | Ok(Method::LdkPaymentHashCustomFinalCltv) => { - derive_ldk_payment_preimage(payment_hash, &iv_bytes, &metadata_bytes, keys).map_err( - |bad_preimage_bytes| APIError::APIMisuseError { - err: format!( - "Payment hash {} did not match decoded preimage {}", - &payment_hash, - log_bytes!(bad_preimage_bytes) - ), - }, + let preimage = derive_ldk_payment_preimage( + payment_hash, + &iv_bytes, + &info_bytes, + payment_metadata.as_deref(), + keys, ) + .map_err(|bad_preimage_bytes| APIError::APIMisuseError { + err: format!( + "Payment hash {} did not match decoded preimage {}", + &payment_hash, + log_bytes!(bad_preimage_bytes) + ), + })?; + + if let Some(metadata) = payment_metadata { + apply_chacha20(keys.metadata_enc_key, iv_bytes, metadata); + } + Ok(preimage) }, Ok(Method::UserPaymentHash) | Ok(Method::UserPaymentHashCustomFinalCltv) => { Err(APIError::APIMisuseError { @@ -471,33 +546,33 @@ pub(super) fn get_payment_preimage( } } -fn decrypt_metadata( +fn decrypt_info( payment_secret: PaymentSecret, keys: &ExpandedKey, -) -> ([u8; IV_LEN], [u8; METADATA_LEN]) { +) -> ([u8; IV_LEN], [u8; INFO_LEN]) { let mut iv_bytes = [0; IV_LEN]; - let (iv_slice, encrypted_metadata_bytes) = payment_secret.0.split_at(IV_LEN); + let (iv_slice, encrypted_info_bytes) = payment_secret.0.split_at(IV_LEN); iv_bytes.copy_from_slice(iv_slice); - let mut metadata_bytes: [u8; METADATA_LEN] = [0; METADATA_LEN]; - ChaCha20::encrypt_single_block( - &keys.metadata_key, - &iv_bytes, - &mut metadata_bytes, - encrypted_metadata_bytes, - ); + let mut info_bytes: [u8; INFO_LEN] = [0; INFO_LEN]; + info_bytes.copy_from_slice(encrypted_info_bytes); + apply_chacha20(keys.info_key, iv_bytes, &mut info_bytes); - (iv_bytes, metadata_bytes) + (iv_bytes, info_bytes) } // Errors if the payment preimage doesn't match `payment_hash`. Returns the bad preimage bytes in // this case. fn derive_ldk_payment_preimage( - payment_hash: PaymentHash, iv_bytes: &[u8; IV_LEN], metadata_bytes: &[u8; METADATA_LEN], - keys: &ExpandedKey, + payment_hash: PaymentHash, iv_bytes: &[u8; IV_LEN], info_bytes: &[u8; INFO_LEN], + payment_metadata: Option<&[u8]>, keys: &ExpandedKey, ) -> Result<PaymentPreimage, [u8; 32]> { let mut hmac = HmacEngine::<Sha256>::new(&keys.ldk_pmt_hash_key); hmac.input(iv_bytes); - hmac.input(metadata_bytes); + hmac.input(info_bytes); + if let Some(metadata) = payment_metadata { + hmac.input(&(metadata.len() as u64).to_le_bytes()); + hmac.input(metadata); + } let decoded_payment_preimage = Hmac::from_engine(hmac).to_byte_array(); if !fixed_time_eq(&payment_hash.0, &Sha256::hash(&decoded_payment_preimage).to_byte_array()) { return Err(decoded_payment_preimage); diff --git a/lightning/src/ln/interactivetxs.rs b/lightning/src/ln/interactivetxs.rs index a004f6e9f14..3a93306a2be 100644 --- a/lightning/src/ln/interactivetxs.rs +++ b/lightning/src/ln/interactivetxs.rs @@ -12,12 +12,13 @@ use crate::io_extras::sink; use crate::prelude::*; use bitcoin::absolute::LockTime as AbsoluteLockTime; -use bitcoin::amount::{Amount, SignedAmount}; +use bitcoin::amount::Amount; use bitcoin::consensus::Encodable; use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::ecdsa::Signature as BitcoinSignature; use bitcoin::key::Secp256k1; use bitcoin::policy::MAX_STANDARD_TX_WEIGHT; +use bitcoin::secp256k1::ecdsa::Signature; use bitcoin::secp256k1::{Message, PublicKey}; use bitcoin::sighash::SighashCache; use bitcoin::transaction::Version; @@ -31,12 +32,12 @@ use crate::ln::chan_utils::{ BASE_INPUT_WEIGHT, EMPTY_SCRIPT_SIG_WEIGHT, FUNDING_TRANSACTION_WITNESS_WEIGHT, SEGWIT_MARKER_FLAG_WEIGHT, }; -use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; -use crate::ln::funding::FundingTxInput; +use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::msgs; use crate::ln::msgs::{MessageSendEvent, SerialId, TxSignatures}; use crate::ln::types::ChannelId; use crate::sign::{EntropySource, P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; +use crate::util::wallet_utils::ConfirmedUtxo; use core::fmt::Display; @@ -90,14 +91,7 @@ impl SerialIdExt for SerialId { } } -#[derive(Clone, Debug)] -pub(crate) struct NegotiationError { - pub reason: AbortReason, - pub contributed_inputs: Vec<BitcoinOutPoint>, - pub contributed_outputs: Vec<TxOut>, -} - -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub(crate) enum AbortReason { InvalidStateTransition, UnexpectedCounterpartyMessage, @@ -136,6 +130,22 @@ pub(crate) enum AbortReason { DuplicateFundingOutput, /// More than one funding (shared) input found. DuplicateFundingInput, + /// The RBF feerate is insufficient (e.g., doesn't satisfy the minimum feerate increase rule or + /// can't accommodate prior contributions). + InsufficientRbfFeerate, + /// A funding negotiation is already in progress. + NegotiationInProgress, + /// The initiator's feerate exceeds our maximum. + FeeRateTooHigh, + /// The user manually intervened to abort the funding negotiation via + /// [`ChannelManager::cancel_funding_contributed`]. + /// + /// [`ChannelManager::cancel_funding_contributed`]: crate::ln::channelmanager::ChannelManager::cancel_funding_contributed + ManualIntervention, + /// The contribution is not valid given the current balances of the channel. + InvalidContribution(String), + /// A RBF is not available at this time. + RbfUnavailable(String), /// Internal error InternalError(&'static str), } @@ -195,6 +205,20 @@ impl Display for AbortReason { f.write_str("More than one funding output found") }, AbortReason::DuplicateFundingInput => f.write_str("More than one funding input found"), + AbortReason::InsufficientRbfFeerate => f.write_str("Insufficient RBF feerate"), + AbortReason::NegotiationInProgress => { + f.write_str("A funding negotiation is already in progress") + }, + AbortReason::FeeRateTooHigh => { + f.write_str("The initiator's feerate exceeds our maximum") + }, + AbortReason::ManualIntervention => f.write_str("Manually aborted funding negotiation"), + AbortReason::InvalidContribution(text) => { + f.write_fmt(format_args!("Invalid contribution: {}", text)) + }, + AbortReason::RbfUnavailable(text) => { + f.write_fmt(format_args!("Rejecting RBF attempt: {}", text)) + }, AbortReason::InternalError(text) => { f.write_fmt(format_args!("Internal error: {}", text)) }, @@ -239,16 +263,16 @@ impl TxOutMetadata { } } -impl_writeable_tlv_based!(TxInMetadata, { +impl_ser_tlv_based!(TxInMetadata, { (1, serial_id, required), (3, prev_output, required), }); -impl_writeable_tlv_based!(TxOutMetadata, { +impl_ser_tlv_based!(TxOutMetadata, { (1, serial_id, required), }); -impl_writeable_tlv_based!(ConstructedTransaction, { +impl_ser_tlv_based!(ConstructedTransaction, { (1, holder_is_initiator, required), (3, input_metadata, required), (5, output_metadata, required), @@ -356,14 +380,8 @@ impl ConstructedTransaction { Ok(tx) } - fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { - let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs(); - NegotiationError { reason, contributed_inputs, contributed_outputs } - } - - fn to_contributed_inputs_and_outputs(&self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) { - let contributed_inputs = self - .tx + fn contributed_inputs(&self) -> impl Iterator<Item = BitcoinOutPoint> + '_ { + self.tx .input .iter() .zip(self.input_metadata.iter()) @@ -375,50 +393,17 @@ impl ConstructedTransaction { .unwrap_or(true) }) .map(|(_, (txin, _))| txin.previous_output) - .collect(); - - let contributed_outputs = self - .tx - .output - .iter() - .zip(self.output_metadata.iter()) - .enumerate() - .filter(|(_, (_, output))| output.is_local(self.holder_is_initiator)) - .filter(|(index, _)| *index != self.shared_output_index as usize) - .map(|(_, (txout, _))| txout.clone()) - .collect(); - - (contributed_inputs, contributed_outputs) } - fn into_contributed_inputs_and_outputs(self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) { - let contributed_inputs = self - .tx - .input - .into_iter() - .zip(self.input_metadata.iter()) - .enumerate() - .filter(|(_, (_, input))| input.is_local(self.holder_is_initiator)) - .filter(|(index, _)| { - self.shared_input_index - .map(|shared_index| *index != shared_index as usize) - .unwrap_or(true) - }) - .map(|(_, (txin, _))| txin.previous_output) - .collect(); - - let contributed_outputs = self - .tx + fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.tx .output - .into_iter() + .iter() .zip(self.output_metadata.iter()) .enumerate() .filter(|(_, (_, output))| output.is_local(self.holder_is_initiator)) .filter(|(index, _)| *index != self.shared_output_index as usize) - .map(|(_, (txout, _))| txout) - .collect(); - - (contributed_inputs, contributed_outputs) + .map(|(_, (txout, _))| txout.script_pubkey.as_script()) } pub fn tx(&self) -> &Transaction { @@ -458,12 +443,12 @@ impl ConstructedTransaction { } fn finalize( - &self, holder_tx_signatures: &TxSignatures, counterparty_tx_signatures: &TxSignatures, - shared_input_sig: Option<&SharedInputSignature>, + &self, holder_tx_signatures: TxSignatures, counterparty_tx_signatures: TxSignatures, + shared_input_sig: Option<SharedInputSignature>, ) -> Option<Transaction> { let mut tx = self.tx.clone(); - self.add_local_witnesses(&mut tx, holder_tx_signatures.witnesses.clone()); - self.add_remote_witnesses(&mut tx, counterparty_tx_signatures.witnesses.clone()); + self.add_local_witnesses(&mut tx, holder_tx_signatures.witnesses); + self.add_remote_witnesses(&mut tx, counterparty_tx_signatures.witnesses); if let Some(shared_input_index) = self.shared_input_index { let holder_shared_input_sig = @@ -555,7 +540,7 @@ pub(crate) struct SharedInputSignature { witness_script: ScriptBuf, } -impl_writeable_tlv_based!(SharedInputSignature, { +impl_ser_tlv_based!(SharedInputSignature, { (1, holder_signature_first, required), (3, witness_script, required), }); @@ -594,8 +579,30 @@ impl InteractiveTxSigningSession { self.counterparty_tx_signatures.is_some() } - pub fn holder_tx_signatures(&self) -> &Option<TxSignatures> { - &self.holder_tx_signatures + pub fn has_holder_witnesses(&self) -> bool { + self.holder_tx_signatures.is_some() + } + + pub fn awaiting_holder_shared_input_signature(&self) -> bool { + self.holder_tx_signatures + .as_ref() + .map(|tx_signatures| { + self.shared_input().is_some() && tx_signatures.shared_input_signature.is_none() + }) + .unwrap_or(false) + } + + pub fn holder_tx_signatures(&self) -> Option<TxSignatures> { + self.holder_tx_signatures + .as_ref() + .filter(|tx_signatures| { + self.shared_input().is_none() || tx_signatures.shared_input_signature.is_some() + }) + .filter(|_| { + (self.has_received_commitment_signed && self.holder_sends_tx_signatures_first) + || self.has_received_tx_signatures() + }) + .cloned() } pub fn received_commitment_signed(&mut self) { @@ -631,48 +638,75 @@ impl InteractiveTxSigningSession { self.counterparty_tx_signatures = Some(tx_signatures.clone()); - let holder_tx_signatures = if !self.holder_sends_tx_signatures_first { - self.holder_tx_signatures.clone() - } else { - None - }; + let holder_tx_signatures = + if !self.holder_sends_tx_signatures_first { self.holder_tx_signatures() } else { None }; - let funding_tx_opt = self.maybe_finalize_funding_tx(); + let funding_tx_opt = self.signed_tx(); Ok((holder_tx_signatures, funding_tx_opt)) } - /// Provides the holder witnesses for the unsigned transaction. + /// Provides the holder witnesses for the unsigned transaction's non-shared inputs. + /// + /// For splices, call [`Self::provide_holder_shared_input_signature`] separately after the + /// shared input signature is available. /// /// Returns an error if the witness count does not equal the holder's input count in the /// unsigned transaction. pub fn provide_holder_witnesses<C: bitcoin::secp256k1::Verification>( - &mut self, tx_signatures: TxSignatures, secp_ctx: &Secp256k1<C>, + &mut self, channel_id: ChannelId, funding_txid_signed: Txid, witnesses: Vec<Witness>, + secp_ctx: &Secp256k1<C>, ) -> Result<(Option<TxSignatures>, Option<Transaction>), String> { if self.holder_tx_signatures.is_some() { return Err("Holder witnesses were already provided".to_string()); } + if funding_txid_signed != self.unsigned_tx().compute_txid() { + return Err("Transaction was malleated prior to signing".to_string()); + } + let local_inputs_count = self.local_inputs_count(); - if tx_signatures.witnesses.len() != local_inputs_count { + if witnesses.len() != local_inputs_count { return Err(format!( "Provided witness count of {} does not match required count for {} non-shared inputs", - tx_signatures.witnesses.len(), + witnesses.len(), local_inputs_count )); } - self.verify_interactive_tx_signatures(secp_ctx, &tx_signatures.witnesses)?; + self.verify_interactive_tx_signatures(secp_ctx, &witnesses)?; - self.holder_tx_signatures = Some(tx_signatures); - - let funding_tx_opt = self.maybe_finalize_funding_tx(); - let holder_tx_signatures = (self.has_received_commitment_signed - && (self.holder_sends_tx_signatures_first || self.has_received_tx_signatures())) - .then(|| { - self.holder_tx_signatures.clone().expect("Holder tx_signatures were just provided") + self.holder_tx_signatures = Some(TxSignatures { + channel_id, + tx_hash: funding_txid_signed, + witnesses, + shared_input_signature: None, }); + let holder_tx_signatures = self.holder_tx_signatures(); + let funding_tx_opt = self.signed_tx(); + + Ok((holder_tx_signatures, funding_tx_opt)) + } + + pub fn provide_holder_shared_input_signature( + &mut self, shared_input_signature: Signature, + ) -> Result<(Option<TxSignatures>, Option<Transaction>), String> { + if self.shared_input().is_none() { + return Err("No shared input exists for this transaction".to_string()); + } + + let holder_tx_signatures = self.holder_tx_signatures.as_mut().ok_or_else(|| { + "Holder witnesses must be provided before the shared input signature".to_string() + })?; + if holder_tx_signatures.shared_input_signature.is_some() { + return Err("The shared input signature was already provided".to_string()); + } + + holder_tx_signatures.shared_input_signature = Some(shared_input_signature); + + let funding_tx_opt = self.signed_tx(); + let holder_tx_signatures = self.holder_tx_signatures(); Ok((holder_tx_signatures, funding_tx_opt)) } @@ -723,10 +757,12 @@ impl InteractiveTxSigningSession { }) } - fn maybe_finalize_funding_tx(&mut self) -> Option<Transaction> { - let holder_tx_signatures = self.holder_tx_signatures.as_ref()?; - let counterparty_tx_signatures = self.counterparty_tx_signatures.as_ref()?; - let shared_input_signature = self.shared_input_signature.as_ref(); + /// Returns `Some` with the fully signed transaction if both holder and counterparty signatures + /// are available. + pub fn signed_tx(&self) -> Option<Transaction> { + let holder_tx_signatures = self.holder_tx_signatures()?; + let counterparty_tx_signatures = self.counterparty_tx_signatures.clone()?; + let shared_input_signature = self.shared_input_signature.clone(); self.unsigned_tx.finalize( holder_tx_signatures, counterparty_tx_signatures, @@ -895,20 +931,16 @@ impl InteractiveTxSigningSession { Ok(()) } - pub(crate) fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { - self.unsigned_tx.into_negotiation_error(reason) - } - - pub(super) fn to_contributed_inputs_and_outputs(&self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) { - self.unsigned_tx.to_contributed_inputs_and_outputs() + pub(super) fn contributed_inputs(&self) -> impl Iterator<Item = BitcoinOutPoint> + '_ { + self.unsigned_tx.contributed_inputs() } - pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) { - self.unsigned_tx.into_contributed_inputs_and_outputs() + pub(super) fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.unsigned_tx.contributed_outputs() } } -impl_writeable_tlv_based!(InteractiveTxSigningSession, { +impl_ser_tlv_based!(InteractiveTxSigningSession, { (1, unsigned_tx, required), (3, has_received_commitment_signed, required), (5, holder_tx_signatures, required), @@ -1220,13 +1252,9 @@ impl NegotiationContext { // with witness versions V1 and up are always considered standard. Yes, the scripts can be // anyone-can-spend-able, but if our counterparty wants to add an output like that then it's none // of our concern really ¯\_(ツ)_/¯ - // - // TODO: The last check would be simplified when https://github.com/rust-bitcoin/rust-bitcoin/commit/1656e1a09a1959230e20af90d20789a4a8f0a31b - // hits the next release of rust-bitcoin. if !(msg.script.is_p2wpkh() || msg.script.is_p2wsh() - || (msg.script.is_witness_program() - && msg.script.witness_version().map(|v| v.to_num() >= 1).unwrap_or(false))) + || msg.script.witness_version().map(|v| v.to_num() >= 1).unwrap_or(false)) { return Err(AbortReason::InvalidOutputScript); } @@ -1634,7 +1662,7 @@ enum AddingRole { Remote, } -impl_writeable_tlv_based_enum!(AddingRole, +impl_ser_tlv_based_enum!(AddingRole, (1, Local) => {}, (3, Remote) => {}, ); @@ -1784,7 +1812,7 @@ pub(super) struct SharedOwnedOutput { local_owned: u64, } -impl_writeable_tlv_based!(SharedOwnedOutput, { +impl_ser_tlv_based!(SharedOwnedOutput, { (1, tx_out, required), (3, local_owned, required), }); @@ -1814,7 +1842,7 @@ enum OutputOwned { Shared(SharedOwnedOutput), } -impl_writeable_tlv_based_enum!(OutputOwned, +impl_ser_tlv_based_enum!(OutputOwned, {1, Single} => (), {3, Shared} => (), ); @@ -1943,7 +1971,6 @@ impl InteractiveTxInput { pub(super) struct InteractiveTxConstructor { state_machine: StateMachine, is_initiator: bool, - initiator_first_message: Option<InteractiveTxMessageSend>, channel_id: ChannelId, inputs_to_contribute: Vec<(SerialId, InputOwned)>, outputs_to_contribute: Vec<(SerialId, OutputOwned)>, @@ -2012,9 +2039,8 @@ pub(super) struct InteractiveTxConstructorArgs<'a, ES: EntropySource> { pub counterparty_node_id: PublicKey, pub channel_id: ChannelId, pub feerate_sat_per_kw: u32, - pub is_initiator: bool, pub funding_tx_locktime: AbsoluteLockTime, - pub inputs_to_contribute: Vec<FundingTxInput>, + pub inputs_to_contribute: Vec<ConfirmedUtxo>, pub shared_funding_input: Option<SharedOwnedInput>, pub shared_funding_output: SharedOwnedOutput, pub outputs_to_contribute: Vec<TxOut>, @@ -2023,18 +2049,15 @@ pub(super) struct InteractiveTxConstructorArgs<'a, ES: EntropySource> { impl InteractiveTxConstructor { /// Instantiates a new `InteractiveTxConstructor`. /// - /// If the holder is the initiator, they need to send the first message which is a `TxAddInput` - /// message. - pub fn new<ES: EntropySource>( - args: InteractiveTxConstructorArgs<ES>, - ) -> Result<Self, NegotiationError> { + /// Use [`Self::new_for_outbound`] or [`Self::new_for_inbound`] instead to also prepare the + /// first message for the initiator. + fn new<ES: EntropySource>(args: InteractiveTxConstructorArgs<ES>, is_initiator: bool) -> Self { let InteractiveTxConstructorArgs { entropy_source, holder_node_id, counterparty_node_id, channel_id, feerate_sat_per_kw, - is_initiator, funding_tx_locktime, inputs_to_contribute, shared_funding_input, @@ -2054,9 +2077,13 @@ impl InteractiveTxConstructor { let mut inputs_to_contribute: Vec<(SerialId, InputOwned)> = inputs_to_contribute .into_iter() - .map(|FundingTxInput { utxo, sequence, prevtx: prev_tx }| { + .map(|ConfirmedUtxo { utxo, prevtx: prev_tx }| { let serial_id = generate_holder_serial_id(entropy_source, is_initiator); - let txin = TxIn { previous_output: utxo.outpoint, sequence, ..Default::default() }; + let txin = TxIn { + previous_output: utxo.outpoint, + sequence: utxo.sequence, + ..Default::default() + }; let prev_output = utxo.output; let input = InputOwned::Single(SingleOwnedInput { input: txin, @@ -2100,75 +2127,63 @@ impl InteractiveTxConstructor { let next_input_index = (!inputs_to_contribute.is_empty()).then_some(0); let next_output_index = (!outputs_to_contribute.is_empty()).then_some(0); - let mut constructor = Self { + Self { state_machine, is_initiator, - initiator_first_message: None, channel_id, inputs_to_contribute, outputs_to_contribute, next_input_index, next_output_index, - }; - // We'll store the first message for the initiator. - if is_initiator { - match constructor.maybe_send_message() { - Ok(message) => { - constructor.initiator_first_message = Some(message); - }, - Err(reason) => { - return Err(constructor.into_negotiation_error(reason)); - }, - } } - Ok(constructor) } - fn into_negotiation_error(self, reason: AbortReason) -> NegotiationError { - let (contributed_inputs, contributed_outputs) = self.into_contributed_inputs_and_outputs(); - NegotiationError { reason, contributed_inputs, contributed_outputs } + /// Instantiates a new `InteractiveTxConstructor` for the initiator (outbound splice). + /// + /// The initiator always has the shared funding output added internally, so preparing the + /// first message should never fail. Debug asserts verify this invariant. + pub fn new_for_outbound<ES: EntropySource>( + args: InteractiveTxConstructorArgs<ES>, + ) -> (Self, Option<InteractiveTxMessageSend>) { + let mut constructor = Self::new(args, true); + let message = match constructor.maybe_send_message() { + Ok(message) => Some(message), + Err(reason) => { + debug_assert!( + false, + "Outbound constructor should always have inputs: {:?}", + reason + ); + None + }, + }; + (constructor, message) } - pub(super) fn into_contributed_inputs_and_outputs(self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) { - let contributed_inputs = self - .inputs_to_contribute - .into_iter() - .filter(|(_, input)| !input.is_shared()) - .map(|(_, input)| input.into_tx_in().previous_output) - .collect(); - let contributed_outputs = self - .outputs_to_contribute - .into_iter() - .filter(|(_, output)| !output.is_shared()) - .map(|(_, output)| output.into_tx_out()) - .collect(); - (contributed_inputs, contributed_outputs) + /// Instantiates a new `InteractiveTxConstructor` for the non-initiator (inbound splice or + /// dual-funded channel acceptor). + pub fn new_for_inbound<ES: EntropySource>(args: InteractiveTxConstructorArgs<ES>) -> Self { + Self::new(args, false) } - pub(super) fn to_contributed_inputs_and_outputs(&self) -> (Vec<BitcoinOutPoint>, Vec<TxOut>) { - let contributed_inputs = self - .inputs_to_contribute + pub(super) fn contributed_inputs(&self) -> impl Iterator<Item = BitcoinOutPoint> + '_ { + self.inputs_to_contribute .iter() .filter(|(_, input)| !input.is_shared()) .map(|(_, input)| input.tx_in().previous_output) - .collect(); - let contributed_outputs = self - .outputs_to_contribute + } + + pub(super) fn contributed_outputs(&self) -> impl Iterator<Item = &bitcoin::Script> + '_ { + self.outputs_to_contribute .iter() .filter(|(_, output)| !output.is_shared()) - .map(|(_, output)| output.tx_out().clone()) - .collect(); - (contributed_inputs, contributed_outputs) + .map(|(_, output)| output.tx_out().script_pubkey.as_script()) } pub fn is_initiator(&self) -> bool { self.is_initiator } - pub fn take_initiator_first_message(&mut self) -> Option<InteractiveTxMessageSend> { - self.initiator_first_message.take() - } - fn maybe_send_message(&mut self) -> Result<InteractiveTxMessageSend, AbortReason> { let channel_id = self.channel_id; @@ -2309,106 +2324,20 @@ impl InteractiveTxConstructor { } } -/// Determine whether a change output should be added, and if yes, of what size, considering our -/// given inputs and outputs, and intended contribution. Takes into account the fees and the dust -/// limit. -/// -/// Three outcomes are possible: -/// - Inputs are sufficient for intended contribution, fees, and a larger-than-dust change: -/// `Ok(Some(change_amount))` -/// - Inputs are sufficient for intended contribution and fees, and a change output isn't needed: -/// `Ok(None)` -/// - Inputs are not sufficient to cover contribution and fees: -/// `Err(AbortReason::InsufficientFees)` -/// -/// Parameters: -/// - `context` - Context of the funding negotiation, including non-shared inputs and feerate. -/// - `is_splice` - Whether we splicing an existing channel or dual-funding a new one. -/// - `shared_output_funding_script` - The script of the shared output. -/// - `funding_outputs` - Our funding outputs. -/// - `change_output_dust_limit` - The dust limit (in sats) to consider. -pub(super) fn calculate_change_output_value( - context: &FundingNegotiationContext, is_splice: bool, shared_output_funding_script: &ScriptBuf, - change_output_dust_limit: u64, -) -> Result<Option<Amount>, AbortReason> { - let mut total_input_value = Amount::ZERO; - let mut our_funding_inputs_weight = 0u64; - for FundingTxInput { utxo, .. } in context.our_funding_inputs.iter() { - total_input_value = total_input_value.checked_add(utxo.output.value).unwrap_or(Amount::MAX); - - let weight = BASE_INPUT_WEIGHT + utxo.satisfaction_weight; - our_funding_inputs_weight = our_funding_inputs_weight.saturating_add(weight); - } - - let funding_outputs = &context.our_funding_outputs; - let total_output_value = funding_outputs - .iter() - .fold(Amount::ZERO, |total, out| total.checked_add(out.value).unwrap_or(Amount::MAX)); - - let our_funding_outputs_weight = funding_outputs.iter().fold(0u64, |weight, out| { - weight.saturating_add(get_output_weight(&out.script_pubkey).to_wu()) - }); - let mut weight = our_funding_outputs_weight.saturating_add(our_funding_inputs_weight); - - // If we are the initiator, we must pay for the weight of the funding output and - // all common fields in the funding transaction. - if context.is_initiator { - weight = weight.saturating_add(get_output_weight(shared_output_funding_script).to_wu()); - weight = weight.saturating_add(TX_COMMON_FIELDS_WEIGHT); - if is_splice { - // TODO(taproot): Needs to consider different weights based on channel type - weight = weight.saturating_add(BASE_INPUT_WEIGHT); - weight = weight.saturating_add(EMPTY_SCRIPT_SIG_WEIGHT); - weight = weight.saturating_add(FUNDING_TRANSACTION_WITNESS_WEIGHT); - #[cfg(feature = "grind_signatures")] - { - // Guarantees a low R signature - weight -= 1; - } - } - } - - let contributed_fees = - Amount::from_sat(fee_for_weight(context.funding_feerate_sat_per_1000_weight, weight)); - - let contributed_input_value = - context.our_funding_contribution + total_output_value.to_signed().unwrap(); - assert!(contributed_input_value > SignedAmount::ZERO); - let contributed_input_value = contributed_input_value.unsigned_abs(); - - let total_input_value_less_fees = - total_input_value.checked_sub(contributed_fees).unwrap_or(Amount::ZERO); - if total_input_value_less_fees < contributed_input_value { - // Not enough to cover contribution plus fees - return Err(AbortReason::InsufficientFees); - } - - let remaining_value = total_input_value_less_fees - .checked_sub(contributed_input_value) - .expect("remaining_value should not be negative"); - if remaining_value.to_sat() < change_output_dust_limit { - // Enough to cover contribution plus fees, but leftover is below dust limit; no change - Ok(None) - } else { - // Enough to have over-dust change - Ok(Some(remaining_value)) - } -} - #[cfg(test)] mod tests { use crate::chain::chaininterface::{fee_for_weight, FEERATE_FLOOR_SATS_PER_KW}; - use crate::ln::channel::{FundingNegotiationContext, TOTAL_BITCOIN_SUPPLY_SATOSHIS}; - use crate::ln::funding::FundingTxInput; + use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; use crate::ln::interactivetxs::{ - calculate_change_output_value, generate_holder_serial_id, AbortReason, - HandleTxCompleteValue, InteractiveTxConstructor, InteractiveTxConstructorArgs, - InteractiveTxMessageSend, SharedOwnedInput, SharedOwnedOutput, MAX_INPUTS_OUTPUTS_COUNT, - MAX_RECEIVED_TX_ADD_INPUT_COUNT, MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, + generate_holder_serial_id, AbortReason, HandleTxCompleteValue, InteractiveTxConstructor, + InteractiveTxConstructorArgs, InteractiveTxMessageSend, SharedOwnedInput, + SharedOwnedOutput, MAX_INPUTS_OUTPUTS_COUNT, MAX_RECEIVED_TX_ADD_INPUT_COUNT, + MAX_RECEIVED_TX_ADD_OUTPUT_COUNT, }; use crate::ln::types::ChannelId; use crate::sign::EntropySource; use crate::util::atomic_counter::AtomicCounter; + use crate::util::wallet_utils::ConfirmedUtxo; use bitcoin::absolute::LockTime as AbsoluteLockTime; use bitcoin::amount::Amount; use bitcoin::hashes::Hash; @@ -2419,8 +2348,7 @@ mod tests { use bitcoin::transaction::Version; use bitcoin::{opcodes, WScriptHash, Weight, XOnlyPublicKey}; use bitcoin::{ - OutPoint, PubkeyHash, ScriptBuf, Sequence, SignedAmount, Transaction, TxIn, TxOut, - WPubkeyHash, + OutPoint, PubkeyHash, ScriptBuf, Sequence, Transaction, TxIn, TxOut, WPubkeyHash, }; use super::{ @@ -2473,12 +2401,12 @@ mod tests { struct TestSession { description: &'static str, - inputs_a: Vec<FundingTxInput>, + inputs_a: Vec<ConfirmedUtxo>, a_shared_input: Option<(OutPoint, TxOut, u64)>, /// The funding output, with the value contributed shared_output_a: (TxOut, u64), outputs_a: Vec<TxOut>, - inputs_b: Vec<FundingTxInput>, + inputs_b: Vec<ConfirmedUtxo>, b_shared_input: Option<(OutPoint, TxOut, u64)>, /// The funding output, with the value contributed shared_output_b: (TxOut, u64), @@ -2511,84 +2439,64 @@ mod tests { &SecretKey::from_slice(&[43; 32]).unwrap(), ); - let mut constructor_a = match InteractiveTxConstructor::new(InteractiveTxConstructorArgs { - entropy_source, - channel_id, - feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, - holder_node_id, - counterparty_node_id, - is_initiator: true, - funding_tx_locktime, - inputs_to_contribute: session.inputs_a, - shared_funding_input: session.a_shared_input.map(|(op, prev_output, lo)| { - SharedOwnedInput::new( - TxIn { - previous_output: op, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }, - prev_output, - lo, - true, // holder_sig_first - generate_funding_script_pubkey(), // witness_script for test - ) - }), - shared_funding_output: SharedOwnedOutput::new( - session.shared_output_a.0, - session.shared_output_a.1, - ), - outputs_to_contribute: session.outputs_a, - }) { - Ok(r) => Some(r), - Err(e) => { - assert_eq!( - Some((e.reason, ErrorCulprit::NodeA)), - session.expect_error, - "Test: {}", - session.description - ); - return; - }, - }; - let mut constructor_b = match InteractiveTxConstructor::new(InteractiveTxConstructorArgs { - entropy_source, - holder_node_id, - counterparty_node_id, - channel_id, - feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, - is_initiator: false, - funding_tx_locktime, - inputs_to_contribute: session.inputs_b, - shared_funding_input: session.b_shared_input.map(|(op, prev_output, lo)| { - SharedOwnedInput::new( - TxIn { - previous_output: op, - sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, - ..Default::default() - }, - prev_output, - lo, - false, // holder_sig_first - generate_funding_script_pubkey(), // witness_script for test - ) - }), - shared_funding_output: SharedOwnedOutput::new( - session.shared_output_b.0, - session.shared_output_b.1, - ), - outputs_to_contribute: session.outputs_b, - }) { - Ok(r) => Some(r), - Err(e) => { - assert_eq!( - Some((e.reason, ErrorCulprit::NodeB)), - session.expect_error, - "Test: {}", - session.description - ); - return; - }, - }; + let (constructor_a, mut message_send_a) = + InteractiveTxConstructor::new_for_outbound(InteractiveTxConstructorArgs { + entropy_source, + channel_id, + feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, + holder_node_id, + counterparty_node_id, + funding_tx_locktime, + inputs_to_contribute: session.inputs_a, + shared_funding_input: session.a_shared_input.map(|(op, prev_output, lo)| { + SharedOwnedInput::new( + TxIn { + previous_output: op, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }, + prev_output, + lo, + true, // holder_sig_first + generate_funding_script_pubkey(), // witness_script for test + ) + }), + shared_funding_output: SharedOwnedOutput::new( + session.shared_output_a.0, + session.shared_output_a.1, + ), + outputs_to_contribute: session.outputs_a, + }); + let mut constructor_a = Some(constructor_a); + let mut constructor_b = + Some(InteractiveTxConstructor::new_for_inbound(InteractiveTxConstructorArgs { + entropy_source, + holder_node_id, + counterparty_node_id, + channel_id, + feerate_sat_per_kw: TEST_FEERATE_SATS_PER_KW, + funding_tx_locktime, + inputs_to_contribute: session.inputs_b, + shared_funding_input: session.b_shared_input.map(|(op, prev_output, lo)| { + SharedOwnedInput::new( + TxIn { + previous_output: op, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + ..Default::default() + }, + prev_output, + lo, + false, // holder_sig_first + generate_funding_script_pubkey(), // witness_script for test + ) + }), + shared_funding_output: SharedOwnedOutput::new( + session.shared_output_b.0, + session.shared_output_b.1, + ), + outputs_to_contribute: session.outputs_b, + })); + let mut message_send_b = None; let handle_message_send = |msg: InteractiveTxMessageSend, for_constructor: &mut InteractiveTxConstructor| { @@ -2612,8 +2520,6 @@ mod tests { } }; - let mut message_send_a = constructor_a.as_mut().unwrap().take_initiator_first_message(); - let mut message_send_b = None; let mut final_tx_a = None; let mut final_tx_b = None; while constructor_a.is_some() || constructor_b.is_some() { @@ -2742,22 +2648,20 @@ mod tests { } } - fn generate_inputs(outputs: &[TestOutput]) -> Vec<FundingTxInput> { + fn generate_inputs(outputs: &[TestOutput]) -> Vec<ConfirmedUtxo> { let tx = generate_tx(outputs); outputs .iter() .enumerate() .map(|(idx, output)| match output { - TestOutput::P2WPKH(_) => { - FundingTxInput::new_p2wpkh(tx.clone(), idx as u32).unwrap() - }, + TestOutput::P2WPKH(_) => ConfirmedUtxo::new_p2wpkh(tx.clone(), idx as u32).unwrap(), TestOutput::P2WSH(_) => { - FundingTxInput::new_p2wsh(tx.clone(), idx as u32, Weight::from_wu(42)).unwrap() + ConfirmedUtxo::new_p2wsh(tx.clone(), idx as u32, Weight::from_wu(42)).unwrap() }, TestOutput::P2TR(_) => { - FundingTxInput::new_p2tr_key_spend(tx.clone(), idx as u32).unwrap() + ConfirmedUtxo::new_p2tr_key_spend(tx.clone(), idx as u32).unwrap() }, - TestOutput::P2PKH(_) => FundingTxInput::new_p2pkh(tx.clone(), idx as u32).unwrap(), + TestOutput::P2PKH(_) => ConfirmedUtxo::new_p2pkh(tx.clone(), idx as u32).unwrap(), }) .collect() } @@ -2805,12 +2709,12 @@ mod tests { (generate_txout(&TestOutput::P2WSH(value)), local_value) } - fn generate_fixed_number_of_inputs(count: u16) -> Vec<FundingTxInput> { + fn generate_fixed_number_of_inputs(count: u16) -> Vec<ConfirmedUtxo> { // Generate transactions with a total `count` number of outputs such that no transaction has a // serialized length greater than u16::MAX. let max_outputs_per_prevtx = 1_500; let mut remaining = count; - let mut inputs: Vec<FundingTxInput> = Vec::with_capacity(count as usize); + let mut inputs: Vec<ConfirmedUtxo> = Vec::with_capacity(count as usize); while remaining > 0 { let tx_output_count = remaining.min(max_outputs_per_prevtx); @@ -2821,10 +2725,10 @@ mod tests { // Use unique locktime for each tx so outpoints are different across transactions let tx = generate_tx_with_locktime(&outputs, (1337 + remaining).into()); - let mut temp: Vec<FundingTxInput> = outputs + let mut temp: Vec<ConfirmedUtxo> = outputs .iter() .enumerate() - .map(|(idx, _)| FundingTxInput::new_p2wpkh(tx.clone(), idx as u32).unwrap()) + .map(|(idx, _)| ConfirmedUtxo::new_p2wpkh(tx.clone(), idx as u32).unwrap()) .collect(); inputs.append(&mut temp); @@ -3035,7 +2939,7 @@ mod tests { }); let tx = generate_tx(&[TestOutput::P2WPKH(1_000_000)]); - let mut invalid_sequence_input = FundingTxInput::new_p2wpkh(tx.clone(), 0).unwrap(); + let mut invalid_sequence_input = ConfirmedUtxo::new_p2wpkh(tx.clone(), 0).unwrap(); invalid_sequence_input.set_sequence(Default::default()); do_test_interactive_tx_constructor(TestSession { description: "Invalid input sequence from initiator", @@ -3049,7 +2953,7 @@ mod tests { outputs_b: vec![], expect_error: Some((AbortReason::IncorrectInputSequenceValue, ErrorCulprit::NodeA)), }); - let duplicate_input = FundingTxInput::new_p2wpkh(tx.clone(), 0).unwrap(); + let duplicate_input = ConfirmedUtxo::new_p2wpkh(tx.clone(), 0).unwrap(); do_test_interactive_tx_constructor(TestSession { description: "Duplicate prevout from initiator", inputs_a: vec![duplicate_input.clone(), duplicate_input], @@ -3063,7 +2967,7 @@ mod tests { expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeB)), }); // Non-initiator uses same prevout as initiator. - let duplicate_input = FundingTxInput::new_p2wpkh(tx.clone(), 0).unwrap(); + let duplicate_input = ConfirmedUtxo::new_p2wpkh(tx.clone(), 0).unwrap(); do_test_interactive_tx_constructor(TestSession { description: "Non-initiator uses same prevout as initiator", inputs_a: vec![duplicate_input.clone()], @@ -3076,7 +2980,7 @@ mod tests { outputs_b: vec![], expect_error: Some((AbortReason::PrevTxOutInvalid, ErrorCulprit::NodeA)), }); - let duplicate_input = FundingTxInput::new_p2wpkh(tx.clone(), 0).unwrap(); + let duplicate_input = ConfirmedUtxo::new_p2wpkh(tx.clone(), 0).unwrap(); do_test_interactive_tx_constructor(TestSession { description: "Non-initiator uses same prevout as initiator", inputs_a: vec![duplicate_input.clone()], @@ -3384,119 +3288,6 @@ mod tests { assert_eq!(generate_holder_serial_id(&&entropy_source, false) % 2, 1) } - #[test] - fn test_calculate_change_output_value_open() { - let input_prevouts = [ - TxOut { - value: Amount::from_sat(70_000), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - }, - TxOut { - value: Amount::from_sat(60_000), - script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), - }, - ]; - let inputs = input_prevouts - .iter() - .map(|txout| { - let prevtx = Transaction { - input: Vec::new(), - output: vec![(*txout).clone()], - lock_time: AbsoluteLockTime::ZERO, - version: Version::TWO, - }; - - FundingTxInput::new_p2wpkh(prevtx, 0).unwrap() - }) - .collect(); - let txout = TxOut { value: Amount::from_sat(10_000), script_pubkey: ScriptBuf::new() }; - let outputs = vec![txout]; - let funding_feerate_sat_per_1000_weight = 3000; - - let total_inputs: Amount = input_prevouts.iter().map(|o| o.value).sum(); - let total_outputs: Amount = outputs.iter().map(|o| o.value).sum(); - let fees = if cfg!(feature = "grind_signatures") { - Amount::from_sat(1734) - } else { - Amount::from_sat(1740) - }; - let common_fees = Amount::from_sat(234); - - // There is leftover for change - let context = FundingNegotiationContext { - is_initiator: true, - our_funding_contribution: SignedAmount::from_sat(110_000), - funding_tx_locktime: AbsoluteLockTime::ZERO, - funding_feerate_sat_per_1000_weight, - shared_funding_input: None, - our_funding_inputs: inputs, - our_funding_outputs: outputs, - change_script: None, - }; - let gross_change = - total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some(gross_change - fees - common_fees)), - ); - - // There is leftover for change, without common fees - let context = FundingNegotiationContext { is_initiator: false, ..context }; - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some(gross_change - fees)), - ); - - // Insufficient inputs, no leftover - let context = FundingNegotiationContext { - is_initiator: false, - our_funding_contribution: SignedAmount::from_sat(130_000), - ..context - }; - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Err(AbortReason::InsufficientFees), - ); - - // Very small leftover - let context = FundingNegotiationContext { - is_initiator: false, - our_funding_contribution: SignedAmount::from_sat(118_000), - ..context - }; - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(None), - ); - - // Small leftover, but not dust - let context = FundingNegotiationContext { - is_initiator: false, - our_funding_contribution: SignedAmount::from_sat(117_992), - ..context - }; - let gross_change = - total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 100), - Ok(Some(gross_change - fees)), - ); - - // Larger fee, smaller change - let context = FundingNegotiationContext { - is_initiator: true, - our_funding_contribution: SignedAmount::from_sat(110_000), - funding_feerate_sat_per_1000_weight: funding_feerate_sat_per_1000_weight * 3, - ..context - }; - let gross_change = - total_inputs - total_outputs - context.our_funding_contribution.to_unsigned().unwrap(); - assert_eq!( - calculate_change_output_value(&context, false, &ScriptBuf::new(), 300), - Ok(Some(gross_change - fees * 3 - common_fees * 3)), - ); - } - fn do_verify_tx_signatures( transaction: Transaction, prev_outputs: Vec<TxOut>, ) -> Result<(), String> { diff --git a/lightning/src/ln/interception_tests.rs b/lightning/src/ln/interception_tests.rs index c83ef177628..5fece51c027 100644 --- a/lightning/src/ln/interception_tests.rs +++ b/lightning/src/ln/interception_tests.rs @@ -51,7 +51,16 @@ fn do_test_htlc_interception_flags( let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(intercept_config), None]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); - create_announced_chan_between_nodes(&nodes, 0, 1); + let inbound_private = match flag { + Flag::FromPrivateChannels => { + create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 100000, 0); + true + }, + _ => { + create_announced_chan_between_nodes(&nodes, 0, 1); + false + }, + }; let node_0_id = nodes[0].node.get_our_node_id(); let node_1_id = nodes[1].node.get_our_node_id(); @@ -59,29 +68,31 @@ fn do_test_htlc_interception_flags( // First open the right type of channel (and get it in the right state) for the bit we're // testing. - let (target_scid, target_chan_id) = match flag { - Flag::ToOfflinePrivateChannels | Flag::ToOnlinePrivateChannels => { + let (target_scid, target_chan_id, outbound_private_for_known_scids) = match flag { + Flag::ToOfflinePrivateChannels + | Flag::ToOnlinePrivateChannels + | Flag::FromPublicToPrivateChannels => { create_unannounced_chan_between_nodes_with_value(&nodes, 1, 2, 100000, 0); let chan_id = nodes[2].node.list_channels()[0].channel_id; let scid = nodes[2].node.list_channels()[0].short_channel_id.unwrap(); if flag == Flag::ToOfflinePrivateChannels { nodes[1].node.peer_disconnected(node_2_id); nodes[2].node.peer_disconnected(node_1_id); - } else { - assert_eq!(flag, Flag::ToOnlinePrivateChannels); } - (scid, chan_id) + (scid, chan_id, Some(true)) }, - Flag::ToInterceptSCIDs | Flag::ToPublicChannels | Flag::ToUnknownSCIDs => { + Flag::ToInterceptSCIDs + | Flag::ToPublicChannels + | Flag::FromPrivateChannels + | Flag::FromPublicToPublicChannels + | Flag::ToUnknownSCIDs => { let (chan_upd, _, chan_id, _) = create_announced_chan_between_nodes(&nodes, 1, 2); if flag == Flag::ToInterceptSCIDs { - (nodes[1].node.get_intercept_scid(), chan_id) - } else if flag == Flag::ToPublicChannels { - (chan_upd.contents.short_channel_id, chan_id) + (nodes[1].node.get_intercept_scid(), chan_id, None) } else if flag == Flag::ToUnknownSCIDs { - (42424242, chan_id) + (42424242, chan_id, None) } else { - panic!(); + (chan_upd.contents.short_channel_id, chan_id, Some(false)) } }, _ => panic!("Combined flags aren't allowed"), @@ -101,21 +112,50 @@ fn do_test_htlc_interception_flags( get_route_and_payment_hash!(nodes[0], nodes[2], pay_params, amt_msat); route.paths[0].hops[1].short_channel_id = target_scid; - let interception_bit_match = (flags_bitmask & (flag as u8)) != 0; + let mut should_intercept = false; + for a_flag in ALL_FLAGS { + if flags_bitmask & (a_flag as u8) != 0 { + match a_flag { + Flag::ToInterceptSCIDs => { + should_intercept |= flag == Flag::ToInterceptSCIDs; + }, + Flag::ToOfflinePrivateChannels => { + should_intercept |= flag == Flag::ToOfflinePrivateChannels; + }, + Flag::ToOnlinePrivateChannels => { + should_intercept |= flag != Flag::ToOfflinePrivateChannels + && outbound_private_for_known_scids == Some(true); + }, + Flag::ToPublicChannels => { + should_intercept |= outbound_private_for_known_scids == Some(false); + }, + Flag::ToUnknownSCIDs => { + should_intercept |= flag == Flag::ToUnknownSCIDs; + }, + Flag::FromPrivateChannels => { + should_intercept |= inbound_private; + }, + Flag::FromPublicToPrivateChannels => { + should_intercept |= + !inbound_private && outbound_private_for_known_scids == Some(true); + }, + Flag::FromPublicToPublicChannels => { + should_intercept |= + !inbound_private && outbound_private_for_known_scids == Some(false); + }, + _ => panic!("Combined flags aren't allowed"), + } + } + } + match modification { Some(ForwardingMod::FeeTooLow) => { - assert!( - interception_bit_match, - "No reason to test failing if we aren't trying to intercept", - ); + assert!(should_intercept, "No reason to test failing if we aren't trying to intercept"); route.paths[0].hops[0].fee_msat = 500; }, Some(ForwardingMod::CLTVBelowConfig) => { route.paths[0].hops[0].cltv_expiry_delta = 6 * 12; - assert!( - interception_bit_match, - "No reason to test failing if we aren't trying to intercept", - ); + assert!(should_intercept, "No reason to test failing if we aren't trying to intercept"); }, Some(ForwardingMod::CLTVBelowMin) => { route.paths[0].hops[0].cltv_expiry_delta = 6; @@ -123,7 +163,7 @@ fn do_test_htlc_interception_flags( None => {}, } - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let payment_id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -133,7 +173,7 @@ fn do_test_htlc_interception_flags( do_commitment_signed_dance(&nodes[1], &nodes[0], &payment_event.commitment_msg, false, true); expect_and_process_pending_htlcs(&nodes[1], false); - if interception_bit_match && modification.is_none() { + if should_intercept && modification.is_none() { // If we were set to intercept, check that we got an interception event then // forward the HTLC on to nodes[2] and claim the payment. let intercept_id; @@ -172,7 +212,14 @@ fn do_test_htlc_interception_flags( // If we were not set to intercept, check that the HTLC either failed or was // automatically forwarded as appropriate. match (modification, flag) { - (None, Flag::ToOnlinePrivateChannels | Flag::ToPublicChannels) => { + ( + None, + Flag::ToOnlinePrivateChannels + | Flag::ToPublicChannels + | Flag::FromPrivateChannels + | Flag::FromPublicToPrivateChannels + | Flag::FromPublicToPublicChannels, + ) => { check_added_monitors(&nodes[1], 1); let forward_ev = SendEvent::from_node(&nodes[1]); @@ -241,31 +288,55 @@ fn do_test_htlc_interception_flags( } const MAX_BITMASK: u8 = HTLCInterceptionFlags::AllValidHTLCs as u8; -const ALL_FLAGS: [HTLCInterceptionFlags; 5] = [ +const ALL_FLAGS: [HTLCInterceptionFlags; 8] = [ HTLCInterceptionFlags::ToInterceptSCIDs, HTLCInterceptionFlags::ToOfflinePrivateChannels, HTLCInterceptionFlags::ToOnlinePrivateChannels, HTLCInterceptionFlags::ToPublicChannels, HTLCInterceptionFlags::ToUnknownSCIDs, + HTLCInterceptionFlags::FromPrivateChannels, + HTLCInterceptionFlags::FromPublicToPrivateChannels, + HTLCInterceptionFlags::FromPublicToPublicChannels, ]; - #[test] -fn test_htlc_interception_flags() { +fn check_all_flags() { let mut all_flag_bits = 0; for flag in ALL_FLAGS { all_flag_bits |= flag as isize; } assert_eq!(all_flag_bits, MAX_BITMASK as isize, "all flags must test all bits"); +} +fn test_htlc_interception_flags_subrange<I: Iterator<Item = u8>>(r: I) { // Test all 2^5 = 32 combinations of the HTLCInterceptionFlags bitmask // For each combination, test 5 different HTLC forwards and verify correct interception behavior - for flags_bitmask in 0..=MAX_BITMASK { + for flags_bitmask in r { for flag in ALL_FLAGS { do_test_htlc_interception_flags(flags_bitmask, flag, None); } } } +#[test] +fn test_htlc_interception_flags_a() { + test_htlc_interception_flags_subrange(0..MAX_BITMASK / 4); +} + +#[test] +fn test_htlc_interception_flags_b() { + test_htlc_interception_flags_subrange(MAX_BITMASK / 4..MAX_BITMASK / 2); +} + +#[test] +fn test_htlc_interception_flags_c() { + test_htlc_interception_flags_subrange(MAX_BITMASK / 2..MAX_BITMASK / 4 * 3); +} + +#[test] +fn test_htlc_interception_flags_d() { + test_htlc_interception_flags_subrange(MAX_BITMASK / 4 * 3..=MAX_BITMASK); +} + #[test] fn test_htlc_bad_for_chan_config() { // Test that interception won't be done if an HTLC fails to meet the target channel's channel @@ -274,6 +345,9 @@ fn test_htlc_bad_for_chan_config() { HTLCInterceptionFlags::ToOfflinePrivateChannels, HTLCInterceptionFlags::ToOnlinePrivateChannels, HTLCInterceptionFlags::ToPublicChannels, + HTLCInterceptionFlags::FromPrivateChannels, + HTLCInterceptionFlags::FromPublicToPrivateChannels, + HTLCInterceptionFlags::FromPublicToPublicChannels, ]; for flag in have_chan_flags { do_test_htlc_interception_flags(flag as u8, flag, Some(ForwardingMod::FeeTooLow)); diff --git a/lightning/src/ln/invoice_utils.rs b/lightning/src/ln/invoice_utils.rs index 1503a9a3a63..10cda068b68 100644 --- a/lightning/src/ln/invoice_utils.rs +++ b/lightning/src/ln/invoice_utils.rs @@ -184,26 +184,30 @@ fn _create_phantom_invoice<ES: EntropySource, NS: NodeSigner, L: Logger>( let keys = node_signer.get_expanded_key(); let (payment_hash, payment_secret) = if let Some(payment_hash) = payment_hash { - let payment_secret = create_from_hash( + let (payment_secret, _no_metadata) = create_from_hash( &keys, amt_msat, payment_hash, invoice_expiry_delta_secs, + &entropy_source, duration_since_epoch.as_secs(), min_final_cltv_expiry_delta, + None, ) .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; (payment_hash, payment_secret) } else { - create( + let (payment_hash, payment_secret, _no_metadata) = create( &keys, amt_msat, invoice_expiry_delta_secs, &entropy_source, duration_since_epoch.as_secs(), min_final_cltv_expiry_delta, + None, ) - .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))? + .map_err(|_| SignOrCreationError::CreationError(CreationError::InvalidAmount))?; + (payment_hash, payment_secret) }; log_trace!( @@ -670,7 +674,10 @@ mod test { let (payment_hash, payment_secret) = (invoice.payment_hash(), *invoice.payment_secret()); - let preimage = nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap(); + let preimage = nodes[1] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap(); // Invoice SCIDs should always use inbound SCID aliases over the real channel ID, if one is // available. @@ -690,6 +697,7 @@ mod test { custom_tlvs: custom_tlvs.clone(), route_params_config: RouteParametersConfig::default(), retry_strategy: Retry::Attempts(0), + declared_total_mpp_value_msat_override: None, }; nodes[0] @@ -1254,7 +1262,10 @@ mod test { let payment_preimage = if user_generated_pmt_hash { user_payment_preimage } else { - nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap() + nodes[1] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap() }; assert_eq!(invoice.min_final_cltv_expiry_delta(), MIN_FINAL_CLTV_EXPIRY_DELTA as u64); @@ -1270,7 +1281,7 @@ mod test { assert!(!invoice.features().unwrap().supports_basic_mpp()); let payment_params = PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), + invoice.get_payee_pub_key(), invoice.min_final_cltv_expiry_delta() as u32, ) .with_bolt11_features(invoice.features().unwrap().clone()) @@ -1284,7 +1295,10 @@ mod test { let payment_hash = invoice.payment_hash(); let id = PaymentId(payment_hash.0); - let onion = RecipientOnionFields::secret_only(*invoice.payment_secret()); + let onion = RecipientOnionFields::secret_only( + *invoice.payment_secret(), + invoice.amount_milli_satoshis().unwrap(), + ); nodes[0].node.send_payment(payment_hash, onion, id, params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1333,7 +1347,7 @@ mod test { payment_secret, payment_amt, payment_preimage_opt, - invoice.recover_payee_pub_key(), + invoice.get_payee_pub_key(), ); do_claim_payment_along_route(ClaimAlongRouteArgs::new( &nodes[0], @@ -1358,8 +1372,8 @@ mod test { create_unannounced_chan_between_nodes_with_value(&nodes, 0, 2, 100000, 10001); let payment_amt = 20_000; - let (payment_hash, _payment_secret) = - nodes[1].node.create_inbound_payment(Some(payment_amt), 3600, None).unwrap(); + let (payment_hash, _payment_secret, _) = + nodes[1].node.create_inbound_payment(Some(payment_amt), 3600, None, None).unwrap(); let route_hints = vec![nodes[1].node.get_phantom_route_hints(), nodes[2].node.get_phantom_route_hints()]; diff --git a/lightning/src/ln/max_payment_path_len_tests.rs b/lightning/src/ln/max_payment_path_len_tests.rs index b947273115e..242ba8000b8 100644 --- a/lightning/src/ln/max_payment_path_len_tests.rs +++ b/lightning/src/ln/max_payment_path_len_tests.rs @@ -80,15 +80,37 @@ fn large_payment_metadata() { - final_payload_len_without_metadata; let mut payment_metadata = vec![42; max_metadata_len]; + macro_rules! get_payment_hash { + ($node: expr, $metadata: expr) => {{ + let (payment_hash, payment_secret, encrypted_metadata) = $node + .node + .create_inbound_payment(Some(amt_msat), 7200, None, Some($metadata)) + .unwrap(); + let encrypted_metadata = encrypted_metadata.unwrap(); + let mut metadata_for_preimage = encrypted_metadata.clone(); + let payment_preimage = $node + .node + .get_payment_preimage_decrypt_metadata( + payment_hash, + payment_secret, + Some(metadata_for_preimage.as_mut_slice()), + ) + .unwrap(); + (payment_hash, payment_preimage, payment_secret, encrypted_metadata) + }}; + } + // Check that the maximum-size metadata is sendable. - let (mut route_0_1, payment_hash, payment_preimage, payment_secret) = - get_route_and_payment_hash!(&nodes[0], &nodes[1], amt_msat); + let (payment_hash, payment_preimage, payment_secret, encrypted_metadata) = + get_payment_hash!(nodes[1], payment_metadata.clone()); + let (mut route_0_1, ..) = get_route_and_payment_hash!(&nodes[0], &nodes[1], amt_msat); let mut max_sized_onion = RecipientOnionFields { payment_secret: Some(payment_secret), - payment_metadata: Some(payment_metadata.clone()), + payment_metadata: Some(encrypted_metadata), custom_tlvs: Vec::new(), + total_mpp_amount_msat: amt_msat, }; - let route_params = route_0_1.route_params.clone().unwrap(); + let route_params = route_0_1.route_params.clone(); let id = PaymentId(payment_hash.0); nodes[0] .node @@ -101,6 +123,7 @@ fn large_payment_metadata() { let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, events.pop().unwrap()) .with_payment_secret(payment_secret) + .with_payment_preimage(payment_preimage) .with_payment_metadata(payment_metadata.clone()); do_pass_along_path(args); claim_payment_along_route(ClaimAlongRouteArgs::new( @@ -111,14 +134,18 @@ fn large_payment_metadata() { // Check that the payment parameter for max path length will prevent us from routing past our // next-hop peer given the payment_metadata size. - let (mut route_0_2, payment_hash_2, payment_preimage_2, payment_secret_2) = - get_route_and_payment_hash!(&nodes[0], &nodes[2], amt_msat); - let mut route_params_0_2 = route_0_2.route_params.clone().unwrap(); + + let (payment_hash_2, _, payment_secret_2, encrypted_metadata_2) = + get_payment_hash!(nodes[2], payment_metadata.clone()); + let (mut route_0_2, ..) = get_route_and_payment_hash!(&nodes[0], &nodes[2], amt_msat); + let mut route_params_0_2 = route_0_2.route_params.clone(); route_params_0_2.payment_params.max_path_length = 1; nodes[0].router.expect_find_route_query(route_params_0_2); + max_sized_onion.payment_secret = Some(payment_secret_2); + max_sized_onion.payment_metadata = Some(encrypted_metadata_2); let id = PaymentId(payment_hash_2.0); - let route_params = route_0_2.route_params.clone().unwrap(); + let mut route_params = route_0_2.route_params.clone(); let err = nodes[0] .node .send_payment(payment_hash_2, max_sized_onion.clone(), id, route_params, Retry::Attempts(0)) @@ -127,7 +154,12 @@ fn large_payment_metadata() { // If our payment_metadata contains 1 additional byte, we'll fail prior to pathfinding. let mut too_large_onion = max_sized_onion.clone(); - too_large_onion.payment_metadata.as_mut().map(|mut md| md.push(42)); + too_large_onion.payment_metadata.as_mut().map(|md| md.push(42)); + too_large_onion.total_mpp_amount_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; + let mut too_large_metadata = payment_metadata.clone(); + too_large_metadata.push(42); + let (payment_hash_2, _, payment_secret_2, _) = get_payment_hash!(nodes[2], too_large_metadata); + too_large_onion.payment_secret = Some(payment_secret_2); // First confirm we'll fail to create the onion packet directly. let secp_ctx = Secp256k1::signing_only(); @@ -137,7 +169,6 @@ fn large_payment_metadata() { &secp_ctx, &route_0_1.paths[0], &test_utils::privkey(42), - MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, &too_large_onion, nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &payment_hash, @@ -148,12 +179,12 @@ fn large_payment_metadata() { .unwrap_err(); match err { APIError::InvalidRoute { err } => { - assert_eq!(err, "Route size too large considering onion data"); + assert_eq!(err, "Route size too large (or empty) considering onion data"); }, _ => panic!(), } - let route_params = route_0_1.route_params.clone().unwrap(); + let route_params = route_0_1.route_params.clone(); let err = nodes[0] .node .send_payment(payment_hash_2, too_large_onion, id, route_params, Retry::Attempts(0)) @@ -163,15 +194,18 @@ fn large_payment_metadata() { // If we remove enough payment_metadata bytes to allow for 2 hops, we're now able to send to // nodes[2]. let two_hop_metadata = vec![42; max_metadata_len - INTERMED_PAYLOAD_LEN_ESTIMATE]; + let (payment_hash_2, payment_preimage_2, payment_secret_2, two_hop_encrypted_metadata) = + get_payment_hash!(nodes[2], two_hop_metadata.clone()); let mut onion_allowing_2_hops = RecipientOnionFields { payment_secret: Some(payment_secret_2), - payment_metadata: Some(two_hop_metadata.clone()), + payment_metadata: Some(two_hop_encrypted_metadata), custom_tlvs: Vec::new(), + total_mpp_amount_msat: amt_msat, }; - let mut route_params_0_2 = route_0_2.route_params.clone().unwrap(); + let mut route_params_0_2 = route_0_2.route_params.clone(); route_params_0_2.payment_params.max_path_length = 2; nodes[0].router.expect_find_route_query(route_params_0_2); - let route_params = route_0_2.route_params.unwrap(); + let route_params = route_0_2.route_params; nodes[0] .node .send_payment(payment_hash_2, onion_allowing_2_hops, id, route_params, Retry::Attempts(0)) @@ -183,6 +217,7 @@ fn large_payment_metadata() { let args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash_2, events.pop().unwrap()) .with_payment_secret(payment_secret_2) + .with_payment_preimage(payment_preimage_2) .with_payment_metadata(two_hop_metadata); do_pass_along_path(args); claim_payment_along_route(ClaimAlongRouteArgs::new( @@ -220,7 +255,9 @@ fn one_hop_blinded_path_with_custom_tlv() { max_cltv_expiry: u32::max_value(), htlc_minimum_msat: chan_upd_1_2.htlc_minimum_msat, }, - payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext {}), + payment_context: PaymentContext::Bolt12Refund(Bolt12RefundContext { + payment_metadata: None, + }), }; let receive_auth_key = chanmon_cfgs[2].keys_manager.get_receive_auth_key(); let mut secp_ctx = Secp256k1::new(); @@ -261,7 +298,7 @@ fn one_hop_blinded_path_with_custom_tlv() { - final_payload_len_without_custom_tlv; // Check that we can send the maximum custom TLV with 1 blinded hop. - let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs( + let max_sized_onion = RecipientOnionFields::spontaneous_empty(amt_msat).with_custom_tlvs( RecipientCustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(), ); let id = PaymentId(payment_hash.0); @@ -366,10 +403,9 @@ fn blinded_path_with_custom_tlv() { // Calculate the maximum custom TLV value size where a valid onion packet is still possible. const CUSTOM_TLV_TYPE: u64 = 65537; let mut route = get_route(&nodes[1], &route_params).unwrap(); - let reserved_packet_bytes_without_custom_tlv: usize = onion_utils::build_onion_payloads( + let reserved_packet_bytes_without_custom_tlv: usize = onion_utils::test_build_onion_payloads( &route.paths[0], - MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, - &RecipientOnionFields::spontaneous_empty(), + &RecipientOnionFields::spontaneous_empty(MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY), nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &None, None, @@ -387,7 +423,7 @@ fn blinded_path_with_custom_tlv() { - reserved_packet_bytes_without_custom_tlv; // Check that we can send the maximum custom TLV size with 0 intermediate unblinded hops. - let max_sized_onion = RecipientOnionFields::spontaneous_empty().with_custom_tlvs( + let max_sized_onion = RecipientOnionFields::spontaneous_empty(amt_msat).with_custom_tlvs( RecipientCustomTlvs::new(vec![(CUSTOM_TLV_TYPE, vec![42; max_custom_tlv_len])]).unwrap(), ); let no_retry = Retry::Attempts(0); @@ -420,15 +456,16 @@ fn blinded_path_with_custom_tlv() { .unwrap_err(); assert_eq!(err, RetryableSendFailure::OnionPacketSizeExceeded); - // Confirm that we can't construct an onion packet given this too-large custom TLV. + // Confirm that we can't construct an onion packet given this too-large custom TLV (as long as + // we actually use the amount the payment logic uses when validating). let secp_ctx = Secp256k1::signing_only(); route.paths[0].hops[0].fee_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; route.paths[0].hops[0].cltv_expiry_delta = DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA; + too_large_onion.total_mpp_amount_msat = MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY; let err = onion_utils::create_payment_onion( &secp_ctx, &route.paths[0], &test_utils::privkey(42), - MIN_FINAL_VALUE_ESTIMATE_WITH_OVERPAY, &too_large_onion, nodes[0].best_block_info().1 + DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, &payment_hash, @@ -439,7 +476,7 @@ fn blinded_path_with_custom_tlv() { .unwrap_err(); match err { APIError::InvalidRoute { err } => { - assert_eq!(err, "Route size too large considering onion data"); + assert_eq!(err, "Route size too large (or empty) considering onion data"); }, _ => panic!(), } diff --git a/lightning/src/ln/mod.rs b/lightning/src/ln/mod.rs index d6e0b92f1d0..30a8109fc43 100644 --- a/lightning/src/ln/mod.rs +++ b/lightning/src/ln/mod.rs @@ -118,6 +118,8 @@ mod reorg_tests; mod shutdown_tests; #[cfg(any(feature = "_test_utils", test))] pub mod splicing_tests; +#[cfg(test)] +mod trampoline_forward_tests; #[cfg(any(test, feature = "_externalize_tests"))] #[allow(unused_mut)] pub mod update_fee_tests; diff --git a/lightning/src/ln/monitor_tests.rs b/lightning/src/ln/monitor_tests.rs index fd33ec217ca..f52f093917b 100644 --- a/lightning/src/ln/monitor_tests.rs +++ b/lightning/src/ln/monitor_tests.rs @@ -68,7 +68,7 @@ fn chanmon_fail_from_stale_commitment() { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], 1_000_000); nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 1_000_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let bs_txn = get_local_commitment_txn!(nodes[1], chan_id_2); @@ -84,7 +84,7 @@ fn chanmon_fail_from_stale_commitment() { // Don't bother delivering the new HTLC add/commits, instead confirming the pre-HTLC commitment // transaction for nodes[1]. mine_transaction(&nodes[1], &bs_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[2].node.get_our_node_id()], 100000); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); @@ -140,7 +140,7 @@ fn revoked_output_htlc_resolution_timing() { // Confirm the revoked commitment transaction, closing the channel. mine_transaction(&nodes[1], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); @@ -187,7 +187,7 @@ fn archive_fully_resolved_monitors() { let message = "Channel force-closed".to_owned(); nodes[0].node.force_close_broadcasting_latest_txn(&chan_id, &nodes[1].node.get_our_node_id(), message.clone()).unwrap(); check_added_monitors(&nodes[0], 1); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], 1_000_000); @@ -369,9 +369,9 @@ fn do_chanmon_claim_value_coop_close(keyed_anchors: bool, p2a_anchor: bool) { nodes[1].node.handle_closing_signed(nodes[0].node.get_our_node_id(), &node_0_closing_signed); let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, nodes[0].node.get_our_node_id()); nodes[0].node.handle_closing_signed(nodes[1].node.get_our_node_id(), &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, nodes[1].node.get_our_node_id()); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], nodes[1].node.get_our_node_id()); nodes[1].node.handle_closing_signed(nodes[0].node.get_our_node_id(), &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, nodes[0].node.get_our_node_id()); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], nodes[0].node.get_our_node_id()); assert!(node_1_none.is_none()); let shutdown_tx = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); @@ -678,11 +678,11 @@ fn do_test_claim_value_force_close(keyed_anchors: bool, p2a_anchor: bool, prev_c assert_eq!(remote_txn[0].output[b_broadcast_txn[0].input[0].previous_output.vout as usize].value.to_sat(), 3_000); assert_eq!(remote_txn[0].output[b_broadcast_txn[1].input[0].previous_output.vout as usize].value.to_sat(), 4_000); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000); assert!(nodes[0].node.list_channels().is_empty()); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); assert!(nodes[1].node.list_channels().is_empty()); @@ -881,7 +881,7 @@ fn do_test_balances_on_local_commitment_htlcs(keyed_anchors: bool, p2a_anchor: b let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 10_000_000); let htlc_cltv_timeout = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + 1; // Note ChannelManager adds one to CLTV timeouts for safety nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 10_000_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); @@ -893,7 +893,7 @@ fn do_test_balances_on_local_commitment_htlcs(keyed_anchors: bool, p2a_anchor: b let (route_2, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[0], nodes[1], 20_000_000); nodes[0].node.send_payment_with_route(route_2, payment_hash_2, - RecipientOnionFields::secret_only(payment_secret_2), PaymentId(payment_hash_2.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret_2, 20_000_000), PaymentId(payment_hash_2.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); @@ -916,7 +916,7 @@ fn do_test_balances_on_local_commitment_htlcs(keyed_anchors: bool, p2a_anchor: b let node_a_commitment_claimable = nodes[0].best_block_info().1 + BREAKDOWN_TIMEOUT as u32; nodes[0].node.force_close_broadcasting_latest_txn(&chan_id, &nodes[1].node.get_our_node_id(), message.clone()).unwrap(); check_added_monitors(&nodes[0], 1); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], 1000000); if keyed_anchors || p2a_anchor { @@ -976,7 +976,7 @@ fn do_test_balances_on_local_commitment_htlcs(keyed_anchors: bool, p2a_anchor: b // Get nodes[1]'s HTLC claim tx for the second HTLC mine_transaction(&nodes[1], &commitment_tx); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); let bs_htlc_claim_txn = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); @@ -1207,7 +1207,7 @@ fn test_no_preimage_inbound_htlc_balances() { mine_transaction(&nodes[0], &as_txn[0]); nodes[0].tx_broadcaster.clear(); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000); @@ -1215,7 +1215,7 @@ fn test_no_preimage_inbound_htlc_balances() { sorted_vec(nodes[0].chain_monitor.chain_monitor.get_monitor(chan_id).unwrap().get_claimable_balances())); mine_transaction(&nodes[1], &as_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); @@ -1428,7 +1428,7 @@ fn do_test_revoked_counterparty_commitment_balances(keyed_anchors: bool, p2a_anc let _b_htlc_msgs = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); connect_blocks(&nodes[0], htlc_cltv_timeout + 1 - 10); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let mut events = nodes[0].node.get_and_clear_pending_events(); @@ -1457,7 +1457,7 @@ fn do_test_revoked_counterparty_commitment_balances(keyed_anchors: bool, p2a_anc } connect_blocks(&nodes[1], htlc_cltv_timeout + 1 - 10); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); check_closed_events(&nodes[1], &[ExpectedCloseEvent { channel_capacity_sats: Some(1_000_000), @@ -1718,7 +1718,7 @@ fn do_test_revoked_counterparty_htlc_tx_balances(keyed_anchors: bool, p2a_anchor // B will generate an HTLC-Success from its revoked commitment tx mine_transaction(&nodes[1], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); if keyed_anchors || p2a_anchor { @@ -1762,7 +1762,7 @@ fn do_test_revoked_counterparty_htlc_tx_balances(keyed_anchors: bool, p2a_anchor &[HTLCHandlingFailureType::Receive { payment_hash: failed_payment_hash }]); // A will generate justice tx from B's revoked commitment/HTLC tx mine_transaction(&nodes[0], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000); let to_remote_conf_height = nodes[0].best_block_info().1 + ANTI_REORG_DELAY - 1; @@ -2042,7 +2042,7 @@ fn do_test_revoked_counterparty_aggregated_claims(keyed_anchors: bool, p2a_ancho sorted_vec(nodes[1].chain_monitor.chain_monitor.get_monitor(chan_id).unwrap().get_claimable_balances())); mine_transaction(&nodes[1], &as_revoked_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); check_added_monitors(&nodes[1], 1); @@ -2414,7 +2414,7 @@ fn do_test_monitor_rebroadcast_pending_claims(keyed_anchors: bool, p2a_anchor: b assert_eq!(commitment_txn.len(), if keyed_anchors || p2a_anchor { 1 /* commitment tx only */} else { 2 /* commitment and htlc timeout tx */ }); check_spends!(&commitment_txn[0], &funding_tx); mine_transaction(&nodes[0], &commitment_txn[0]); - check_closed_broadcast!(&nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000); check_added_monitors(&nodes[0], 1); @@ -2724,6 +2724,8 @@ fn do_test_anchors_aggregated_revoked_htlc_tx(p2a_anchor: bool) { anchors_config.channel_handshake_config.announce_for_forwarding = true; anchors_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; anchors_config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = p2a_anchor; + // Set the percentage to the default value at the time this test was written + anchors_config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = 10; let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(anchors_config.clone()), Some(anchors_config.clone())]); let bob_deserialized; @@ -3156,13 +3158,13 @@ fn do_test_monitor_claims_with_random_signatures(keyed_anchors: bool, p2a_anchor if p2a_anchor { mine_transaction(closing_node, anchor_tx.as_ref().unwrap()); } - check_closed_broadcast!(closing_node, true); + check_closed_broadcast(closing_node, 1, true); check_added_monitors(&closing_node, 1); let message = "ChannelMonitor-initiated commitment transaction broadcast".to_string(); check_closed_event(&closing_node, 1, ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }, &[other_node.node.get_our_node_id()], 1_000_000); mine_transaction(other_node, &commitment_tx); - check_closed_broadcast!(other_node, true); + check_closed_broadcast(other_node, 1, true); check_added_monitors(&other_node, 1); check_closed_event(&other_node, 1, ClosureReason::CommitmentTxConfirmed, &[closing_node.node.get_our_node_id()], 1_000_000); @@ -3384,6 +3386,7 @@ fn test_claim_event_never_handled() { let chan_0_monitor_serialized = get_monitor!(nodes[1], chan.2).encode(); let mons = &[&chan_0_monitor_serialized[..]]; reload_node!(nodes[1], &init_node_ser, mons, persister, new_chain_mon, nodes_1_reload); + nodes[1].disable_monitor_completeness_assertion(); expect_payment_claimed!(nodes[1], payment_hash_a, 1_000_000); // The reload logic spuriously generates a redundant payment preimage-containing @@ -3630,7 +3633,7 @@ fn do_test_lost_timeout_monitor_events(confirm_tx: CommitmentType, dust_htlcs: b let (route, hash_b, _, payment_secret_b) = get_route_and_payment_hash!(nodes[1], nodes[2], amt); - let onion = RecipientOnionFields::secret_only(payment_secret_b); + let onion = RecipientOnionFields::secret_only(payment_secret_b, amt); nodes[1].node.send_payment_with_route(route, hash_b, onion, PaymentId(hash_b.0)).unwrap(); check_added_monitors(&nodes[1], 1); @@ -3780,8 +3783,8 @@ fn do_test_lost_timeout_monitor_events(confirm_tx: CommitmentType, dust_htlcs: b Event::PaymentFailed { payment_hash, .. } => { assert_eq!(payment_hash, Some(hash_b)); }, - Event::HTLCHandlingFailed { prev_channel_id, .. } => { - assert_eq!(prev_channel_id, chan_a); + Event::HTLCHandlingFailed { prev_channel_ids, .. } => { + assert_eq!(prev_channel_ids[0], chan_a); }, _ => panic!("Wrong event {ev:?}"), } diff --git a/lightning/src/ln/msgs.rs b/lightning/src/ln/msgs.rs index 67f7807a487..c6539552d84 100644 --- a/lightning/src/ln/msgs.rs +++ b/lightning/src/ln/msgs.rs @@ -56,7 +56,7 @@ use core::str::FromStr; #[cfg(feature = "std")] use std::net::SocketAddr; -use crate::crypto::streams::ChaChaDualPolyReadAdapter; +use crate::crypto::streams::{ChaChaTriPolyReadAdapter, TriPolyAADUsed}; use crate::util::base32; use crate::util::logger; use crate::util::ser::{ @@ -69,14 +69,6 @@ use crate::routing::gossip::{NodeAlias, NodeId}; /// 21 million * 10^8 * 1000 pub(crate) const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000; -#[cfg(taproot)] -/// A partial signature that also contains the Musig2 nonce its signer used -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct PartialSignatureWithNonce( - pub musig2::types::PartialSignature, - pub musig2::types::PublicNonce, -); - /// An error in decoding a message or struct. #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum DecodeError { @@ -110,6 +102,8 @@ pub enum DecodeError { /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager /// [`ChannelMonitor`]: crate::chain::channelmonitor::ChannelMonitor DangerousValue, + /// This a custom error used by Bitcoinfuzz to skip some errors. + SkipCase } /// An [`init`] message to be sent to or received from a peer. @@ -311,6 +305,8 @@ pub struct OpenChannelV2 { pub second_per_commitment_point: PublicKey, /// Optionally, a requirement that only confirmed inputs can be added pub require_confirmed_inputs: Option<()>, + /// Optionally, disables the channel reserve of the receiver + pub disable_channel_reserve: Option<()>, } /// Contains fields that are both common to [`accept_channel`] and [`accept_channel2`] messages. @@ -370,9 +366,6 @@ pub struct AcceptChannel { pub common_fields: CommonAcceptChannelFields, /// The minimum value unencumbered by HTLCs for the counterparty to keep in the channel pub channel_reserve_satoshis: u64, - #[cfg(taproot)] - /// Next nonce the channel initiator should use to create a funding output signature against - pub next_local_nonce: Option<musig2::types::PublicNonce>, } /// An [`accept_channel2`] message to be sent by or received from the channel accepter. @@ -390,6 +383,8 @@ pub struct AcceptChannelV2 { pub second_per_commitment_point: PublicKey, /// Optionally, a requirement that only confirmed inputs can be added pub require_confirmed_inputs: Option<()>, + /// Optionally, disables the channel reserve of the receiver + pub disable_channel_reserve: Option<()>, } /// A [`funding_created`] message to be sent to or received from a peer. @@ -407,12 +402,6 @@ pub struct FundingCreated { pub funding_output_index: u16, /// The signature of the channel initiator (funder) on the initial commitment transaction pub signature: Signature, - #[cfg(taproot)] - /// The partial signature of the channel initiator (funder) - pub partial_signature_with_nonce: Option<PartialSignatureWithNonce>, - #[cfg(taproot)] - /// Next nonce the channel acceptor should use to finalize the funding output signature - pub next_local_nonce: Option<musig2::types::PublicNonce>, } /// A [`funding_signed`] message to be sent to or received from a peer. @@ -426,9 +415,6 @@ pub struct FundingSigned { pub channel_id: ChannelId, /// The signature of the channel acceptor (fundee) on the initial commitment transaction pub signature: Signature, - #[cfg(taproot)] - /// The partial signature of the channel acceptor (fundee) - pub partial_signature_with_nonce: Option<PartialSignatureWithNonce>, } /// A [`channel_ready`] message to be sent to or received from a peer. @@ -779,10 +765,10 @@ pub struct UpdateAddHTLC { struct AccountableBool<T>(T); -impl Writeable for AccountableBool<bool> { +impl Writeable for AccountableBool<&bool> { #[inline] fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { - let wire_value = if self.0 { 7u8 } else { 0u8 }; + let wire_value = if *self.0 { 7u8 } else { 0u8 }; writer.write_all(&[wire_value]) } } @@ -906,9 +892,6 @@ pub struct CommitmentSigned { pub htlc_signatures: Vec<Signature>, /// The funding transaction, to discriminate among multiple pending funding transactions (e.g. in case of splicing) pub funding_txid: Option<Txid>, - #[cfg(taproot)] - /// The partial Taproot signature on the commitment transaction - pub partial_signature_with_nonce: Option<PartialSignatureWithNonce>, } /// A [`revoke_and_ack`] message to be sent to or received from a peer. @@ -922,9 +905,6 @@ pub struct RevokeAndACK { pub per_commitment_secret: [u8; 32], /// The next sender-broadcast commitment transaction's per-commitment point pub next_per_commitment_point: PublicKey, - #[cfg(taproot)] - /// Musig nonce the recipient should use in their next commitment signature message - pub next_local_nonce: Option<musig2::types::PublicNonce>, /// A list of `(htlc_id, blinded_path)`. The receiver of this message will use the blinded paths /// as reply paths to [`HeldHtlcAvailable`] onion messages that they send to the often-offline /// receiver of this HTLC. The `htlc_id` is used by the receiver of this message to identify which @@ -1513,9 +1493,9 @@ pub struct UnsignedChannelUpdate { /// The number of blocks such that if: /// `incoming_htlc.cltv_expiry < outgoing_htlc.cltv_expiry + cltv_expiry_delta` /// then we need to fail the HTLC backwards. When forwarding an HTLC, `cltv_expiry_delta` determines - /// the outgoing HTLC's minimum `cltv_expiry` value -- so, if an incoming HTLC comes in with a + /// the outgoing HTLC's maximum `cltv_expiry` value -- so, if an incoming HTLC comes in with a /// `cltv_expiry` of 100000, and the node we're forwarding to has a `cltv_expiry_delta` value of 10, - /// then we'll check that the outgoing HTLC's `cltv_expiry` value is at least 100010 before + /// then we'll check that the outgoing HTLC's `cltv_expiry` value is at most 99990 before /// forwarding. Note that the HTLC sender is the one who originally sets this value when /// constructing the route. pub cltv_expiry_delta: u16, @@ -2750,10 +2730,7 @@ mod fuzzy_internal_msgs { pub attribution_data: Option<AttributionData>, } } -#[cfg(fuzzing)] pub use self::fuzzy_internal_msgs::*; -#[cfg(not(fuzzing))] -pub(crate) use self::fuzzy_internal_msgs::*; use super::onion_utils::AttributionData; @@ -2879,6 +2856,9 @@ impl fmt::Display for DecodeError { DecodeError::DangerousValue => { f.write_str("Value would be dangerous to continue execution with") }, + DecodeError::SkipCase => { + f.write_str("Should be skipped by bitcoinfuzz") + }, } } } @@ -2909,17 +2889,10 @@ impl Writeable for AcceptChannel { self.common_fields.delayed_payment_basepoint.write(w)?; self.common_fields.htlc_basepoint.write(w)?; self.common_fields.first_per_commitment_point.write(w)?; - #[cfg(not(taproot))] encode_tlv_stream!(w, { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), }); - #[cfg(taproot)] - encode_tlv_stream!(w, { - (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. - (1, self.common_fields.channel_type, option), - (4, self.next_local_nonce, option), - }); Ok(()) } } @@ -2943,19 +2916,10 @@ impl LengthReadable for AcceptChannel { let mut shutdown_scriptpubkey: Option<ScriptBuf> = None; let mut channel_type: Option<ChannelTypeFeatures> = None; - #[cfg(not(taproot))] decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), }); - #[cfg(taproot)] - let mut next_local_nonce: Option<musig2::types::PublicNonce> = None; - #[cfg(taproot)] - decode_tlv_stream!(r, { - (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), - (1, channel_type, option), - (4, next_local_nonce, option), - }); Ok(AcceptChannel { common_fields: CommonAcceptChannelFields { @@ -2976,8 +2940,6 @@ impl LengthReadable for AcceptChannel { channel_type, }, channel_reserve_satoshis, - #[cfg(taproot)] - next_local_nonce, }) } } @@ -3004,6 +2966,7 @@ impl Writeable for AcceptChannelV2 { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), (2, self.require_confirmed_inputs, option), + (103, self.disable_channel_reserve, option), }); Ok(()) } @@ -3030,10 +2993,12 @@ impl LengthReadable for AcceptChannelV2 { let mut shutdown_scriptpubkey: Option<ScriptBuf> = None; let mut channel_type: Option<ChannelTypeFeatures> = None; let mut require_confirmed_inputs: Option<()> = None; + let mut disable_channel_reserve: Option<()> = None; decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), (2, require_confirmed_inputs, option), + (103, disable_channel_reserve, option), }); Ok(AcceptChannelV2 { @@ -3057,6 +3022,7 @@ impl LengthReadable for AcceptChannelV2 { funding_satoshis, second_per_commitment_point, require_confirmed_inputs, + disable_channel_reserve, }) } } @@ -3245,23 +3211,12 @@ impl_writeable!(ClosingSignedFeeRange, { max_fee_satoshis }); -#[cfg(not(taproot))] -impl_writeable_msg!(CommitmentSigned, { - channel_id, - signature, - htlc_signatures -}, { - (1, funding_txid, option), -}); - -#[cfg(taproot)] impl_writeable_msg!(CommitmentSigned, { channel_id, signature, htlc_signatures }, { (1, funding_txid, option), - (2, partial_signature_with_nonce, option), }); impl_writeable!(DecodedOnionErrorPacket, { @@ -3270,38 +3225,18 @@ impl_writeable!(DecodedOnionErrorPacket, { pad }); -#[cfg(not(taproot))] impl_writeable_msg!(FundingCreated, { temporary_channel_id, funding_txid, funding_output_index, signature }, {}); -#[cfg(taproot)] -impl_writeable_msg!(FundingCreated, { - temporary_channel_id, - funding_txid, - funding_output_index, - signature -}, { - (2, partial_signature_with_nonce, option), - (4, next_local_nonce, option) -}); -#[cfg(not(taproot))] impl_writeable_msg!(FundingSigned, { channel_id, signature }, {}); -#[cfg(taproot)] -impl_writeable_msg!(FundingSigned, { - channel_id, - signature -}, { - (2, partial_signature_with_nonce, option) -}); - impl_writeable_msg!(ChannelReady, { channel_id, next_per_commitment_point, @@ -3465,6 +3400,7 @@ impl Writeable for OpenChannelV2 { (0, self.common_fields.shutdown_scriptpubkey.as_ref().map(|s| WithoutLength(s)), option), // Don't encode length twice. (1, self.common_fields.channel_type, option), (2, self.require_confirmed_inputs, option), + (103, self.disable_channel_reserve, option), }); Ok(()) } @@ -3495,10 +3431,12 @@ impl LengthReadable for OpenChannelV2 { let mut shutdown_scriptpubkey: Option<ScriptBuf> = None; let mut channel_type: Option<ChannelTypeFeatures> = None; let mut require_confirmed_inputs: Option<()> = None; + let mut disable_channel_reserve: Option<()> = None; decode_tlv_stream!(r, { (0, shutdown_scriptpubkey, (option, encoding: (ScriptBuf, WithoutLength))), (1, channel_type, option), (2, require_confirmed_inputs, option), + (103, disable_channel_reserve, option), }); Ok(OpenChannelV2 { common_fields: CommonOpenChannelFields { @@ -3525,26 +3463,16 @@ impl LengthReadable for OpenChannelV2 { locktime, second_per_commitment_point, require_confirmed_inputs, + disable_channel_reserve, }) } } -#[cfg(not(taproot))] -impl_writeable_msg!(RevokeAndACK, { - channel_id, - per_commitment_secret, - next_per_commitment_point -}, { - (75537, release_htlc_message_paths, optional_vec) -}); - -#[cfg(taproot)] impl_writeable_msg!(RevokeAndACK, { channel_id, per_commitment_secret, next_per_commitment_point }, { - (4, next_local_nonce, option), (75537, release_htlc_message_paths, optional_vec) }); @@ -3871,6 +3799,9 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo let mut custom_tlvs = Vec::new(); let tlv_len = BigSize::read(r)?; + if tlv_len.0 < 2 { + return Err(DecodeError::SkipCase); + } let mut rd = FixedLengthReader::new(r, tlv_len.0); decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { @@ -3895,7 +3826,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo }); if amt.unwrap_or(0) > MAX_VALUE_MSAT { - return Err(DecodeError::InvalidValue); + return Err(DecodeError::SkipCase); } if intro_node_blinding_point.is_some() && update_add_blinding_point.is_some() { return Err(DecodeError::InvalidValue); @@ -3924,10 +3855,13 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo .map_err(|_| DecodeError::InvalidValue)?; let rho = onion_utils::gen_rho_from_shared_secret(&enc_tlvs_ss.secret_bytes()); let receive_auth_key = node_signer.get_receive_auth_key(); + let phantom_auth_key = node_signer.get_expanded_key().phantom_node_blinded_path_key; + let read_args = (rho, receive_auth_key.0, phantom_auth_key); + let mut s = Cursor::new(&enc_tlvs); let mut reader = FixedLengthReader::new(&mut s, enc_tlvs.len() as u64); - match ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))? { - ChaChaDualPolyReadAdapter { + match ChaChaTriPolyReadAdapter::read(&mut reader, read_args)? { + ChaChaTriPolyReadAdapter { readable: BlindedPaymentTlvs::Forward(ForwardTlvs { short_channel_id, @@ -3939,10 +3873,11 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo used_aad, } => { if amt.is_some() - || cltv_value.is_some() || total_msat.is_some() + || cltv_value.is_some() + || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() - || used_aad + || used_aad != TriPolyAADUsed::None { return Err(DecodeError::InvalidValue); } @@ -3955,7 +3890,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo next_blinding_override, })) }, - ChaChaDualPolyReadAdapter { + ChaChaTriPolyReadAdapter { readable: BlindedPaymentTlvs::Dummy(DummyTlvs { payment_relay, payment_constraints }), used_aad, @@ -3964,7 +3899,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo || cltv_value.is_some() || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() - || !used_aad + || used_aad == TriPolyAADUsed::None { return Err(DecodeError::InvalidValue); } @@ -3974,11 +3909,11 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo intro_node_blinding_point, })) }, - ChaChaDualPolyReadAdapter { + ChaChaTriPolyReadAdapter { readable: BlindedPaymentTlvs::Receive(receive_tlvs), used_aad, } => { - if !used_aad { + if used_aad == TriPolyAADUsed::None { return Err(DecodeError::InvalidValue); } @@ -3986,7 +3921,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo receive_tlvs; if total_msat.unwrap_or(0) > MAX_VALUE_MSAT { - return Err(DecodeError::InvalidValue); + return Err(DecodeError::SkipCase); } Ok(Self::BlindedReceive(InboundOnionBlindedReceivePayload { sender_intended_htlc_amt_msat: amt.ok_or(DecodeError::InvalidValue)?, @@ -4009,7 +3944,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo || total_msat.is_some() || invoice_request.is_some() { - return Err(DecodeError::InvalidValue); + return Err(DecodeError::SkipCase); } Ok(Self::Forward(InboundOnionForwardPayload { short_channel_id, @@ -4018,11 +3953,11 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundOnionPaylo })) } else { if encrypted_tlvs_opt.is_some() || total_msat.is_some() || invoice_request.is_some() { - return Err(DecodeError::InvalidValue); + return Err(DecodeError::SkipCase); } if let Some(data) = &payment_data { if data.total_msat > MAX_VALUE_MSAT { - return Err(DecodeError::InvalidValue); + return Err(DecodeError::SkipCase); } } Ok(Self::Receive(InboundOnionReceivePayload { @@ -4041,6 +3976,7 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline fn read<R: Read>(r: &mut R, args: (Option<PublicKey>, NS)) -> Result<Self, DecodeError> { let (update_add_blinding_point, node_signer) = args; let receive_auth_key = node_signer.get_receive_auth_key(); + let phantom_auth_key = node_signer.get_expanded_key().phantom_node_blinded_path_key; let mut amt = None; let mut cltv_value = None; @@ -4094,8 +4030,9 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline let rho = onion_utils::gen_rho_from_shared_secret(&enc_tlvs_ss.secret_bytes()); let mut s = Cursor::new(&enc_tlvs); let mut reader = FixedLengthReader::new(&mut s, enc_tlvs.len() as u64); - match ChaChaDualPolyReadAdapter::read(&mut reader, (rho, receive_auth_key.0))? { - ChaChaDualPolyReadAdapter { + let read_args = (rho, receive_auth_key.0, phantom_auth_key); + match ChaChaTriPolyReadAdapter::read(&mut reader, read_args)? { + ChaChaTriPolyReadAdapter { readable: BlindedTrampolineTlvs::Forward(TrampolineForwardTlvs { next_trampoline, @@ -4107,10 +4044,11 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline used_aad, } => { if amt.is_some() - || cltv_value.is_some() || total_msat.is_some() + || cltv_value.is_some() + || total_msat.is_some() || keysend_preimage.is_some() || invoice_request.is_some() - || used_aad + || used_aad != TriPolyAADUsed::None { return Err(DecodeError::InvalidValue); } @@ -4123,11 +4061,11 @@ impl<NS: NodeSigner> ReadableArgs<(Option<PublicKey>, NS)> for InboundTrampoline next_blinding_override, })) }, - ChaChaDualPolyReadAdapter { + ChaChaTriPolyReadAdapter { readable: BlindedTrampolineTlvs::Receive(receive_tlvs), used_aad, } => { - if !used_aad { + if used_aad == TriPolyAADUsed::None { return Err(DecodeError::InvalidValue); } @@ -5268,6 +5206,7 @@ mod tests { fn do_encoding_open_channelv2( random_bit: bool, shutdown: bool, incl_chan_type: bool, require_confirmed_inputs: bool, + disable_channel_reserve: bool, ) { let secp_ctx = Secp256k1::new(); let (_, pubkey_1) = get_keys_from!( @@ -5336,7 +5275,8 @@ mod tests { funding_feerate_sat_per_1000_weight: 821716, locktime: 305419896, second_per_commitment_point: pubkey_7, - require_confirmed_inputs: if require_confirmed_inputs { Some(()) } else { None }, + require_confirmed_inputs: require_confirmed_inputs.then_some(()), + disable_channel_reserve: disable_channel_reserve.then_some(()), }; let encoded_value = open_channelv2.encode(); let mut target_value = Vec::new(); @@ -5421,27 +5361,46 @@ mod tests { if require_confirmed_inputs { target_value.append(&mut <Vec<u8>>::from_hex("0200").unwrap()); } + if disable_channel_reserve { + target_value.append(&mut <Vec<u8>>::from_hex("6700").unwrap()); + } assert_eq!(encoded_value, target_value); } #[test] fn encoding_open_channelv2() { - do_encoding_open_channelv2(false, false, false, false); - do_encoding_open_channelv2(false, false, false, true); - do_encoding_open_channelv2(false, false, true, false); - do_encoding_open_channelv2(false, false, true, true); - do_encoding_open_channelv2(false, true, false, false); - do_encoding_open_channelv2(false, true, false, true); - do_encoding_open_channelv2(false, true, true, false); - do_encoding_open_channelv2(false, true, true, true); - do_encoding_open_channelv2(true, false, false, false); - do_encoding_open_channelv2(true, false, false, true); - do_encoding_open_channelv2(true, false, true, false); - do_encoding_open_channelv2(true, false, true, true); - do_encoding_open_channelv2(true, true, false, false); - do_encoding_open_channelv2(true, true, false, true); - do_encoding_open_channelv2(true, true, true, false); - do_encoding_open_channelv2(true, true, true, true); + do_encoding_open_channelv2(false, false, false, false, false); + do_encoding_open_channelv2(false, false, false, false, true); + do_encoding_open_channelv2(false, false, false, true, false); + do_encoding_open_channelv2(false, false, false, true, true); + do_encoding_open_channelv2(false, false, true, false, false); + do_encoding_open_channelv2(false, false, true, false, true); + do_encoding_open_channelv2(false, false, true, true, false); + do_encoding_open_channelv2(false, false, true, true, true); + do_encoding_open_channelv2(false, true, false, false, false); + do_encoding_open_channelv2(false, true, false, false, true); + do_encoding_open_channelv2(false, true, false, true, false); + do_encoding_open_channelv2(false, true, false, true, true); + do_encoding_open_channelv2(false, true, true, false, false); + do_encoding_open_channelv2(false, true, true, false, true); + do_encoding_open_channelv2(false, true, true, true, false); + do_encoding_open_channelv2(false, true, true, true, true); + do_encoding_open_channelv2(true, false, false, false, false); + do_encoding_open_channelv2(true, false, false, false, true); + do_encoding_open_channelv2(true, false, false, true, false); + do_encoding_open_channelv2(true, false, false, true, true); + do_encoding_open_channelv2(true, false, true, false, false); + do_encoding_open_channelv2(true, false, true, false, true); + do_encoding_open_channelv2(true, false, true, true, false); + do_encoding_open_channelv2(true, false, true, true, true); + do_encoding_open_channelv2(true, true, false, false, false); + do_encoding_open_channelv2(true, true, false, false, true); + do_encoding_open_channelv2(true, true, false, true, false); + do_encoding_open_channelv2(true, true, false, true, true); + do_encoding_open_channelv2(true, true, true, false, false); + do_encoding_open_channelv2(true, true, true, false, true); + do_encoding_open_channelv2(true, true, true, true, false); + do_encoding_open_channelv2(true, true, true, true, true); } fn do_encoding_accept_channel(shutdown: bool) { @@ -5499,8 +5458,6 @@ mod tests { channel_type: None, }, channel_reserve_satoshis: 3608586615801332854, - #[cfg(taproot)] - next_local_nonce: None, }; let encoded_value = accept_channel.encode(); let mut target_value = <Vec<u8>>::from_hex("020202020202020202020202020202020202020202020202020202020202020212345678901234562334032891223698321446687011447600083a840000034d000c89d4c0bcc0bc031b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f024d4b6cd1361032ca9bd2aeb9d900aa4d45d9ead80ac9423374c451a7254d076602531fe6068134503d2723133227c867ac8fa6c83c537e9a44c3c5bdbdcb1fe33703462779ad4aad39514614751a71085f2f10e1c7a593e4e030efb5b8721ce55b0b0362c0a046dacce86ddd0343c6d3c7c79c2208ba0d9c9cf24a6d046d21d21f90f703f006a18d5653c4edf5391ff23a61f03ff83d237e880ee61187fa9f379a028e0a").unwrap(); @@ -5519,7 +5476,10 @@ mod tests { do_encoding_accept_channel(true); } - fn do_encoding_accept_channelv2(shutdown: bool) { + fn do_encoding_accept_channelv2( + shutdown: bool, incl_chan_type: bool, require_confirmed_inputs: bool, + disable_channel_reserve: bool, + ) { let secp_ctx = Secp256k1::new(); let (_, pubkey_1) = get_keys_from!( "0101010101010101010101010101010101010101010101010101010101010101", @@ -5575,11 +5535,16 @@ mod tests { } else { None }, - channel_type: None, + channel_type: if incl_chan_type { + Some(ChannelTypeFeatures::empty()) + } else { + None + }, }, funding_satoshis: 1311768467284833366, second_per_commitment_point: pubkey_7, - require_confirmed_inputs: None, + require_confirmed_inputs: require_confirmed_inputs.then_some(()), + disable_channel_reserve: disable_channel_reserve.then_some(()), }; let encoded_value = accept_channelv2.encode(); let mut target_value = @@ -5640,13 +5605,36 @@ mod tests { .unwrap(), ); } + if incl_chan_type { + target_value.append(&mut <Vec<u8>>::from_hex("0100").unwrap()); + } + if require_confirmed_inputs { + target_value.append(&mut <Vec<u8>>::from_hex("0200").unwrap()); + } + if disable_channel_reserve { + target_value.append(&mut <Vec<u8>>::from_hex("6700").unwrap()); + } assert_eq!(encoded_value, target_value); } #[test] fn encoding_accept_channelv2() { - do_encoding_accept_channelv2(false); - do_encoding_accept_channelv2(true); + do_encoding_accept_channelv2(false, false, false, false); + do_encoding_accept_channelv2(false, false, false, true); + do_encoding_accept_channelv2(false, false, true, false); + do_encoding_accept_channelv2(false, false, true, true); + do_encoding_accept_channelv2(false, true, false, false); + do_encoding_accept_channelv2(false, true, false, true); + do_encoding_accept_channelv2(false, true, true, false); + do_encoding_accept_channelv2(false, true, true, true); + do_encoding_accept_channelv2(true, false, false, false); + do_encoding_accept_channelv2(true, false, false, true); + do_encoding_accept_channelv2(true, false, true, false); + do_encoding_accept_channelv2(true, false, true, true); + do_encoding_accept_channelv2(true, true, false, false); + do_encoding_accept_channelv2(true, true, false, true); + do_encoding_accept_channelv2(true, true, true, false); + do_encoding_accept_channelv2(true, true, true, true); } #[test] @@ -5666,10 +5654,6 @@ mod tests { .unwrap(), funding_output_index: 255, signature: sig_1, - #[cfg(taproot)] - partial_signature_with_nonce: None, - #[cfg(taproot)] - next_local_nonce: None, }; let encoded_value = funding_created.encode(); let target_value = <Vec<u8>>::from_hex("02020202020202020202020202020202020202020202020202020202020202026e96fe9f8b0ddcd729ba03cfafa5a27b050b39d354dd980814268dfa9a44d4c200ffd977cb9b53d93a6ff64bb5f1e158b4094b66e798fb12911168a3ccdf80a83096340a6a95da0ae8d9f776528eecdbb747eb6b545495a4319ed5378e35b21e073a").unwrap(); @@ -5685,12 +5669,8 @@ mod tests { ); let sig_1 = get_sig_on!(privkey_1, secp_ctx, String::from("01010101010101010101010101010101")); - let funding_signed = msgs::FundingSigned { - channel_id: ChannelId::from_bytes([2; 32]), - signature: sig_1, - #[cfg(taproot)] - partial_signature_with_nonce: None, - }; + let funding_signed = + msgs::FundingSigned { channel_id: ChannelId::from_bytes([2; 32]), signature: sig_1 }; let encoded_value = funding_signed.encode(); let target_value = <Vec<u8>>::from_hex("0202020202020202020202020202020202020202020202020202020202020202d977cb9b53d93a6ff64bb5f1e158b4094b66e798fb12911168a3ccdf80a83096340a6a95da0ae8d9f776528eecdbb747eb6b545495a4319ed5378e35b21e073a").unwrap(); assert_eq!(encoded_value, target_value); @@ -6229,8 +6209,6 @@ mod tests { Txid::from_str("c2d4449afa8d26140898dd54d3390b057ba2a5afcf03ba29d7dc0d8b9ffe966e") .unwrap(), ), - #[cfg(taproot)] - partial_signature_with_nonce: None, }; let encoded_value = commitment_signed.encode(); let mut target_value = "0202020202020202020202020202020202020202020202020202020202020202d977cb9b53d93a6ff64bb5f1e158b4094b66e798fb12911168a3ccdf80a83096340a6a95da0ae8d9f776528eecdbb747eb6b545495a4319ed5378e35b21e073a".to_string(); @@ -6265,8 +6243,6 @@ mod tests { 1, 1, 1, 1, ], next_per_commitment_point: pubkey_1, - #[cfg(taproot)] - next_local_nonce: None, release_htlc_message_paths: Vec::new(), }; let encoded_value = raa.encode(); diff --git a/lightning/src/ln/offers_tests.rs b/lightning/src/ln/offers_tests.rs index 12e631b4042..68a89ba6a91 100644 --- a/lightning/src/ln/offers_tests.rs +++ b/lightning/src/ln/offers_tests.rs @@ -42,31 +42,34 @@ //! Nodes without channels are disconnected and connected as needed to ensure that deterministic //! blinded paths are used. +use alloc::collections::BTreeMap; + use bitcoin::network::Network; use bitcoin::secp256k1::{PublicKey, Secp256k1}; use core::time::Duration; use crate::blinded_path::IntroductionNode; use crate::blinded_path::message::BlindedMessagePath; use crate::blinded_path::payment::{Bolt12OfferContext, Bolt12RefundContext, DummyTlvs, PaymentContext}; -use crate::blinded_path::message::OffersContext; +use crate::blinded_path::message::{MessageContext, OffersContext}; use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaidBolt12Invoice, PaymentFailureReason, PaymentPurpose}; use crate::ln::channelmanager::{PaymentId, RecentPaymentDetails, self}; use crate::ln::outbound_payment::{Bolt12PaymentError, RecipientOnionFields, Retry}; use crate::types::features::Bolt12InvoiceFeatures; use crate::ln::functional_test_utils::*; -use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, NodeAnnouncement, OnionMessage, OnionMessageHandler, RoutingMessageHandler, SocketAddress, UnsignedGossipMessage, UnsignedNodeAnnouncement}; +use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, OnionMessage, OnionMessageHandler}; use crate::ln::outbound_payment::IDEMPOTENCY_TIMEOUT_TICKS; use crate::offers::invoice::Bolt12Invoice; use crate::offers::invoice_error::InvoiceError; use crate::offers::invoice_request::{InvoiceRequest, InvoiceRequestFields, InvoiceRequestVerifiedFromOffer}; use crate::offers::nonce::Nonce; +use crate::offers::offer::OfferBuilder; use crate::offers::parse::Bolt12SemanticError; -use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH}; +use crate::offers::payer_proof::PayerProof; +use crate::onion_message::messenger::{DefaultMessageRouter, Destination, MessageRouter, MessageSendInstructions, NodeIdMessageRouter, NullMessageRouter, PeeledOnion, DUMMY_HOPS_PATH_LENGTH, QR_CODED_DUMMY_HOPS_PATH_LENGTH}; use crate::onion_message::offers::OffersMessage; -use crate::routing::gossip::{NodeAlias, NodeId}; use crate::routing::router::{DEFAULT_PAYMENT_DUMMY_HOPS, PaymentParameters, RouteParameters, RouteParametersConfig}; -use crate::sign::{NodeSigner, Recipient}; -use crate::util::ser::Writeable; +use crate::sign::NodeSigner; +use crate::util::ser::{MaybeReadable, Writeable}; /// This used to determine whether we built a compact path or not, but now its just a random /// constant we apply to blinded path expiry in these tests. @@ -75,15 +78,21 @@ const MAX_SHORT_LIVED_RELATIVE_EXPIRY: Duration = Duration::from_secs(60 * 60 * use crate::prelude::*; macro_rules! expect_recent_payment { - ($node: expr, $payment_state: path, $payment_id: expr) => { - match $node.node.list_recent_payments().first() { - Some(&$payment_state { payment_id: actual_payment_id, .. }) => { - assert_eq!($payment_id, actual_payment_id); - }, - Some(_) => panic!("Unexpected recent payment state"), - None => panic!("No recent payments"), + ($node: expr, $payment_state: path, $payment_id: expr) => {{ + let mut found_payment = false; + for payment in $node.node.list_recent_payments().iter() { + match payment { + $payment_state { payment_id: actual_payment_id, .. } => { + if $payment_id == *actual_payment_id { + found_payment = true; + break; + } + }, + _ => {}, + } } - } + assert!(found_payment); + }} } fn connect_peers<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>) { @@ -116,38 +125,6 @@ fn disconnect_peers<'a, 'b, 'c>(node_a: &Node<'a, 'b, 'c>, peers: &[&Node<'a, 'b } } -fn announce_node_address<'a, 'b, 'c>( - node: &Node<'a, 'b, 'c>, peers: &[&Node<'a, 'b, 'c>], address: SocketAddress, -) { - let features = node.onion_messenger.provided_node_features() - | node.gossip_sync.provided_node_features(); - let rgb = [0u8; 3]; - let announcement = UnsignedNodeAnnouncement { - features, - timestamp: 1000, - node_id: NodeId::from_pubkey(&node.keys_manager.get_node_id(Recipient::Node).unwrap()), - rgb, - alias: NodeAlias([0u8; 32]), - addresses: vec![address], - excess_address_data: Vec::new(), - excess_data: Vec::new(), - }; - let signature = node.keys_manager.sign_gossip_message( - UnsignedGossipMessage::NodeAnnouncement(&announcement) - ).unwrap(); - - let msg = NodeAnnouncement { - signature, - contents: announcement - }; - - let node_pubkey = node.node.get_our_node_id(); - node.gossip_sync.handle_node_announcement(None, &msg).unwrap(); - for peer in peers { - peer.gossip_sync.handle_node_announcement(Some(node_pubkey), &msg).unwrap(); - } -} - fn resolve_introduction_node<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, path: &BlindedMessagePath) -> PublicKey { path.public_introduction_node_id(&node.network_graph.read_only()) .and_then(|node_id| node_id.as_pubkey().ok()) @@ -250,7 +227,7 @@ fn claim_bolt12_payment_with_extra_fees<'a, 'b, 'c>( fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> Nonce { match node.onion_messenger.peel_onion_message(message) { - Ok(PeeledOnion::Offers(_, Some(OffersContext::InvoiceRequest { nonce }), _)) => nonce, + Ok(PeeledOnion::Offers(_, Some(OffersContext::InvoiceRequest { nonce, payment_metadata: _ }), _)) => nonce, Ok(PeeledOnion::Offers(_, context, _)) => panic!("Unexpected onion message context: {:?}", context), Ok(PeeledOnion::Forward(_, _)) => panic!("Unexpected onion message forward"), Ok(_) => panic!("Unexpected onion message"), @@ -258,6 +235,22 @@ fn extract_offer_nonce<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessa } } +/// Extract the payer's [`PaymentId`] from an invoice onion message received by the payer. +/// +/// When the payer receives an invoice through their reply path, the blinded path context carries +/// the [`PaymentId`] for the payment. The payer signing key needed to build a +/// [`PayerProof`](crate::offers::payer_proof::PayerProof) via +/// [`PaidBolt12Invoice::prove_payer_derived`] is re-derived from the invoice's own payer metadata. +fn extract_payer_context<'a, 'b, 'c>(node: &Node<'a, 'b, 'c>, message: &OnionMessage) -> PaymentId { + match node.onion_messenger.peel_onion_message(message) { + Ok(PeeledOnion::Offers(_, Some(OffersContext::OutboundPaymentForOffer { payment_id, .. }), _)) => payment_id, + Ok(PeeledOnion::Offers(_, context, _)) => panic!("Expected OutboundPaymentForOffer context, got: {:?}", context), + Ok(PeeledOnion::Forward(_, _)) => panic!("Unexpected onion message forward"), + Ok(_) => panic!("Unexpected onion message"), + Err(e) => panic!("Failed to process onion message {:?}", e), + } +} + pub(super) fn extract_invoice_request<'a, 'b, 'c>( node: &Node<'a, 'b, 'c>, message: &OnionMessage ) -> (InvoiceRequest, BlindedMessagePath) { @@ -353,126 +346,6 @@ fn create_refund_with_no_blinded_path() { assert!(refund.paths().is_empty()); } -/// Checks that blinded paths without Tor-only nodes are preferred when constructing an offer. -#[test] -fn prefers_non_tor_nodes_in_blinded_paths() { - let mut accept_forward_cfg = test_default_channel_config(); - accept_forward_cfg.accept_forwards_to_priv_channels = true; - - let mut features = channelmanager::provided_init_features(&accept_forward_cfg); - features.set_onion_messages_optional(); - features.set_route_blinding_optional(); - - let chanmon_cfgs = create_chanmon_cfgs(6); - let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); - - *node_cfgs[1].override_init_features.borrow_mut() = Some(features); - - let node_chanmgrs = create_node_chanmgrs( - 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] - ); - let nodes = create_network(6, &node_cfgs, &node_chanmgrs); - - create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); - - // Add an extra channel so that more than one of Bob's peers have MIN_PEER_CHANNELS. - create_announced_chan_between_nodes_with_value(&nodes, 4, 5, 10_000_000, 1_000_000_000); - - let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); - let bob_id = bob.node.get_our_node_id(); - let charlie_id = charlie.node.get_our_node_id(); - - disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); - disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - - let tor = SocketAddress::OnionV2([255, 254, 253, 252, 251, 250, 249, 248, 247, 246, 38, 7]); - announce_node_address(charlie, &[alice, bob, david, &nodes[4], &nodes[5]], tor.clone()); - - let offer = bob.node - .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); - assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); - assert!(!offer.paths().is_empty()); - for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_ne!(introduction_node_id, bob_id); - assert_ne!(introduction_node_id, charlie_id); - } - - // Use a one-hop blinded path when Bob is announced and all his peers are Tor-only. - announce_node_address(&nodes[4], &[alice, bob, charlie, david, &nodes[5]], tor.clone()); - announce_node_address(&nodes[5], &[alice, bob, charlie, david, &nodes[4]], tor.clone()); - - let offer = bob.node - .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); - assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); - assert!(!offer.paths().is_empty()); - for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_eq!(introduction_node_id, bob_id); - } -} - -/// Checks that blinded paths prefer an introduction node that is the most connected. -#[test] -fn prefers_more_connected_nodes_in_blinded_paths() { - let mut accept_forward_cfg = test_default_channel_config(); - accept_forward_cfg.accept_forwards_to_priv_channels = true; - - let mut features = channelmanager::provided_init_features(&accept_forward_cfg); - features.set_onion_messages_optional(); - features.set_route_blinding_optional(); - - let chanmon_cfgs = create_chanmon_cfgs(6); - let node_cfgs = create_node_cfgs(6, &chanmon_cfgs); - - *node_cfgs[1].override_init_features.borrow_mut() = Some(features); - - let node_chanmgrs = create_node_chanmgrs( - 6, &node_cfgs, &[None, Some(accept_forward_cfg), None, None, None, None] - ); - let nodes = create_network(6, &node_cfgs, &node_chanmgrs); - - create_unannounced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); - create_unannounced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 1, 5, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 2, 5, 10_000_000, 1_000_000_000); - - // Add extra channels so that more than one of Bob's peers have MIN_PEER_CHANNELS and one has - // more than the others. - create_announced_chan_between_nodes_with_value(&nodes, 0, 4, 10_000_000, 1_000_000_000); - create_announced_chan_between_nodes_with_value(&nodes, 3, 4, 10_000_000, 1_000_000_000); - - let (alice, bob, charlie, david) = (&nodes[0], &nodes[1], &nodes[2], &nodes[3]); - let bob_id = bob.node.get_our_node_id(); - - disconnect_peers(alice, &[charlie, david, &nodes[4], &nodes[5]]); - disconnect_peers(david, &[bob, &nodes[4], &nodes[5]]); - - let offer = bob.node - .create_offer_builder().unwrap() - .amount_msats(10_000_000) - .build().unwrap(); - assert_ne!(offer.issuer_signing_pubkey(), Some(bob_id)); - assert!(!offer.paths().is_empty()); - for path in offer.paths() { - let introduction_node_id = resolve_introduction_node(david, &path); - assert_eq!(introduction_node_id, nodes[4].node.get_our_node_id()); - } -} - /// Tests the dummy hop behavior of Offers based on the message router used: /// - Compact paths (`DefaultMessageRouter`) should not include dummy hops. /// - Node ID paths (`NodeIdMessageRouter`) may include 0 to [`MAX_DUMMY_HOPS_COUNT`] dummy hops. @@ -722,6 +595,7 @@ fn creates_and_pays_for_offer_using_two_hop_blinded_path() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), david_id); @@ -808,7 +682,7 @@ fn creates_and_pays_for_refund_using_two_hop_blinded_path() { } expect_recent_payment!(david, RecentPaymentDetails::AwaitingInvoice, payment_id); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); connect_peers(alice, charlie); @@ -880,6 +754,7 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); @@ -904,6 +779,158 @@ fn creates_and_pays_for_offer_using_one_hop_blinded_path() { expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); } +/// Checks that a `Router` can attach `payment_metadata` to the [`PaymentContext`] of a blinded +/// payment path while building it in response to an invoice request, and that the metadata is +/// surfaced back via [`Event::PaymentClaimable`] when the payment is received. +#[test] +fn router_modifies_payment_metadata_in_blinded_path() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + // Configure Alice's router to inject `payment_metadata` into the `PaymentContext` of the + // `ReceiveTlvs` it builds blinded payment paths from. This simulates a recipient-side router + // that ties extra recipient data (e.g. an order ID) to the blinded path created in response to + // an inbound invoice request. + let mut expected_metadata = BTreeMap::new(); + expected_metadata.insert(0u64, vec![1, 2, 3, 4]); + expected_metadata.insert(7u64, vec![0xab, 0xcd]); + alice.router.set_next_payment_context_metadata(expected_metadata.clone()); + + let offer = alice.node + .create_offer_builder().unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + // Bob -> Alice: invoice_request. When Alice handles it, her flow asks the router for blinded + // payment paths; the router applies the configured metadata override before the path is built + // and embedded in the invoice. + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + + // Alice -> Bob: invoice (carrying the blinded path with the modified payment_context). + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let (invoice, _) = extract_invoice(bob, &onion_message); + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + human_readable_name: None, + }, + payment_metadata: Some(expected_metadata), + }); + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + // Verifies that Alice's `Event::PaymentClaimable` carries the `payment_metadata` injected by + // the router (via the `expected_payment_context` equality check inside this helper). + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); +} + +/// Checks that `payment_metadata` set in the [`OffersContext::InvoiceRequest`] of an offer's +/// blinded message path is propagated to the [`Bolt12OfferContext`] in the resulting invoice's +/// blinded payment paths and surfaced via [`Event::PaymentClaimable`] when the payment is received. +#[test] +fn pays_for_offer_with_payment_metadata_in_invoice_request_context() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; + let bob_id = bob.node.get_our_node_id(); + + // Manually build an offer whose blinded message path carries `payment_metadata` in its + // `OffersContext::InvoiceRequest` context. The HEAD commit causes Alice's `ChannelManager` to + // copy this metadata onto the `Bolt12OfferContext` when she handles the inbound invoice + // request, embedding it in the invoice's blinded payment paths. + let mut expected_metadata = BTreeMap::new(); + expected_metadata.insert(0u64, vec![1, 2, 3, 4]); + expected_metadata.insert(7u64, vec![0xab, 0xcd]); + + let secp_ctx = Secp256k1::new(); + let nonce = Nonce::from_entropy_source(alice.keys_manager); + let context = MessageContext::Offers(OffersContext::InvoiceRequest { + nonce, + payment_metadata: Some(expected_metadata.clone()), + }); + let paths = alice.message_router.create_blinded_paths( + alice_id, + alice.keys_manager.get_receive_auth_key(), + context, + alice.node.test_get_peers_for_blinded_path(), + &secp_ctx, + ).unwrap(); + assert!(!paths.is_empty()); + + let expanded_key = alice.keys_manager.get_expanded_key(); + let mut builder = OfferBuilder::deriving_signing_pubkey(alice_id, &expanded_key, nonce, &secp_ctx) + .chain(Network::Testnet) + .amount_msats(10_000_000); + for path in paths { + builder = builder.path(path); + } + let offer = builder.build().unwrap(); + + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let (invoice, _) = extract_invoice(bob, &onion_message); + + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + human_readable_name: None, + }, + payment_metadata: Some(expected_metadata), + }); + + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + // `claim_bolt12_payment` asserts the surfaced `PaymentContext` matches `payment_context` + // above, including the embedded `payment_metadata`. + claim_bolt12_payment(bob, &[alice], payment_context, &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); +} + /// Checks that a refund can be paid through a one-hop blinded path and that ephemeral pubkeys are /// used rather than exposing a node's pubkey. However, the node's pubkey is still used as the /// introduction node of the blinded path. @@ -936,7 +963,7 @@ fn creates_and_pays_for_refund_using_one_hop_blinded_path() { } expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1001,6 +1028,7 @@ fn pays_for_offer_without_blinded_paths() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1041,7 +1069,7 @@ fn pays_for_refund_without_blinded_paths() { assert!(refund.paths().is_empty()); expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let expected_invoice = alice.node.request_refund_payment(&refund).unwrap(); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1269,6 +1297,7 @@ fn creates_and_pays_for_offer_with_retry() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_eq!(invoice_request.amount_msats(), Some(10_000_000)); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); @@ -1334,6 +1363,7 @@ fn pays_bolt12_invoice_asynchronously() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); @@ -1431,6 +1461,7 @@ fn creates_offer_with_blinded_path_using_unannounced_introduction_node() { payer_note_truncated: None, human_readable_name: None, }, + payment_metadata: None, }); assert_ne!(invoice_request.payer_signing_pubkey(), bob_id); assert_eq!(reply_path.introduction_node(), &IntroductionNode::NodeId(alice_id)); @@ -2274,7 +2305,7 @@ fn fails_paying_invoice_more_than_once() { david.onion_messenger.handle_onion_message(charlie_id, &onion_message); // David initiates paying the first invoice - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata: None }); let (invoice1, _) = extract_invoice(david, &onion_message); route_bolt12_payment(david, &[charlie, bob, alice], &invoice1); @@ -2463,7 +2494,7 @@ fn rejects_keysend_to_non_static_invoice_path() { let route_params = RouteParameters::from_payment_params_and_value(pay_params, amt_msat); let keysend_payment_id = PaymentId([2; 32]); let payment_hash = nodes[0].node.send_spontaneous_payment( - Some(payment_preimage), RecipientOnionFields::spontaneous_empty(), keysend_payment_id, + Some(payment_preimage), RecipientOnionFields::spontaneous_empty(amt_msat), keysend_payment_id, route_params, Retry::Attempts(0) ).unwrap(); check_added_monitors(&nodes[0], 1); @@ -2572,3 +2603,231 @@ fn no_double_pay_with_stale_channelmanager() { // generated in response to the duplicate invoice. assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); } + +#[test] +fn creates_and_pays_for_phantom_offer() { + // Tests that we can pay a "phantom offer" to any participating node. + let mut chanmon_cfgs = create_chanmon_cfgs(1); + chanmon_cfgs.append(&mut create_phantom_chanmon_cfgs(2)); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 10_000_000, 1_000_000_000); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + + let offer = nodes[1].node + .create_phantom_offer_builder(vec![(node_c_id, nodes[2].node.list_channels())], 2) + .unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + // The offer should be resolvable by either of node B or C but signed by a derived key + assert!(offer.issuer_signing_pubkey().is_some()); + assert_ne!(offer.issuer_signing_pubkey(), Some(node_b_id)); + assert_ne!(offer.issuer_signing_pubkey(), Some(node_c_id)); + assert_eq!(offer.paths().len(), 2); + let mut b_path_count = 0; + let mut c_path_count = 0; + for path in offer.paths() { + if check_compact_path_introduction_node(&path, &nodes[0], node_b_id) { + b_path_count += 1; + } + if check_compact_path_introduction_node(&path, &nodes[0], node_c_id) { + c_path_count += 1; + } + } + assert_eq!(b_path_count, 1); + assert_eq!(c_path_count, 1); + + // Pay twice, first via node B (the node that actually built the offer) then pay via node C + // (which won't have seen the offer until it receives the invoice_request). + for (payment_id, recipient) in [([1; 32], &nodes[1]), ([2; 32], &nodes[2])] { + let payment_id = PaymentId(payment_id); + nodes[0].node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(nodes[0], RecentPaymentDetails::AwaitingInvoice, payment_id); + + let recipient_id = recipient.node.get_our_node_id(); + let non_recipient_id = if node_b_id == recipient_id { + node_c_id + } else { + node_b_id + }; + + let onion_message = + nodes[0].onion_messenger.next_onion_message_for_peer(recipient_id).unwrap(); + let _discard = + nodes[0].onion_messenger.next_onion_message_for_peer(non_recipient_id).unwrap(); + recipient.onion_messenger.handle_onion_message(node_a_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(&recipient, &onion_message); + let payment_context = PaymentContext::Bolt12Offer(Bolt12OfferContext { + offer_id: offer.id(), + invoice_request: InvoiceRequestFields { + payer_signing_pubkey: invoice_request.payer_signing_pubkey(), + quantity: None, + payer_note_truncated: None, + human_readable_name: None, + }, + payment_metadata: None, + }); + + let onion_message = + recipient.onion_messenger.next_onion_message_for_peer(node_a_id).unwrap(); + nodes[0].onion_messenger.handle_onion_message(recipient_id, &onion_message); + + let (invoice, _) = extract_invoice(&nodes[0], &onion_message); + assert_eq!(invoice.amount_msats(), 10_000_000); + + route_bolt12_payment(&nodes[0], &[recipient], &invoice); + expect_recent_payment!(&nodes[0], RecentPaymentDetails::Pending, payment_id); + + claim_bolt12_payment(&nodes[0], &[recipient], payment_context, &invoice); + expect_recent_payment!(&nodes[0], RecentPaymentDetails::Fulfilled, payment_id); + + assert!(nodes[0].onion_messenger.next_onion_message_for_peer(node_b_id).is_none()); + assert!(nodes[0].onion_messenger.next_onion_message_for_peer(node_c_id).is_none()); + } +} + +/// Tests the full payer proof lifecycle: offer -> invoice_request -> invoice -> payment -> +/// proof creation with derived key signing -> verification -> bech32 round-trip. +/// +/// This exercises the primary API path where a wallet pays a BOLT 12 offer and then creates +/// a payer proof using the derived signing key (same key derivation as the invoice request). +#[test] +fn creates_and_verifies_payer_proof_after_offer_payment() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 1_000_000_000); + + let alice = &nodes[0]; // recipient (offer creator) + let alice_id = alice.node.get_our_node_id(); + let bob = &nodes[1]; // payer + let bob_id = bob.node.get_our_node_id(); + + // Alice creates an offer + let offer = alice.node + .create_offer_builder().unwrap() + .amount_msats(10_000_000) + .build().unwrap(); + + // Bob initiates payment + let payment_id = PaymentId([1; 32]); + bob.node.pay_for_offer(&offer, None, payment_id, Default::default()).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::AwaitingInvoice, payment_id); + + // Bob sends invoice request to Alice + let onion_message = bob.onion_messenger.next_onion_message_for_peer(alice_id).unwrap(); + alice.onion_messenger.handle_onion_message(bob_id, &onion_message); + + let (invoice_request, _) = extract_invoice_request(alice, &onion_message); + + // Alice sends invoice back to Bob + let onion_message = alice.onion_messenger.next_onion_message_for_peer(bob_id).unwrap(); + bob.onion_messenger.handle_onion_message(alice_id, &onion_message); + + let (invoice, _) = extract_invoice(bob, &onion_message); + assert_eq!(invoice.amount_msats(), 10_000_000); + + // Extract the payment_id from Bob's reply path context. In a real wallet it would be + // persisted alongside the payment for later payer proof creation. + let context_payment_id = extract_payer_context(bob, &onion_message); + assert_eq!(context_payment_id, payment_id); + + // Route the payment + route_bolt12_payment(bob, &[alice], &invoice); + expect_recent_payment!(bob, RecentPaymentDetails::Pending, payment_id); + + // Get the payment preimage from Alice's PaymentClaimable event and claim it. + // In a real wallet, the payer receives the preimage via Event::PaymentSent after the + // recipient claims. For the test, we extract it from the recipient's claimable event. + let payment_preimage = match get_event!(alice, Event::PaymentClaimable) { + Event::PaymentClaimable { purpose, .. } => { + match &purpose { + PaymentPurpose::Bolt12OfferPayment { payment_context, .. } => { + assert_eq!(payment_context.offer_id, offer.id()); + assert_eq!( + payment_context.invoice_request.payer_signing_pubkey, + invoice_request.payer_signing_pubkey(), + ); + }, + _ => panic!("Expected Bolt12OfferPayment purpose"), + } + purpose.preimage().unwrap() + }, + _ => panic!("Expected Event::PaymentClaimable"), + }; + + let paid_invoice = claim_payment(bob, &[alice], payment_preimage).unwrap(); + expect_recent_payment!(bob, RecentPaymentDetails::Fulfilled, payment_id); + + // The paid invoice is carried so the payer can re-derive their signing key (from the invoice's + // own payer metadata) when building a payer proof. + assert!(paid_invoice.bolt12_invoice().is_some()); + + // Regression guard: the `Event::PaymentSent` container persists the paid invoice and reads it + // back. Round-tripping the event must preserve the invoice. + let payment_sent = Event::PaymentSent { + payment_id: Some(payment_id), + payment_preimage, + payment_hash: invoice.payment_hash(), + amount_msat: Some(10_000_000), + fee_paid_msat: None, + bolt12_invoice: Some(paid_invoice.clone()), + }; + let encoded = payment_sent.encode(); + let decoded = Event::read(&mut &encoded[..]).unwrap().unwrap(); + assert_eq!(decoded, payment_sent); + match decoded { + Event::PaymentSent { bolt12_invoice: Some(decoded_invoice), .. } => { + assert!(decoded_invoice.bolt12_invoice().is_some()); + }, + _ => panic!("expected a PaymentSent event carrying a paid invoice"), + } + + // --- Payer Proof Creation --- + // Bob (the payer) creates a proof-of-payment with selective disclosure, end to end from the + // invoice he actually paid. The negative paths (`PreimageMismatch`, `KeyDerivationFailed`) are + // covered by the unit tests in `offers::payer_proof::tests`. + let expanded_key = bob.keys_manager.get_expanded_key(); + let secp_ctx = Secp256k1::new(); + let payer_proof = paid_invoice.prove_payer_derived( + payment_preimage, &expanded_key, payment_id, &secp_ctx, + ).unwrap() + .include_offer_description() + .include_invoice_amount() + .include_invoice_created_at() + .build_and_sign() + .unwrap(); + + // The proof binds the payment Bob actually made. + assert_eq!(payer_proof.payment_preimage(), payment_preimage); + assert_eq!(payer_proof.payment_hash(), invoice.payment_hash()); + + // Parsing the bech32 string back re-runs verification (preimage, invoice and proof signatures), + // just as a third-party verifier would. + let encoded = payer_proof.to_string(); + let verified: PayerProof = encoded.parse().unwrap(); + assert_eq!(verified.bytes(), payer_proof.bytes()); + assert_eq!(verified.to_string(), encoded); + + // The verified proof binds the same payment and preserves every disclosed field. + assert_eq!(verified.payment_preimage(), payment_preimage); + assert_eq!(verified.payment_hash(), invoice.payment_hash()); + assert_eq!(verified.payer_signing_pubkey(), invoice_request.payer_signing_pubkey()); + assert_eq!(verified.issuer_signing_pubkey(), invoice.signing_pubkey()); + assert_eq!(verified.invoice_amount_msats(), Some(invoice.amount_msats())); + assert_eq!(verified.invoice_created_at(), Some(invoice.created_at())); + assert_eq!( + verified.offer_description().map(|desc| desc.to_string()), + offer.description().map(|desc| desc.to_string()), + ); +} diff --git a/lightning/src/ln/onion_payment.rs b/lightning/src/ln/onion_payment.rs index 555cc7a87af..08ebfe6b5bc 100644 --- a/lightning/src/ln/onion_payment.rs +++ b/lightning/src/ln/onion_payment.rs @@ -66,7 +66,7 @@ fn check_blinded_forward( let outgoing_cltv_value = inbound_cltv_expiry.checked_sub( payment_relay.cltv_expiry_delta as u32 ).ok_or(())?; - check_blinded_payment_constraints(inbound_amt_msat, outgoing_cltv_value, payment_constraints)?; + check_blinded_payment_constraints(inbound_amt_msat, inbound_cltv_expiry, payment_constraints)?; if features.requires_unknown_bits_from(&BlindedHopFeatures::empty()) { return Err(()) } Ok((amt_to_forward, outgoing_cltv_value)) @@ -111,6 +111,9 @@ enum RoutingInfo { next_hop_hmac: [u8; 32], shared_secret: SharedSecret, current_path_key: Option<PublicKey>, + incoming_multipath_data: Option<msgs::FinalOnionHopData>, + next_trampoline_amt_msat: u64, + next_trampoline_cltv: u32, }, } @@ -167,24 +170,31 @@ pub(super) fn create_fwd_pending_htlc_info( reason: LocalHTLCFailureReason::InvalidOnionPayload, err_data: Vec::new(), }), - onion_utils::Hop::TrampolineForward { next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { + onion_utils::Hop::TrampolineForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { ( RoutingInfo::Trampoline { next_trampoline: next_trampoline_hop_data.next_trampoline, new_packet_bytes: new_trampoline_packet_bytes, next_hop_hmac: next_trampoline_hop_hmac, shared_secret: trampoline_shared_secret, - current_path_key: None + current_path_key: None, + incoming_multipath_data: outer_hop_data.multipath_trampoline_data, + next_trampoline_amt_msat: next_trampoline_hop_data.amt_to_forward, + next_trampoline_cltv: next_trampoline_hop_data.outgoing_cltv_value, }, - next_trampoline_hop_data.amt_to_forward, - next_trampoline_hop_data.outgoing_cltv_value, + outer_hop_data.amt_to_forward, + outer_hop_data.outgoing_cltv_value, None, None ) }, onion_utils::Hop::TrampolineBlindedForward { outer_hop_data, next_trampoline_hop_data, next_trampoline_hop_hmac, new_trampoline_packet_bytes, trampoline_shared_secret, .. } => { - let (amt_to_forward, outgoing_cltv_value) = check_blinded_forward( - msg.amount_msat, msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features + // The blinded path's payment_relay and payment_constraints apply to the aggregate + // amount that the trampoline node will forward onward, not the individual amount that + // arrives in a single (incoming MPP) HTLC. We used the desired total amount to + // calculate our outbound values. + let (next_hop_amount, next_hop_cltv) = check_blinded_forward( + outer_hop_data.multipath_trampoline_data.as_ref().map(|f| f.total_msat).unwrap_or(msg.amount_msat), msg.cltv_expiry, &next_trampoline_hop_data.payment_relay, &next_trampoline_hop_data.payment_constraints, &next_trampoline_hop_data.features ).map_err(|()| { // We should be returning malformed here if `msg.blinding_point` is set, but this is // unreachable right now since we checked it in `decode_update_add_htlc_onion`. @@ -200,10 +210,13 @@ pub(super) fn create_fwd_pending_htlc_info( new_packet_bytes: new_trampoline_packet_bytes, next_hop_hmac: next_trampoline_hop_hmac, shared_secret: trampoline_shared_secret, - current_path_key: outer_hop_data.current_path_key + current_path_key: outer_hop_data.current_path_key, + incoming_multipath_data: outer_hop_data.multipath_trampoline_data, + next_trampoline_amt_msat: next_hop_amount, + next_trampoline_cltv: next_hop_cltv, }, - amt_to_forward, - outgoing_cltv_value, + outer_hop_data.amt_to_forward, + outer_hop_data.outgoing_cltv_value, next_trampoline_hop_data.intro_node_blinding_point, next_trampoline_hop_data.next_blinding_override ) @@ -233,7 +246,7 @@ pub(super) fn create_fwd_pending_htlc_info( }), } } - RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key } => { + RoutingInfo::Trampoline { next_trampoline, new_packet_bytes, next_hop_hmac, shared_secret, current_path_key, incoming_multipath_data, next_trampoline_amt_msat, next_trampoline_cltv } => { let next_trampoline_packet_pubkey = match next_packet_pubkey_opt { Some(Ok(pubkey)) => pubkey, _ => return Err(InboundHTLCErr { @@ -249,7 +262,7 @@ pub(super) fn create_fwd_pending_htlc_info( hmac: next_hop_hmac, }; PendingHTLCRouting::TrampolineForward { - incoming_shared_secret: shared_secret.secret_bytes(), + trampoline_shared_secret: shared_secret.secret_bytes(), onion_packet: outgoing_packet, node_id: next_trampoline, incoming_cltv_expiry: msg.cltv_expiry, @@ -260,7 +273,11 @@ pub(super) fn create_fwd_pending_htlc_info( failure: intro_node_blinding_point .map(|_| BlindedFailure::FromIntroductionNode) .unwrap_or(BlindedFailure::FromBlindedNode), - }) + }), + incoming_multipath_data, + next_trampoline_amt_msat, + next_trampoline_cltv_expiry: next_trampoline_cltv, + } } }; @@ -438,7 +455,7 @@ pub(super) fn create_recv_pending_htlc_info( payment_data, payment_preimage, payment_metadata, - incoming_cltv_expiry: onion_cltv_expiry, + incoming_cltv_expiry: cltv_expiry, custom_tlvs, requires_blinded_error, has_recipient_created_payment_secret, @@ -450,7 +467,7 @@ pub(super) fn create_recv_pending_htlc_info( payment_data: data, payment_metadata, payment_context, - incoming_cltv_expiry: onion_cltv_expiry, + incoming_cltv_expiry: cltv_expiry, phantom_shared_secret, trampoline_shared_secret, custom_tlvs, @@ -515,7 +532,7 @@ pub fn peel_payment_onion<NS: NodeSigner, L: Logger, T: secp256k1::Verification> }; if let Err(reason) = check_incoming_htlc_cltv( - cur_height, outgoing_cltv_value, msg.cltv_expiry, + cur_height, outgoing_cltv_value, msg.cltv_expiry, MIN_CLTV_EXPIRY_DELTA, ) { return Err(InboundHTLCErr { msg: "incoming cltv check failed", @@ -630,7 +647,7 @@ pub(super) fn decode_incoming_update_add_htlc_onion<NS: NodeSigner, L: Logger, T let next_hop = match onion_utils::decode_next_payment_hop( Recipient::Node, &msg.onion_routing_packet.public_key.unwrap(), &msg.onion_routing_packet.hop_data[..], msg.onion_routing_packet.hmac, - msg.payment_hash, msg.blinding_point, node_signer + Some(msg.payment_hash), msg.blinding_point, node_signer ) { Ok(res) => res, Err(onion_utils::OnionDecodeErr::Malformed { err_msg, reason }) => { @@ -683,33 +700,24 @@ pub(super) fn decode_incoming_update_add_htlc_onion<NS: NodeSigner, L: Logger, T Some(NextPacketDetails { next_packet_pubkey, outgoing_connector: HopConnector::Dummy, outgoing_amt_msat: amt_to_forward, outgoing_cltv_value }) } - onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { amt_to_forward, outgoing_cltv_value, next_trampoline }, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { + onion_utils::Hop::TrampolineForward { next_trampoline_hop_data: msgs::InboundTrampolineForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { next_packet_pubkey: next_trampoline_packet_pubkey, outgoing_connector: HopConnector::Trampoline(next_trampoline), - outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value, + outgoing_amt_msat: outer_hop_data.amt_to_forward, + outgoing_cltv_value: outer_hop_data.outgoing_cltv_value, }) } - onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, ref payment_relay, ref payment_constraints, ref features, .. }, outer_shared_secret, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { - let (amt_to_forward, outgoing_cltv_value) = match check_blinded_forward( - msg.amount_msat, msg.cltv_expiry, &payment_relay, &payment_constraints, &features - ) { - Ok((amt, cltv)) => (amt, cltv), - Err(()) => { - return encode_relay_error("Underflow calculating outbound amount or cltv value for blinded trampoline forward", - LocalHTLCFailureReason::InvalidOnionBlinding, outer_shared_secret.secret_bytes(), Some(trampoline_shared_secret.secret_bytes()), &[0; 32]); - } - }; + onion_utils::Hop::TrampolineBlindedForward { next_trampoline_hop_data: msgs::InboundTrampolineBlindedForwardPayload { next_trampoline, .. }, ref outer_hop_data, trampoline_shared_secret, incoming_trampoline_public_key, .. } => { let next_trampoline_packet_pubkey = onion_utils::next_hop_pubkey(secp_ctx, incoming_trampoline_public_key, &trampoline_shared_secret.secret_bytes()); Some(NextPacketDetails { next_packet_pubkey: next_trampoline_packet_pubkey, outgoing_connector: HopConnector::Trampoline(next_trampoline), - outgoing_amt_msat: amt_to_forward, - outgoing_cltv_value, + outgoing_amt_msat: outer_hop_data.amt_to_forward, + outgoing_cltv_value: outer_hop_data.outgoing_cltv_value, }) } _ => None @@ -719,9 +727,9 @@ pub(super) fn decode_incoming_update_add_htlc_onion<NS: NodeSigner, L: Logger, T } pub(super) fn check_incoming_htlc_cltv( - cur_height: u32, outgoing_cltv_value: u32, cltv_expiry: u32, + cur_height: u32, outgoing_cltv_value: u32, cltv_expiry: u32, min_cltv_expiry_delta: u16, ) -> Result<(), LocalHTLCFailureReason> { - if (cltv_expiry as u64) < (outgoing_cltv_value) as u64 + MIN_CLTV_EXPIRY_DELTA as u64 { + if (cltv_expiry as u64) < (outgoing_cltv_value) as u64 + min_cltv_expiry_delta as u64 { return Err(LocalHTLCFailureReason::IncorrectCLTVExpiry); } // Theoretically, channel counterparty shouldn't send us a HTLC expiring now, @@ -779,7 +787,7 @@ mod tests { let charlie_pk = PublicKey::from_secret_key(&secp_ctx, &charlie.get_node_secret_key()); let ( - session_priv, total_amt_msat, cur_height, mut recipient_onion, keysend_preimage, payment_hash, + session_priv, _total_amt_msat, cur_height, mut recipient_onion, keysend_preimage, payment_hash, prng_seed, hops, .. ) = payment_onion_args(bob_pk, charlie_pk); @@ -788,8 +796,8 @@ mod tests { let path = Path { hops, blinded_tail: None, }; let onion_keys = super::onion_utils::construct_onion_keys(&secp_ctx, &path, &session_priv); - let (onion_payloads, ..) = super::onion_utils::build_onion_payloads( - &path, total_amt_msat, &recipient_onion, cur_height + 1, &Some(keysend_preimage), None, None + let (onion_payloads, ..) = super::onion_utils::test_build_onion_payloads( + &path, &recipient_onion, cur_height + 1, &Some(keysend_preimage), None, None ).unwrap(); assert!(super::onion_utils::construct_onion_packet( @@ -817,7 +825,7 @@ mod tests { }; let (onion, amount_msat, cltv_expiry) = create_payment_onion( - &secp_ctx, &path, &session_priv, total_amt_msat, &recipient_onion, + &secp_ctx, &path, &session_priv, &recipient_onion, cur_height, &payment_hash, &Some(preimage), None, prng_seed ).unwrap(); @@ -842,7 +850,7 @@ mod tests { PendingHTLCRouting::ReceiveKeysend { payment_preimage, payment_data, incoming_cltv_expiry, .. } => { assert_eq!(payment_preimage, preimage); assert_eq!(peeled2.outgoing_amt_msat, recipient_amount); - assert_eq!(incoming_cltv_expiry, peeled2.outgoing_cltv_value); + assert_eq!(incoming_cltv_expiry, msg.cltv_expiry); let msgs::FinalOnionHopData{total_msat, payment_secret} = payment_data.unwrap(); assert_eq!(total_msat, total_amt_msat); assert_eq!(payment_secret, pay_secret); @@ -879,7 +887,7 @@ mod tests { let total_amt_msat = 1000; let cur_height = 1000; let pay_secret = PaymentSecret([99; 32]); - let recipient_onion = RecipientOnionFields::secret_only(pay_secret); + let recipient_onion = RecipientOnionFields::secret_only(pay_secret, total_amt_msat); let preimage_bytes = [43; 32]; let preimage = PaymentPreimage(preimage_bytes); let rhash_bytes = Sha256::hash(&preimage_bytes).to_byte_array(); diff --git a/lightning/src/ln/onion_route_tests.rs b/lightning/src/ln/onion_route_tests.rs index 27e0cfafade..df5e98a62dd 100644 --- a/lightning/src/ln/onion_route_tests.rs +++ b/lightning/src/ln/onion_route_tests.rs @@ -25,7 +25,7 @@ use crate::ln::msgs::{ OutboundOnionPayload, OutboundTrampolinePayload, }; use crate::ln::onion_utils::{ - self, build_onion_payloads, construct_onion_keys, LocalHTLCFailureReason, + self, construct_onion_keys, test_build_onion_payloads, LocalHTLCFailureReason, }; use crate::ln::outbound_payment::RecipientOnionFields; use crate::ln::wire::Encode; @@ -128,7 +128,8 @@ fn run_onion_failure_test_with_fail_intercept<F1, F2, F3>( // 0 ~~> 2 send payment let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - let recipient_onion = RecipientOnionFields::secret_only(*payment_secret); + let recipient_onion = + RecipientOnionFields::secret_only(*payment_secret, route.get_total_amount()); nodes[0] .node .send_payment_with_route(route.clone(), *payment_hash, recipient_onion, payment_id) @@ -399,7 +400,7 @@ fn test_fee_failures() { // positive case let (route, payment_hash_success, payment_preimage_success, payment_secret_success) = get_route_and_payment_hash!(nodes[0], nodes[2], 40_000); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success, 40_000); let payment_id = PaymentId(payment_hash_success.0); nodes[0] .node @@ -418,7 +419,7 @@ fn test_fee_failures() { // If the hop gives fee_insufficient but enough fees were provided, then the previous hop // malleated the payment before forwarding, taking funds when they shouldn't have. However, // because we ignore channel update contents, we will still blame the 2nd channel. - let (_, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[2]); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], None, None); let short_channel_id = channels[1].0.contents.short_channel_id; run_onion_failure_test( "fee_insufficient", @@ -449,8 +450,8 @@ fn test_fee_failures() { } let (payment_preimage_success, payment_hash_success, payment_secret_success) = - get_payment_preimage_hash!(nodes[2]); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success); + get_payment_preimage_hash(&nodes[2], None, None); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret_success, 40_000); let payment_id = PaymentId(payment_hash_success.0); nodes[0] .node @@ -523,10 +524,10 @@ fn test_onion_failure() { let cur_height = nodes[0].best_block_info().1 + 1; let onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (mut onion_payloads, _htlc_msat, _htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) + test_build_onion_payloads(path, &recipient_fields, cur_height, &None, None, None) .unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { @@ -565,10 +566,10 @@ fn test_onion_failure() { let cur_height = nodes[0].best_block_info().1 + 1; let onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (mut onion_payloads, _htlc_msat, _htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) + test_build_onion_payloads(path, &recipient_fields, cur_height, &None, None, None) .unwrap(); let mut new_payloads = Vec::new(); for payload in onion_payloads.drain(..) { @@ -667,7 +668,7 @@ fn test_onion_failure() { Some(route.paths[0].hops[1].short_channel_id), None, ); - let (_, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[2]); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], None, None); // intermediate node failure run_onion_failure_test_with_fail_intercept( @@ -738,7 +739,7 @@ fn test_onion_failure() { Some(route.paths[0].hops[1].short_channel_id), None, ); - let (_, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[2]); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], None, None); // intermediate node failure run_onion_failure_test_with_fail_intercept( @@ -811,16 +812,19 @@ fn test_onion_failure() { Some(route.paths[0].hops[1].short_channel_id), None, ); - let (_, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[2]); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], None, None); // Our immediate peer sent UpdateFailMalformedHTLC because it couldn't understand the onion in - // the UpdateAddHTLC that we sent. + // the UpdateAddHTLC that we sent. These tests explicitly route via the real SCID (not the + // alias) so the expected_short_channel_id assertions below match. let short_channel_id = channels[0].0.contents.short_channel_id; + let mut route_via_real_scid = route.clone(); + route_via_real_scid.paths[0].hops[0].short_channel_id = short_channel_id; run_onion_failure_test( "invalid_onion_version", 0, &nodes, - &route, + &route_via_real_scid, &payment_hash, &payment_secret, |msg| { @@ -838,7 +842,7 @@ fn test_onion_failure() { "invalid_onion_hmac", 0, &nodes, - &route, + &route_via_real_scid, &payment_hash, &payment_secret, |msg| { @@ -856,7 +860,7 @@ fn test_onion_failure() { "invalid_onion_key", 0, &nodes, - &route, + &route_via_real_scid, &payment_hash, &payment_secret, |msg| { @@ -1142,7 +1146,7 @@ fn test_onion_failure() { None, None, ); - let (_, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[2]); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], None, None); run_onion_failure_test( "final_expiry_too_soon", @@ -1284,10 +1288,10 @@ fn test_onion_failure() { CLTV_FAR_FAR_AWAY + route.paths[0].hops[0].cltv_expiry_delta + 1; let onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (onion_payloads, _, htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, height, &None, None, None) + test_build_onion_payloads(path, &recipient_fields, height, &None, None, None) .unwrap(); let onion_packet = onion_utils::construct_onion_packet( onion_payloads, @@ -1542,7 +1546,7 @@ fn test_overshoot_final_cltv() { get_route_and_payment_hash!(nodes[0], nodes[2], 40000); let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, 40000); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, payment_id) @@ -1837,11 +1841,10 @@ fn test_always_create_tlv_format_onion_payloads() { assert!(!hops[1].node_features.supports_variable_length_onion()); let cur_height = nodes[0].best_block_info().1 + 1; - let recipient_fields = RecipientOnionFields::spontaneous_empty(); + let recipient_fields = RecipientOnionFields::spontaneous_empty(40000); let path = &route.paths[0]; let (onion_payloads, _htlc_msat, _htlc_cltv) = - build_onion_payloads(path, 40000, &recipient_fields, cur_height, &None, None, None) - .unwrap(); + test_build_onion_payloads(path, &recipient_fields, cur_height, &None, None, None).unwrap(); match onion_payloads[0] { msgs::OutboundOnionPayload::Forward { .. } => {}, @@ -1918,7 +1921,7 @@ fn test_trampoline_onion_payload_assembly_values() { short_channel_id: (572330 << 40) + (42 << 16) + 2821, channel_features: ChannelFeatures::empty(), fee_msat: 153_000, - cltv_expiry_delta: 0, + cltv_expiry_delta: 36 + 24, // Last hop should include the CLTV of the trampoline hops maybe_announced_channel: false, }, ], @@ -1973,19 +1976,16 @@ fn test_trampoline_onion_payload_assembly_values() { let payment_secret = PaymentSecret( SecretKey::from_slice(&<Vec<u8>>::from_hex(SECRET_HEX).unwrap()).unwrap().secret_bytes(), ); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = - onion_utils::build_trampoline_onion_payloads( - &path.blinded_tail.as_ref().unwrap(), - amt_msat, - &recipient_onion_fields, - cur_height, - &None, - ) - .unwrap(); + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); + let (trampoline_payloads, outer_total_msat) = onion_utils::build_trampoline_onion_payloads( + &path.blinded_tail.as_ref().unwrap(), + &recipient_onion_fields, + cur_height, + &None, + ) + .unwrap(); assert_eq!(trampoline_payloads.len(), 3); assert_eq!(outer_total_msat, 150_153_000); - assert_eq!(outer_starting_htlc_offset, 800_060); let trampoline_carol_payload = &trampoline_payloads[0]; let trampoline_dave_payload = &trampoline_payloads[1]; @@ -2038,11 +2038,12 @@ fn test_trampoline_onion_payload_assembly_values() { ) .unwrap(); - let (outer_payloads, total_msat, total_htlc_offset) = build_onion_payloads( + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, outer_total_msat); + let (outer_payloads, total_msat, total_htlc_offset) = test_build_onion_payloads( &path, - outer_total_msat, &recipient_onion_fields, - outer_starting_htlc_offset, + cur_height, &None, None, Some(trampoline_packet), @@ -2067,16 +2068,16 @@ fn test_trampoline_onion_payload_assembly_values() { outer_bob_payload { assert_eq!(amt_to_forward, &150_153_000); - assert_eq!(outgoing_cltv_value, &800_084); + assert_eq!(outgoing_cltv_value, &800_060); } else { panic!("Bob payload must be Forward"); } + let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret, amt_msat); let (_, total_msat_combined, total_htlc_offset_combined) = onion_utils::create_payment_onion( &Secp256k1::new(), &path, &session_priv, - amt_msat, &recipient_onion_fields, cur_height, &payment_hash, @@ -2280,7 +2281,7 @@ fn do_test_fail_htlc_backwards_with_reason(failure_code: FailureCode) { let payment_amount = 100_000; let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], payment_amount); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2426,11 +2427,11 @@ fn test_phantom_onion_hmac_failure() { // Get the route. let recv_value_msat = 10_000; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_value_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_value_msat), None); let (route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2496,13 +2497,13 @@ fn test_phantom_invalid_onion_payload() { // Get the route. let recv_value_msat = 10_000; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_value_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_value_msat), None); let (route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // We'll use the session priv later when constructing an invalid onion packet. let session_priv = [3; 32]; *nodes[0].keys_manager.override_random_bytes.lock().unwrap() = Some(session_priv); - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); let payment_id = PaymentId(payment_hash.0); nodes[0] .node @@ -2534,10 +2535,10 @@ fn test_phantom_invalid_onion_payload() { let session_priv = SecretKey::from_slice(&session_priv).unwrap(); let mut onion_keys = construct_onion_keys(&Secp256k1::new(), &route.paths[0], &session_priv); - let recipient_onion_fields = RecipientOnionFields::secret_only(payment_secret); - let (mut onion_payloads, _, _) = build_onion_payloads( + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, msgs::MAX_VALUE_MSAT + 1); + let (mut onion_payloads, _, _) = test_build_onion_payloads( &route.paths[0], - msgs::MAX_VALUE_MSAT + 1, &recipient_onion_fields, height + 1, &None, @@ -2598,11 +2599,11 @@ fn test_phantom_final_incorrect_cltv_expiry() { // Get the route. let recv_value_msat = 10_000; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_value_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_value_msat), None); let (route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2664,14 +2665,14 @@ fn test_phantom_failure_too_low_cltv() { // Get the route. let recv_value_msat = 10_000; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_value_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_value_msat), None); let (mut route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Modify the route to have a too-low cltv. route.paths[0].hops[1].cltv_expiry_delta = 5; // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2720,11 +2721,11 @@ fn test_phantom_failure_modified_cltv() { // Get the route. let recv_value_msat = 10_000; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_value_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_value_msat), None); let (mut route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2775,11 +2776,11 @@ fn test_phantom_failure_expires_too_soon() { // Get the route. let recv_value_msat = 10_000; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_value_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_value_msat), None); let (mut route, phantom_scid) = get_phantom_route!(nodes, recv_value_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_value_msat); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2825,11 +2826,12 @@ fn test_phantom_failure_too_low_recv_amt() { let recv_amt_msat = 10_000; let bad_recv_amt_msat = recv_amt_msat - 10; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_amt_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_amt_msat), None); let (mut route, phantom_scid) = get_phantom_route!(nodes, bad_recv_amt_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = + RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); nodes[0] .node .send_payment_with_route(route, payment_hash, recipient_onion, PaymentId(payment_hash.0)) @@ -2894,11 +2896,11 @@ fn do_test_phantom_dust_exposure_failure(multiplier_dust_limit: bool) { // Get the route with an amount exceeding the dust exposure threshold of nodes[1]. let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(max_dust_exposure + 1)); + get_payment_preimage_hash(&nodes[1], Some(max_dust_exposure + 1), None); let (mut route, phantom_scid) = get_phantom_route!(nodes, max_dust_exposure + 1, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, max_dust_exposure + 1); let payment_id = PaymentId(payment_hash.0); nodes[0] .node @@ -2944,11 +2946,11 @@ fn test_phantom_failure_reject_payment() { // Get the route with a too-low amount. let recv_amt_msat = 10_000; let (_, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[1], Some(recv_amt_msat)); + get_payment_preimage_hash(&nodes[1], Some(recv_amt_msat), None); let (mut route, phantom_scid) = get_phantom_route!(nodes, recv_amt_msat, channel); // Route the HTLC through to the destination. - let recipient_onion = RecipientOnionFields::secret_only(payment_secret); + let recipient_onion = RecipientOnionFields::secret_only(payment_secret, recv_amt_msat); let payment_id = PaymentId(payment_hash.0); nodes[0] .node diff --git a/lightning/src/ln/onion_utils.rs b/lightning/src/ln/onion_utils.rs index 605f27e9666..15e795a5c27 100644 --- a/lightning/src/ln/onion_utils.rs +++ b/lightning/src/ln/onion_utils.rs @@ -11,7 +11,6 @@ use super::msgs::OnionErrorPacket; use crate::blinded_path::BlindedHop; -use crate::crypto::chacha20::ChaCha20; use crate::crypto::streams::ChaChaReader; use crate::events::HTLCHandlingFailureReason; use crate::ln::channel::TOTAL_BITCOIN_SUPPLY_SATOSHIS; @@ -40,6 +39,8 @@ use bitcoin::secp256k1; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey}; +use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; + use crate::io::{Cursor, Read}; #[allow(unused_imports)] @@ -116,7 +117,7 @@ pub(super) fn gen_pad_from_shared_secret(shared_secret: &[u8]) -> [u8; 32] { } /// Calculates a pubkey for the next hop, such as the next hop's packet pubkey or blinding point. -pub(crate) fn next_hop_pubkey<T: secp256k1::Verification>( +pub fn next_hop_pubkey<T: secp256k1::Verification>( secp_ctx: &Secp256k1<T>, curr_pubkey: PublicKey, shared_secret: &[u8], ) -> Result<PublicKey, secp256k1::Error> { let blinding_factor = { @@ -193,7 +194,7 @@ trait OnionPayload<'a, 'b> { ) -> Self; fn new_receive( recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option<PaymentPreimage>, - sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + sender_intended_htlc_amt_msat: u64, cltv_expiry_height: u32, ) -> Result<Self::ReceiveType, APIError>; fn new_blinded_forward( encrypted_tlvs: &'a Vec<u8>, intro_node_blinding_point: Option<PublicKey>, @@ -205,8 +206,8 @@ trait OnionPayload<'a, 'b> { custom_tlvs: &'a Vec<(u64, Vec<u8>)>, ) -> Self; fn new_trampoline_entry( - total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, - recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, + packet: msgs::TrampolineOnionPacket, ) -> Result<Self::ReceiveType, APIError>; } impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { @@ -217,12 +218,15 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { } fn new_receive( recipient_onion: &'a RecipientOnionFields, keysend_preimage: Option<PaymentPreimage>, - sender_intended_htlc_amt_msat: u64, total_msat: u64, cltv_expiry_height: u32, + sender_intended_htlc_amt_msat: u64, cltv_expiry_height: u32, ) -> Result<Self::ReceiveType, APIError> { Ok(Self::Receive { - payment_data: recipient_onion - .payment_secret - .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + payment_data: recipient_onion.payment_secret.map(|payment_secret| { + msgs::FinalOnionHopData { + payment_secret, + total_msat: recipient_onion.total_mpp_amount_msat, + } + }), payment_metadata: recipient_onion.payment_metadata.as_ref(), keysend_preimage, custom_tlvs: &recipient_onion.custom_tlvs, @@ -254,15 +258,18 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundOnionPayload<'a> { } fn new_trampoline_entry( - total_msat: u64, amt_to_forward: u64, outgoing_cltv_value: u32, - recipient_onion: &'a RecipientOnionFields, packet: msgs::TrampolineOnionPacket, + amt_to_forward: u64, outgoing_cltv_value: u32, recipient_onion: &'a RecipientOnionFields, + packet: msgs::TrampolineOnionPacket, ) -> Result<Self, APIError> { Ok(Self::TrampolineEntrypoint { amt_to_forward, outgoing_cltv_value, - multipath_trampoline_data: recipient_onion - .payment_secret - .map(|payment_secret| msgs::FinalOnionHopData { payment_secret, total_msat }), + multipath_trampoline_data: recipient_onion.payment_secret.map(|payment_secret| { + msgs::FinalOnionHopData { + payment_secret, + total_msat: recipient_onion.total_mpp_amount_msat, + } + }), trampoline_packet: packet, }) } @@ -277,7 +284,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { } fn new_receive( _recipient_onion: &'a RecipientOnionFields, _keysend_preimage: Option<PaymentPreimage>, - _sender_intended_htlc_amt_msat: u64, _total_msat: u64, _cltv_expiry_height: u32, + _sender_intended_htlc_amt_msat: u64, _cltv_expiry_height: u32, ) -> Result<Self::ReceiveType, APIError> { Err(APIError::InvalidRoute { err: "Unblinded receiving is not supported for Trampoline!".to_string(), @@ -306,7 +313,7 @@ impl<'a, 'b> OnionPayload<'a, 'b> for msgs::OutboundTrampolinePayload<'a> { } fn new_trampoline_entry( - _total_msat: u64, _amt_to_forward: u64, _outgoing_cltv_value: u32, + _amt_to_forward: u64, _outgoing_cltv_value: u32, _recipient_onion: &'a RecipientOnionFields, _packet: msgs::TrampolineOnionPacket, ) -> Result<Self::ReceiveType, APIError> { Err(APIError::InvalidRoute { @@ -408,9 +415,9 @@ pub(super) fn construct_trampoline_onion_keys<T: secp256k1::Signing>( } pub(super) fn build_trampoline_onion_payloads<'a>( - blinded_tail: &'a BlindedTail, total_msat: u64, recipient_onion: &'a RecipientOnionFields, - starting_htlc_offset: u32, keysend_preimage: &Option<PaymentPreimage>, -) -> Result<(Vec<msgs::OutboundTrampolinePayload<'a>>, u64, u32), APIError> { + blinded_tail: &'a BlindedTail, recipient_onion: &'a RecipientOnionFields, + cur_block_height: u32, keysend_preimage: &Option<PaymentPreimage>, +) -> Result<(Vec<msgs::OutboundTrampolinePayload<'a>>, u64), APIError> { let mut res: Vec<msgs::OutboundTrampolinePayload> = Vec::with_capacity(blinded_tail.trampoline_hops.len() + blinded_tail.hops.len()); let blinded_tail_with_hop_iter = BlindedTailDetails::DirectEntry { @@ -420,12 +427,11 @@ pub(super) fn build_trampoline_onion_payloads<'a>( excess_final_cltv_expiry_delta: blinded_tail.excess_final_cltv_expiry_delta, }; - let (value_msat, cltv) = build_onion_payloads_callback( + let (value_msat, _) = build_onion_payloads_callback( blinded_tail.trampoline_hops.iter(), Some(blinded_tail_with_hop_iter), - total_msat, recipient_onion, - starting_htlc_offset, + cur_block_height, keysend_preimage, None, |action, payload| match action { @@ -433,14 +439,30 @@ pub(super) fn build_trampoline_onion_payloads<'a>( PayloadCallbackAction::PushFront => res.insert(0, payload), }, )?; - Ok((res, value_msat, cltv)) + Ok((res, value_msat)) } /// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. -pub(super) fn build_onion_payloads<'a>( - path: &'a Path, total_msat: u64, recipient_onion: &'a RecipientOnionFields, - starting_htlc_offset: u32, keysend_preimage: &Option<PaymentPreimage>, - invoice_request: Option<&'a InvoiceRequest>, +#[cfg(any(test, feature = "_externalize_tests"))] +pub(crate) fn test_build_onion_payloads<'a>( + path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, + keysend_preimage: &Option<PaymentPreimage>, invoice_request: Option<&'a InvoiceRequest>, + trampoline_packet: Option<msgs::TrampolineOnionPacket>, +) -> Result<(Vec<msgs::OutboundOnionPayload<'a>>, u64, u32), APIError> { + build_onion_payloads( + path, + recipient_onion, + cur_block_height, + keysend_preimage, + invoice_request, + trampoline_packet, + ) +} + +/// returns the hop data, as well as the first-hop value_msat and CLTV value we should send. +fn build_onion_payloads<'a>( + path: &'a Path, recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, + keysend_preimage: &Option<PaymentPreimage>, invoice_request: Option<&'a InvoiceRequest>, trampoline_packet: Option<msgs::TrampolineOnionPacket>, ) -> Result<(Vec<msgs::OutboundOnionPayload<'a>>, u64, u32), APIError> { let mut res: Vec<msgs::OutboundOnionPayload> = Vec::with_capacity( @@ -468,9 +490,8 @@ pub(super) fn build_onion_payloads<'a>( let (value_msat, cltv) = build_onion_payloads_callback( path.hops.iter(), blinded_tail_with_hop_iter, - total_msat, recipient_onion, - starting_htlc_offset, + cur_block_height, keysend_preimage, invoice_request, |action, payload| match action { @@ -499,8 +520,8 @@ enum PayloadCallbackAction { PushFront, } fn build_onion_payloads_callback<'a, 'b, H, B, F, OP>( - hops: H, mut blinded_tail: Option<BlindedTailDetails<'a, B>>, total_msat: u64, - recipient_onion: &'a RecipientOnionFields, starting_htlc_offset: u32, + hops: H, mut blinded_tail: Option<BlindedTailDetails<'a, B>>, + recipient_onion: &'a RecipientOnionFields, cur_block_height: u32, keysend_preimage: &Option<PaymentPreimage>, invoice_request: Option<&'a InvoiceRequest>, mut callback: F, ) -> Result<(u64, u32), APIError> @@ -511,7 +532,7 @@ where OP: OnionPayload<'a, 'b, ReceiveType = OP>, { let mut cur_value_msat = 0u64; - let mut cur_cltv = starting_htlc_offset; + let mut cur_cltv = cur_block_height; let mut last_hop_id = None; for (idx, hop) in hops.rev().enumerate() { @@ -519,12 +540,8 @@ where // exactly as it should be (and the next hop isn't trying to probe to find out if we're // the intended recipient). let value_msat = if cur_value_msat == 0 { hop.fee_msat() } else { cur_value_msat }; - let cltv = if cur_cltv == starting_htlc_offset { - hop.cltv_expiry_delta().saturating_add(starting_htlc_offset) - } else { - cur_cltv - }; if idx == 0 { + let declared_incoming_cltv = hop.cltv_expiry_delta().saturating_add(cur_cltv); match blinded_tail.take() { Some(BlindedTailDetails::DirectEntry { blinding_point, @@ -542,8 +559,8 @@ where PayloadCallbackAction::PushBack, OP::new_blinded_receive( final_value_msat, - total_msat, - cur_cltv + excess_final_cltv_expiry_delta, + recipient_onion.total_mpp_amount_msat, + cur_block_height + excess_final_cltv_expiry_delta, &blinded_hop.encrypted_payload, blinding_point.take(), *keysend_preimage, @@ -570,9 +587,8 @@ where callback( PayloadCallbackAction::PushBack, OP::new_trampoline_entry( - total_msat, final_value_msat + hop.fee_msat(), - cur_cltv, + declared_incoming_cltv, &recipient_onion, trampoline_packet, )?, @@ -585,8 +601,7 @@ where &recipient_onion, *keysend_preimage, value_msat, - total_msat, - cltv, + declared_incoming_cltv, )?, ); }, @@ -597,7 +612,7 @@ where err: "Next hop ID must be known for non-final hops".to_string(), })?, value_msat, - cltv, + cur_cltv, ); callback(PayloadCallbackAction::PushFront, payload); } @@ -661,11 +676,14 @@ pub(crate) fn set_max_path_length( maybe_announced_channel: false, }; let mut num_reserved_bytes: usize = 0; + // TODO: Find a way to avoid `clone`ing the whole recipient onion without re-adding the + // explicit amount parameter to build_onion_payloads_callback. + let mut recipient_onion_with_excess_value = recipient_onion.clone(); + recipient_onion_with_excess_value.total_mpp_amount_msat = final_value_msat_with_overpay_buffer; let build_payloads_res = build_onion_payloads_callback( core::iter::once(&unblinded_route_hop), blinded_tail_opt, - final_value_msat_with_overpay_buffer, - &recipient_onion, + &recipient_onion_with_excess_value, best_block_height, &keysend_preimage, invoice_request, @@ -708,8 +726,8 @@ pub(super) fn construct_onion_packet( ) -> Result<msgs::OnionPacket, ()> { let mut packet_data = [0; ONION_DATA_LEN]; - let mut chacha = ChaCha20::new(&prng_seed, &[0; 8]); - chacha.process(&[0; ONION_DATA_LEN], &mut packet_data); + let mut chacha = ChaCha20::new(Key::new(prng_seed), Nonce::new([0; 12]), 0); + chacha.apply_keystream(&mut packet_data); debug_assert_eq!(payloads.len(), onion_keys.len(), "Payloads and keys must have equal lengths"); @@ -746,8 +764,8 @@ pub(super) fn construct_trampoline_onion_packet( } let mut packet_data = vec![0u8; packet_length]; - let mut chacha = ChaCha20::new(&prng_seed, &[0; 8]); - chacha.process_in_place(&mut packet_data); + let mut chacha = ChaCha20::new(Key::new(prng_seed), Nonce::new([0; 12]), 0); + chacha.apply_keystream(&mut packet_data); construct_onion_packet_with_init_noise::<_, _>( payloads, @@ -766,8 +784,8 @@ pub(super) fn construct_onion_packet_with_writable_hopdata<HD: Writeable>( ) -> Result<msgs::OnionPacket, ()> { let mut packet_data = [0; ONION_DATA_LEN]; - let mut chacha = ChaCha20::new(&prng_seed, &[0; 8]); - chacha.process(&[0; ONION_DATA_LEN], &mut packet_data); + let mut chacha = ChaCha20::new(Key::new(prng_seed), Nonce::new([0; 12]), 0); + chacha.apply_keystream(&mut packet_data); let packet = FixedSizeOnionPacket(packet_data); construct_onion_packet_with_init_noise::<_, _>( @@ -805,8 +823,8 @@ pub(crate) fn construct_onion_message_packet<HD: Writeable, P: Packet<Data = Vec ) -> Result<P, ()> { let mut packet_data = vec![0; packet_data_len]; - let mut chacha = ChaCha20::new(&prng_seed, &[0; 8]); - chacha.process_in_place(&mut packet_data); + let mut chacha = ChaCha20::new(Key::new(prng_seed), Nonce::new([0; 12]), 0); + chacha.apply_keystream(&mut packet_data); construct_onion_packet_with_init_noise::<_, _>(payloads, onion_keys, packet_data, None) } @@ -815,6 +833,10 @@ fn construct_onion_packet_with_init_noise<HD: Writeable, P: Packet>( mut payloads: Vec<HD>, onion_keys: Vec<OnionKeys>, mut packet_data: P::Data, associated_data: Option<&PaymentHash>, ) -> Result<P, ()> { + if payloads.is_empty() { + return Err(()); + } + let filler = { let packet_data = packet_data.as_mut(); const ONION_HOP_DATA_LEN: usize = 65; // We may decrease this eventually after TLV is common @@ -822,12 +844,9 @@ fn construct_onion_packet_with_init_noise<HD: Writeable, P: Packet>( let mut pos = 0; for (i, (payload, keys)) in payloads.iter().zip(onion_keys.iter()).enumerate() { - let mut chacha = ChaCha20::new(&keys.rho, &[0u8; 8]); - // TODO: Batch this. - for _ in 0..(packet_data.len() - pos) { - let mut dummy = [0; 1]; - chacha.process_in_place(&mut dummy); // We don't have a seek function :( - } + // Seek to the position in the keystream where we want to start encrypting + let seek_pos = (packet_data.len() - pos) as u32; + let mut chacha = ChaCha20::new(Key::new(keys.rho), Nonce::new([0; 12]), seek_pos); let mut payload_len = LengthCalculatingWriter(0); payload.write(&mut payload_len).expect("Failed to calculate length"); @@ -841,7 +860,7 @@ fn construct_onion_packet_with_init_noise<HD: Writeable, P: Packet>( } res.resize(pos, 0u8); - chacha.process_in_place(&mut res); + chacha.apply_keystream(&mut res); } res }; @@ -856,8 +875,8 @@ fn construct_onion_packet_with_init_noise<HD: Writeable, P: Packet>( packet_data[0..payload_len.0].copy_from_slice(&payload.encode()[..]); packet_data[payload_len.0..(payload_len.0 + 32)].copy_from_slice(&hmac_res); - let mut chacha = ChaCha20::new(&keys.rho, &[0u8; 8]); - chacha.process_in_place(packet_data); + let mut chacha = ChaCha20::new(Key::new(keys.rho), Nonce::new([0; 12]), 0); + chacha.apply_keystream(packet_data); if i == 0 { let stop_index = packet_data.len(); @@ -879,8 +898,8 @@ fn construct_onion_packet_with_init_noise<HD: Writeable, P: Packet>( /// Encrypts/decrypts a failure packet. fn crypt_failure_packet(shared_secret: &[u8], packet: &mut OnionErrorPacket) { let ammag = gen_ammag_from_shared_secret(&shared_secret); - let mut chacha = ChaCha20::new(&ammag, &[0u8; 8]); - chacha.process_in_place(&mut packet.data); + let mut chacha = ChaCha20::new(Key::new(ammag), Nonce::new([0; 12]), 0); + chacha.apply_keystream(&mut packet.data); if let Some(ref mut attribution_data) = packet.attribution_data { attribution_data.crypt(shared_secret); @@ -1941,7 +1960,7 @@ impl Readable for HTLCFailReason { } } -impl_writeable_tlv_based_enum!(HTLCFailReasonRepr, +impl_ser_tlv_based_enum!(HTLCFailReasonRepr, (0, LightningError) => { (0, data, (legacy, Vec<u8>, |_| Ok(()), |us| if let &HTLCFailReasonRepr::LightningError { err: msgs::OnionErrorPacket { ref data, .. }, .. } = us { @@ -2107,6 +2126,10 @@ impl HTLCFailReason { let mut err = err.clone(); let hold_time = hold_time.unwrap_or(0); + if let Some(secondary_shared_secret) = secondary_shared_secret { + process_failure_packet(&mut err, secondary_shared_secret, hold_time); + crypt_failure_packet(secondary_shared_secret, &mut err); + } process_failure_packet(&mut err, incoming_packet_shared_secret, hold_time); crypt_failure_packet(incoming_packet_shared_secret, &mut err); @@ -2118,33 +2141,44 @@ impl HTLCFailReason { pub(super) fn decode_onion_failure<T: secp256k1::Signing, L: Logger>( &self, secp_ctx: &Secp256k1<T>, logger: &L, htlc_source: &HTLCSource, ) -> DecodedOnionFailure { + let decoded_onion_failure = |short_channel_id: Option<u64>, + _failure_reason: LocalHTLCFailureReason, + _data: &[u8]| { + DecodedOnionFailure { + network_update: None, + payment_failed_permanently: false, + short_channel_id, + failed_within_blinded_path: false, + hold_times: Vec::new(), + #[cfg(any(test, feature = "_test_utils"))] + onion_error_code: Some(_failure_reason), + #[cfg(any(test, feature = "_test_utils"))] + onion_error_data: Some(_data.to_vec()), + #[cfg(test)] + attribution_failed_channel: None, + } + }; match self.0 { HTLCFailReasonRepr::LightningError { ref err, .. } => { process_onion_failure(secp_ctx, logger, &htlc_source, err.clone()) }, - #[allow(unused)] HTLCFailReasonRepr::Reason { ref data, ref failure_reason } => { // we get a fail_malformed_htlc from the first hop // TODO: We'd like to generate a NetworkUpdate for temporary // failures here, but that would be insufficient as find_route // generally ignores its view of our own channels as we provide them via // ChannelDetails. - if let &HTLCSource::OutboundRoute { ref path, .. } = htlc_source { - DecodedOnionFailure { - network_update: None, - payment_failed_permanently: false, - short_channel_id: Some(path.hops[0].short_channel_id), - failed_within_blinded_path: false, - hold_times: Vec::new(), - #[cfg(any(test, feature = "_test_utils"))] - onion_error_code: Some(*failure_reason), - #[cfg(any(test, feature = "_test_utils"))] - onion_error_data: Some(data.clone()), - #[cfg(test)] - attribution_failed_channel: None, - } - } else { - unreachable!(); + match htlc_source { + &HTLCSource::OutboundRoute { ref path, .. } => decoded_onion_failure( + Some(path.hops[0].short_channel_id), + *failure_reason, + data, + ), + &HTLCSource::TrampolineForward { ref outbound_payment, .. } => { + debug_assert!(outbound_payment.is_none()); + decoded_onion_failure(None, *failure_reason, data) + }, + _ => unreachable!(), } }, } @@ -2153,7 +2187,7 @@ impl HTLCFailReason { /// Allows `decode_next_hop` to return the next hop packet bytes for either payments or onion /// message forwards. -pub(crate) trait NextPacketBytes: AsMut<[u8]> { +pub trait NextPacketBytes: AsMut<[u8]> { fn new(len: usize) -> Self; } @@ -2170,7 +2204,7 @@ impl NextPacketBytes for Vec<u8> { } /// Data decrypted from a payment's onion payload. -pub(crate) enum Hop { +pub enum Hop { /// This onion payload needs to be forwarded to a next-hop. Forward { /// Onion payload data used in forwarding the payment. @@ -2295,7 +2329,7 @@ impl Hop { /// Error returned when we fail to decode the onion packet. #[derive(Debug)] -pub(crate) enum OnionDecodeErr { +pub enum OnionDecodeErr { /// The HMAC of the onion packet did not match the hop data. Malformed { err_msg: &'static str, reason: LocalHTLCFailureReason }, /// We failed to decode the onion payload. @@ -2310,9 +2344,9 @@ pub(crate) enum OnionDecodeErr { }, } -pub(crate) fn decode_next_payment_hop<NS: NodeSigner>( +pub fn decode_next_payment_hop<NS: NodeSigner>( recipient: Recipient, hop_pubkey: &PublicKey, hop_data: &[u8], hmac_bytes: [u8; 32], - payment_hash: PaymentHash, blinding_point: Option<PublicKey>, node_signer: NS, + payment_hash: Option<PaymentHash>, blinding_point: Option<PublicKey>, node_signer: NS, ) -> Result<Hop, OnionDecodeErr> { let blinded_node_id_tweak = blinding_point.map(|bp| { let blinded_tlvs_ss = node_signer.ecdh(recipient, &bp, None).unwrap().secret_bytes(); @@ -2327,7 +2361,7 @@ pub(crate) fn decode_next_payment_hop<NS: NodeSigner>( shared_secret.secret_bytes(), hop_data, hmac_bytes, - Some(payment_hash), + payment_hash, (blinding_point, &node_signer), ); match decoded_hop { @@ -2401,8 +2435,8 @@ pub(crate) fn decode_next_payment_hop<NS: NodeSigner>( trampoline_shared_secret, &hop_data.trampoline_packet.hop_data, hop_data.trampoline_packet.hmac, - Some(payment_hash), - (blinding_point, &node_signer), + payment_hash, + (blinding_point, node_signer), ); match decoded_trampoline_hop { Ok(( @@ -2586,7 +2620,7 @@ pub(super) fn peel_dummy_hop_update_add_htlc<NS: NodeSigner, T: secp256k1::Verif /// /// `cur_block_height` should be set to the best known block height + 1. pub fn create_payment_onion<T: secp256k1::Signing>( - secp_ctx: &Secp256k1<T>, path: &Path, session_priv: &SecretKey, total_msat: u64, + secp_ctx: &Secp256k1<T>, path: &Path, session_priv: &SecretKey, recipient_onion: &RecipientOnionFields, cur_block_height: u32, payment_hash: &PaymentHash, keysend_preimage: &Option<PaymentPreimage>, invoice_request: Option<&InvoiceRequest>, prng_seed: [u8; 32], @@ -2595,7 +2629,6 @@ pub fn create_payment_onion<T: secp256k1::Signing>( secp_ctx, path, session_priv, - total_msat, recipient_onion, cur_block_height, payment_hash, @@ -2617,27 +2650,40 @@ pub(super) fn compute_trampoline_session_priv(outer_onion_session_priv: &SecretK /// Build a payment onion, returning the first hop msat and cltv values as well. /// `cur_block_height` should be set to the best known block height + 1. pub(crate) fn create_payment_onion_internal<T: secp256k1::Signing>( - secp_ctx: &Secp256k1<T>, path: &Path, session_priv: &SecretKey, total_msat: u64, + secp_ctx: &Secp256k1<T>, path: &Path, session_priv: &SecretKey, recipient_onion: &RecipientOnionFields, cur_block_height: u32, payment_hash: &PaymentHash, keysend_preimage: &Option<PaymentPreimage>, invoice_request: Option<&InvoiceRequest>, prng_seed: [u8; 32], trampoline_session_priv_override: Option<SecretKey>, trampoline_prng_seed_override: Option<[u8; 32]>, ) -> Result<(msgs::OnionPacket, u64, u32), APIError> { - let mut outer_total_msat = total_msat; - let mut outer_starting_htlc_offset = cur_block_height; - let mut trampoline_packet_option = None; + // If we're paying to a recipient through a trampoline, we use the `payment_secret` provided in + // `recipient_onion` as the MPP identifier for the trampoline entry point, allowing it to + // detect when when it has received all the MPP parts. + // A `total_mpp_amount_msat` is also provided to the trampoline entry point, but set in the + // below `if` block. + let mut trampoline_outer_onion = RecipientOnionFields { + payment_secret: recipient_onion.payment_secret, + total_mpp_amount_msat: 0, + payment_metadata: None, + custom_tlvs: Vec::new(), + }; + let (outer_onion, trampoline_packet_option) = if let Some(blinded_tail) = &path.blinded_tail { + if recipient_onion.payment_metadata.is_some() { + return Err(APIError::InvalidRoute { + err: "Cannot pass payment_metadata to a blinded recipient".to_owned(), + }); + } - if let Some(blinded_tail) = &path.blinded_tail { if !blinded_tail.trampoline_hops.is_empty() { let trampoline_payloads; - (trampoline_payloads, outer_total_msat, outer_starting_htlc_offset) = - build_trampoline_onion_payloads( - &blinded_tail, - total_msat, - recipient_onion, - cur_block_height, - keysend_preimage, - )?; + let outer_total_msat; + (trampoline_payloads, outer_total_msat) = build_trampoline_onion_payloads( + &blinded_tail, + recipient_onion, + cur_block_height, + keysend_preimage, + )?; + trampoline_outer_onion.total_mpp_amount_msat = outer_total_msat; let trampoline_session_priv = trampoline_session_priv_override .unwrap_or_else(|| compute_trampoline_session_priv(session_priv)); @@ -2653,27 +2699,31 @@ pub(crate) fn create_payment_onion_internal<T: secp256k1::Signing>( None, ) .map_err(|_| APIError::InvalidRoute { - err: "Route size too large considering onion data".to_owned(), + err: "Route size too large (or empty) considering onion data".to_owned(), })?; - trampoline_packet_option = Some(trampoline_packet); + (&trampoline_outer_onion, Some(trampoline_packet)) + } else { + (recipient_onion, None) } - } + } else { + (recipient_onion, None) + }; let (onion_payloads, htlc_msat, htlc_cltv) = build_onion_payloads( &path, - outer_total_msat, - recipient_onion, - outer_starting_htlc_offset, + outer_onion, + cur_block_height, keysend_preimage, invoice_request, trampoline_packet_option, )?; + debug_assert_eq!(htlc_cltv - cur_block_height, path.total_cltv_expiry_delta()); let onion_keys = construct_onion_keys(&secp_ctx, &path, session_priv); let onion_packet = construct_onion_packet(onion_payloads, onion_keys, prng_seed, payment_hash) .map_err(|_| APIError::InvalidRoute { - err: "Route size too large considering onion data".to_owned(), + err: "Route size too large (or empty) considering onion data".to_owned(), })?; Ok((onion_packet, htlc_msat, htlc_cltv)) } @@ -2701,22 +2751,32 @@ fn decode_next_hop<T, R: ReadableArgs<T>, N: NextPacketBytes>( }); } - let mut chacha = ChaCha20::new(&rho, &[0u8; 8]); + let mut chacha = ChaCha20::new(Key::new(rho), Nonce::new([0; 12]), 0); let mut chacha_stream = ChaChaReader { chacha: &mut chacha, read: Cursor::new(&hop_data[..]) }; match R::read(&mut chacha_stream, read_args) { Err(err) => { - let reason = match err { + let (reason, err_msg) = match err { // Unknown version - msgs::DecodeError::UnknownVersion => LocalHTLCFailureReason::InvalidOnionVersion, + msgs::DecodeError::UnknownVersion => { + (LocalHTLCFailureReason::InvalidOnionVersion, "Unable to decode our hop data") + }, // invalid_onion_payload + msgs::DecodeError::SkipCase => ( + LocalHTLCFailureReason::InvalidOnionPayload, + "Should be skipped by bitcoinfuzz", + ), msgs::DecodeError::UnknownRequiredFeature | msgs::DecodeError::InvalidValue - | msgs::DecodeError::ShortRead => LocalHTLCFailureReason::InvalidOnionPayload, + | msgs::DecodeError::ShortRead => { + (LocalHTLCFailureReason::InvalidOnionPayload, "Unable to decode our hop data") + }, // Should never happen - _ => LocalHTLCFailureReason::TemporaryNodeFailure, + _ => { + (LocalHTLCFailureReason::TemporaryNodeFailure, "Unable to decode our hop data") + }, }; return Err(OnionDecodeErr::Relay { - err_msg: "Unable to decode our hop data", + err_msg, reason, shared_secret: SharedSecret::from_bytes(shared_secret), trampoline_shared_secret: None, @@ -2766,7 +2826,7 @@ fn decode_next_hop<T, R: ReadableArgs<T>, N: NextPacketBytes>( } // Once we've emptied the set of bytes our peer gave us, encrypt 0 bytes until we // fill the onion hop data we'll forward to our next-hop peer. - chacha_stream.chacha.process_in_place(&mut new_packet_bytes.as_mut()[read_pos..]); + chacha_stream.chacha.apply_keystream(&mut new_packet_bytes.as_mut()[read_pos..]); return Ok((msg, Some((hmac, new_packet_bytes)))); // This packet needs forwarding } }, @@ -2789,8 +2849,8 @@ pub(crate) const HMAC_COUNT: usize = MAX_HOPS * (MAX_HOPS + 1) / 2; /// Additionally, it allows a sender to identify how long each hop along a path held an HTLC, with /// 100ms granularity. pub struct AttributionData { - hold_times: [u8; MAX_HOPS * HOLD_TIME_LEN], - hmacs: [u8; HMAC_LEN * HMAC_COUNT], + pub hold_times: [u8; MAX_HOPS * HOLD_TIME_LEN], + pub hmacs: [u8; HMAC_LEN * HMAC_COUNT], } impl AttributionData { @@ -2808,9 +2868,9 @@ impl AttributionData { /// Encrypts or decrypts the attribution data using the provided shared secret. pub(crate) fn crypt(&mut self, shared_secret: &[u8]) { let ammagext = gen_ammagext_from_shared_secret(&shared_secret); - let mut chacha = ChaCha20::new(&ammagext, &[0u8; 8]); - chacha.process_in_place(&mut self.hold_times); - chacha.process_in_place(&mut self.hmacs); + let mut chacha = ChaCha20::new(Key::new(ammagext), Nonce::new([0; 12]), 0); + chacha.apply_keystream(&mut self.hold_times); + chacha.apply_keystream(&mut self.hmacs); } /// Adds the current node's HMACs for all possible positions to this packet. @@ -2999,7 +3059,7 @@ mod tests { use crate::ln::channelmanager::PaymentId; use crate::ln::msgs::{self, UpdateFailHTLC}; use crate::ln::types::ChannelId; - use crate::routing::router::{Path, PaymentParameters, Route, RouteHop}; + use crate::routing::router::{Path, PaymentParameters, Route, RouteHop, RouteParameters}; use crate::types::features::{ChannelFeatures, NodeFeatures}; use crate::types::payment::PaymentHash; use crate::util::ser::{VecWriter, Writeable, Writer}; @@ -3107,7 +3167,10 @@ mod tests { let secp_ctx = Secp256k1::new(); let path = build_test_path(); - let route = Route { paths: vec![path], route_params: None }; + let payment_params = PaymentParameters::from_node_id(path.hops.last().unwrap().pubkey, 0); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, path.final_value_msat()); + let route = Route { paths: vec![path], route_params }; let onion_keys = super::construct_onion_keys(&secp_ctx, &route.paths[0], &get_test_session_key()); @@ -4029,7 +4092,7 @@ mod tests { max_total_routing_fee_msat: Some(u64::MAX), }; route_params.payment_params.max_total_cltv_expiry_delta = u32::MAX; - let recipient_onion = RecipientOnionFields::spontaneous_empty(); + let recipient_onion = RecipientOnionFields::spontaneous_empty(u64::MAX); set_max_path_length(&mut route_params, &recipient_onion, None, None, 42).unwrap(); } @@ -4071,4 +4134,33 @@ mod tests { assert_eq!(buffer.len(), 65535); } + + #[test] + fn create_payment_onion_fails_for_empty_route() { + let secp_ctx = Secp256k1::new(); + let session_priv = get_test_session_key(); + let recipient_onion = RecipientOnionFields::spontaneous_empty(1000); + let payment_hash = PaymentHash([0; 32]); + let empty_path = Path { hops: vec![], blinded_tail: None }; + + let err = super::create_payment_onion( + &secp_ctx, + &empty_path, + &session_priv, + &recipient_onion, + 100, + &payment_hash, + &None, + None, + [0; 32], + ) + .unwrap_err(); + + match err { + APIError::InvalidRoute { err } => { + assert_eq!(err, "Route size too large (or empty) considering onion data"); + }, + _ => panic!("Expected InvalidRoute error, got {:?}", err), + } + } } diff --git a/lightning/src/ln/our_peer_storage.rs b/lightning/src/ln/our_peer_storage.rs index ab0e9783ffa..e8939a15f15 100644 --- a/lightning/src/ln/our_peer_storage.rs +++ b/lightning/src/ln/our_peer_storage.rs @@ -14,18 +14,18 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine}; use bitcoin::secp256k1::PublicKey; +use chacha20_poly1305::{ChaCha20Poly1305, Key, Nonce}; use crate::ln::types::ChannelId; use crate::sign::PeerStorageKey; -use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; use crate::prelude::*; /// [`DecryptedOurPeerStorage`] is used to store serialised channel information that allows for the creation of a /// `peer_storage` backup. /// /// This structure is designed to serialize channel data for backup and supports encryption -/// using `ChaCha20Poly1305RFC` for transmission. +/// using `ChaCha20Poly1305` for transmission. /// /// # Key Methods /// - [`DecryptedOurPeerStorage::new`]: Returns [`DecryptedOurPeerStorage`] with the given data. @@ -66,9 +66,8 @@ impl DecryptedOurPeerStorage { let plaintext_len = data.len(); let nonce = derive_nonce(key, random_bytes); - let mut chacha = ChaCha20Poly1305RFC::new(&key.inner, &nonce, b""); - let mut tag = [0; 16]; - chacha.encrypt_full_message_in_place(&mut data[0..plaintext_len], &mut tag); + let chacha = ChaCha20Poly1305::new(Key::new(key.inner), Nonce::new(nonce)); + let tag = chacha.encrypt(&mut data[0..plaintext_len], None); data.extend_from_slice(&tag); @@ -122,9 +121,11 @@ impl EncryptedOurPeerStorage { let nonce = derive_nonce(key, random_bytes); - let mut chacha = ChaCha20Poly1305RFC::new(&key.inner, &nonce, b""); + let chacha = ChaCha20Poly1305::new(Key::new(key.inner), Nonce::new(nonce)); - if chacha.check_decrypt_in_place(encrypted_data, tag).is_err() { + let mut decrypt_tag = [0; 16]; + decrypt_tag.copy_from_slice(tag); + if chacha.decrypt(encrypted_data, decrypt_tag, None).is_err() { return Err(()); } @@ -169,7 +170,7 @@ pub(crate) struct PeerStorageMonitorHolder { pub(crate) monitor_bytes: Vec<u8>, } -impl_writeable_tlv_based!(PeerStorageMonitorHolder, { +impl_ser_tlv_based!(PeerStorageMonitorHolder, { (0, channel_id, required), (2, counterparty_node_id, required), (4, min_seen_secret, required), diff --git a/lightning/src/ln/outbound_payment.rs b/lightning/src/ln/outbound_payment.rs index 170e4e13830..24533ba2a72 100644 --- a/lightning/src/ln/outbound_payment.rs +++ b/lightning/src/ln/outbound_payment.rs @@ -11,7 +11,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use bitcoin::secp256k1::{self, Secp256k1, SecretKey}; +use bitcoin::secp256k1::{self, PublicKey, Secp256k1, SecretKey}; use lightning_invoice::Bolt11Invoice; use crate::blinded_path::{IntroductionNode, NodeIdLookUp}; @@ -21,6 +21,7 @@ use crate::ln::channelmanager::{ EventCompletionAction, HTLCSource, OptionalBolt11PaymentParams, PaymentCompleteUpdate, PaymentId, }; +use crate::ln::msgs::{DecodeError, TrampolineOnionPacket}; use crate::ln::onion_utils; use crate::ln::onion_utils::{DecodedOnionFailure, HTLCFailReason}; use crate::offers::invoice::{Bolt12Invoice, DerivedSigningPubkey, InvoiceBuilder}; @@ -37,15 +38,17 @@ use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::util::errors::APIError; use crate::util::logger::{Logger, WithContext}; use crate::util::ser::ReadableArgs; -#[cfg(feature = "std")] +#[cfg(all(feature = "std", not(fuzzing)))] use crate::util::time::Instant; use core::fmt::{self, Display, Formatter}; use core::sync::atomic::{AtomicBool, Ordering}; use core::time::Duration; +use crate::io; use crate::prelude::*; use crate::sync::Mutex; +use crate::util::ser; /// The number of ticks of [`ChannelManager::timer_tick_occurred`] until we time-out the idempotency /// of payments by [`PaymentId`]. See [`OutboundPayments::remove_stale_payments`]. @@ -130,6 +133,11 @@ pub(crate) enum PendingOutboundPayment { pending_fee_msat: Option<u64>, /// The total payment amount across all paths, used to verify that a retry is not overpaying. total_msat: u64, + /// The total payment amount which is set in the onion. + /// + /// This is generally equal to [`Self::Retryable::total_msat`] but may differ when making + /// payments which are sent MPP from different sources. + onion_total_msat: u64, /// Our best known block height at the time this payment was initiated. starting_block_height: u32, remaining_max_total_routing_fee_msat: Option<u64>, @@ -144,6 +152,8 @@ pub(crate) enum PendingOutboundPayment { timer_ticks_without_htlcs: u8, /// The total payment amount across all paths, used to be able to issue `PaymentSent`. total_msat: Option<u64>, + /// Total routing fees paid, as reported in `PaymentSent::fee_paid_msat`. + fee_paid_msat: Option<u64>, }, /// When we've decided to give up retrying a payment, we mark it as abandoned so we can eventually /// generate a `PaymentFailed` event when all HTLCs have irrevocably failed. @@ -156,20 +166,46 @@ pub(crate) enum PendingOutboundPayment { /// The total payment amount across all paths, used to be able to issue `PaymentSent` if /// an HTLC still happens to succeed after we marked the payment as abandoned. total_msat: Option<u64>, + /// Preserved from `Retryable` so we can still report `fee_paid_msat` if an HTLC succeeds after + /// the payment was abandoned. Added in 0.3. + pending_fee_msat: Option<u64>, }, } +#[derive(Clone, Eq, PartialEq)] +pub(crate) struct NextTrampolineHopInfo { + /// The Trampoline packet to include for the next Trampoline hop. + pub(crate) onion_packet: TrampolineOnionPacket, + /// If blinded, the current_path_key to set at the next Trampoline hop. + pub(crate) blinding_point: Option<PublicKey>, + /// The amount that the next trampoline is expecting to receive. + pub(crate) amount_msat: u64, + /// The cltv expiry height that the next trampoline is expecting. + pub(crate) cltv_expiry_height: u32, +} + +impl_ser_tlv_based!(NextTrampolineHopInfo, { + (1, onion_packet, required), + (3, blinding_point, option), + (5, amount_msat, required), + (7, cltv_expiry_height, required), +}); + #[derive(Clone)] pub(crate) struct RetryableInvoiceRequest { pub(crate) invoice_request: InvoiceRequest, - pub(crate) nonce: Nonce, + // No longer used, but written so that the payment can be retried after downgrading to a + // version that verifies invoices using the nonce instead of the payer metadata. Set when + // creating an invoice request and otherwise retains the value read from disk, which may have + // been written by such a version. + pub(crate) nonce: Option<Nonce>, pub(super) needs_retry: bool, } -impl_writeable_tlv_based!(RetryableInvoiceRequest, { +impl_ser_tlv_based!(RetryableInvoiceRequest, { (0, invoice_request, required), (1, needs_retry, (default_value, true)), - (2, nonce, required), + (2, nonce, option), }); impl PendingOutboundPayment { @@ -244,6 +280,8 @@ impl PendingOutboundPayment { fn get_pending_fee_msat(&self) -> Option<u64> { match self { PendingOutboundPayment::Retryable { pending_fee_msat, .. } => pending_fee_msat.clone(), + PendingOutboundPayment::Abandoned { pending_fee_msat, .. } => pending_fee_msat.clone(), + PendingOutboundPayment::Fulfilled { fee_paid_msat, .. } => fee_paid_msat.clone(), _ => None, } } @@ -286,7 +324,8 @@ impl PendingOutboundPayment { }); let payment_hash = self.payment_hash(); let total_msat = self.total_msat(); - *self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0, total_msat }; + let fee_paid_msat = self.get_pending_fee_msat(); + *self = PendingOutboundPayment::Fulfilled { session_privs, payment_hash, timer_ticks_without_htlcs: 0, total_msat, fee_paid_msat }; } #[rustfmt::skip] @@ -300,6 +339,7 @@ impl PendingOutboundPayment { _ => new_hash_set(), }; let total_msat = self.total_msat(); + let pending_fee_msat = self.get_pending_fee_msat(); match self { Self::Retryable { payment_hash, .. } | Self::InvoiceReceived { payment_hash, .. } | @@ -310,6 +350,7 @@ impl PendingOutboundPayment { payment_hash: *payment_hash, reason: Some(reason), total_msat, + pending_fee_msat, }; }, _ => {} @@ -419,13 +460,13 @@ pub enum Retry { } #[cfg(not(feature = "std"))] -impl_writeable_tlv_based_enum_legacy!(Retry, +impl_ser_tlv_based_enum_legacy!(Retry, ; (0, Attempts) ); #[cfg(feature = "std")] -impl_writeable_tlv_based_enum_legacy!(Retry, +impl_ser_tlv_based_enum_legacy!(Retry, ; (0, Attempts), (2, Timeout) @@ -438,14 +479,16 @@ impl Retry { (Retry::Attempts(max_retry_count), PaymentAttempts { count, .. }) => { max_retry_count > count }, - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] (Retry::Timeout(max_duration), PaymentAttempts { first_attempted_at, .. }) => *max_duration >= Instant::now().duration_since(*first_attempted_at), + #[cfg(all(feature = "std", fuzzing))] + (Retry::Timeout(_), _) => true, } } } -#[cfg(feature = "std")] +#[cfg(all(feature = "std", not(fuzzing)))] #[rustfmt::skip] pub(super) fn has_expired(route_params: &RouteParameters) -> bool { if let Some(expiry_time) = route_params.payment_params.expiry_time { @@ -456,6 +499,11 @@ pub(super) fn has_expired(route_params: &RouteParameters) -> bool { false } +#[cfg(all(feature = "std", fuzzing))] +pub(super) fn has_expired(_route_params: &RouteParameters) -> bool { + false +} + /// Storing minimal payment attempts information required for determining if a outbound payment can /// be retried. pub(crate) struct PaymentAttempts { @@ -463,7 +511,7 @@ pub(crate) struct PaymentAttempts { /// it means the result of the first attempt is not known yet. pub(crate) count: u32, /// This field is only used when retry is `Retry::Timeout` which is only build with feature std - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] first_attempted_at: Instant, } @@ -471,7 +519,7 @@ impl PaymentAttempts { pub(crate) fn new() -> Self { PaymentAttempts { count: 0, - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] first_attempted_at: Instant::now(), } } @@ -479,9 +527,9 @@ impl PaymentAttempts { impl Display for PaymentAttempts { fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] return write!(f, "attempts: {}", self.count); - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] return write!( f, "attempts: {}, duration: {}s", @@ -502,7 +550,7 @@ pub(crate) enum StaleExpiration { AbsoluteTimeout(core::time::Duration), } -impl_writeable_tlv_based_enum_legacy!(StaleExpiration, +impl_ser_tlv_based_enum_legacy!(StaleExpiration, ; (0, TimerTicks), (2, AbsoluteTimeout) @@ -619,7 +667,12 @@ pub(crate) enum PaymentSendFailure { #[derive(Debug)] pub enum Bolt11PaymentError { /// Incorrect amount was provided to [`ChannelManager::pay_for_bolt11_invoice`]. - /// This happens when the user-provided amount is less than an amount specified in the [`Bolt11Invoice`]. + /// + /// This happens when the payment amount (either the [`ChannelManager::pay_for_bolt11_invoice`] + /// `amount` or [`OptionalBolt11PaymentParams::declared_total_mpp_value_msat_override`]) is less than + /// [`Bolt11Invoice::amount_milli_satoshis`] or the amount set at + /// [`OptionalBolt11PaymentParams::declared_total_mpp_value_msat_override`] was lower than the + /// explicit amount provided to [`ChannelManager::pay_for_bolt11_invoice`]. /// /// [`Bolt11Invoice`]: lightning_invoice::Bolt11Invoice /// [`ChannelManager::pay_for_bolt11_invoice`]: crate::ln::channelmanager::ChannelManager::pay_for_bolt11_invoice @@ -758,33 +811,83 @@ pub struct RecipientOnionFields { pub payment_metadata: Option<Vec<u8>>, /// See [`Self::custom_tlvs`] for more info. pub(super) custom_tlvs: Vec<(u64, Vec<u8>)>, + /// The total payment amount which is being sent. + /// + /// This is communicated to the recipient as an indication that they should delay claiming the + /// payment until they've received multiple payment parts totaling at least this amount. + /// + /// Note that in order to properly communicate this, the recipient must either be paid using + /// blinded paths or a [`Self::payment_secret`] must be set. + pub total_mpp_amount_msat: u64, } -impl_writeable_tlv_based!(RecipientOnionFields, { - (0, payment_secret, option), - (1, custom_tlvs, optional_vec), - (2, payment_metadata, option), -}); +impl ser::Writeable for RecipientOnionFields { + fn write<W: ser::Writer>(&self, writer: &mut W) -> Result<(), io::Error> { + write_tlv_fields!(writer, { + (0, self.payment_secret, option), + (1, self.custom_tlvs, optional_vec), + (2, self.payment_metadata, option), + (3, self.total_mpp_amount_msat, required), + }); + Ok(()) + } +} + +impl ser::ReadableArgs<u64> for RecipientOnionFields { + fn read<R: io::Read>( + reader: &mut R, default_total_mpp_amount_msat: u64, + ) -> Result<Self, DecodeError> { + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, payment_secret, option), + (1, custom_tlvs, optional_vec), + (2, payment_metadata, option), + // Added and always written in LDK 0.3 + (3, total_mpp_amount_msat, option), + }); + Ok(Self { + payment_secret, + custom_tlvs: custom_tlvs.unwrap_or(Vec::new()), + payment_metadata, + total_mpp_amount_msat: total_mpp_amount_msat.unwrap_or(default_total_mpp_amount_msat), + }) + } +} impl RecipientOnionFields { - /// Creates a [`RecipientOnionFields`] from only a [`PaymentSecret`]. This is the most common - /// set of onion fields for today's BOLT11 invoices - most nodes require a [`PaymentSecret`] - /// but do not require or provide any further data. + /// Creates a [`RecipientOnionFields`] from only a [`PaymentSecret`] and total MPP amount. This + /// is the most common set of onion fields for today's BOLT11 invoices - most nodes require a + /// [`PaymentSecret`] but do not require or provide any further data. #[rustfmt::skip] - pub fn secret_only(payment_secret: PaymentSecret) -> Self { - Self { payment_secret: Some(payment_secret), payment_metadata: None, custom_tlvs: Vec::new() } + pub fn secret_only(payment_secret: PaymentSecret, total_mpp_amount_msat: u64) -> Self { + Self { + payment_secret: Some(payment_secret), + payment_metadata: None, + custom_tlvs: Vec::new(), + total_mpp_amount_msat, + } } - /// Creates a new [`RecipientOnionFields`] with no fields. This generally does not create - /// payable HTLCs except for single-path spontaneous payments, i.e. this should generally - /// only be used for calls to [`ChannelManager::send_spontaneous_payment`]. If you are sending - /// a spontaneous MPP this will not work as all MPP require payment secrets; you may - /// instead want to use [`RecipientOnionFields::secret_only`]. + /// Creates a new [`RecipientOnionFields`] with no fields but the total MPP amount. This is + /// useful when paying a blinded path, where the `payment_secret` and `payment_metadata` are + /// not provided but rather stored transparently in the blinded path itself. + /// + /// Otherwise, this generally does not create payable HTLCs except for single-path spontaneous + /// payments, i.e. those for calls to [`ChannelManager::send_spontaneous_payment`]. + /// + /// Note that due to protocol limitations, in non-blinded-path cases, you cannot make an MPP + /// payment without a `payment_secret`. Thus, in such cases `total_mpp_amount_msat` is ignored. + /// If you intend to send a spontaneous MPP you may instead want to use + /// [`RecipientOnionFields::secret_only`]. /// /// [`ChannelManager::send_spontaneous_payment`]: super::channelmanager::ChannelManager::send_spontaneous_payment /// [`RecipientOnionFields::secret_only`]: RecipientOnionFields::secret_only - pub fn spontaneous_empty() -> Self { - Self { payment_secret: None, payment_metadata: None, custom_tlvs: Vec::new() } + pub fn spontaneous_empty(total_mpp_amount_msat: u64) -> Self { + Self { + payment_secret: None, + payment_metadata: None, + custom_tlvs: Vec::new(), + total_mpp_amount_msat, + } } /// Creates a new [`RecipientOnionFields`] from an existing one, adding validated custom TLVs. @@ -837,6 +940,9 @@ impl RecipientOnionFields { pub(super) fn check_merge(&mut self, further_htlc_fields: &mut Self) -> Result<(), ()> { if self.payment_secret != further_htlc_fields.payment_secret { return Err(()); } if self.payment_metadata != further_htlc_fields.payment_metadata { return Err(()); } + if self.total_mpp_amount_msat != further_htlc_fields.total_mpp_amount_msat { + return Err(()); + } let tlvs = &mut self.custom_tlvs; let further_tlvs = &mut further_htlc_fields.custom_tlvs; @@ -857,7 +963,6 @@ pub(super) struct SendAlongPathArgs<'a> { pub path: &'a Path, pub payment_hash: &'a PaymentHash, pub recipient_onion: &'a RecipientOnionFields, - pub total_value: u64, pub cur_height: u32, pub payment_id: PaymentId, pub keysend_preimage: &'a Option<PaymentPreimage>, @@ -894,6 +999,30 @@ impl OutboundPayments { } } +/// Validate that a [`Route`] picked by our [`Router`] is sane for the [`RouteParameters`] used to +/// request it. Failure here indicates a critical bug in the [`Router`]. +fn validate_found_route<L: Logger>( + route: &mut Route, route_params: &RouteParameters, logger: &WithContext<L>, +) -> Result<(), ()> { + if route.route_params != *route_params { + debug_assert!( + false, + "Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {route_params:?}", + route.route_params + ); + log_error!( + logger, + "Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {route_params:?}", + route.route_params + ); + route.route_params = route_params.clone(); + } + + route.debug_assert_route_meets_params(logger)?; + + Ok(()) +} + impl OutboundPayments { #[rustfmt::skip] pub(super) fn send_payment<R: Router, ES: EntropySource, NS: NodeSigner, IH, SP, L: Logger>( @@ -953,17 +1082,30 @@ impl OutboundPayments { { let payment_hash = invoice.payment_hash(); + let partial_payment = optional_params.declared_total_mpp_value_msat_override.is_some(); let amount = match (invoice.amount_milli_satoshis(), amount_msats) { (Some(amt), None) | (None, Some(amt)) => amt, - (Some(inv_amt), Some(user_amt)) if user_amt < inv_amt => return Err(Bolt11PaymentError::InvalidAmount), + (Some(inv_amt), Some(user_amt)) if user_amt < inv_amt && !partial_payment => + return Err(Bolt11PaymentError::InvalidAmount), (Some(_), Some(user_amt)) => user_amt, (None, None) => return Err(Bolt11PaymentError::InvalidAmount), }; - let mut recipient_onion = RecipientOnionFields::secret_only(*invoice.payment_secret()) - .with_custom_tlvs(optional_params.custom_tlvs); + let mut recipient_onion = + RecipientOnionFields::secret_only(*invoice.payment_secret(), amount) + .with_custom_tlvs(optional_params.custom_tlvs); recipient_onion.payment_metadata = invoice.payment_metadata().map(|v| v.clone()); + if let Some(mpp_amt) = optional_params.declared_total_mpp_value_msat_override { + if mpp_amt < amount { + return Err(Bolt11PaymentError::InvalidAmount); + } + if invoice.amount_milli_satoshis().is_some_and(|invoice_amt| mpp_amt < invoice_amt) { + return Err(Bolt11PaymentError::InvalidAmount); + } + recipient_onion.total_mpp_amount_msat = mpp_amt; + } + let payment_params = PaymentParameters::from_bolt11_invoice(invoice) .with_user_config_ignoring_fee_limit(optional_params.route_params_config); @@ -1060,6 +1202,7 @@ impl OutboundPayments { payment_secret: None, payment_metadata: None, custom_tlvs: vec![], + total_mpp_amount_msat: route_params.final_value_msat, }; let route = match self.find_initial_route( payment_id, payment_hash, &recipient_onion, keysend_preimage, invoice_request, @@ -1079,14 +1222,13 @@ impl OutboundPayments { }, }; - let payment_params = Some(route_params.payment_params.clone()); let mut outbounds = self.pending_outbound_payments.lock().unwrap(); let onion_session_privs = match outbounds.entry(payment_id) { hash_map::Entry::Occupied(entry) => match entry.get() { PendingOutboundPayment::InvoiceReceived { .. } => { let (retryable_payment, onion_session_privs) = Self::create_pending_payment( payment_hash, recipient_onion.clone(), keysend_preimage, None, Some(bolt12_invoice.clone()), &route, - Some(retry_strategy), payment_params, entropy_source, best_block_height, + Some(retry_strategy), entropy_source, best_block_height, ); *entry.into_mut() = retryable_payment; onion_session_privs @@ -1097,7 +1239,7 @@ impl OutboundPayments { } else { unreachable!() }; let (retryable_payment, onion_session_privs) = Self::create_pending_payment( payment_hash, recipient_onion.clone(), keysend_preimage, Some(invreq), Some(bolt12_invoice.clone()), &route, - Some(retry_strategy), payment_params, entropy_source, best_block_height + Some(retry_strategy), entropy_source, best_block_height ); outbounds.insert(payment_id, retryable_payment); onion_session_privs @@ -1110,7 +1252,7 @@ impl OutboundPayments { let result = self.pay_route_internal( &route, payment_hash, &recipient_onion, keysend_preimage, invoice_request, Some(&bolt12_invoice), payment_id, - Some(route_params.final_value_msat), &onion_session_privs, hold_htlcs_at_next_hop, node_signer, + &onion_session_privs, hold_htlcs_at_next_hop, node_signer, best_block_height, &send_payment_along_path ); log_info!( @@ -1200,7 +1342,7 @@ impl OutboundPayments { if let Err(()) = onion_utils::set_max_path_length( &mut route_params, - &RecipientOnionFields::spontaneous_empty(), + &RecipientOnionFields::spontaneous_empty(amount_msat), Some(keysend_preimage), Some(invreq), best_block_height, @@ -1462,12 +1604,8 @@ impl OutboundPayments { RetryableSendFailure::RouteNotFound })?; - if route.route_params.as_ref() != Some(route_params) { - debug_assert!(false, - "Routers are expected to return a Route which includes the requested RouteParameters. Got {:?}, expected {:?}", - route.route_params, route_params); - route.route_params = Some(route_params.clone()); - } + validate_found_route(&mut route, route_params, logger) + .map_err(|()| RetryableSendFailure::RouteNotFound)?; Ok(route) } @@ -1497,7 +1635,7 @@ impl OutboundPayments { let onion_session_privs = self.add_new_pending_payment(payment_hash, recipient_onion.clone(), payment_id, keysend_preimage, &route, Some(retry_strategy), - Some(route_params.payment_params.clone()), entropy_source, best_block_height, None) + entropy_source, best_block_height, None) .map_err(|_| { log_error!(logger, "Payment with id {} is already pending. New payment had payment hash {}", payment_id, payment_hash); @@ -1505,7 +1643,7 @@ impl OutboundPayments { })?; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, None, &onion_session_privs, false, node_signer, + keysend_preimage, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Sending payment with id {} and hash {} returned {:?}", payment_id, payment_hash, res); @@ -1552,18 +1690,9 @@ impl OutboundPayments { } }; - if route.route_params.as_ref() != Some(&route_params) { - debug_assert!(false, - "Routers are expected to return a Route which includes the requested RouteParameters"); - route.route_params = Some(route_params.clone()); - } - - for path in route.paths.iter() { - if path.hops.len() == 0 { - log_error!(logger, "Unusable path in route (path.hops.len() must be at least 1"); - self.abandon_payment(payment_id, PaymentFailureReason::UnexpectedError, pending_events); - return - } + if validate_found_route(&mut route, &route_params, logger).is_err() { + self.abandon_payment(payment_id, PaymentFailureReason::RouteNotFound, pending_events); + return } macro_rules! abandon_with_entry { @@ -1581,14 +1710,14 @@ impl OutboundPayments { } } } - let (total_msat, recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice) = { + let (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice) = { let mut outbounds = self.pending_outbound_payments.lock().unwrap(); match outbounds.entry(payment_id) { hash_map::Entry::Occupied(mut payment) => { match payment.get() { PendingOutboundPayment::Retryable { total_msat, keysend_preimage, payment_secret, payment_metadata, - custom_tlvs, pending_amt_msat, invoice_request, .. + custom_tlvs, pending_amt_msat, invoice_request, onion_total_msat, .. } => { const RETRY_OVERFLOW_PERCENTAGE: u64 = 10; let retry_amt_msat = route.get_total_amount(); @@ -1604,11 +1733,11 @@ impl OutboundPayments { return } - let total_msat = *total_msat; let recipient_onion = RecipientOnionFields { payment_secret: *payment_secret, payment_metadata: payment_metadata.clone(), custom_tlvs: custom_tlvs.clone(), + total_mpp_amount_msat: *onion_total_msat, }; let keysend_preimage = *keysend_preimage; let invoice_request = invoice_request.clone(); @@ -1625,7 +1754,7 @@ impl OutboundPayments { payment.get_mut().increment_attempts(); let bolt12_invoice = payment.get().bolt12_invoice(); - (total_msat, recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) + (recipient_onion, keysend_preimage, onion_session_privs, invoice_request, bolt12_invoice.cloned()) }, PendingOutboundPayment::Legacy { .. } => { log_error!(logger, "Unable to retry payments that were initially sent on LDK versions prior to 0.0.102"); @@ -1665,7 +1794,7 @@ impl OutboundPayments { } }; let res = self.pay_route_internal(&route, payment_hash, &recipient_onion, keysend_preimage, - invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, Some(total_msat), + invoice_request.as_ref(), bolt12_invoice.as_ref(), payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path); log_info!(logger, "Result retrying payment id {}: {:?}", &payment_id, res); if let Err(e) = res { @@ -1812,18 +1941,29 @@ impl OutboundPayments { })) } - let route = Route { paths: vec![path], route_params: None }; + let route_params = { + let last_hop = path.hops.last().unwrap(); + let payment_params = + PaymentParameters::from_node_id(last_hop.pubkey, last_hop.cltv_expiry_delta); + RouteParameters { + payment_params, + final_value_msat: path.final_value_msat(), + max_total_routing_fee_msat: Some(path.fee_msat()), + } + }; + let route = Route { paths: vec![path], route_params }; + let recipient_onion_fields = + RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let onion_session_privs = self.add_new_pending_payment(payment_hash, - RecipientOnionFields::secret_only(payment_secret), payment_id, None, &route, None, None, + recipient_onion_fields.clone(), payment_id, None, &route, None, entropy_source, best_block_height, None ).map_err(|e| { debug_assert!(matches!(e, PaymentSendFailure::DuplicatePayment)); ProbeSendFailure::DuplicateProbe })?; - let recipient_onion_fields = RecipientOnionFields::spontaneous_empty(); match self.pay_route_internal(&route, payment_hash, &recipient_onion_fields, - None, None, None, payment_id, None, &onion_session_privs, false, node_signer, + None, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path ) { Ok(()) => Ok((payment_hash, payment_id)), @@ -1871,14 +2011,14 @@ impl OutboundPayments { &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, route: &Route, retry_strategy: Option<Retry>, entropy_source: &ES, best_block_height: u32 ) -> Result<Vec<[u8; 32]>, PaymentSendFailure> { - self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, None, entropy_source, best_block_height, None) + self.add_new_pending_payment(payment_hash, recipient_onion, payment_id, None, route, retry_strategy, entropy_source, best_block_height, None) } #[rustfmt::skip] pub(super) fn add_new_pending_payment<ES: EntropySource>( &self, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, payment_id: PaymentId, keysend_preimage: Option<PaymentPreimage>, route: &Route, retry_strategy: Option<Retry>, - payment_params: Option<PaymentParameters>, entropy_source: &ES, best_block_height: u32, + entropy_source: &ES, best_block_height: u32, bolt12_invoice: Option<PaidBolt12Invoice> ) -> Result<Vec<[u8; 32]>, PaymentSendFailure> { let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); @@ -1887,7 +2027,7 @@ impl OutboundPayments { hash_map::Entry::Vacant(entry) => { let (payment, onion_session_privs) = Self::create_pending_payment( payment_hash, recipient_onion, keysend_preimage, None, bolt12_invoice, route, retry_strategy, - payment_params, entropy_source, best_block_height + entropy_source, best_block_height ); entry.insert(payment); Ok(onion_session_privs) @@ -1900,7 +2040,7 @@ impl OutboundPayments { payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, keysend_preimage: Option<PaymentPreimage>, invoice_request: Option<InvoiceRequest>, bolt12_invoice: Option<PaidBolt12Invoice>, route: &Route, retry_strategy: Option<Retry>, - payment_params: Option<PaymentParameters>, entropy_source: &ES, best_block_height: u32 + entropy_source: &ES, best_block_height: u32 ) -> (PendingOutboundPayment, Vec<[u8; 32]>) { let mut onion_session_privs = Vec::with_capacity(route.paths.len()); for _ in 0..route.paths.len() { @@ -1910,7 +2050,7 @@ impl OutboundPayments { let mut payment = PendingOutboundPayment::Retryable { retry_strategy, attempts: PaymentAttempts::new(), - payment_params, + payment_params: Some(route.route_params.payment_params.clone()), session_privs: new_hash_set(), pending_amt_msat: 0, pending_fee_msat: Some(0), @@ -1923,8 +2063,8 @@ impl OutboundPayments { custom_tlvs: recipient_onion.custom_tlvs, starting_block_height: best_block_height, total_msat: route.get_total_amount(), - remaining_max_total_routing_fee_msat: - route.route_params.as_ref().and_then(|p| p.max_total_routing_fee_msat), + onion_total_msat: recipient_onion.total_mpp_amount_msat, + remaining_max_total_routing_fee_msat: route.route_params.max_total_routing_fee_msat, }; for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { @@ -1934,65 +2074,6 @@ impl OutboundPayments { (payment, onion_session_privs) } - #[cfg(feature = "dnssec")] - pub(super) fn add_new_awaiting_offer( - &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, - route_params_config: RouteParametersConfig, amount_msats: u64, payer_note: Option<String>, - ) -> Result<(), ()> { - let mut pending_outbounds = self.pending_outbound_payments.lock().unwrap(); - match pending_outbounds.entry(payment_id) { - hash_map::Entry::Occupied(_) => Err(()), - hash_map::Entry::Vacant(entry) => { - entry.insert(PendingOutboundPayment::AwaitingOffer { - expiration, - retry_strategy, - route_params_config, - amount_msats, - payer_note, - }); - - Ok(()) - }, - } - } - - #[cfg(feature = "dnssec")] - #[rustfmt::skip] - pub(super) fn params_for_payment_awaiting_offer(&self, payment_id: PaymentId) -> Result<(u64, Option<String>), ()> { - match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { - hash_map::Entry::Occupied(entry) => match entry.get() { - PendingOutboundPayment::AwaitingOffer { amount_msats, payer_note, .. } => Ok((*amount_msats, payer_note.clone())), - _ => Err(()), - }, - _ => Err(()), - } - } - - #[cfg(feature = "dnssec")] - #[rustfmt::skip] - pub(super) fn received_offer( - &self, payment_id: PaymentId, retryable_invoice_request: Option<RetryableInvoiceRequest>, - ) -> Result<(), ()> { - match self.pending_outbound_payments.lock().unwrap().entry(payment_id) { - hash_map::Entry::Occupied(entry) => match entry.get() { - PendingOutboundPayment::AwaitingOffer { - expiration, retry_strategy, route_params_config, .. - } => { - let mut new_val = PendingOutboundPayment::AwaitingInvoice { - expiration: *expiration, - retry_strategy: *retry_strategy, - route_params_config: *route_params_config, - retryable_invoice_request, - }; - core::mem::swap(&mut new_val, entry.into_mut()); - Ok(()) - }, - _ => Err(()), - }, - hash_map::Entry::Vacant(_) => Err(()), - } - } - pub(super) fn add_new_awaiting_invoice( &self, payment_id: PaymentId, expiration: StaleExpiration, retry_strategy: Retry, route_params_config: RouteParametersConfig, @@ -2068,7 +2149,7 @@ impl OutboundPayments { fn pay_route_internal<NS: NodeSigner, F>( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: &RecipientOnionFields, keysend_preimage: Option<PaymentPreimage>, invoice_request: Option<&InvoiceRequest>, bolt12_invoice: Option<&PaidBolt12Invoice>, - payment_id: PaymentId, recv_value_msat: Option<u64>, onion_session_privs: &Vec<[u8; 32]>, + payment_id: PaymentId, onion_session_privs: &Vec<[u8; 32]>, hold_htlcs_at_next_hop: bool, node_signer: &NS, best_block_height: u32, send_payment_along_path: &F ) -> Result<(), PaymentSendFailure> where @@ -2082,7 +2163,6 @@ impl OutboundPayments { { return Err(PaymentSendFailure::ParameterError(APIError::APIMisuseError{err: "Payment secret is required for multi-path payments".to_owned()})); } - let mut total_value = 0; let our_node_id = node_signer.get_node_id(Recipient::Node).unwrap(); // TODO no unwrap let mut path_errs = Vec::with_capacity(route.paths.len()); 'path_check: for path in route.paths.iter() { @@ -2105,22 +2185,18 @@ impl OutboundPayments { continue 'path_check; } } - total_value += path.final_value_msat(); path_errs.push(Ok(())); } if path_errs.iter().any(|e| e.is_err()) { return Err(PaymentSendFailure::PathParameterError(path_errs)); } - if let Some(amt_msat) = recv_value_msat { - total_value = amt_msat; - } let cur_height = best_block_height + 1; let mut results = Vec::new(); debug_assert_eq!(route.paths.len(), onion_session_privs.len()); for (path, session_priv_bytes) in route.paths.iter().zip(onion_session_privs.iter()) { let path_res = send_payment_along_path(SendAlongPathArgs { - path: &path, payment_hash: &payment_hash, recipient_onion, total_value, + path: &path, payment_hash: &payment_hash, recipient_onion, cur_height, payment_id, keysend_preimage: &keysend_preimage, invoice_request, bolt12_invoice, hold_htlc_at_next_hop: hold_htlcs_at_next_hop, session_priv_bytes: *session_priv_bytes @@ -2155,19 +2231,17 @@ impl OutboundPayments { results, payment_id, failed_paths_retry: if has_unsent { - if let Some(route_params) = &route.route_params { - let mut route_params = route_params.clone(); - // We calculate the leftover fee budget we're allowed to spend by - // subtracting the used fee from the total fee budget. - route_params.max_total_routing_fee_msat = route_params - .max_total_routing_fee_msat.map(|m| m.saturating_sub(total_ok_fees_msat)); - - // We calculate the remaining target amount by subtracting the succeded - // path values. - route_params.final_value_msat = route_params.final_value_msat - .saturating_sub(total_ok_amt_sent_msat); - Some(route_params) - } else { None } + let mut route_params = route.route_params.clone(); + // We calculate the leftover fee budget we're allowed to spend by + // subtracting the used fee from the total fee budget. + route_params.max_total_routing_fee_msat = route_params + .max_total_routing_fee_msat.map(|m| m.saturating_sub(total_ok_fees_msat)); + + // We calculate the remaining target amount by subtracting the succeded + // path values. + route_params.final_value_msat = route_params.final_value_msat + .saturating_sub(total_ok_amt_sent_msat); + Some(route_params) } else { None }, }) } else if has_err { @@ -2181,7 +2255,7 @@ impl OutboundPayments { #[rustfmt::skip] pub(super) fn test_send_payment_internal<NS: NodeSigner, F>( &self, route: &Route, payment_hash: PaymentHash, recipient_onion: RecipientOnionFields, - keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, recv_value_msat: Option<u64>, + keysend_preimage: Option<PaymentPreimage>, payment_id: PaymentId, onion_session_privs: Vec<[u8; 32]>, node_signer: &NS, best_block_height: u32, send_payment_along_path: F ) -> Result<(), PaymentSendFailure> @@ -2189,7 +2263,7 @@ impl OutboundPayments { F: Fn(SendAlongPathArgs) -> Result<(), APIError>, { self.pay_route_internal(route, payment_hash, &recipient_onion, - keysend_preimage, None, None, payment_id, recv_value_msat, &onion_session_privs, + keysend_preimage, None, None, payment_id, &onion_session_privs, false, node_signer, best_block_height, &send_payment_along_path) .map_err(|e| { self.remove_outbound_if_all_failed(payment_id, &e); e }) } @@ -2635,6 +2709,7 @@ impl OutboundPayments { pending_amt_msat: path_amt, pending_fee_msat: Some(path_fee), total_msat: path_amt, + onion_total_msat: path_amt, starting_block_height: best_block_height, remaining_max_total_routing_fee_msat: None, // only used for retries, and we'll never retry on startup } @@ -2701,6 +2776,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (1, payment_hash, option), (3, timer_ticks_without_htlcs, (default_value, 0)), (5, total_msat, option), + (7, fee_paid_msat, option), }, (2, Retryable) => { (0, session_privs, required), @@ -2717,6 +2793,21 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (9, custom_tlvs, optional_vec), (10, starting_block_height, required), (11, remaining_max_total_routing_fee_msat, option), + (12, onion_total_msat, (custom, u64, + // Once we get here, `total_msat` will have been read (or we'll fail to read) + |read_val: Option<u64>| Ok(read_val.unwrap_or(total_msat.0.unwrap())), + |us: &PendingOutboundPayment| { + match us { + PendingOutboundPayment::Retryable { total_msat, onion_total_msat, .. } => { + if total_msat != onion_total_msat { + Some(*onion_total_msat) + } else { + None + } + }, + _ => unreachable!(), + } + })), (13, invoice_request, option), (15, bolt12_invoice, option), (not_written, retry_strategy, (static_value, None)), @@ -2727,6 +2818,7 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, (1, reason, upgradable_option), (2, payment_hash, required), (3, total_msat, option), + (5, pending_fee_msat, option), }, (5, AwaitingInvoice) => { (0, expiration, required), @@ -2776,6 +2868,8 @@ impl_writeable_tlv_based_enum_upgradable!(PendingOutboundPayment, }, // Added in 0.1. Prior versions will drop these outbounds on downgrade, which is safe because // no HTLCs are in-flight. + // No longer created in 0.3 as we now expect BIP 353 to happen before a payment makes it into + // the `lightning` crate. (11, AwaitingOffer) => { (0, expiration, required), (2, retry_strategy, required), @@ -2817,6 +2911,7 @@ mod tests { use crate::offers::invoice_request::InvoiceRequest; use crate::offers::nonce::Nonce; use crate::offers::offer::OfferBuilder; + use crate::offers::payer_proof::PaidBolt12Invoice; use crate::offers::test_utils::*; use crate::routing::gossip::NetworkGraph; use crate::routing::router::{ @@ -2827,16 +2922,19 @@ mod tests { use crate::types::features::{Bolt12InvoiceFeatures, ChannelFeatures, NodeFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage}; use crate::util::errors::APIError; - use crate::util::hash_tables::new_hash_map; + use crate::util::hash_tables::{new_hash_map, new_hash_set}; use crate::util::logger::WithContext; + use crate::util::ser::{MaybeReadable, Writeable}; use crate::util::test_utils; + use super::PaymentAttempts; + use alloc::collections::VecDeque; #[test] #[rustfmt::skip] fn test_recipient_onion_fields_with_custom_tlvs() { - let onion_fields = RecipientOnionFields::spontaneous_empty(); + let onion_fields = RecipientOnionFields::spontaneous_empty(42); let bad_type_range_tlvs = RecipientCustomTlvs::new(vec![ (0, vec![42]), @@ -2884,9 +2982,9 @@ mod tests { let expired_route_params = RouteParameters::from_payment_params_and_value(payment_params, 0); let pending_events = Mutex::new(VecDeque::new()); if on_retry { - outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), - PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, - Some(Retry::Attempts(1)), Some(expired_route_params.payment_params.clone()), + outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), + PaymentId([0; 32]), None, &Route { paths: vec![], route_params: expired_route_params.clone() }, + Some(Retry::Attempts(1)), &&keys_manager, 0, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), expired_route_params, &&router, vec![], @@ -2899,7 +2997,7 @@ mod tests { } else { panic!("Unexpected event"); } } else { let err = outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), Retry::Attempts(0), expired_route_params, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(()), &log).unwrap_err(); if let RetryableSendFailure::PaymentExpired = err { } else { panic!("Unexpected error"); } @@ -2930,9 +3028,9 @@ mod tests { let pending_events = Mutex::new(VecDeque::new()); if on_retry { - outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), - PaymentId([0; 32]), None, &Route { paths: vec![], route_params: None }, - Some(Retry::Attempts(1)), Some(route_params.payment_params.clone()), + outbound_payments.add_new_pending_payment(PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), + PaymentId([0; 32]), None, &Route { paths: vec![], route_params: route_params.clone() }, + Some(Retry::Attempts(1)), &&keys_manager, 0, None).unwrap(); outbound_payments.find_route_and_send_payment( PaymentHash([0; 32]), PaymentId([0; 32]), route_params, &&router, vec![], @@ -2943,7 +3041,7 @@ mod tests { if let Event::PaymentFailed { .. } = events[0].0 { } else { panic!("Unexpected event"); } } else { let err = outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(0), PaymentId([0; 32]), Retry::Attempts(0), route_params, &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Ok(()), &log).unwrap_err(); if let RetryableSendFailure::RouteNotFound = err { @@ -2967,7 +3065,7 @@ mod tests { let sender_pk = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); let receiver_pk = PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()); let payment_params = PaymentParameters::from_node_id(sender_pk, 0); - let route_params = RouteParameters::from_payment_params_and_value(payment_params.clone(), 0); + let route_params = RouteParameters::from_payment_params_and_value(payment_params.clone(), 1); let failed_scid = 42; let route = Route { paths: vec![Path { hops: vec![RouteHop { @@ -2975,17 +3073,17 @@ mod tests { node_features: NodeFeatures::empty(), short_channel_id: failed_scid, channel_features: ChannelFeatures::empty(), - fee_msat: 0, + fee_msat: 1, cltv_expiry_delta: 0, maybe_announced_channel: true, }], blinded_tail: None }], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; router.expect_find_route(route_params.clone(), Ok(route.clone())); let mut route_params_w_failed_scid = route_params.clone(); route_params_w_failed_scid.payment_params.previously_failed_channels.push(failed_scid); let mut route_w_failed_scid = route.clone(); - route_w_failed_scid.route_params = Some(route_params_w_failed_scid.clone()); + route_w_failed_scid.route_params = route_params_w_failed_scid.clone(); router.expect_find_route(route_params_w_failed_scid, Ok(route_w_failed_scid)); router.expect_find_route(route_params.clone(), Ok(route.clone())); router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -2994,7 +3092,7 @@ mod tests { // PaymentPathFailed event. let pending_events = Mutex::new(VecDeque::new()); outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(1), PaymentId([0; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Err(APIError::ChannelUnavailable { err: "test".to_owned() }), &log).unwrap(); @@ -3012,7 +3110,7 @@ mod tests { // Ensure that a MonitorUpdateInProgress "error" will not result in a PaymentPathFailed event. outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(1), PaymentId([0; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Err(APIError::MonitorUpdateInProgress), &log).unwrap(); @@ -3020,7 +3118,7 @@ mod tests { // Ensure that any other error will result in a PaymentPathFailed event but no blamed scid. outbound_payments.send_payment( - PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(), PaymentId([1; 32]), + PaymentHash([0; 32]), RecipientOnionFields::spontaneous_empty(1), PaymentId([1; 32]), Retry::Attempts(0), route_params.clone(), &&router, vec![], || InFlightHtlcs::new(), &&keys_manager, &&keys_manager, 0, &pending_events, |_| Err(APIError::APIMisuseError { err: "test".to_owned() }), &log).unwrap(); @@ -3349,7 +3447,7 @@ mod tests { blinded_tail: None, } ], - route_params: Some(route_params), + route_params, }) ); @@ -3397,6 +3495,62 @@ mod tests { assert!(pending_events.lock().unwrap().is_empty()); } + #[test] + fn retryable_payment_round_trips_bolt12_invoice() { + // A `Retryable` payment serializes its `bolt12_invoice` and reads it back. This guards that + // the paid invoice (needed to build payer proofs on retried paths) survives the round-trip. + let secp_ctx = Secp256k1::new(); + let expanded_key = ExpandedKey::new([42; 32]); + let nonce = Nonce([7; 16]); + let payment_id = PaymentId([3; 32]); + + let invoice = OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .build() + .unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .unwrap() + .build_and_sign() + .unwrap() + .respond_with_no_std(payment_paths(), payment_hash(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let mut session_privs = new_hash_set(); + session_privs.insert([1; 32]); + let payment = PendingOutboundPayment::Retryable { + retry_strategy: Some(Retry::Attempts(0)), + attempts: PaymentAttempts::new(), + payment_params: None, + session_privs, + payment_hash: payment_hash(), + payment_secret: None, + payment_metadata: None, + keysend_preimage: None, + invoice_request: None, + bolt12_invoice: Some(PaidBolt12Invoice::Bolt12Invoice(invoice)), + custom_tlvs: Vec::new(), + pending_amt_msat: 1000, + pending_fee_msat: None, + total_msat: 1000, + onion_total_msat: 1000, + starting_block_height: 0, + remaining_max_total_routing_fee_msat: None, + }; + + let encoded = payment.encode(); + let decoded = PendingOutboundPayment::read(&mut &encoded[..]).unwrap().unwrap(); + match decoded { + PendingOutboundPayment::Retryable { bolt12_invoice, .. } => { + assert!(matches!(bolt12_invoice, Some(PaidBolt12Invoice::Bolt12Invoice(_)))); + }, + _ => panic!("expected a Retryable payment"), + } + } + #[rustfmt::skip] fn dummy_invoice_request() -> InvoiceRequest { let expanded_key = ExpandedKey::new([42; 32]); diff --git a/lightning/src/ln/payment_tests.rs b/lightning/src/ln/payment_tests.rs index 6e47e21ca8b..e86245abc60 100644 --- a/lightning/src/ln/payment_tests.rs +++ b/lightning/src/ln/payment_tests.rs @@ -25,8 +25,9 @@ use crate::ln::channel::{ EXPIRE_PREV_CONFIG_TICKS, }; use crate::ln::channelmanager::{ - HTLCForwardInfo, PaymentId, PendingAddHTLCInfo, PendingHTLCRouting, RecentPaymentDetails, - BREAKDOWN_TIMEOUT, MIN_CLTV_EXPIRY_DELTA, MPP_TIMEOUT_TICKS, + Bolt11InvoiceParameters, HTLCForwardInfo, OptionalBolt11PaymentParams, PaymentId, + PendingAddHTLCInfo, PendingHTLCRouting, RecentPaymentDetails, BREAKDOWN_TIMEOUT, + MIN_CLTV_EXPIRY_DELTA, MPP_TIMEOUT_TICKS, }; use crate::ln::msgs; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; @@ -38,24 +39,22 @@ use crate::ln::outbound_payment::{ use crate::ln::types::ChannelId; use crate::routing::gossip::{EffectiveCapacity, RoutingFees}; use crate::routing::router::{ - get_route, Path, PaymentParameters, Route, RouteHint, RouteHintHop, RouteHop, RouteParameters, - Router, + Path, PaymentParameters, Route, RouteHint, RouteHintHop, RouteHop, RouteParameters, Router, }; use crate::routing::scoring::ChannelUsage; use crate::sign::EntropySource; use crate::types::features::{Bolt11InvoiceFeatures, ChannelTypeFeatures}; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::types::string::UntrustedString; -use crate::util::config::HTLCInterceptionFlags; +use crate::util::config::{HTLCInterceptionFlags, UserConfig}; use crate::util::errors::APIError; use crate::util::ser::Writeable; -use crate::util::test_utils; - use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use bitcoin::network::Network; use bitcoin::secp256k1::{Secp256k1, SecretKey}; +use lightning_invoice::{Bolt11InvoiceDescription, Description}; + use crate::prelude::*; use crate::ln::functional_test_utils; @@ -97,6 +96,8 @@ fn mpp_failure() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_id; route.paths[1].hops[1].short_channel_id = chan_4_id; + route.route_params.final_value_msat *= 2; + let paths: &[&[_]] = &[&[&nodes[1], &nodes[3]], &[&nodes[2], &nodes[3]]]; send_along_route_with_secret(&nodes[0], route, paths, 200_000, payment_hash, payment_secret); fail_payment_along_route(&nodes[0], paths, false, payment_hash); @@ -137,13 +138,14 @@ fn mpp_retry() { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_update.contents.short_channel_id; route.paths[1].hops[1].short_channel_id = chan_4_update.contents.short_channel_id; + route.route_params.final_value_msat *= 2; // Initiate the MPP payment. let id = PaymentId(hash.0); - let mut route_params = route.route_params.clone().unwrap(); + let mut route_params = route.route_params.clone(); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(pay_secret); + let onion = RecipientOnionFields::secret_only(pay_secret, amt_msat * 2); let retry = Retry::Attempts(1); nodes[0].node.send_payment(hash, onion, id, route_params.clone(), retry).unwrap(); check_added_monitors(&nodes[0], 2); // one monitor per path @@ -190,7 +192,7 @@ fn mpp_retry() { // Check the remaining max total routing fee for the second attempt is 50_000 - 1_000 msat fee // used by the first path route_params.max_total_routing_fee_msat = Some(max_fee - 1_000); - route.route_params = Some(route_params.clone()); + route.route_params = route_params.clone(); nodes[0].router.expect_find_route(route_params, Ok(route)); expect_and_process_pending_htlcs(&nodes[0], false); check_added_monitors(&nodes[0], 1); @@ -213,7 +215,9 @@ fn mpp_retry_overpay() { let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); let mut user_config = test_legacy_channel_config(); - user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + user_config + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 100; let mut limited_1 = user_config.clone(); limited_1.channel_handshake_config.our_htlc_minimum_msat = 35_000_000; let mut limited_2 = user_config.clone(); @@ -246,22 +250,25 @@ fn mpp_retry_overpay() { let (mut route, hash, payment_preimage, pay_secret) = get_route_and_payment_hash!(nodes[0], nodes[3], payment_params, amt_msat, max_fee); - // Check we overpay on the second path which we're about to fail. + // Check we overpay on the second path which we're about to fail. Path ordering is not fixed, + // so we identify paths by first-hop pubkey. assert_eq!(chan_1_update.contents.fee_proportional_millionths, 0); - let overpaid_amount_1 = route.paths[0].fee_msat() as u32 - chan_1_update.contents.fee_base_msat; + let path_via_b = route.paths.iter().find(|p| p.hops[0].pubkey == node_b_id).unwrap(); + let overpaid_amount_1 = path_via_b.fee_msat() as u32 - chan_1_update.contents.fee_base_msat; assert_eq!(overpaid_amount_1, 0); assert_eq!(chan_2_update.contents.fee_proportional_millionths, 0); - let overpaid_amount_2 = route.paths[1].fee_msat() as u32 - chan_2_update.contents.fee_base_msat; + let path_via_c = route.paths.iter().find(|p| p.hops[0].pubkey == node_c_id).unwrap(); + let overpaid_amount_2 = path_via_c.fee_msat() as u32 - chan_2_update.contents.fee_base_msat; let total_overpaid_amount = overpaid_amount_1 + overpaid_amount_2; // Initiate the payment. let id = PaymentId(hash.0); - let mut route_params = route.route_params.clone().unwrap(); + let mut route_params = route.route_params.clone(); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(pay_secret); + let onion = RecipientOnionFields::secret_only(pay_secret, amt_msat); let retry = Retry::Attempts(1); nodes[0].node.send_payment(hash, onion, id, route_params.clone(), retry).unwrap(); check_added_monitors(&nodes[0], 2); // one monitor per path @@ -300,11 +307,13 @@ fn mpp_retry_overpay() { // Rebalance the channel so the second half of the payment can succeed. send_payment(&nodes[3], &[&nodes[2]], 38_000_000); - // Retry the second half of the payment and make sure it succeeds. - let first_path_value = route.paths[0].final_value_msat(); + // Retry the second half of the payment and make sure it succeeds. Identify the successful + // path (through nodes[1]) by first-hop pubkey, since path ordering is not stable. + let path_via_b_idx = route.paths.iter().position(|p| p.hops[0].pubkey == node_b_id).unwrap(); + let first_path_value = route.paths[path_via_b_idx].final_value_msat(); assert_eq!(first_path_value, 36_000_000); - route.paths.remove(0); + route.paths.remove(path_via_b_idx); route_params.final_value_msat -= first_path_value; let chan_4_scid = chan_4_update.contents.short_channel_id; route_params.payment_params.previously_failed_channels.push(chan_4_scid); @@ -312,7 +321,7 @@ fn mpp_retry_overpay() { // base fee, but not for overpaid value of the first try. route_params.max_total_routing_fee_msat.as_mut().map(|m| *m -= 1000); - route.route_params = Some(route_params.clone()); + route.route_params = route_params.clone(); nodes[0].router.expect_find_route(route_params, Ok(route)); nodes[0].node.process_pending_htlc_forwards(); @@ -334,7 +343,7 @@ fn mpp_retry_overpay() { expect_payment_sent!(&nodes[0], payment_preimage, Some(expected_total_fee_msat)); } -fn do_mpp_receive_timeout(send_partial_mpp: bool) { +fn do_mpp_receive_timeout(send_partial_mpp: bool, keysend: bool) { let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); @@ -350,8 +359,12 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { let (chan_3_update, _, chan_3_id, _) = create_announced_chan_between_nodes(&nodes, 1, 3); let (chan_4_update, _, _, _) = create_announced_chan_between_nodes(&nodes, 2, 3); - let (mut route, hash, payment_preimage, payment_secret) = - get_route_and_payment_hash!(nodes[0], nodes[3], 100_000); + let (mut route, hash, payment_preimage, payment_secret) = if keysend { + let payment_params = PaymentParameters::for_keysend(node_d_id, TEST_FINAL_CLTV, true); + get_route_and_payment_hash!(nodes[0], nodes[3], payment_params, 100_000) + } else { + get_route_and_payment_hash!(nodes[0], nodes[3], 100_000) + }; let path = route.paths[0].clone(); route.paths.push(path); route.paths[0].hops[0].pubkey = node_b_id; @@ -360,10 +373,26 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { route.paths[1].hops[0].pubkey = node_c_id; route.paths[1].hops[0].short_channel_id = chan_2_update.contents.short_channel_id; route.paths[1].hops[1].short_channel_id = chan_4_update.contents.short_channel_id; + route.route_params.final_value_msat *= 2; // Initiate the MPP payment. - let onion = RecipientOnionFields::secret_only(payment_secret); - nodes[0].node.send_payment_with_route(route, hash, onion, PaymentId(hash.0)).unwrap(); + let onion = RecipientOnionFields::secret_only(payment_secret, 200_000); + if keysend { + let route_params = route.route_params.clone(); + nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); + nodes[0] + .node + .send_spontaneous_payment( + Some(payment_preimage), + onion, + PaymentId(hash.0), + route_params, + Retry::Attempts(0), + ) + .unwrap(); + } else { + nodes[0].node.send_payment_with_route(route, hash, onion, PaymentId(hash.0)).unwrap(); + } check_added_monitors(&nodes[0], 2); // one monitor per path let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 2); @@ -412,7 +441,17 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { let node_2_msgs = remove_first_msg_event_to_node(&node_c_id, &mut events); let path = &[&nodes[2], &nodes[3]]; let payment_secret = Some(payment_secret); - pass_along_path(&nodes[0], path, 200_000, hash, payment_secret, node_2_msgs, true, None); + let expected_preimage = if keysend { Some(payment_preimage) } else { None }; + pass_along_path( + &nodes[0], + path, + 200_000, + hash, + payment_secret, + node_2_msgs, + true, + expected_preimage, + ); // Even after MPP_TIMEOUT_TICKS we should not timeout the MPP if we have all the parts for _ in 0..MPP_TIMEOUT_TICKS { @@ -426,8 +465,14 @@ fn do_mpp_receive_timeout(send_partial_mpp: bool) { #[test] fn mpp_receive_timeout() { - do_mpp_receive_timeout(true); - do_mpp_receive_timeout(false); + do_mpp_receive_timeout(true, false); + do_mpp_receive_timeout(false, false); +} + +#[test] +fn keysend_mpp_receive_timeout() { + do_mpp_receive_timeout(true, true); + do_mpp_receive_timeout(false, true); } #[test] @@ -457,7 +502,7 @@ fn do_test_keysend_payments(public_node: bool) { { let preimage = Some(PaymentPreimage([42; 32])); - let onion = RecipientOnionFields::spontaneous_empty(); + let onion = RecipientOnionFields::spontaneous_empty(10000); let retry = Retry::Attempts(1); let id = PaymentId([42; 32]); nodes[0].node.send_spontaneous_payment(preimage, onion, id, route_params, retry).unwrap(); @@ -487,7 +532,12 @@ fn do_test_keysend_payments(public_node: bool) { fn test_mpp_keysend() { let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; + let configs: [Option<UserConfig>; 4] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let nodes = create_network(4, &node_cfgs, &node_chanmgrs); let node_b_id = nodes[1].node.get_our_node_id(); @@ -507,7 +557,7 @@ fn test_mpp_keysend() { let preimage = Some(PaymentPreimage([42; 32])); let payment_secret = PaymentSecret([42; 32]); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, recv_value); let retry = Retry::Attempts(0); let id = PaymentId([42; 32]); let hash = @@ -550,7 +600,7 @@ fn test_fulfill_hold_times() { let preimage = Some(PaymentPreimage([42; 32])); let payment_secret = PaymentSecret([42; 32]); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, recv_value); let retry = Retry::Attempts(0); let id = PaymentId([42; 32]); let hash = @@ -618,9 +668,9 @@ fn test_reject_mpp_keysend_htlc_mismatching_secret() { route.paths[0].hops[1].short_channel_id = chan_3_id; let payment_id_0 = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - nodes[0].router.expect_find_route(route.route_params.clone().unwrap(), Ok(route.clone())); - let params = route.route_params.clone().unwrap(); - let onion = RecipientOnionFields::spontaneous_empty(); + nodes[0].router.expect_find_route(route.route_params.clone(), Ok(route.clone())); + let params = route.route_params.clone(); + let onion = RecipientOnionFields::spontaneous_empty(amount); let retry = Retry::Attempts(0); nodes[0].node.send_spontaneous_payment(preimage, onion, payment_id_0, params, retry).unwrap(); check_added_monitors(&nodes[0], 1); @@ -666,10 +716,10 @@ fn test_reject_mpp_keysend_htlc_mismatching_secret() { route.paths[0].hops[1].short_channel_id = chan_4_id; let payment_id_1 = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); - nodes[0].router.expect_find_route(route.route_params.clone().unwrap(), Ok(route.clone())); + nodes[0].router.expect_find_route(route.route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::spontaneous_empty(); - let params = route.route_params.clone().unwrap(); + let onion = RecipientOnionFields::spontaneous_empty(amount); + let params = route.route_params.clone(); let retry = Retry::Attempts(0); nodes[0].node.send_spontaneous_payment(preimage, onion, payment_id_1, params, retry).unwrap(); check_added_monitors(&nodes[0], 1); @@ -757,7 +807,7 @@ fn no_pending_leak_on_initial_send_failure() { nodes[0].node.peer_disconnected(node_b_id); nodes[1].node.peer_disconnected(node_a_id); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let payment_id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { ref err }, @@ -809,8 +859,8 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { let (payment_preimage_1, payment_hash_1, _, payment_id_1) = send_along_route(&nodes[0], route.clone(), &[&nodes[1], &nodes[2]], 1_000_000); - let route_params = route.route_params.unwrap().clone(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let route_params = route.route_params.clone(); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); check_added_monitors(&nodes[0], 1); @@ -895,15 +945,20 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { } => { assert_eq!(node_id, node_b_id); nodes[1].node.handle_error(node_a_id, msg); - check_closed_event(&nodes[1], 1, ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", - &node_b_id)) }, &[node_a_id], 100000); + let peer_msg = format!( + "Got a message for a channel from the wrong node! No such channel_id {} for the passed counterparty_node_id {}", + chan_id, node_b_id + ); + let reason = + ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(peer_msg) }; + check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); check_added_monitors(&nodes[1], 1); assert_eq!(nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().len(), 1); nodes[1].tx_broadcaster.clear(); }, _ => panic!("Unexpected event"), } - check_closed_broadcast!(nodes[1], false); + check_closed_broadcast(&nodes[1], 1, false); // Now claim the first payment, which should allow nodes[1] to claim the payment on-chain when // we close in a moment. @@ -987,7 +1042,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { nodes[1].node.timer_tick_occurred(); } - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); // Check that we cannot retry a fulfilled payment nodes[0] .node @@ -995,7 +1050,7 @@ fn do_retry_with_no_persist(confirm_before_reload: bool) { .unwrap_err(); // ...but if we send with a different PaymentId the payment should fly let id = PaymentId(payment_hash.0); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); nodes[0].node.send_payment_with_route(new_route.clone(), payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1101,18 +1156,19 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { } => { assert_eq!(node_id, node_b_id); nodes[1].node.handle_error(node_a_id, msg); - let msg = format!( - "Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", - &node_b_id + let peer_msg = format!( + "Got a message for a channel from the wrong node! No such channel_id {} for the passed counterparty_node_id {}", + chan_id, node_b_id ); - let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(msg) }; + let reason = + ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(peer_msg) }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); check_added_monitors(&nodes[1], 1); bs_commitment_tx = nodes[1].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); }, _ => panic!("Unexpected event"), } - check_closed_broadcast!(nodes[1], false); + check_closed_broadcast(&nodes[1], 1, false); // Now fail back the payment from nodes[2] to nodes[1]. This doesn't really matter as the // previous hop channel is already on-chain, but it makes nodes[2] willing to see additional @@ -1163,7 +1219,7 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { // If we attempt to retry prior to the HTLC-Timeout (or commitment transaction, for dust HTLCs) // confirming, we will fail as it's considered still-pending... let (new_route, _, _, _) = get_route_and_payment_hash!(nodes[0], nodes[2], amt); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); match nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id) { Err(RetryableSendFailure::DuplicatePayment) => {}, _ => panic!("Unexpected error"), @@ -1183,7 +1239,7 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { node_a_ser = nodes[0].node.encode(); // After the payment failed, we're free to send it again. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id).unwrap(); assert!(!nodes[0].node.get_and_clear_pending_msg_events().is_empty()); @@ -1200,13 +1256,13 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { // Now resend the payment, delivering the HTLC and actually claiming it this time. This ensures // the payment is not (spuriously) listed as still pending. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1], &nodes[2]]], amt, hash, payment_secret); claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); match nodes[0].node.send_payment_with_route(new_route.clone(), hash, onion, payment_id) { Err(RetryableSendFailure::DuplicatePayment) => {}, _ => panic!("Unexpected error"), @@ -1228,7 +1284,7 @@ fn do_test_completed_payment_not_retryable_on_reload(use_dust: bool) { reconnect_nodes(ReconnectArgs::new(&nodes[0], &nodes[1])); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt); match nodes[0].node.send_payment_with_route(new_route, hash, onion, payment_id) { Err(RetryableSendFailure::DuplicatePayment) => {}, _ => panic!("Unexpected error"), @@ -1277,7 +1333,7 @@ fn do_test_dup_htlc_onchain_doesnt_fail_on_reload( .node .force_close_broadcasting_latest_txn(&chan_id, &node_b_id, message.clone()) .unwrap(); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, reason, &[node_b_id], 100000); @@ -1493,41 +1549,31 @@ fn get_ldk_payment_preimage() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); create_announced_chan_between_nodes(&nodes, 0, 1); let amt_msat = 60_000; let expiry_secs = 60 * 60; - let (payment_hash, payment_secret) = - nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs, None).unwrap(); + let (payment_hash, payment_secret, _) = + nodes[1].node.create_inbound_payment(Some(amt_msat), expiry_secs, None, None).unwrap(); let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) .unwrap(); - let scorer = test_utils::TestScorer::new(); - let keys_manager = test_utils::TestKeysInterface::new(&[0u8; 32], Network::Testnet); - let random_seed_bytes = keys_manager.get_secure_random_bytes(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); - let first_hops = nodes[0].node.list_usable_channels(); - let route = get_route( - &node_a_id, - &route_params, - &nodes[0].network_graph.read_only(), - Some(&first_hops.iter().collect::<Vec<_>>()), - nodes[0].logger, - &scorer, - &Default::default(), - &random_seed_bytes, - ); - let onion = RecipientOnionFields::secret_only(payment_secret); + let route = get_route(&nodes[0], &route_params).unwrap(); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); - nodes[0].node.send_payment_with_route(route.unwrap(), payment_hash, onion, id).unwrap(); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); - // Make sure to use `get_payment_preimage` - let preimage = Some(nodes[1].node.get_payment_preimage(payment_hash, payment_secret).unwrap()); + let preimage = Some( + nodes[1] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap(), + ); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(events.len(), 1); let event = events.pop().unwrap(); @@ -1537,6 +1583,182 @@ fn get_ldk_payment_preimage() { claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage.unwrap())); } +#[derive(Clone, Copy)] +enum PaymentMetadataSource { + Bolt11Invoice, + CreateInboundPayment, + CreateInboundPaymentForHash, +} + +fn do_payment_metadata_end_to_end(source: PaymentMetadataSource) { + // Generate a payment under each source, send a payment for it from another node, and verify + // that the `PaymentClaimable` event sees the (decrypted) payment_metadata that was originally + // provided. For sources which generate the preimage on our behalf, also check that + // `get_payment_preimage_decrypt_metadata` recovers the preimage and decrypts the metadata. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + create_announced_chan_between_nodes(&nodes, 0, 1); + + let amt_msat = 50_000; + let node_b_id = nodes[1].node.get_our_node_id(); + let plaintext_metadata = vec![0xde, 0xad, 0xbe, 0xef, 0x01, 0x02, 0x03, 0x04, 0x05]; + + // Whenever LDK is computing the preimage itself (the `Bolt11Invoice` and + // `CreateInboundPayment` cases), `encrypted_metadata` holds the encrypted bytes so we can feed + // them back into `get_payment_preimage_decrypt_metadata` below. For the user-hash case we know + // the preimage up front so we stash it in `provided_preimage` instead. + let (payment_hash, payment_secret, encrypted_metadata, provided_preimage) = match source { + PaymentMetadataSource::Bolt11Invoice => { + let description = + Bolt11InvoiceDescription::Direct(Description::new("test".to_string()).unwrap()); + let invoice_params = Bolt11InvoiceParameters { + amount_msats: Some(amt_msat), + description, + payment_metadata: Some(plaintext_metadata.clone()), + ..Default::default() + }; + let invoice = nodes[1].node.create_bolt11_invoice(invoice_params).unwrap(); + let payment_hash = invoice.payment_hash(); + let payment_secret = *invoice.payment_secret(); + let encrypted_metadata = invoice.payment_metadata().unwrap().clone(); + // The encryption must produce different bytes than the plaintext for this test to be + // meaningful (otherwise the decryption could be a no-op and we wouldn't notice). + assert_ne!(encrypted_metadata, plaintext_metadata); + + nodes[0] + .node + .pay_for_bolt11_invoice( + &invoice, + PaymentId(payment_hash.0), + None, + OptionalBolt11PaymentParams::default(), + ) + .unwrap(); + (payment_hash, payment_secret, encrypted_metadata, None) + }, + PaymentMetadataSource::CreateInboundPayment => { + let (payment_hash, payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment( + Some(amt_msat), + 7200, + None, + Some(plaintext_metadata.clone()), + ) + .unwrap(); + let encrypted_metadata = encrypted_metadata.unwrap(); + assert_ne!(encrypted_metadata, plaintext_metadata); + + let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) + .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) + .unwrap(); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amt_msat); + let route = get_route(&nodes[0], &route_params).unwrap(); + let onion = RecipientOnionFields { + payment_secret: Some(payment_secret), + payment_metadata: Some(encrypted_metadata.clone()), + custom_tlvs: vec![], + total_mpp_amount_msat: amt_msat, + }; + nodes[0] + .node + .send_payment_with_route(route, payment_hash, onion, PaymentId(payment_hash.0)) + .unwrap(); + (payment_hash, payment_secret, encrypted_metadata, None) + }, + PaymentMetadataSource::CreateInboundPaymentForHash => { + let payment_preimage = PaymentPreimage([0x77; 32]); + let payment_hash = PaymentHash(Sha256::hash(&payment_preimage.0).to_byte_array()); + let (payment_secret, encrypted_metadata) = nodes[1] + .node + .create_inbound_payment_for_hash( + payment_hash, + Some(amt_msat), + 7200, + None, + Some(plaintext_metadata.clone()), + ) + .unwrap(); + let encrypted_metadata = encrypted_metadata.unwrap(); + assert_ne!(encrypted_metadata, plaintext_metadata); + + let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) + .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) + .unwrap(); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, amt_msat); + let route = get_route(&nodes[0], &route_params).unwrap(); + let onion = RecipientOnionFields { + payment_secret: Some(payment_secret), + payment_metadata: Some(encrypted_metadata.clone()), + custom_tlvs: vec![], + total_mpp_amount_msat: amt_msat, + }; + nodes[0] + .node + .send_payment_with_route(route, payment_hash, onion, PaymentId(payment_hash.0)) + .unwrap(); + (payment_hash, payment_secret, encrypted_metadata, Some(payment_preimage)) + }, + }; + + check_added_monitors(&nodes[0], 1); + + // For sources where LDK derived the preimage, exercise + // `get_payment_preimage_decrypt_metadata`: it must recover the preimage *and* decrypt the + // metadata buffer in place. For the user-hash source we just use the preimage we picked. + let preimage = if let Some(preimage) = provided_preimage { + preimage + } else { + let mut decrypted_metadata = encrypted_metadata.clone(); + let preimage = nodes[1] + .node + .get_payment_preimage_decrypt_metadata( + payment_hash, + payment_secret, + Some(decrypted_metadata.as_mut_slice()), + ) + .unwrap(); + assert_eq!(decrypted_metadata, plaintext_metadata); + preimage + }; + assert_eq!(PaymentHash(Sha256::hash(&preimage.0).to_byte_array()), payment_hash); + + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev = events.pop().unwrap(); + let path = &[&nodes[1]]; + let mut args = PassAlongPathArgs::new(&nodes[0], path, amt_msat, payment_hash, ev) + .with_payment_secret(payment_secret) + .with_payment_metadata(plaintext_metadata.clone()); + // Only set the expected preimage when LDK is responsible for surfacing it on the receiver + // side (i.e. LDK-derived hashes). For user-supplied hashes, `PaymentClaimable` carries + // `payment_preimage: None`. + if provided_preimage.is_none() { + args = args.with_payment_preimage(preimage); + } + do_pass_along_path(args); + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage)); +} + +#[test] +fn payment_metadata_end_to_end_bolt11_invoice() { + do_payment_metadata_end_to_end(PaymentMetadataSource::Bolt11Invoice); +} + +#[test] +fn payment_metadata_end_to_end_create_inbound_payment() { + do_payment_metadata_end_to_end(PaymentMetadataSource::CreateInboundPayment); +} + +#[test] +fn payment_metadata_end_to_end_create_inbound_payment_for_hash() { + do_payment_metadata_end_to_end(PaymentMetadataSource::CreateInboundPaymentForHash); +} + #[test] fn sent_probe_is_probe_of_sending_node() { let chanmon_cfgs = create_chanmon_cfgs(3); @@ -1557,17 +1779,32 @@ fn sent_probe_is_probe_of_sending_node() { // Then build an actual two-hop probing path let (route, _, _, _) = get_route_and_payment_hash!(&nodes[0], nodes[2], 100_000); - match nodes[0].node.send_probe(route.paths[0].clone()) { - Ok((payment_hash, payment_id)) => { - assert!(nodes[0].node.payment_is_probe(&payment_hash, &payment_id)); - assert!(!nodes[1].node.payment_is_probe(&payment_hash, &payment_id)); - assert!(!nodes[2].node.payment_is_probe(&payment_hash, &payment_id)); - }, - _ => panic!(), - } + let (payment_hash, payment_id) = nodes[0].node.send_probe(route.paths[0].clone()).unwrap(); + assert!(nodes[0].node.payment_is_probe(&payment_hash, &payment_id)); + assert!(!nodes[1].node.payment_is_probe(&payment_hash, &payment_id)); + assert!(!nodes[2].node.payment_is_probe(&payment_hash, &payment_id)); + assert!(matches!( + nodes[0].node.list_recent_payments().as_slice(), + [RecentPaymentDetails::Pending { + payment_id: listed_payment_id, + payment_hash: listed_payment_hash, + is_probe: true, + .. + }] if *listed_payment_id == payment_id && *listed_payment_hash == payment_hash + )); get_htlc_update_msgs(&nodes[0], &node_b_id); check_added_monitors(&nodes[0], 1); + + nodes[0].node.abandon_payment(payment_id); + assert!(matches!( + nodes[0].node.list_recent_payments().as_slice(), + [RecentPaymentDetails::Abandoned { + payment_id: listed_payment_id, + payment_hash: listed_payment_hash, + is_probe: true, + }] if *listed_payment_id == payment_id && *listed_payment_hash == payment_hash + )); } #[test] @@ -1680,7 +1917,7 @@ fn onchain_failed_probe_yields_event() { // Node A, which after 6 confirmations should result in a probe failure event. let bs_txn = get_local_commitment_txn!(nodes[1], chan_id); confirm_transaction(&nodes[0], &bs_txn[0]); - check_closed_broadcast!(&nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); check_added_monitors(&nodes[0], 0); @@ -1710,7 +1947,8 @@ fn preflight_probes_yield_event_skip_private_hop() { // We alleviate the HTLC max-in-flight limit, as otherwise we'd always be limited through that. let mut config = test_default_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let config = Some(config); let configs = [config.clone(), config.clone(), config.clone(), config.clone(), config]; @@ -1757,7 +1995,8 @@ fn preflight_probes_yield_event() { // We alleviate the HTLC max-in-flight limit, as otherwise we'd always be limited through that. let mut config = test_default_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let config = Some(config); let configs = [config.clone(), config.clone(), config.clone(), config]; @@ -1789,8 +2028,18 @@ fn preflight_probes_yield_event() { let route_params = RouteParameters::from_payment_params_and_value(payment_params, recv_value); let res = nodes[0].node.send_preflight_probes(route_params, None).unwrap(); + // Path ordering depends on outbound SCID selection. Determine which res entry corresponds + // to which path by comparing the alias SCIDs of the two channels. + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + let chans = nodes[0].node.list_usable_channels(); + let chan_to_b = chans.iter().find(|c| c.counterparty.node_id == node_b_id).unwrap(); + let chan_to_c = chans.iter().find(|c| c.counterparty.node_id == node_c_id).unwrap(); + let b_first = chan_to_b.get_outbound_payment_scid() < chan_to_c.get_outbound_payment_scid(); + let (hash_b, hash_c) = if b_first { (res[0].0, res[1].0) } else { (res[1].0, res[0].0) }; + let expected_route: &[(&[&Node], PaymentHash)] = - &[(&[&nodes[1], &nodes[3]], res[0].0), (&[&nodes[2], &nodes[3]], res[1].0)]; + &[(&[&nodes[1], &nodes[3]], hash_b), (&[&nodes[2], &nodes[3]], hash_c)]; assert_eq!(res.len(), expected_route.len()); @@ -1808,7 +2057,8 @@ fn preflight_probes_yield_event_and_skip() { // We alleviate the HTLC max-in-flight limit, as otherwise we'd always be limited through that. let mut config = test_default_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let config = Some(config); let configs = @@ -1874,7 +2124,7 @@ fn claimed_send_payment_idempotent() { () => { // If we try to resend a new payment with a different payment_hash but with the same // payment_id, it should be rejected. - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); let send_result = nodes[0].node.send_payment_with_route(route.clone(), hash_b, onion, payment_id); match send_result { @@ -1886,9 +2136,9 @@ fn claimed_send_payment_idempotent() { // also be rejected. let send_result = nodes[0].node.send_spontaneous_payment( None, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(100_000), payment_id, - route.route_params.clone().unwrap(), + route.route_params.clone(), Retry::Attempts(0), ); match send_result { @@ -1930,7 +2180,7 @@ fn claimed_send_payment_idempotent() { nodes[0].node.timer_tick_occurred(); } - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); nodes[0].node.send_payment_with_route(route, hash_b, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], 100_000, hash_b, second_payment_secret); @@ -1957,7 +2207,7 @@ fn abandoned_send_payment_idempotent() { () => { // If we try to resend a new payment with a different payment_hash but with the same // payment_id, it should be rejected. - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); let send_result = nodes[0].node.send_payment_with_route(route.clone(), hash_b, onion, payment_id); match send_result { @@ -1969,9 +2219,9 @@ fn abandoned_send_payment_idempotent() { // also be rejected. let send_result = nodes[0].node.send_spontaneous_payment( None, - RecipientOnionFields::spontaneous_empty(), + RecipientOnionFields::spontaneous_empty(100_000), payment_id, - route.route_params.clone().unwrap(), + route.route_params.clone(), Retry::Attempts(0), ); match send_result { @@ -1999,13 +2249,43 @@ fn abandoned_send_payment_idempotent() { // However, we can reuse the PaymentId immediately after we `abandon_payment` upon passing the // failed payment back. - let onion = RecipientOnionFields::secret_only(second_payment_secret); + let onion = RecipientOnionFields::secret_only(second_payment_secret, 100_000); nodes[0].node.send_payment_with_route(route, hash_b, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); pass_along_route(&nodes[0], &[&[&nodes[1]]], 100_000, hash_b, second_payment_secret); claim_payment(&nodes[0], &[&nodes[1]], second_payment_preimage); } +#[test] +fn abandoned_payment_fulfilled_preserves_fee_paid_msat() { + // Previously, if we abandoned a payment with HTLCs in-flight and the payment eventually + // succeeded, we would set the `Event::PaymentSent::fee_paid_msat` to None, even though we had + // docs guaranteeing that it would always be Some after 0.0.103. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + create_announced_chan_between_nodes(&nodes, 0, 1); + create_announced_chan_between_nodes(&nodes, 1, 2); + + let amt_msat = 10_000_000; + let (route, payment_hash, payment_preimage, payment_secret) = + get_route_and_payment_hash!(&nodes[0], nodes[2], amt_msat); + let payment_id = PaymentId(payment_hash.0); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + check_added_monitors(&nodes[0], 1); + + let path: &[&Node] = &[&nodes[1], &nodes[2]]; + pass_along_route(&nodes[0], &[path], amt_msat, payment_hash, payment_secret); + + nodes[0].node.abandon_payment(payment_id); + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + + claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], payment_preimage)); +} + #[derive(PartialEq)] enum InterceptTest { Forward, @@ -2063,7 +2343,11 @@ fn test_trivial_inflight_htlc_tracking() { } let pending_payments = nodes[0].node.list_recent_payments(); assert_eq!(pending_payments.len(), 1); - let details = RecentPaymentDetails::Fulfilled { payment_hash: Some(payment_hash), payment_id }; + let details = RecentPaymentDetails::Fulfilled { + payment_hash: Some(payment_hash), + payment_id, + fee_paid_msat: Some(1000), + }; assert_eq!(pending_payments[0], details); // Remove fulfilled payment @@ -2084,7 +2368,7 @@ fn test_trivial_inflight_htlc_tracking() { let chan_1_used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel_1.funding().get_short_channel_id().unwrap(), + channel_1.context().outbound_scid_alias(), ); // First hop accounts for expected 1000 msat fee assert_eq!(chan_1_used_liquidity, Some(501000)); @@ -2105,7 +2389,13 @@ fn test_trivial_inflight_htlc_tracking() { } let pending_payments = nodes[0].node.list_recent_payments(); assert_eq!(pending_payments.len(), 1); - let details = RecentPaymentDetails::Pending { payment_id, payment_hash, total_msat: 500000 }; + let details = RecentPaymentDetails::Pending { + payment_id, + payment_hash, + total_msat: 500000, + pending_fee_msat: Some(1000), + is_probe: false, + }; assert_eq!(pending_payments[0], details); // Now, let's claim the payment. This should result in the used liquidity to return `None`. @@ -2162,17 +2452,17 @@ fn test_holding_cell_inflight_htlcs() { let (route, payment_hash_1, _, payment_secret_1) = get_route_and_payment_hash!(nodes[0], nodes[1], 1000000); - let (_, payment_hash_2, payment_secret_2) = get_payment_preimage_hash!(nodes[1]); + let (_, payment_hash_2, payment_secret_2) = get_payment_preimage_hash(&nodes[1], None, None); // Queue up two payments - one will be delivered right away, one immediately goes into the // holding cell as nodes[0] is AwaitingRAA. { - let onion = RecipientOnionFields::secret_only(payment_secret_1); + let onion = RecipientOnionFields::secret_only(payment_secret_1, 1000000); let id = PaymentId(payment_hash_1.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash_1, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 1000000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 0); @@ -2189,7 +2479,7 @@ fn test_holding_cell_inflight_htlcs() { let used_liquidity = inflight_htlcs.used_liquidity_msat( &NodeId::from_pubkey(&node_a_id), &NodeId::from_pubkey(&node_b_id), - channel.funding().get_short_channel_id().unwrap(), + channel.context().outbound_scid_alias(), ); assert_eq!(used_liquidity, Some(2000000)); @@ -2228,9 +2518,6 @@ fn do_test_intercepted_payment(test: InterceptTest) { let node_b_id = nodes[1].node.get_our_node_id(); let node_c_id = nodes[2].node.get_our_node_id(); - let scorer = test_utils::TestScorer::new(); - let random_seed_bytes = chanmon_cfgs[0].keys_manager.get_secure_random_bytes(); - let _ = create_announced_chan_between_nodes(&nodes, 0, 1).2; let amt_msat = 100_000; @@ -2248,21 +2535,11 @@ fn do_test_intercepted_payment(test: InterceptTest) { .with_bolt11_features(nodes[2].node.bolt11_invoice_features()) .unwrap(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); - let route = get_route( - &node_a_id, - &route_params, - &nodes[0].network_graph.read_only(), - None, - nodes[0].logger, - &scorer, - &Default::default(), - &random_seed_bytes, - ) - .unwrap(); + let route = get_route(&nodes[0], &route_params).unwrap(); - let (hash, payment_secret) = - nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let (hash, payment_secret, _) = + nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None, None).unwrap(); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment_with_route(route.clone(), hash, onion, id).unwrap(); let payment_event = { @@ -2306,7 +2583,7 @@ fn do_test_intercepted_payment(test: InterceptTest) { let unknown_chan_id_err = nodes[1].node.forward_intercepted_htlc(intercept_id, &chan_id, node_c_id, outbound_amt); let err = format!( - "Channel with id {} not found for the passed counterparty node_id {}", + "No such channel_id {} for the passed counterparty_node_id {}", chan_id, node_c_id, ); assert_eq!(unknown_chan_id_err, Err(APIError::ChannelUnavailable { err })); @@ -2370,7 +2647,12 @@ fn do_test_intercepted_payment(test: InterceptTest) { do_commitment_signed_dance(&nodes[2], &nodes[1], commitment, false, true); expect_and_process_pending_htlcs(&nodes[2], false); - let preimage = Some(nodes[2].node.get_payment_preimage(hash, payment_secret).unwrap()); + let preimage = Some( + nodes[2] + .node + .get_payment_preimage_decrypt_metadata(hash, payment_secret, None) + .unwrap(), + ); expect_payment_claimable!(&nodes[2], hash, payment_secret, amt_msat, preimage, node_c_id); let path: &[&[_]] = &[&[&nodes[1], &nodes[2]]]; @@ -2450,11 +2732,12 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { HTLCInterceptionFlags::ToInterceptSCIDs as u8; intercept_forwards_config .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel = max_in_flight_percent; + .announced_channel_max_inbound_htlc_value_in_flight_percentage = max_in_flight_percent; let mut underpay_config = test_default_channel_config(); underpay_config.channel_config.accept_underpaying_htlcs = true; - underpay_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = - max_in_flight_percent; + underpay_config + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage = max_in_flight_percent; let configs = [None, Some(intercept_forwards_config), Some(underpay_config)]; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &configs); @@ -2495,10 +2778,10 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { .with_bolt11_features(nodes[2].node.bolt11_invoice_features()) .unwrap(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); - let (payment_hash, payment_secret) = - nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None).unwrap(); + let (payment_hash, payment_secret, _) = + nodes[2].node.create_inbound_payment(Some(amt_msat), 60 * 60, None, None).unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(0)).unwrap(); @@ -2551,8 +2834,10 @@ fn do_accept_underpaying_htlcs_config(num_mpp_parts: usize) { } // Claim the payment and check that the skimmed fee is as expected. - let payment_preimage = - nodes[2].node.get_payment_preimage(payment_hash, payment_secret).unwrap(); + let payment_preimage = nodes[2] + .node + .get_payment_preimage_decrypt_metadata(payment_hash, payment_secret, None) + .unwrap(); let events = nodes[2].node.get_and_clear_pending_events(); assert_eq!(events.len(), 1); match events[0] { @@ -2710,7 +2995,7 @@ fn do_automatic_retries(test: AutoRetry) { if test == AutoRetry::Success { // Test that we can succeed on the first retry. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); let retry = Retry::Attempts(1); nodes[0].node.send_payment(hash, onion, id, route_params, retry).unwrap(); @@ -2736,7 +3021,7 @@ fn do_automatic_retries(test: AutoRetry) { preimage, )); } else if test == AutoRetry::Spontaneous { - let onion = RecipientOnionFields::spontaneous_empty(); + let onion = RecipientOnionFields::spontaneous_empty(amt_msat); let id = PaymentId(hash.0); nodes[0] .node @@ -2761,7 +3046,7 @@ fn do_automatic_retries(test: AutoRetry) { claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage)); } else if test == AutoRetry::FailAttempts { // Ensure ChannelManager will not retry a payment if it has run out of payment attempts. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); pass_failed_attempt_with_retry_along_path!(channel_id_2, true); @@ -2782,7 +3067,7 @@ fn do_automatic_retries(test: AutoRetry) { #[cfg(feature = "std")] { // Ensure ChannelManager will not retry a payment if it times out due to Retry::Timeout. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); let retry = Retry::Timeout(Duration::from_secs(60)); nodes[0].node.send_payment(hash, onion, id, route_params, retry).unwrap(); @@ -2810,7 +3095,7 @@ fn do_automatic_retries(test: AutoRetry) { } else if test == AutoRetry::FailOnRestart { // Ensure ChannelManager will not retry a payment after restart, even if there were retry // attempts remaining prior to restart. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(2)).unwrap(); pass_failed_attempt_with_retry_along_path!(channel_id_2, true); @@ -2844,7 +3129,7 @@ fn do_automatic_retries(test: AutoRetry) { _ => panic!("Unexpected event"), } } else if test == AutoRetry::FailOnRetry { - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); pass_failed_attempt_with_retry_along_path!(channel_id_2, true); @@ -2939,7 +3224,7 @@ fn auto_retry_partial_failure() { blinded_tail: None, }, ], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; nodes[0].router.expect_find_route(route_params.clone(), Ok(send_route)); @@ -2977,7 +3262,7 @@ fn auto_retry_partial_failure() { blinded_tail: None, }, ], - route_params: Some(retry_1_params.clone()), + route_params: retry_1_params.clone(), }; nodes[0].router.expect_find_route(retry_1_params.clone(), Ok(retry_1_route)); @@ -3001,12 +3286,12 @@ fn auto_retry_partial_failure() { }], blinded_tail: None, }], - route_params: Some(retry_2_params.clone()), + route_params: retry_2_params.clone(), }; nodes[0].router.expect_find_route(retry_2_params, Ok(retry_2_route)); // Send a payment that will partially fail on send, then partially fail on retry, then succeed. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(3)).unwrap(); @@ -3164,11 +3449,11 @@ fn auto_retry_zero_attempts_send_error() { }], blinded_tail: None, }], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; nodes[0].router.expect_find_route(route_params.clone(), Ok(send_route)); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(0)).unwrap(); @@ -3216,7 +3501,7 @@ fn fails_paying_after_rejected_by_payee() { .unwrap(); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); check_added_monitors(&nodes[0], 1); @@ -3241,7 +3526,12 @@ fn retry_multi_path_single_failed_payment() { // Tests that we can/will retry after a single path of an MPP payment failed immediately let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_b_id = nodes[1].node.get_our_node_id(); @@ -3297,18 +3587,18 @@ fn retry_multi_path_single_failed_payment() { blinded_tail: None, }, ], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); // On retry, split the payment across both channels. route.paths[0].hops[0].fee_msat = 50_000_001; route.paths[1].hops[0].fee_msat = 50_000_000; - let mut pay_params = route.route_params.clone().unwrap().payment_params; + let mut pay_params = route.route_params.clone().payment_params; pay_params.previously_failed_channels.push(chans[1].short_channel_id.unwrap()); let mut retry_params = RouteParameters::from_payment_params_and_value(pay_params, 100_000_000); retry_params.max_total_routing_fee_msat = None; - route.route_params = Some(retry_params.clone()); + route.route_params = retry_params.clone(); nodes[0].router.expect_find_route(retry_params, Ok(route.clone())); { @@ -3332,7 +3622,7 @@ fn retry_multi_path_single_failed_payment() { scorer.expect_usage(chans[1].short_channel_id.unwrap(), usage); } - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); @@ -3360,7 +3650,12 @@ fn immediate_retry_on_failure() { // Tests that we can/will retry immediately after a failure let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_b_id = nodes[1].node.get_our_node_id(); @@ -3399,7 +3694,7 @@ fn immediate_retry_on_failure() { }], blinded_tail: None, }], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); // On retry, split the payment across both channels. @@ -3410,10 +3705,10 @@ fn immediate_retry_on_failure() { let mut pay_params = route_params.payment_params.clone(); pay_params.previously_failed_channels.push(chans[0].short_channel_id.unwrap()); let retry_params = RouteParameters::from_payment_params_and_value(pay_params, amt_msat); - route.route_params = Some(retry_params.clone()); + route.route_params = retry_params.clone(); nodes[0].router.expect_find_route(retry_params, Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); @@ -3534,9 +3829,9 @@ fn no_extra_retries_on_back_to_back_fail() { blinded_tail: None, }, ], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; - route.route_params.as_mut().unwrap().max_total_routing_fee_msat = None; + route.route_params.max_total_routing_fee_msat = None; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); let mut second_payment_params = route_params.payment_params.clone(); second_payment_params.previously_failed_channels = vec![chan_2_scid, chan_2_scid]; @@ -3546,13 +3841,13 @@ fn no_extra_retries_on_back_to_back_fail() { let mut retry_params = RouteParameters::from_payment_params_and_value(second_payment_params, amt_msat); retry_params.max_total_routing_fee_msat = None; - route.route_params = Some(retry_params.clone()); + route.route_params = retry_params.clone(); nodes[0].router.expect_find_route(retry_params, Ok(route.clone())); // We can't use the commitment_signed_dance macro helper because in this test we'll be sending // two HTLCs back-to-back on the same channel, and the macro only expects to handle one at a // time. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); @@ -3779,7 +4074,7 @@ fn test_simple_partial_retry() { blinded_tail: None, }, ], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -3791,13 +4086,13 @@ fn test_simple_partial_retry() { let mut retry_params = RouteParameters::from_payment_params_and_value(second_payment_params, amt_msat / 2); retry_params.max_total_routing_fee_msat = None; - route.route_params = Some(retry_params.clone()); + route.route_params = retry_params.clone(); nodes[0].router.expect_find_route(retry_params, Ok(route.clone())); // We can't use the commitment_signed_dance macro helper because in this test we'll be sending // two HTLCs back-to-back on the same channel, and the macro only expects to handle one at a // time. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment(payment_hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); let first_htlc = SendEvent::from_node(&nodes[0]); @@ -3995,11 +4290,11 @@ fn test_threaded_payment_retries() { blinded_tail: None, }, ], - route_params: Some(route_params.clone()), + route_params: route_params.clone(), }; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(payment_hash.0); let retry = Retry::Attempts(0xdeadbeef); nodes[0].node.send_payment(payment_hash, onion, id, route_params.clone(), retry).unwrap(); @@ -4018,7 +4313,7 @@ fn test_threaded_payment_retries() { // from here on out, the retry `RouteParameters` amount will be amt/1000 route_params.final_value_msat /= 1000; - route.route_params = Some(route_params.clone()); + route.route_params = route_params.clone(); route.paths.pop(); let end_time = Instant::now() + Duration::from_secs(1); @@ -4072,7 +4367,7 @@ fn test_threaded_payment_retries() { previously_failed_channels.clone(); new_route_params.max_total_routing_fee_msat.as_mut().map(|m| *m -= 100_000); route.paths[0].hops[1].short_channel_id += 1; - route.route_params = Some(new_route_params.clone()); + route.route_params = new_route_params.clone(); nodes[0].router.expect_find_route(new_route_params, Ok(route.clone())); let bs_fail_updates = get_htlc_update_msgs(&nodes[1], &node_a_id); @@ -4261,17 +4556,13 @@ fn do_claim_from_closed_chan(fail_payment: bool) { // CLTVs on the paths to different value resulting in a different claim deadline. let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let legacy_cfg = test_legacy_channel_config(); - let node_chanmgrs = create_node_chanmgrs( - 4, - &node_cfgs, - &[ - Some(legacy_cfg.clone()), - Some(legacy_cfg.clone()), - Some(legacy_cfg.clone()), - Some(legacy_cfg), - ], - ); + let mut legacy_cfg = test_legacy_channel_config(); + // Set the percentage to the default value at the time this test was written + legacy_cfg + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 10; + let configs: [Option<UserConfig>; 4] = core::array::from_fn(|_| Some(legacy_cfg.clone())); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let mut nodes = create_network(4, &node_cfgs, &node_chanmgrs); let node_a_id = nodes[0].node.get_our_node_id(); @@ -4284,7 +4575,7 @@ fn do_claim_from_closed_chan(fail_payment: bool) { let chan_bd = create_announced_chan_between_nodes_with_value(&nodes, 1, 3, 1_000_000, 0).2; create_announced_chan_between_nodes(&nodes, 2, 3); - let (payment_preimage, hash, payment_secret) = get_payment_preimage_hash!(nodes[3]); + let (payment_preimage, hash, payment_secret) = get_payment_preimage_hash(&nodes[3], None, None); let payment_params = PaymentParameters::from_node_id(node_d_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) .unwrap(); @@ -4310,7 +4601,7 @@ fn do_claim_from_closed_chan(fail_payment: bool) { let final_cltv = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + 8 + 1; nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let id = PaymentId(hash.0); nodes[0].node.send_payment(hash, onion, id, route_params, Retry::Attempts(1)).unwrap(); @@ -4479,9 +4770,10 @@ fn do_test_custom_tlvs(spontaneous: bool, even_tlvs: bool, known_tlvs: bool) { payment_secret: if spontaneous { None } else { Some(payment_secret) }, payment_metadata: None, custom_tlvs: custom_tlvs.clone(), + total_mpp_amount_msat: amt_msat, }; if spontaneous { - let params = route.route_params.unwrap(); + let params = route.route_params; let retry = Retry::Attempts(0); nodes[0].node.send_spontaneous_payment(Some(preimage), onion, id, params, retry).unwrap(); } else { @@ -4556,10 +4848,10 @@ fn test_retry_custom_tlvs() { // Initiate the payment let id = PaymentId(hash.0); - let mut route_params = route.route_params.clone().unwrap(); + let mut route_params = route.route_params.clone(); let custom_tlvs = vec![((1 << 16) + 1, vec![0x42u8; 16])]; - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let onion = onion.with_custom_tlvs(RecipientCustomTlvs::new(custom_tlvs.clone()).unwrap()); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); @@ -4596,7 +4888,7 @@ fn test_retry_custom_tlvs() { // Retry the payment and make sure it succeeds let chan_2_scid = chan_2_update.contents.short_channel_id; route_params.payment_params.previously_failed_channels.push(chan_2_scid); - route.route_params = Some(route_params.clone()); + route.route_params = route_params.clone(); nodes[0].router.expect_find_route(route_params, Ok(route)); nodes[0].node.process_pending_htlc_forwards(); check_added_monitors(&nodes[0], 1); @@ -4655,7 +4947,12 @@ fn do_test_custom_tlvs_consistency( ) { let chanmon_cfgs = create_chanmon_cfgs(4); let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 10; + let configs: [Option<UserConfig>; 4] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let nodes = create_network(4, &node_cfgs, &node_chanmgrs); let node_a_id = nodes[0].node.get_our_node_id(); @@ -4682,7 +4979,7 @@ fn do_test_custom_tlvs_consistency( } }); - let (preimage, hash, payment_secret) = get_payment_preimage_hash!(&nodes[3]); + let (preimage, hash, payment_secret) = get_payment_preimage_hash(&nodes[3], None, None); let id = PaymentId([42; 32]); let amt_msat = 15_000_000; @@ -4691,6 +4988,7 @@ fn do_test_custom_tlvs_consistency( payment_secret: Some(payment_secret), payment_metadata: None, custom_tlvs: first_tlvs, + total_mpp_amount_msat: amt_msat, }; let session_privs = nodes[0].node.test_add_new_pending_payment(hash, onion.clone(), id, &route).unwrap(); @@ -4699,7 +4997,7 @@ fn do_test_custom_tlvs_consistency( let priv_a = session_privs[0]; nodes[0] .node - .test_send_payment_along_path(path_a, &hash, onion, amt_msat, cur_height, id, &None, priv_a) + .test_send_payment_along_path(path_a, &hash, onion, cur_height, id, &None, priv_a) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -4716,12 +5014,13 @@ fn do_test_custom_tlvs_consistency( payment_secret: Some(payment_secret), payment_metadata: None, custom_tlvs: second_tlvs, + total_mpp_amount_msat: amt_msat, }; let path_b = &route.paths[1]; let priv_b = session_privs[1]; nodes[0] .node - .test_send_payment_along_path(path_b, &hash, onion, amt_msat, cur_height, id, &None, priv_b) + .test_send_payment_along_path(path_b, &hash, onion, cur_height, id, &None, priv_b) .unwrap(); check_added_monitors(&nodes[0], 1); @@ -4806,7 +5105,8 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { let chain_mon; let mut config = test_default_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 50; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 50; let configs = [None, Some(config.clone()), Some(config.clone()), Some(config.clone())]; let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let node_d_reload; @@ -4825,10 +5125,20 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { // Pay more than half of each channel's max, requiring MPP let amt_msat = 750_000_000; - let (payment_preimage, payment_hash, payment_secret) = - get_payment_preimage_hash!(nodes[3], Some(amt_msat)); - let payment_id = PaymentId(payment_hash.0); let payment_metadata = vec![44, 49, 52, 142]; + let payment_preimage = PaymentPreimage([42; 32]); + let payment_hash: PaymentHash = payment_preimage.into(); + let (payment_secret, encrypted_metadata) = nodes[3] + .node + .create_inbound_payment_for_hash( + payment_hash, + Some(amt_msat), + 7200, + None, + Some(payment_metadata.clone()), + ) + .unwrap(); + let payment_id = PaymentId(payment_hash.0); let payment_params = PaymentParameters::from_node_id(node_d_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) @@ -4838,8 +5148,9 @@ fn do_test_payment_metadata_consistency(do_reload: bool, do_modify: bool) { // Send the MPP payment, delivering the updated commitment state to nodes[1]. let onion = RecipientOnionFields { payment_secret: Some(payment_secret), - payment_metadata: Some(payment_metadata), + payment_metadata: encrypted_metadata, custom_tlvs: vec![], + total_mpp_amount_msat: amt_msat, }; let retry = Retry::Attempts(1); nodes[0].node.send_payment(payment_hash, onion, payment_id, route_params, retry).unwrap(); @@ -5002,7 +5313,7 @@ fn test_htlc_forward_considers_anchor_outputs_value() { create_announced_chan_between_nodes_with_value(&nodes, 1, 2, CHAN_AMT, PUSH_MSAT); let channel_reserve_msat = - get_holder_selected_channel_reserve_satoshis(CHAN_AMT, &config) * 1000; + get_holder_selected_channel_reserve_satoshis(CHAN_AMT, 0, &config, false).unwrap() * 1000; let commitment_fee_msat = chan_utils::commit_tx_fee_sat( *nodes[1].fee_estimator.sat_per_kw.lock().unwrap(), 2, @@ -5033,7 +5344,10 @@ fn test_htlc_forward_considers_anchor_outputs_value() { nodes[2], sendable_balance_msat + anchor_outpus_value_msat ); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only( + payment_secret, + sendable_balance_msat + anchor_outpus_value_msat, + ); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5098,7 +5412,7 @@ fn peel_payment_onion_custom_tlvs() { let payment_params = PaymentParameters::for_keysend(node_b_id, TEST_FINAL_CLTV, false); let route_params = RouteParameters::from_payment_params_and_value(payment_params, amt_msat); let route = functional_test_utils::get_route(&nodes[0], &route_params).unwrap(); - let mut recipient_onion = RecipientOnionFields::spontaneous_empty() + let mut recipient_onion = RecipientOnionFields::spontaneous_empty(amt_msat) .with_custom_tlvs(RecipientCustomTlvs::new(vec![(414141, vec![42; 1200])]).unwrap()); let prng_seed = chanmon_cfgs[0].keys_manager.get_secure_random_bytes(); let session_priv = SecretKey::from_slice(&prng_seed[..]).expect("RNG is busted"); @@ -5109,7 +5423,6 @@ fn peel_payment_onion_custom_tlvs() { &secp_ctx, &route.paths[0], &session_priv, - amt_msat, &recipient_onion, nodes[0].best_block_info().1, &payment_hash, @@ -5162,7 +5475,8 @@ fn test_non_strict_forwarding() { let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); let mut config = test_legacy_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let configs = [Some(config.clone()), Some(config.clone()), Some(config)]; let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &configs); @@ -5193,7 +5507,7 @@ fn test_non_strict_forwarding() { for i in 0..4 { let (payment_preimage, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(payment_value), None); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_value); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -5232,7 +5546,7 @@ fn test_non_strict_forwarding() { // Send a 5th payment which will fail. let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[2], Some(payment_value), None); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_value); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); @@ -5289,11 +5603,11 @@ fn remove_pending_outbounds_on_buggy_router() { // Extend the path by itself, essentially simulating route going through same channel twice let cloned_hops = route.paths[0].hops.clone(); route.paths[0].hops.extend_from_slice(&cloned_hops); - let route_params = route.route_params.clone().unwrap(); + let route_params = route.route_params.clone(); nodes[0].router.expect_find_route(route_params.clone(), Ok(route.clone())); // Send the payment with one retry allowed, but the payment should still fail - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); let retry = Retry::Attempts(1); nodes[0].node.send_payment(payment_hash, onion, payment_id, route_params, retry).unwrap(); let events = nodes[0].node.get_and_clear_pending_events(); @@ -5348,40 +5662,6 @@ fn remove_pending_outbound_probe_on_buggy_path() { assert!(nodes[0].node.list_recent_payments().is_empty()); } -#[test] -fn pay_route_without_params() { - // Make sure we can use ChannelManager::send_payment_with_route to pay a route where - // Route::route_parameters is None. - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - - let node_b_id = nodes[1].node.get_our_node_id(); - - create_announced_chan_between_nodes(&nodes, 0, 1); - - let amt_msat = 10_000; - let payment_params = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) - .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) - .unwrap(); - let (mut route, hash, preimage, payment_secret) = - get_route_and_payment_hash!(nodes[0], nodes[1], payment_params, amt_msat); - route.route_params.take(); - - let onion = RecipientOnionFields::secret_only(payment_secret); - let id = PaymentId(hash.0); - nodes[0].node.send_payment_with_route(route, hash, onion, id).unwrap(); - - check_added_monitors(&nodes[0], 1); - let mut events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(events.len(), 1); - let node_1_msgs = remove_first_msg_event_to_node(&node_b_id, &mut events); - let path = &[&nodes[1]]; - pass_along_path(&nodes[0], path, amt_msat, hash, Some(payment_secret), node_1_msgs, true, None); - claim_payment_along_route(ClaimAlongRouteArgs::new(&nodes[0], &[path], preimage)); -} - #[test] fn max_out_mpp_path() { // In this setup, the sender is attempting to route an MPP payment split across the two channels @@ -5401,11 +5681,15 @@ fn max_out_mpp_path() { let mut user_cfg = test_default_channel_config(); user_cfg.channel_config.forwarding_fee_base_msat = 0; - user_cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + user_cfg + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage = 100; let mut lsp_cfg = test_default_channel_config(); lsp_cfg.channel_config.forwarding_fee_base_msat = 0; lsp_cfg.channel_config.forwarding_fee_proportional_millionths = 3000; - lsp_cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + lsp_cfg + .channel_handshake_config + .unannounced_channel_max_inbound_htlc_value_in_flight_percentage = 100; let chanmon_cfgs = create_chanmon_cfgs(3); let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); @@ -5432,3 +5716,449 @@ fn max_out_mpp_path() { check_added_monitors(&nodes[0], 2); // one monitor update per MPP part nodes[0].node.get_and_clear_pending_msg_events(); } + +fn do_bolt11_multi_node_mpp(use_bolt11_pay: bool) { + // Test that multiple nodes can collaborate to pay a single BOLT 11 invoice, with each node + // paying a portion of the total invoice amount. This is useful for scenarios like: + // - Paying from multiple wallets (e.g., ecash wallets with funds in multiple mints) + // - Graduated wallets (funds split between trusted and self-custodial wallets) + + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + // Create channels: A<>C and B<>C + create_announced_chan_between_nodes(&nodes, 0, 2); + create_announced_chan_between_nodes(&nodes, 1, 2); + + // Node C creates a BOLT 11 invoice for 100_000 msat + let invoice_amt_msat = 100_000; + let invoice_params = crate::ln::channelmanager::Bolt11InvoiceParameters { + amount_msats: Some(invoice_amt_msat), + ..Default::default() + }; + let invoice = nodes[2].node.create_bolt11_invoice(invoice_params).unwrap(); + let pmt_hash = invoice.payment_hash(); + + // Node A pays 60_000 msat (part of the total) + let node_a_payment_amt = 60_000; + let payment_id_a = PaymentId([1; 32]); + if use_bolt11_pay { + let params = crate::ln::channelmanager::OptionalBolt11PaymentParams { + declared_total_mpp_value_msat_override: Some(invoice_amt_msat), + ..Default::default() + }; + nodes[0] + .node + .pay_for_bolt11_invoice(&invoice, payment_id_a, Some(node_a_payment_amt), params) + .unwrap(); + } else { + let onion = RecipientOnionFields::secret_only(*invoice.payment_secret(), invoice_amt_msat); + let pay_params = PaymentParameters::from_bolt11_invoice(&invoice); + let route_params = + RouteParameters::from_payment_params_and_value(pay_params, node_a_payment_amt); + let retry = Retry::Attempts(0); + nodes[0].node.send_payment(pmt_hash, onion, payment_id_a, route_params, retry).unwrap(); + } + check_added_monitors(&nodes[0], 1); + + // Node B pays 40_000 msat (the remaining part) + let node_b_payment_amt = 40_000; + let payment_id_b = PaymentId([2; 32]); + let optional_params_b = crate::ln::channelmanager::OptionalBolt11PaymentParams { + declared_total_mpp_value_msat_override: Some(invoice_amt_msat), + ..Default::default() + }; + nodes[1] + .node + .pay_for_bolt11_invoice(&invoice, payment_id_b, Some(node_b_payment_amt), optional_params_b) + .unwrap(); + check_added_monitors(&nodes[1], 1); + + let payment_event_a = SendEvent::from_node(&nodes[0]); + nodes[2].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &payment_event_a.msgs[0]); + do_commitment_signed_dance(&nodes[2], &nodes[0], &payment_event_a.commitment_msg, false, false); + + let payment_event_b = SendEvent::from_node(&nodes[1]); + nodes[2].node.handle_update_add_htlc(nodes[1].node.get_our_node_id(), &payment_event_b.msgs[0]); + do_commitment_signed_dance(&nodes[2], &nodes[1], &payment_event_b.commitment_msg, false, false); + + // Process the pending HTLCs on node C and generate the PaymentClaimable event + assert!(nodes[2].node.get_and_clear_pending_events().is_empty()); + expect_and_process_pending_htlcs(&nodes[2], false); + let events = nodes[2].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let payment_preimage = match &events[0] { + Event::PaymentClaimable { + payment_hash, + amount_msat, + onion_fields, + purpose: PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. }, + .. + } => { + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, invoice_amt_msat); + assert_eq!(onion_fields.as_ref().unwrap().total_mpp_amount_msat, invoice_amt_msat); + payment_preimage.unwrap() + }, + _ => panic!("Unexpected event: {:?}", events[0]), + }; + + nodes[2].node.claim_funds(payment_preimage); + + expect_payment_claimed!(nodes[2], invoice.payment_hash(), invoice_amt_msat); + check_added_monitors(&nodes[2], 2); + + // Get the fulfill messages from C to both A and B + let mut events_c = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(events_c.len(), 2); + + // Handle fulfill message from C to A + let fulfill_idx_a = events_c + .iter() + .position(|ev| { + if let MessageSendEvent::UpdateHTLCs { node_id, .. } = ev { + *node_id == nodes[0].node.get_our_node_id() + } else { + false + } + }) + .unwrap(); + let fulfill_idx_b = 1 - fulfill_idx_a; + + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = events_c[fulfill_idx_a] { + nodes[0].node.handle_update_fulfill_htlc( + nodes[2].node.get_our_node_id(), + updates.update_fulfill_htlcs[0].clone(), + ); + do_commitment_signed_dance(&nodes[0], &nodes[2], &updates.commitment_signed, false, false); + } + + let payment_sent = nodes[0].node.get_and_clear_pending_events(); + check_added_monitors(&nodes[0], 1); + + assert_eq!(payment_sent.len(), 2, "{payment_sent:?}"); + if let Event::PaymentSent { payment_id, payment_hash, amount_msat, fee_paid_msat, .. } = + &payment_sent[0] + { + assert_eq!(*payment_id, Some(payment_id_a)); + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, Some(node_a_payment_amt)); + assert_eq!(*fee_paid_msat, Some(0)); + } else { + panic!("{payment_sent:?}"); + } + if let Event::PaymentPathSuccessful { payment_id, .. } = &payment_sent[1] { + assert_eq!(*payment_id, payment_id_a); + } else { + panic!("{payment_sent:?}"); + } + + // Handle fulfill message from C to B + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = events_c[fulfill_idx_b] { + nodes[1].node.handle_update_fulfill_htlc( + nodes[2].node.get_our_node_id(), + updates.update_fulfill_htlcs[0].clone(), + ); + do_commitment_signed_dance(&nodes[1], &nodes[2], &updates.commitment_signed, false, false); + } + + let payment_sent = nodes[1].node.get_and_clear_pending_events(); + check_added_monitors(&nodes[1], 1); + + assert_eq!(payment_sent.len(), 2, "{payment_sent:?}"); + if let Event::PaymentSent { payment_id, payment_hash, amount_msat, fee_paid_msat, .. } = + &payment_sent[0] + { + assert_eq!(*payment_id, Some(payment_id_b)); + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, Some(node_b_payment_amt)); + assert_eq!(*fee_paid_msat, Some(0)); + } else { + panic!("{payment_sent:?}"); + } + if let Event::PaymentPathSuccessful { payment_id, .. } = &payment_sent[1] { + assert_eq!(*payment_id, payment_id_b); + } else { + panic!("{payment_sent:?}"); + } +} + +#[test] +fn bolt11_multi_node_mpp() { + do_bolt11_multi_node_mpp(true); + do_bolt11_multi_node_mpp(false); +} + +#[test] +fn bolt11_multi_node_mpp_with_retry() { + // Test that multi-node MPP payments work correctly when one node's initial payment attempt + // fails and needs to be retried. Node A pays through an intermediate node C, whose first + // forwarding attempt fails (due to insufficient fee on the injected route). After A + // automatically retries with a corrected route, and B's direct payment also arrives at D, + // D can claim the full payment. + // + // Network topology: A(0) -> C(2) -> D(3), B(1) -> D(3) + + let chanmon_cfgs = create_chanmon_cfgs(4); + let node_cfgs = create_node_cfgs(4, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let nodes = create_network(4, &node_cfgs, &node_chanmgrs); + + let node_a_id = nodes[0].node.get_our_node_id(); + let node_b_id = nodes[1].node.get_our_node_id(); + let node_c_id = nodes[2].node.get_our_node_id(); + let node_d_id = nodes[3].node.get_our_node_id(); + + // Create channels: A<>C, C<>D, B<>D + create_announced_chan_between_nodes_with_value(&nodes, 0, 2, 10_000_000, 0); + let chan_c_d = create_announced_chan_between_nodes_with_value(&nodes, 2, 3, 10_000_000, 0); + let chan_c_d_scid = chan_c_d.0.contents.short_channel_id; + create_announced_chan_between_nodes(&nodes, 1, 3); + + // Sync all nodes to the same block height, since create_announced_chan_between_nodes only + // connects blocks on the two nodes involved in each channel. + let max_height = nodes.iter().map(|n| n.best_block_info().1).max().unwrap(); + for node in &nodes { + let height = node.best_block_info().1; + if height < max_height { + connect_blocks(node, max_height - height); + } + } + + // Node D creates a BOLT 11 invoice for 100_000 msat + let invoice_amt_msat = 100_000; + let invoice_params = crate::ln::channelmanager::Bolt11InvoiceParameters { + amount_msats: Some(invoice_amt_msat), + ..Default::default() + }; + let invoice = nodes[3].node.create_bolt11_invoice(invoice_params).unwrap(); + + // Construct the RouteParameters that pay_for_bolt11_invoice will generate internally, + // then use get_route to compute a natural route from A through C to D. + let node_a_payment_amt = 60_000; + let payment_params = PaymentParameters::from_bolt11_invoice(&invoice); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params.clone(), node_a_payment_amt); + + let mut route = get_route(&nodes[0], &route_params).unwrap(); + assert_eq!(route.paths.len(), 1); + assert_eq!(route.paths[0].hops.len(), 2); // A -> C -> D + let expected_fee = route.paths[0].hops[0].fee_msat; + + // First route for A: same path but with fee_msat=0 at C to trigger a forwarding failure + let mut first_route = route.clone(); + first_route.paths[0].hops[0].fee_msat = 0; + first_route.route_params = route_params.clone(); + nodes[0].router.expect_find_route(route_params.clone(), Ok(first_route)); + + // Retry route for A: the natural route with correct fees (will succeed) + let mut retry_payment_params = payment_params.clone(); + retry_payment_params.previously_failed_channels = vec![chan_c_d_scid]; + let retry_route_params = RouteParameters { + final_value_msat: node_a_payment_amt, + payment_params: retry_payment_params, + max_total_routing_fee_msat: route_params.max_total_routing_fee_msat, + }; + route.route_params = retry_route_params.clone(); + nodes[0].router.expect_find_route(retry_route_params, Ok(route)); + + // Node A pays 60_000 msat (part of the total) with retry enabled + let payment_id_a = PaymentId([1; 32]); + let optional_params_a = crate::ln::channelmanager::OptionalBolt11PaymentParams { + declared_total_mpp_value_msat_override: Some(invoice_amt_msat), + retry_strategy: Retry::Attempts(1), + ..Default::default() + }; + nodes[0] + .node + .pay_for_bolt11_invoice(&invoice, payment_id_a, Some(node_a_payment_amt), optional_params_a) + .unwrap(); + check_added_monitors(&nodes[0], 1); + + // Node B pays 40_000 msat (the remaining part) + let node_b_payment_amt = 40_000; + let payment_id_b = PaymentId([2; 32]); + let optional_params_b = crate::ln::channelmanager::OptionalBolt11PaymentParams { + declared_total_mpp_value_msat_override: Some(invoice_amt_msat), + ..Default::default() + }; + nodes[1] + .node + .pay_for_bolt11_invoice(&invoice, payment_id_b, Some(node_b_payment_amt), optional_params_b) + .unwrap(); + check_added_monitors(&nodes[1], 1); + + // Forward B's HTLC directly to D first (it will be held pending at D) + let payment_event_b = SendEvent::from_node(&nodes[1]); + nodes[3].node.handle_update_add_htlc(node_b_id, &payment_event_b.msgs[0]); + do_commitment_signed_dance(&nodes[3], &nodes[1], &payment_event_b.commitment_msg, false, false); + + // Forward A's first HTLC to C + let payment_event_a = SendEvent::from_node(&nodes[0]); + nodes[2].node.handle_update_add_htlc(node_a_id, &payment_event_a.msgs[0]); + do_commitment_signed_dance(&nodes[2], &nodes[0], &payment_event_a.commitment_msg, false, false); + + // C tries to forward to D but fails (fee too low) + expect_and_process_pending_htlcs(&nodes[2], false); + let next_hop_failure = + HTLCHandlingFailureType::Forward { node_id: Some(node_d_id), channel_id: chan_c_d.2 }; + expect_htlc_handling_failed_destinations!( + nodes[2].node.get_and_clear_pending_events(), + core::slice::from_ref(&next_hop_failure) + ); + check_added_monitors(&nodes[2], 1); + + // C sends update_fail_htlc back to A + let c_fail_updates = get_htlc_update_msgs(&nodes[2], &node_a_id); + assert_eq!(c_fail_updates.update_fail_htlcs.len(), 1); + nodes[0].node.handle_update_fail_htlc(node_c_id, &c_fail_updates.update_fail_htlcs[0]); + do_commitment_signed_dance( + &nodes[0], + &nodes[2], + &c_fail_updates.commitment_signed, + false, + false, + ); + + // A receives PaymentPathFailed (not permanent, can retry) + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::PaymentPathFailed { payment_hash, payment_failed_permanently, .. } => { + assert_eq!(*payment_hash, invoice.payment_hash()); + assert!(!payment_failed_permanently); + }, + _ => panic!("Expected PaymentPathFailed, got: {:?}", events[0]), + } + + // A automatically retries by processing pending HTLC forwards + nodes[0].node.process_pending_htlc_forwards(); + let retry_event = SendEvent::from_node(&nodes[0]); + check_added_monitors(&nodes[0], 1); + + // Forward retry HTLC from A to C + nodes[2].node.handle_update_add_htlc(node_a_id, &retry_event.msgs[0]); + do_commitment_signed_dance(&nodes[2], &nodes[0], &retry_event.commitment_msg, false, false); + + // C successfully forwards to D this time + expect_and_process_pending_htlcs(&nodes[2], false); + check_added_monitors(&nodes[2], 1); + let c_forward = get_htlc_update_msgs(&nodes[2], &node_d_id); + nodes[3].node.handle_update_add_htlc(node_c_id, &c_forward.update_add_htlcs[0]); + do_commitment_signed_dance(&nodes[3], &nodes[2], &c_forward.commitment_signed, false, false); + + // D now has both HTLCs (A's retry via C and B's direct). Process and claim. + assert!(nodes[3].node.get_and_clear_pending_events().is_empty()); + expect_and_process_pending_htlcs(&nodes[3], false); + let events = nodes[3].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let payment_preimage = match &events[0] { + Event::PaymentClaimable { + payment_hash, + amount_msat, + onion_fields, + purpose: PaymentPurpose::Bolt11InvoicePayment { payment_preimage, .. }, + .. + } => { + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, invoice_amt_msat); + assert_eq!(onion_fields.as_ref().unwrap().total_mpp_amount_msat, invoice_amt_msat); + payment_preimage.unwrap() + }, + _ => panic!("Unexpected event: {:?}", events[0]), + }; + + nodes[3].node.claim_funds(payment_preimage); + + expect_payment_claimed!(nodes[3], invoice.payment_hash(), invoice_amt_msat); + check_added_monitors(&nodes[3], 2); + + // Get the fulfill messages from D to both C (for A) and B + let mut events_d = nodes[3].node.get_and_clear_pending_msg_events(); + assert_eq!(events_d.len(), 2); + + // Find which event goes to C and which to B + let fulfill_idx_c = events_d + .iter() + .position(|ev| { + if let MessageSendEvent::UpdateHTLCs { node_id, .. } = ev { + *node_id == node_c_id + } else { + false + } + }) + .unwrap(); + let fulfill_idx_b = 1 - fulfill_idx_c; + + // Handle fulfill from D to C (intermediate node). C persists the preimage to + // the upstream A<>C channel monitor, generates a PaymentForwarded event, and + // queues a fulfill message for A. + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = events_d[fulfill_idx_c] { + nodes[2] + .node + .handle_update_fulfill_htlc(node_d_id, updates.update_fulfill_htlcs[0].clone()); + expect_payment_forwarded!(nodes[2], nodes[0], nodes[3], Some(expected_fee), false, false); + check_added_monitors(&nodes[2], 1); + + // C has a pending fulfill to send to A; retrieve it before the C<>D dance + let c_fulfill = get_htlc_update_msgs(&nodes[2], &node_a_id); + + do_commitment_signed_dance(&nodes[2], &nodes[3], &updates.commitment_signed, false, false); + + // Forward the fulfill from C to A + nodes[0] + .node + .handle_update_fulfill_htlc(node_c_id, c_fulfill.update_fulfill_htlcs[0].clone()); + do_commitment_signed_dance( + &nodes[0], + &nodes[2], + &c_fulfill.commitment_signed, + false, + false, + ); + } + + let payment_sent_a = nodes[0].node.get_and_clear_pending_events(); + check_added_monitors(&nodes[0], 1); + + assert_eq!(payment_sent_a.len(), 2, "{payment_sent_a:?}"); + if let Event::PaymentSent { payment_id, payment_hash, amount_msat, .. } = &payment_sent_a[0] { + assert_eq!(*payment_id, Some(payment_id_a)); + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, Some(node_a_payment_amt)); + } else { + panic!("{payment_sent_a:?}"); + } + if let Event::PaymentPathSuccessful { payment_id, .. } = &payment_sent_a[1] { + assert_eq!(*payment_id, payment_id_a); + } else { + panic!("{payment_sent_a:?}"); + } + + // Handle fulfill from D to B + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = events_d[fulfill_idx_b] { + nodes[1] + .node + .handle_update_fulfill_htlc(node_d_id, updates.update_fulfill_htlcs[0].clone()); + do_commitment_signed_dance(&nodes[1], &nodes[3], &updates.commitment_signed, false, false); + } + + let payment_sent_b = nodes[1].node.get_and_clear_pending_events(); + check_added_monitors(&nodes[1], 1); + + assert_eq!(payment_sent_b.len(), 2, "{payment_sent_b:?}"); + if let Event::PaymentSent { payment_id, payment_hash, amount_msat, .. } = &payment_sent_b[0] { + assert_eq!(*payment_id, Some(payment_id_b)); + assert_eq!(*payment_hash, invoice.payment_hash()); + assert_eq!(*amount_msat, Some(node_b_payment_amt)); + } else { + panic!("{payment_sent_b:?}"); + } + if let Event::PaymentPathSuccessful { payment_id, .. } = &payment_sent_b[1] { + assert_eq!(*payment_id, payment_id_b); + } else { + panic!("{payment_sent_b:?}"); + } +} diff --git a/lightning/src/ln/peer_channel_encryptor.rs b/lightning/src/ln/peer_channel_encryptor.rs index 5554c5a8c19..5f461315712 100644 --- a/lightning/src/ln/peer_channel_encryptor.rs +++ b/lightning/src/ln/peer_channel_encryptor.rs @@ -25,8 +25,8 @@ use bitcoin::secp256k1; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; +use chacha20_poly1305::{ChaCha20Poly1305, Key, Nonce}; -use crate::crypto::chacha20poly1305rfc::ChaCha20Poly1305RFC; use crate::crypto::utils::hkdf_extract_expand_twice; use crate::util::ser::VecWriter; @@ -150,10 +150,11 @@ impl PeerChannelEncryptor { fn encrypt_with_ad(res: &mut [u8], n: u64, key: &[u8; 32], h: &[u8], plaintext: &[u8]) { let mut nonce = [0; 12]; nonce[4..].copy_from_slice(&n.to_le_bytes()[..]); + res[0..plaintext.len()].copy_from_slice(plaintext); + + let chacha = ChaCha20Poly1305::new(Key::new(*key), Nonce::new(nonce)); + let tag = chacha.encrypt(&mut res[0..plaintext.len()], Some(h)); - let mut chacha = ChaCha20Poly1305RFC::new(key, &nonce, h); - let mut tag = [0; 16]; - chacha.encrypt(plaintext, &mut res[0..plaintext.len()], &mut tag); res[plaintext.len()..].copy_from_slice(&tag); } @@ -166,9 +167,8 @@ impl PeerChannelEncryptor { let mut nonce = [0; 12]; nonce[4..].copy_from_slice(&n.to_le_bytes()[..]); - let mut chacha = ChaCha20Poly1305RFC::new(key, &nonce, h); - let mut tag = [0; 16]; - chacha.encrypt_full_message_in_place(&mut res[offset..], &mut tag); + let chacha = ChaCha20Poly1305::new(Key::new(*key), Nonce::new(nonce)); + let tag = chacha.encrypt(&mut res[offset..], Some(h)); res.extend_from_slice(&tag); } @@ -178,9 +178,11 @@ impl PeerChannelEncryptor { let mut nonce = [0; 12]; nonce[4..].copy_from_slice(&n.to_le_bytes()[..]); - let mut chacha = ChaCha20Poly1305RFC::new(key, &nonce, h); + let chacha = ChaCha20Poly1305::new(Key::new(*key), Nonce::new(nonce)); let (inout, tag) = inout.split_at_mut(inout.len() - 16); - if chacha.check_decrypt_in_place(inout, tag).is_err() { + let mut decrypt_tag = [0; 16]; + decrypt_tag.copy_from_slice(tag); + if chacha.decrypt(inout, decrypt_tag, Some(h)).is_err() { return Err(LightningError { err: "Bad MAC".to_owned(), action: msgs::ErrorAction::DisconnectPeer { msg: None }, @@ -197,9 +199,13 @@ impl PeerChannelEncryptor { nonce[4..].copy_from_slice(&n.to_le_bytes()[..]); let (data, hmac) = cyphertext.split_at(cyphertext.len() - 16); + let mut tag = [0; 16]; + tag.copy_from_slice(hmac); + res.copy_from_slice(data); + let mac_check = - ChaCha20Poly1305RFC::new(key, &nonce, h).variable_time_decrypt(&data, res, hmac); - mac_check.map_err(|()| LightningError { + ChaCha20Poly1305::new(Key::new(*key), Nonce::new(nonce)).decrypt(res, tag, Some(h)); + mac_check.map_err(|_| LightningError { err: "Bad MAC".to_owned(), action: msgs::ErrorAction::DisconnectPeer { msg: None }, }) @@ -550,7 +556,7 @@ impl PeerChannelEncryptor { /// Encrypts the given message, returning the encrypted version. /// panics if the length of `message`, once encoded, is greater than 65535 or if the Noise /// handshake has not finished. - pub fn encrypt_message<T: wire::Type>(&mut self, message: wire::Message<T>) -> Vec<u8> { + pub(crate) fn encrypt_message<T: wire::Type>(&mut self, message: wire::Message<T>) -> Vec<u8> { // Allocate a buffer with 2KB, fitting most common messages. Reserve the first 16+2 bytes // for the 2-byte message type prefix and its MAC. let mut res = VecWriter(Vec::with_capacity(MSG_BUF_ALLOC_SIZE)); diff --git a/lightning/src/ln/peer_handler.rs b/lightning/src/ln/peer_handler.rs index 759a1e7d887..27c844f42e5 100644 --- a/lightning/src/ln/peer_handler.rs +++ b/lightning/src/ln/peer_handler.rs @@ -35,7 +35,7 @@ use crate::onion_message::async_payments::{ ServeStaticInvoice, StaticInvoicePersisted, }; use crate::onion_message::dns_resolution::{ - DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, + DNSResolverMessage, DNSResolverMessageHandler, DNSSECError, DNSSECProof, DNSSECQuery, }; use crate::onion_message::messenger::{ CustomOnionMessageHandler, MessageSendInstructions, Responder, ResponseInstruction, @@ -45,7 +45,6 @@ use crate::onion_message::packet::OnionMessageContents; use crate::routing::gossip::{NodeAlias, NodeId}; use crate::sign::{NodeSigner, Recipient}; use crate::types::features::{InitFeatures, NodeFeatures}; -use crate::types::string::PrintableString; use crate::util::atomic_counter::AtomicCounter; use crate::util::logger::{Level, Logger, WithContext}; use crate::util::ser::{VecWriter, Writeable, Writer}; @@ -274,6 +273,7 @@ impl DNSResolverMessageHandler for IgnoringMessageHandler { None } fn handle_dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {} + fn handle_dnssec_error(&self, _message: DNSSECError, _context: DNSResolverContext) {} } impl CustomOnionMessageHandler for IgnoringMessageHandler { type CustomMessage = Infallible; @@ -1980,6 +1980,9 @@ impl< (msgs::DecodeError::UnknownVersion, _) => { return Err(PeerHandleError {}) }, + (msgs::DecodeError::SkipCase, _) => { + return Err(PeerHandleError {}) + }, (msgs::DecodeError::InvalidValue, _) => { log_debug!(logger, "Got an invalid value while deserializing message"); return Err(PeerHandleError {}); @@ -2327,7 +2330,7 @@ impl< #[allow(unused_mut)] let mut should_do_full_sync = true; - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] { // Forward ad-hoc gossip if the timestamp range is less than six hours ago. // Otherwise, do a full sync. @@ -2384,7 +2387,7 @@ impl< logger, "Got Err message from {}: {}", their_node_id, - PrintableString(&msg.data) + log_msg!(msg.data) ); self.message_handler.chan_handler.handle_error(their_node_id, &msg); if msg.channel_id.is_zero() { @@ -2392,7 +2395,7 @@ impl< } }, Message::Warning(msg) => { - log_debug!(logger, "Got warning message: {}", PrintableString(&msg.data)); + log_debug!(logger, "Got warning message: {}", log_msg!(msg.data)); }, Message::Ping(msg) => { @@ -3246,7 +3249,7 @@ impl< msgs::ErrorAction::DisconnectPeer { msg } => { if let Some(msg) = msg.as_ref() { log_trace!(logger, "Handling DisconnectPeer HandleError event in peer_handler with message {}", - msg.data); + log_msg!(msg.data)); } else { log_trace!(logger, "Handling DisconnectPeer HandleError event in peer_handler", ); @@ -3260,7 +3263,7 @@ impl< }, msgs::ErrorAction::DisconnectPeerWithWarning { msg } => { log_trace!(logger, "Handling DisconnectPeer HandleError event in peer_handler with message {}", - msg.data); + log_msg!(msg.data)); // We do not have the peers write lock, so we just store that we're // about to disconnect the peer and do it after we finish // processing most messages. @@ -3283,8 +3286,7 @@ impl< }, msgs::ErrorAction::SendErrorMessage { msg } => { log_trace!(logger, "Handling SendErrorMessage HandleError event in peer_handler with message {}", - - msg.data); + log_msg!(msg.data)); let msg = Message::Error(msg); self.enqueue_message( &mut *get_peer_for_forwarding!(&node_id)?, @@ -3293,8 +3295,7 @@ impl< }, msgs::ErrorAction::SendWarningMessage { msg, ref log_level } => { log_given_level!(logger, *log_level, "Handling SendWarningMessage HandleError event in peer_handler with message {}", - - msg.data); + log_msg!(msg.data)); let msg = Message::Warning(msg); self.enqueue_message( &mut *get_peer_for_forwarding!(&node_id)?, diff --git a/lightning/src/ln/priv_short_conf_tests.rs b/lightning/src/ln/priv_short_conf_tests.rs index 9d30d749aa2..7e3adb4adc9 100644 --- a/lightning/src/ln/priv_short_conf_tests.rs +++ b/lightning/src/ln/priv_short_conf_tests.rs @@ -14,7 +14,7 @@ use crate::chain::ChannelMonitorUpdateStatus; use crate::events::{ClosureReason, Event, HTLCHandlingFailureType, PaymentFailureReason}; use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY; -use crate::ln::channelmanager::{PaymentId, MIN_CLTV_EXPIRY_DELTA}; +use crate::ln::channelmanager::{PaymentId, TrustedChannelFeatures, MIN_CLTV_EXPIRY_DELTA}; use crate::ln::msgs; use crate::ln::msgs::{ BaseMessageHandler, ChannelMessageHandler, ErrorAction, MessageSendEvent, RoutingMessageHandler, @@ -81,7 +81,7 @@ fn test_priv_forwarding_rejection() { let (route, our_payment_hash, our_payment_preimage, our_payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], payment_params, 10_000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -164,7 +164,7 @@ fn test_priv_forwarding_rejection() { get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_c_id); get_event_msg!(nodes[2], MessageSendEvent::SendChannelUpdate, node_b_id); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 10_000); let id = PaymentId(our_payment_hash.0); nodes[0].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -255,7 +255,7 @@ fn do_test_1_conf_open(connect_style: ConnectStyle) { assert_eq!(bs_announce_events.len(), 2); let bs_announcement_sigs = if let MessageSendEvent::SendAnnouncementSignatures { ref node_id, ref msg } = - bs_announce_events[1] + bs_announce_events[0] { assert_eq!(*node_id, node_a_id); msg.clone() @@ -264,7 +264,7 @@ fn do_test_1_conf_open(connect_style: ConnectStyle) { }; let (bs_announcement, bs_update) = if let MessageSendEvent::BroadcastChannelAnnouncement { ref msg, ref update_msg } = - bs_announce_events[0] + bs_announce_events[1] { (msg.clone(), update_msg.clone().unwrap()) } else { @@ -348,7 +348,7 @@ fn test_routed_scid_alias() { get_route_and_payment_hash!(nodes[0], nodes[2], payment_params, 100_000); assert_eq!(route.paths[0].hops[1].short_channel_id, last_hop[0].inbound_scid_alias.unwrap()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -578,7 +578,7 @@ fn test_inbound_scid_privacy() { get_route_and_payment_hash!(nodes[0], nodes[2], payment_params, 100_000); assert_eq!(route.paths[0].hops[1].short_channel_id, last_hop[0].inbound_scid_alias.unwrap()); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -599,7 +599,7 @@ fn test_inbound_scid_privacy() { get_route_and_payment_hash!(nodes[0], nodes[2], payment_params_2, 100_000); assert_eq!(route_2.paths[0].hops[1].short_channel_id, last_hop[0].short_channel_id.unwrap()); - let onion = RecipientOnionFields::secret_only(payment_secret_2); + let onion = RecipientOnionFields::secret_only(payment_secret_2, 100_000); let id = PaymentId(payment_hash_2.0); nodes[0].node.send_payment_with_route(route_2, payment_hash_2, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -695,7 +695,7 @@ fn test_scid_alias_returned() { route.paths[0].hops[1].fee_msat = 10_000_000; // Overshoot the last channel's value // Route the HTLC through to the destination. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, route.get_total_amount()); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); @@ -732,7 +732,7 @@ fn test_scid_alias_returned() { route.paths[0].hops[0].fee_msat = 0; // But set fee paid to the middle hop to 0 // Route the HTLC through to the destination. - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 10_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); @@ -774,7 +774,7 @@ fn test_simple_0conf_channel() { // If our peer tells us they will accept our channel with 0 confs, and we funded the channel, // we should trust the funding won't be double-spent (assuming `trust_own_funding_0conf` is // set)! - // Further, if we `accept_inbound_channel_from_trusted_peer_0conf`, `channel_ready` messages + // Further, if we `accept_inbound_channel_from_trusted_peer`, `channel_ready` messages // should fly immediately and the channel should be available for use as soon as they are // received. @@ -818,10 +818,11 @@ fn test_0conf_channel_with_async_monitor() { Event::OpenChannelRequest { temporary_channel_id, .. } => { nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( &temporary_channel_id, &node_a_id, 0, + TrustedChannelFeatures::ZeroConf, None, ) .unwrap(); @@ -934,7 +935,7 @@ fn test_0conf_channel_with_async_monitor() { let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], 1_000_000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); let id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1107,8 +1108,8 @@ fn test_0conf_channel_reorg() { mine_transaction(&nodes[1], &tx); mine_transaction(&nodes[2], &tx); - // Send a payment using the channel's real SCID, which will be public in a few blocks once we - // can generate a channel_announcement. + // Send a payment using the channel's alias SCID. The channel itself will be public in a few + // blocks once we can generate a channel_announcement. let bs_chans = nodes[1].node.list_usable_channels(); let bs_chan = bs_chans.iter().find(|chan| chan.counterparty.node_id == node_c_id).unwrap(); let original_scid = bs_chan.short_channel_id.unwrap(); @@ -1116,7 +1117,7 @@ fn test_0conf_channel_reorg() { let (mut route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[2], 10_000); - assert_eq!(route.paths[0].hops[0].short_channel_id, original_scid); + assert_eq!(route.paths[0].hops[0].short_channel_id, bs_chan.outbound_scid_alias.unwrap()); send_along_route_with_secret( &nodes[1], route.clone(), @@ -1187,7 +1188,7 @@ fn test_0conf_channel_reorg() { assert_ne!(original_scid, new_scid); assert_eq!(nodes[2].node.list_usable_channels()[0].short_channel_id.unwrap(), new_scid); - // At this point, the channel should happily forward or send payments with either the old SCID + // At this point, the channel should happily forward or send payments with either the alias SCID // or the new SCID... send_along_route_with_secret( &nodes[1], @@ -1283,14 +1284,24 @@ fn test_0conf_channel_reorg() { ); claim_payment(&nodes[0], &[&nodes[1], &nodes[2]], payment_preimage); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 10_000); let id = PaymentId([0; 32]); - nodes[1].node.send_payment_with_route(route, payment_hash, onion.clone(), id).unwrap(); + + // The route uses the alias SCID, which is stable across reorgs. To verify the old real SCID + // is invalidated after propagation delay, we explicitly build a route using original_scid. + let mut old_scid_route = route.clone(); + old_scid_route.paths[0].hops[0].short_channel_id = original_scid; + nodes[1].node.send_payment_with_route(old_scid_route, payment_hash, onion.clone(), id).unwrap(); let mut conditions = PaymentFailedConditions::new(); conditions.reason = Some(PaymentFailureReason::RouteNotFound); expect_payment_failed_conditions(&nodes[1], payment_hash, false, conditions); - nodes[0].node.send_payment_with_route(forwarded_route, payment_hash, onion, id).unwrap(); + let mut old_scid_forwarded_route = forwarded_route.clone(); + old_scid_forwarded_route.paths[0].hops[1].short_channel_id = original_scid; + nodes[0] + .node + .send_payment_with_route(old_scid_forwarded_route, payment_hash, onion, id) + .unwrap(); check_added_monitors(&nodes[0], 1); let mut ev = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(ev.len(), 1); @@ -1369,11 +1380,12 @@ fn test_zero_conf_accept_reject() { // Assert we can accept via the 0conf method assert!(nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( &temporary_channel_id, &node_a_id, 0, - None + TrustedChannelFeatures::ZeroConf, + None, ) .is_ok()); }, @@ -1396,9 +1408,7 @@ fn test_connect_before_funding() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let mut manually_accept_conf = test_default_channel_config(); - - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(manually_accept_conf)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_a_id = nodes[0].node.get_our_node_id(); let node_b_id = nodes[1].node.get_our_node_id(); @@ -1413,10 +1423,11 @@ fn test_connect_before_funding() { Event::OpenChannelRequest { temporary_channel_id, .. } => { nodes[1] .node - .accept_inbound_channel_from_trusted_peer_0conf( + .accept_inbound_channel_from_trusted_peer( &temporary_channel_id, &node_a_id, 0, + TrustedChannelFeatures::ZeroConf, None, ) .unwrap(); diff --git a/lightning/src/ln/quiescence_tests.rs b/lightning/src/ln/quiescence_tests.rs index d972fb6a5c5..495a1622522 100644 --- a/lightning/src/ln/quiescence_tests.rs +++ b/lightning/src/ln/quiescence_tests.rs @@ -35,6 +35,11 @@ fn test_quiescence_tie() { assert!(nodes[0].node.exit_quiescence(&nodes[1].node.get_our_node_id(), &chan_id).unwrap()); assert!(nodes[1].node.exit_quiescence(&nodes[0].node.get_our_node_id(), &chan_id).unwrap()); + + // Since node 1 lost the tie, they'll attempt quiescence again. + let stfu = + get_event_msg!(nodes[1], MessageSendEvent::SendStfu, nodes[0].node.get_our_node_id()); + assert!(stfu.initiator); } #[test] @@ -98,7 +103,7 @@ fn allow_shutdown_while_awaiting_quiescence(local_shutdown: bool) { let payment_amount = 1_000_000; let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(local_node, remote_node, payment_amount); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); let payment_id = PaymentId(payment_hash.0); local_node.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&local_node, 1); @@ -304,7 +309,7 @@ fn test_quiescence_on_final_revoke_and_ack_pending_monitor_update() { let payment_amount = 1_000_000; let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(&nodes[0], &nodes[1], payment_amount); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); let payment_id = PaymentId(payment_hash.0); nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -370,7 +375,7 @@ fn quiescence_updates_go_to_holding_cell(fail_htlc: bool) { let (route1, payment_hash1, payment_preimage1, payment_secret1) = get_route_and_payment_hash!(&nodes[1], &nodes[0], payment_amount); - let onion1 = RecipientOnionFields::secret_only(payment_secret1); + let onion1 = RecipientOnionFields::secret_only(payment_secret1, payment_amount); let payment_id1 = PaymentId(payment_hash1.0); nodes[1].node.send_payment_with_route(route1, payment_hash1, onion1, payment_id1).unwrap(); check_added_monitors(&nodes[1], 0); @@ -380,7 +385,7 @@ fn quiescence_updates_go_to_holding_cell(fail_htlc: bool) { // allowed to make updates. let (route2, payment_hash2, payment_preimage2, payment_secret2) = get_route_and_payment_hash!(&nodes[0], &nodes[1], payment_amount); - let onion2 = RecipientOnionFields::secret_only(payment_secret2); + let onion2 = RecipientOnionFields::secret_only(payment_secret2, payment_amount); let payment_id2 = PaymentId(payment_hash2.0); nodes[0].node.send_payment_with_route(route2, payment_hash2, onion2, payment_id2).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/reload_tests.rs b/lightning/src/ln/reload_tests.rs index c7e7175602d..90bdff48724 100644 --- a/lightning/src/ln/reload_tests.rs +++ b/lightning/src/ln/reload_tests.rs @@ -11,7 +11,7 @@ //! Functional tests which test for correct behavior across node restarts. -use crate::chain::{ChannelMonitorUpdateStatus, Watch}; +use crate::chain::{BlockLocator, ChannelMonitorUpdateStatus, Watch}; use crate::chain::chaininterface::LowerBoundedFeeEstimator; use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdateStep}; use crate::routing::router::{PaymentParameters, RouteParameters}; @@ -30,7 +30,6 @@ use crate::util::ser::{Writeable, ReadableArgs}; use crate::util::config::{HTLCInterceptionFlags, UserConfig}; use bitcoin::hashes::Hash; -use bitcoin::hash_types::BlockHash; use types::payment::{PaymentHash, PaymentPreimage}; use crate::prelude::*; @@ -412,7 +411,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { let mut node_0_stale_monitors = Vec::new(); for serialized in node_0_stale_monitors_serialized.iter() { let mut read = &serialized[..]; - let (_, monitor) = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read(&mut read, (keys_manager, keys_manager)).unwrap(); + let (_, monitor) = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read(&mut read, (keys_manager, keys_manager)).unwrap(); assert!(read.is_empty()); node_0_stale_monitors.push(monitor); } @@ -420,14 +419,14 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { let mut node_0_monitors = Vec::new(); for serialized in node_0_monitors_serialized.iter() { let mut read = &serialized[..]; - let (_, monitor) = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read(&mut read, (keys_manager, keys_manager)).unwrap(); + let (_, monitor) = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read(&mut read, (keys_manager, keys_manager)).unwrap(); assert!(read.is_empty()); node_0_monitors.push(monitor); } let mut nodes_0_read = &nodes_0_serialized[..]; if let Err(msgs::DecodeError::DangerousValue) = - <(BlockHash, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { + <(BlockLocator, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { config: UserConfig::default(), entropy_source: keys_manager, node_signer: keys_manager, @@ -446,7 +445,7 @@ fn test_manager_serialize_deserialize_inconsistent_monitor() { let mut nodes_0_read = &nodes_0_serialized[..]; let (_, nodes_0_deserialized_tmp) = - <(BlockHash, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { + <(BlockLocator, ChannelManager<&test_utils::TestChainMonitor, &test_utils::TestBroadcaster, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestKeysInterface, &test_utils::TestFeeEstimator, &test_utils::TestRouter, &test_utils::TestMessageRouter, &test_utils::TestLogger>)>::read(&mut nodes_0_read, ChannelManagerReadArgs { config: UserConfig::default(), entropy_source: keys_manager, node_signer: keys_manager, @@ -545,7 +544,7 @@ fn do_test_data_loss_protect(reconnect_panicing: bool, substantially_old: bool, // `not_stale` to test the boundary condition. let pay_params = PaymentParameters::for_keysend(nodes[1].node.get_our_node_id(), 100, false); let route_params = RouteParameters::from_payment_params_and_value(pay_params, 40000); - nodes[0].node.send_spontaneous_payment(None, RecipientOnionFields::spontaneous_empty(), PaymentId([0; 32]), route_params, Retry::Attempts(0)).unwrap(); + nodes[0].node.send_spontaneous_payment(None, RecipientOnionFields::spontaneous_empty(40000), PaymentId([0; 32]), route_params, Retry::Attempts(0)).unwrap(); check_added_monitors(&nodes[0], 1); let update_add_commit = SendEvent::from_node(&nodes[0]); @@ -691,7 +690,11 @@ fn do_test_data_loss_protect(reconnect_panicing: bool, substantially_old: bool, if let MessageSendEvent::HandleError { ref action, .. } = nodes[0].node.get_and_clear_pending_msg_events()[1] { match action { &ErrorAction::SendErrorMessage { ref msg } => { - assert_eq!(msg.data, format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", &nodes[1].node.get_our_node_id())); + let peer_msg = format!( + "Got a message for a channel from the wrong node! No such channel_id {} for the passed counterparty_node_id {}", + chan.2, nodes[1].node.get_our_node_id() + ); + assert_eq!(msg.data, peer_msg); err_msgs_0.push(msg.clone()); }, _ => panic!("Unexpected event!"), @@ -703,9 +706,13 @@ fn do_test_data_loss_protect(reconnect_panicing: bool, substantially_old: bool, nodes[1].node.handle_error(nodes[0].node.get_our_node_id(), &err_msgs_0[0]); assert!(nodes[1].node.list_usable_channels().is_empty()); check_added_monitors(&nodes[1], 1); - check_closed_event(&nodes[1], 1, ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", &nodes[1].node.get_our_node_id())) } - , &[nodes[0].node.get_our_node_id()], 1000000); - check_closed_broadcast!(nodes[1], false); + let peer_msg = format!( + "Got a message for a channel from the wrong node! No such channel_id {} for the passed counterparty_node_id {}", + chan.2, nodes[1].node.get_our_node_id() + ); + let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(peer_msg) }; + check_closed_event(&nodes[1], 1, reason, &[nodes[0].node.get_our_node_id()], 1000000); + check_closed_broadcast(&nodes[1], 1, false); } } @@ -738,7 +745,11 @@ fn do_test_partial_claim_before_restart(persist_both_monitors: bool, double_rest let (persist_d_1, persist_d_2); let (chain_d_1, chain_d_2); - let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &[None, None, None, None]); + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = 10; + let configs: [Option<UserConfig>; 4] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(4, &node_cfgs, &configs); let (node_d_1, node_d_2); let mut nodes = create_network(4, &node_cfgs, &node_chanmgrs); @@ -758,7 +769,7 @@ fn do_test_partial_claim_before_restart(persist_both_monitors: bool, double_rest }); nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 15_000_000), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 2); // Send the payment through to nodes[3] *without* clearing the PaymentClaimable event @@ -815,12 +826,14 @@ fn do_test_partial_claim_before_restart(persist_both_monitors: bool, double_rest // Now restart nodes[3]. reload_node!(nodes[3], original_manager.clone(), &[&updated_monitor.0, &original_monitor.0], persist_d_1, chain_d_1, node_d_1); + nodes[3].disable_monitor_completeness_assertion(); if double_restart { // Previously, we had a bug where we'd fail to reload if we re-persist the `ChannelManager` // without updating any `ChannelMonitor`s as we'd fail to double-initiate the claim replay. // We test that here ensuring that we can reload again. reload_node!(nodes[3], node_d_1.encode(), &[&updated_monitor.0, &original_monitor.0], persist_d_2, chain_d_2, node_d_2); + nodes[3].disable_monitor_completeness_assertion(); } // Until the startup background events are processed (in `get_and_clear_pending_events`, @@ -924,6 +937,364 @@ fn test_partial_claim_before_restart() { do_test_partial_claim_before_restart(true, true); } +#[test] +fn test_mpp_claim_htlc_fulfills_unblocked_on_reload() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let persister; + let new_chain_monitor; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes_1_deserialized; + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Open two independent channels between the same nodes. The payment below is large enough to + // force the router to split it across both channels, which is what makes the MPP claim depend + // on both ChannelMonitors durably learning the preimage. + let chan_a = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + let chan_b = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + let chan_id_a = chan_a.2; + let chan_id_b = chan_b.2; + let scid_a = chan_a.0.contents.short_channel_id; + let scid_b = chan_b.0.contents.short_channel_id; + // Routes to a directly-connected peer use the outbound SCID alias, so payment path success + // events report the alias rather than the real SCID announced in gossip. + let payment_scid_a = nodes[0].node.list_channels().iter() + .find(|chan| chan.channel_id == chan_id_a).unwrap().get_outbound_payment_scid().unwrap(); + let payment_scid_b = nodes[0].node.list_channels().iter() + .find(|chan| chan.channel_id == chan_id_b).unwrap().get_outbound_payment_scid().unwrap(); + + // Send an MPP payment to nodes[1]. `send_along_route_with_secret` leaves the payment + // claimable but unclaimed, so nodes[1] still has both inbound HTLCs live when we start + // manipulating monitor persistence below. + let amt_msat = 50_000_000; + let (route, payment_hash, payment_preimage, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], amt_msat); + assert_eq!(route.paths.len(), 2); + send_along_route_with_secret( + &nodes[0], route, &[&[&nodes[1]], &[&nodes[1]]], amt_msat, payment_hash, + payment_secret, + ); + + // Move both channels into `AWAITING_REMOTE_REVOKE` by having nodes[0] send fee updates and + // withholding nodes[1]'s responding `commitment_signed`s. When nodes[1] later claims the + // payment, the fulfill updates cannot be sent immediately and instead sit in each channel's + // holding cell. + { + let mut fee_est = chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap(); + *fee_est *= 2; + } + nodes[0].node.timer_tick_occurred(); + check_added_monitors(&nodes[0], 2); + + let node_0_id = nodes[0].node.get_our_node_id(); + let node_1_id = nodes[1].node.get_our_node_id(); + + let fee_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(fee_msgs.len(), 2); + for ev in &fee_msgs { + match ev { + MessageSendEvent::UpdateHTLCs { updates, .. } => { + nodes[1].node.handle_update_fee(node_0_id, updates.update_fee.as_ref().unwrap()); + nodes[1].node.handle_commitment_signed_batch_test( + node_0_id, &updates.commitment_signed, + ); + check_added_monitors(&nodes[1], 1); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + // nodes[1] responds to each fee update with a `revoke_and_ack` and a new + // `commitment_signed`. Deliver only the `revoke_and_ack`s for now. The held + // `commitment_signed`s are delivered after nodes[1] claims the payment, creating the blocked + // post-claim monitor updates whose release is exercised after reload. + let node_1_msgs = nodes[1].node.get_and_clear_pending_msg_events(); + let mut commitment_signed_msgs = Vec::new(); + for ev in &node_1_msgs { + match ev { + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + nodes[0].node.handle_revoke_and_ack(node_1_id, msg); + check_added_monitors(&nodes[0], 1); + }, + MessageSendEvent::UpdateHTLCs { updates, .. } => { + commitment_signed_msgs.push(updates.commitment_signed.clone()); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + let node_0_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + for ev in &node_0_msgs { + match ev { + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + nodes[1].node.handle_revoke_and_ack(node_0_id, msg); + check_added_monitors(&nodes[1], 1); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + // Snapshot channel B before the claim. The in-memory ChainMonitor applies updates even when + // the persister returns `InProgress`, so taking this snapshot after the claim would not model a + // crash between two separate monitor writes. + let mon_b_serialized = get_monitor!(nodes[1], chan_id_b).encode(); + + // Make both preimage monitor writes asynchronous. `claim_funds` attaches an in-memory MPP RAA + // blocker so neither channel can release later monitor updates until all channels have the + // preimage durably persisted. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.claim_funds(payment_preimage); + check_added_monitors(&nodes[1], 2); + + // Complete only channel A's preimage update. Channel B will be reloaded from the stale snapshot + // above, simulating a crash where one monitor write completed and the other did not. + let (update_id_a, _) = get_latest_mon_update_id(&nodes[1], chan_id_a); + nodes[1].chain_monitor.chain_monitor.force_channel_monitor_updated(chan_id_a, update_id_a); + + // Now finish the fee-update commitment dance we held back. nodes[1] receives nodes[0]'s + // `revoke_and_ack`s while the MPP RAA blocker is still in place, so the resulting monitor + // updates are blocked behind state that is not serialized in the ChannelManager. + for commitment_signed in &commitment_signed_msgs { + nodes[0].node.handle_commitment_signed_batch_test(node_1_id, commitment_signed); + check_added_monitors(&nodes[0], 1); + } + let node_0_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + for ev in &node_0_msgs { + match ev { + MessageSendEvent::SendRevokeAndACK { msg, .. } => { + nodes[1].node.handle_revoke_and_ack(node_0_id, msg); + check_added_monitors(&nodes[1], 0); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + + // Persist the ChannelManager after the blocked post-claim monitor updates have been recorded. + // Reload with channel A's up-to-date monitor and channel B's stale monitor. The preimage update + // for B is replayed during reload, putting both channels' preimages on disk. The remaining state + // under test is the blocked post-claim `revoke_and_ack` monitor updates after the in-memory MPP + // RAA blocker that created them is gone. + let node_1_serialized = nodes[1].node.encode(); + let mon_a_serialized = get_monitor!(nodes[1], chan_id_a).encode(); + + nodes[0].node.peer_disconnected(node_1_id); + reload_node!( + nodes[1], + node_1_serialized, + &[&mon_a_serialized, &mon_b_serialized], + persister, + new_chain_monitor, + nodes_1_deserialized + ); + + // Reconnect both peers by manually exchanging `channel_reestablish`s. This avoids relying on a + // more general reconnect helper while the channels intentionally have asymmetric monitor state. + let node_1_id = nodes[1].node.get_our_node_id(); + nodes[0].node.peer_connected(node_1_id, &msgs::Init { + features: nodes[1].node.init_features(), networks: None, remote_network_address: None, + }, true).unwrap(); + nodes[1].node.peer_connected(node_0_id, &msgs::Init { + features: nodes[0].node.init_features(), networks: None, remote_network_address: None, + }, false).unwrap(); + + let reestablish_0 = nodes[0].node.get_and_clear_pending_msg_events(); + let reestablish_1 = nodes[1].node.get_and_clear_pending_msg_events(); + let mut reestablish_0_chan_ids = Vec::new(); + let mut reestablish_1_chan_ids = Vec::new(); + for ev in &reestablish_1 { + match ev { + MessageSendEvent::SendChannelReestablish { node_id, msg } => { + assert_eq!(*node_id, node_0_id); + reestablish_1_chan_ids.push(msg.channel_id); + nodes[0].node.handle_channel_reestablish(node_1_id, msg); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + for ev in &reestablish_0 { + match ev { + MessageSendEvent::SendChannelReestablish { node_id, msg } => { + assert_eq!(*node_id, node_1_id); + reestablish_0_chan_ids.push(msg.channel_id); + nodes[1].node.handle_channel_reestablish(node_0_id, msg); + }, + _ => panic!("Unexpected message: {:?}", ev), + } + } + assert_eq!(reestablish_0_chan_ids.len(), 2); + assert!(reestablish_0_chan_ids.contains(&chan_id_a)); + assert!(reestablish_0_chan_ids.contains(&chan_id_b)); + assert_eq!(reestablish_1_chan_ids.len(), 2); + assert!(reestablish_1_chan_ids.contains(&chan_id_a)); + assert!(reestablish_1_chan_ids.contains(&chan_id_b)); + // Only nodes[1] was reloaded with stale monitor state. nodes[0] responds to the + // `channel_reestablish`s without touching its monitors. nodes[1] applies the replayed channel B + // preimage update, releases channel A's held RAA update, and frees channel A's held fulfill + // during startup processing. + check_added_monitors(&nodes[0], 0); + check_added_monitors(&nodes[1], 3); + + // The first message batch after reconnect contains channel updates from both nodes. nodes[1] + // also sends the channel A fulfill that startup processing released from the holding cell. + let restart_msgs_0 = nodes[0].node.get_and_clear_pending_msg_events(); + let restart_msgs_1 = nodes[1].node.get_and_clear_pending_msg_events(); + let mut restart_scids_0 = Vec::new(); + let mut restart_scids_1 = Vec::new(); + let mut startup_fulfill_chan_ids = Vec::new(); + for ev in &restart_msgs_0 { + match ev { + MessageSendEvent::SendChannelUpdate { node_id, msg } => { + assert_eq!(*node_id, node_1_id); + restart_scids_0.push(msg.contents.short_channel_id); + }, + _ => panic!("Unexpected restart message from node 0: {:?}", ev), + } + } + for ev in &restart_msgs_1 { + match ev { + MessageSendEvent::SendChannelUpdate { node_id, msg } => { + assert_eq!(*node_id, node_0_id); + restart_scids_1.push(msg.contents.short_channel_id); + }, + MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates } => { + assert_eq!(*node_id, node_0_id); + startup_fulfill_chan_ids.push(*channel_id); + assert_eq!(updates.update_fulfill_htlcs.len(), 1); + assert!(updates.update_add_htlcs.is_empty()); + assert!(updates.update_fail_htlcs.is_empty()); + assert!(updates.update_fail_malformed_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + for fulfill in &updates.update_fulfill_htlcs { + nodes[0].node.handle_update_fulfill_htlc(node_1_id, fulfill.clone()); + } + // Complete the standard commitment handshake for the released fulfill. The helper + // checks nodes[0]'s incoming commitment monitor update, nodes[1]'s response monitor + // updates, and nodes[0]'s held final monitor update. + do_commitment_signed_dance( + &nodes[0], &nodes[1], &updates.commitment_signed, false, false, + ); + }, + _ => panic!("Unexpected restart message from node 1: {:?}", ev), + } + } + assert_eq!(restart_scids_0.len(), 2); + assert!(restart_scids_0.contains(&scid_a)); + assert!(restart_scids_0.contains(&scid_b)); + assert_eq!(restart_scids_1.len(), 2); + assert!(restart_scids_1.contains(&scid_a)); + assert!(restart_scids_1.contains(&scid_b)); + assert_eq!(startup_fulfill_chan_ids, vec![chan_id_a]); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + check_added_monitors(&nodes[0], 0); + check_added_monitors(&nodes[1], 0); + + // Receiving the startup-released fulfill gives nodes[0] the payment preimage. That is enough to + // emit `PaymentSent`, even though channel B's path-level success still needs its own fulfill. + let startup_payment_events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(startup_payment_events.len(), 2); + let mut saw_startup_payment_sent = false; + let mut startup_success_scids = Vec::new(); + for ev in &startup_payment_events { + match ev { + Event::PaymentSent { + payment_preimage: sent_preimage, + payment_hash: sent_hash, + amount_msat: sent_amount, + fee_paid_msat, + .. + } => { + assert_eq!(*sent_preimage, payment_preimage); + assert_eq!(*sent_hash, payment_hash); + assert_eq!(*sent_amount, Some(amt_msat)); + assert_eq!(*fee_paid_msat, Some(0)); + saw_startup_payment_sent = true; + }, + Event::PaymentPathSuccessful { payment_hash: Some(path_hash), path, .. } => { + assert_eq!(*path_hash, payment_hash); + assert_eq!(path.hops.len(), 1); + startup_success_scids.push(path.hops[0].short_channel_id); + }, + _ => panic!("Unexpected startup payment event: {:?}", ev), + } + } + assert!(saw_startup_payment_sent); + assert_eq!(startup_success_scids, vec![payment_scid_a]); + + // Handling the claim event runs the event-completion action that releases the remaining + // RAA-blocked monitor update. The startup unblock path already released channel A, so channel B + // is the only fulfill that should be emitted here. + let claim_events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(claim_events.len(), 1); + match &claim_events[0] { + Event::PaymentClaimed { payment_hash: claimed_hash, amount_msat, htlcs, .. } => { + assert_eq!(*claimed_hash, payment_hash); + assert_eq!(*amount_msat, amt_msat); + assert_eq!(htlcs.len(), 2); + }, + _ => panic!("Unexpected event: {:?}", claim_events[0]), + } + // The `PaymentSent` event above releases the monitor update that nodes[0] held after the final + // channel A startup revocation. + check_added_monitors(&nodes[0], 1); + // Handling `PaymentClaimed` releases channel B's held revocation update and then the fulfill + // that was waiting behind it. + check_added_monitors(&nodes[1], 2); + + // Channel A's fulfill was already sent during startup. The `PaymentClaimed` completion action + // now frees channel B's held fulfill, and no other HTLC update should be bundled with it. + let fulfill_msgs = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(fulfill_msgs.len(), 1); + match &fulfill_msgs[0] { + MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates } => { + assert_eq!(*node_id, node_0_id); + assert_eq!(*channel_id, chan_id_b); + assert_eq!(updates.update_fulfill_htlcs.len(), 1); + assert!(updates.update_add_htlcs.is_empty()); + assert!(updates.update_fail_htlcs.is_empty()); + assert!(updates.update_fail_malformed_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + for fulfill in &updates.update_fulfill_htlcs { + nodes[0].node.handle_update_fulfill_htlc(node_1_id, fulfill.clone()); + } + // Complete the same commitment handshake for channel B. Here nodes[0]'s final monitor + // update is persisted immediately because `PaymentSent` already ran for channel A. + do_commitment_signed_dance( + &nodes[0], &nodes[1], &updates.commitment_signed, false, false, + ); + }, + _ => panic!("Unexpected fulfill message: {:?}", fulfill_msgs[0]), + } + check_added_monitors(&nodes[1], 0); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + let final_payment_events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(final_payment_events.len(), 1); + match &final_payment_events[0] { + Event::PaymentPathSuccessful { payment_hash: Some(path_hash), path, .. } => { + assert_eq!(*path_hash, payment_hash); + assert_eq!(path.hops.len(), 1); + assert_eq!(path.hops[0].short_channel_id, payment_scid_b); + }, + _ => panic!("Unexpected final payment event: {:?}", final_payment_events[0]), + } + check_added_monitors(&nodes[0], 0); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + check_added_monitors(&nodes[0], 0); + check_added_monitors(&nodes[1], 0); + + // Both MPP parts should have been fulfilled back to nodes[0]. If either channel still has a + // pending outbound HTLC, its fulfill remained stuck in nodes[1]'s holding cell after reload. + let pending: Vec<_> = nodes[0].node.list_channels().iter() + .filter(|channel| channel.channel_id == chan_id_a || channel.channel_id == chan_id_b) + .filter(|channel| !channel.pending_outbound_htlcs.is_empty()) + .map(|channel| channel.channel_id) + .collect(); + assert!(pending.is_empty(), "HTLC fulfills remained stuck on channels {:?}", pending); +} + fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_htlc: bool, use_intercept: bool) { if !use_cs_commitment { assert!(!claim_htlc); } // If we go to forward a payment, and the ChannelMonitor persistence completes, but the @@ -956,7 +1327,7 @@ fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_ht let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); let htlc_expiry = nodes[0].best_block_info().1 + TEST_FINAL_CLTV; nodes[0].node.send_payment_with_route(route, payment_hash, - RecipientOnionFields::secret_only(payment_secret), payment_id).unwrap(); + RecipientOnionFields::secret_only(payment_secret, 1_000_000), payment_id).unwrap(); check_added_monitors(&nodes[0], 1); let payment_event = SendEvent::from_node(&nodes[0]); @@ -1014,7 +1385,7 @@ fn do_forwarded_payment_no_manager_persistence(use_cs_commitment: bool, claim_ht check_added_monitors(&nodes[2], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[2], 1, reason, &[nodes[1].node.get_our_node_id()], 100000); - check_closed_broadcast!(nodes[2], true); + check_closed_broadcast(&nodes[2], 1, true); let chan_0_monitor_serialized = get_monitor!(nodes[1], chan_id_1).encode(); let chan_1_monitor_serialized = get_monitor!(nodes[1], chan_id_2).encode(); @@ -1211,7 +1582,7 @@ fn do_manager_persisted_pre_outbound_edge_forward(intercept_htlc: bool) { if intercept_htlc { route.paths[0].hops[1].short_channel_id = nodes[1].node.get_intercept_scid(); } - nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret, amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]); @@ -1304,7 +1675,7 @@ fn test_manager_persisted_post_outbound_edge_forward() { // Lock in the HTLC from node_a <> node_b. let amt_msat = 5000; let (mut route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], amt_msat); - nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret, amt_msat), PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]); @@ -1363,7 +1734,8 @@ fn test_manager_persisted_post_outbound_edge_holding_cell() { // Lock in the HTLC from node_a <> node_b. let amt_msat = 1000; let (route, payment_hash, payment_preimage, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[2], amt_msat); - nodes[0].node.send_payment_with_route(route, payment_hash, RecipientOnionFields::secret_only(payment_secret), PaymentId(payment_hash.0)).unwrap(); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, PaymentId(payment_hash.0)).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); nodes[1].node.handle_update_add_htlc(nodes[0].node.get_our_node_id(), &updates.update_add_htlcs[0]); @@ -1372,7 +1744,8 @@ fn test_manager_persisted_post_outbound_edge_holding_cell() { // Send a 2nd HTLC node_c -> node_b, to force the first HTLC into the holding cell. chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); let (route_2, payment_hash_2, payment_preimage_2, payment_secret_2) = get_route_and_payment_hash!(nodes[2], nodes[1], amt_msat); - nodes[2].node.send_payment_with_route(route_2, payment_hash_2, RecipientOnionFields::secret_only(payment_secret_2), PaymentId(payment_hash_2.0)).unwrap(); + let onion = RecipientOnionFields::secret_only(payment_secret_2, amt_msat); + nodes[2].node.send_payment_with_route(route_2, payment_hash_2, onion, PaymentId(payment_hash_2.0)).unwrap(); let send_event = SendEvent::from_event(nodes[2].node.get_and_clear_pending_msg_events().remove(0)); nodes[1].node.handle_update_add_htlc(nodes[2].node.get_our_node_id(), &send_event.msgs[0]); @@ -1546,9 +1919,9 @@ fn test_htlc_localremoved_persistence() { let test_preimage = PaymentPreimage([42; 32]); let mismatch_payment_hash = PaymentHash([43; 32]); let session_privs = nodes[0].node.test_add_new_pending_payment(mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), PaymentId(mismatch_payment_hash.0), &route).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), PaymentId(mismatch_payment_hash.0), &route).unwrap(); nodes[0].node.test_send_payment_internal(&route, mismatch_payment_hash, - RecipientOnionFields::spontaneous_empty(), Some(test_preimage), PaymentId(mismatch_payment_hash.0), None, session_privs).unwrap(); + RecipientOnionFields::spontaneous_empty(10_000), Some(test_preimage), PaymentId(mismatch_payment_hash.0), session_privs).unwrap(); check_added_monitors(&nodes[0], 1); let updates = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); @@ -1733,7 +2106,7 @@ fn test_hold_completed_inflight_monitor_updates_upon_manager_reload() { let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); let payment_id = PaymentId(payment_hash.0); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); check_added_monitors(&nodes[0], 1); @@ -1958,14 +2331,8 @@ fn test_reload_node_with_preimage_in_monitor_claims_htlc() { ); // When the claim is reconstructed during reload, a PaymentForwarded event is generated. - // This event has next_user_channel_id as None since the outbound HTLC was already removed. // Fetching events triggers the pending monitor update (adding preimage) to be applied. - let events = nodes[1].node.get_and_clear_pending_events(); - assert_eq!(events.len(), 1); - match &events[0] { - Event::PaymentForwarded { total_fee_earned_msat: Some(1000), .. } => {}, - _ => panic!("Expected PaymentForwarded event"), - } + expect_payment_forwarded!(nodes[1], nodes[0], nodes[2], Some(1000), false, false); check_added_monitors(&nodes[1], 1); // Reconnect nodes[1] to nodes[0]. The claim should be in nodes[1]'s holding cell. @@ -2088,3 +2455,152 @@ fn test_reload_node_without_preimage_fails_htlc() { // nodes[0] should now have received the failure and generate PaymentFailed. expect_payment_failed_conditions(&nodes[0], payment_hash, false, PaymentFailedConditions::new()); } + +#[test] +fn test_reload_with_mpp_claims_on_same_channel() { + // Test that if a forwarding node has two HTLCs for the same MPP payment that were both + // irrevocably removed on the outbound edge via claim but are still forwarded-and-unresolved + // on the inbound edge, both HTLCs will be claimed backwards on restart. + // + // Topology: + // nodes[0] ----chan_0_1----> nodes[1] ----chan_1_2_a----> nodes[2] + // \----chan_1_2_b---/ + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let persister; + let new_chain_monitor; + let mut config = test_default_channel_config(); + // Set the percentage to the default value at the time this test was written + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = 10; + let configs: [Option<UserConfig>; 3] = core::array::from_fn(|_| Some(config.clone())); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &configs); + let nodes_1_deserialized; + let mut nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_0_id = nodes[0].node.get_our_node_id(); + let node_1_id = nodes[1].node.get_our_node_id(); + let node_2_id = nodes[2].node.get_our_node_id(); + + let chan_0_1 = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 2_000_000, 0); + let chan_1_2_a = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + let chan_1_2_b = create_announced_chan_between_nodes_with_value(&nodes, 1, 2, 1_000_000, 0); + + let chan_id_0_1 = chan_0_1.2; + let chan_id_1_2_a = chan_1_2_a.2; + let chan_id_1_2_b = chan_1_2_b.2; + + // Send an MPP payment large enough that the router must split it across both outbound channels. + // Each 1M sat outbound channel has 100M msat max in-flight, so 150M msat requires splitting. + let amt_msat = 150_000_000; + let (route, payment_hash, payment_preimage, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[2], amt_msat); + + let payment_id = PaymentId(nodes[0].keys_manager.backing.get_secure_random_bytes()); + let onion = RecipientOnionFields::secret_only(payment_secret, amt_msat); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + check_added_monitors(&nodes[0], 1); + + // Forward the first HTLC nodes[0] -> nodes[1] -> nodes[2]. Note that the second HTLC is released + // from the holding cell during the first HTLC's commitment_signed_dance. + let mut events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let payment_event_1 = SendEvent::from_event(events.remove(0)); + + nodes[1].node.handle_update_add_htlc(node_0_id, &payment_event_1.msgs[0]); + check_added_monitors(&nodes[1], 0); + nodes[1].node.handle_commitment_signed_batch_test(node_0_id, &payment_event_1.commitment_msg); + check_added_monitors(&nodes[1], 1); + let (_, raa, holding_cell_htlcs) = + do_main_commitment_signed_dance(&nodes[1], &nodes[0], false); + assert_eq!(holding_cell_htlcs.len(), 1); + let payment_event_2 = holding_cell_htlcs.into_iter().next().unwrap(); + nodes[1].node.handle_revoke_and_ack(node_0_id, &raa); + check_added_monitors(&nodes[1], 1); + + nodes[1].node.process_pending_htlc_forwards(); + check_added_monitors(&nodes[1], 1); + let mut events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 1); + let ev_1_2 = events.remove(0); + pass_along_path( + &nodes[1], &[&nodes[2]], amt_msat, payment_hash, Some(payment_secret), ev_1_2, false, None, + ); + + // Second HTLC: full path nodes[0] -> nodes[1] -> nodes[2]. PaymentClaimable expected at end. + pass_along_path( + &nodes[0], &[&nodes[1], &nodes[2]], amt_msat, payment_hash, Some(payment_secret), + payment_event_2, true, None, + ); + + // Claim the HTLCs such that they're fully removed from the outbound edge, but disconnect + // node_0<>node_1 so that they can't be claimed backwards by node_1. + nodes[2].node.claim_funds(payment_preimage); + check_added_monitors(&nodes[2], 2); + expect_payment_claimed!(nodes[2], payment_hash, amt_msat); + + nodes[0].node.peer_disconnected(node_1_id); + nodes[1].node.peer_disconnected(node_0_id); + + let mut events = nodes[2].node.get_and_clear_pending_msg_events(); + assert_eq!(events.len(), 2); + for ev in events { + match ev { + MessageSendEvent::UpdateHTLCs { ref node_id, ref updates, .. } => { + assert_eq!(*node_id, node_1_id); + assert_eq!(updates.update_fulfill_htlcs.len(), 1); + nodes[1].node.handle_update_fulfill_htlc(node_2_id, updates.update_fulfill_htlcs[0].clone()); + check_added_monitors(&nodes[1], 1); + do_commitment_signed_dance(&nodes[1], &nodes[2], &updates.commitment_signed, false, false); + }, + _ => panic!("Unexpected event"), + } + } + + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + for event in events { + expect_payment_forwarded( + event, &nodes[1], &nodes[0], &nodes[2], Some(1000), None, false, false, false, + ); + } + + // Clear the holding cell's claim entries on chan_0_1 before serialization. + // This simulates a crash where both HTLCs were fully removed on the outbound edges but are + // still present on the inbound edge without a resolution. + nodes[1].node.test_clear_channel_holding_cell(node_0_id, chan_id_0_1); + + let node_1_serialized = nodes[1].node.encode(); + let mon_0_1_serialized = get_monitor!(nodes[1], chan_id_0_1).encode(); + let mon_1_2_a_serialized = get_monitor!(nodes[1], chan_id_1_2_a).encode(); + let mon_1_2_b_serialized = get_monitor!(nodes[1], chan_id_1_2_b).encode(); + + reload_node!( + nodes[1], + node_1_serialized, + &[&mon_0_1_serialized, &mon_1_2_a_serialized, &mon_1_2_b_serialized], + persister, + new_chain_monitor, + nodes_1_deserialized, + Some(true) + ); + nodes[1].disable_monitor_completeness_assertion(); + + // When the claims are reconstructed during reload, PaymentForwarded events are regenerated. + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + for event in events { + expect_payment_forwarded( + event, &nodes[1], &nodes[0], &nodes[2], Some(1000), None, false, false, false, + ); + } + // Fetching events triggers the pending monitor updates (one for each HTLC preimage) to be applied. + check_added_monitors(&nodes[1], 2); + + // Reconnect nodes[1] to nodes[0]. Both claims should be in nodes[1]'s holding cell. + let mut reconnect_args = ReconnectArgs::new(&nodes[1], &nodes[0]); + reconnect_args.pending_cell_htlc_claims = (0, 2); + reconnect_nodes(reconnect_args); + + // nodes[0] should now have received both fulfills and generate PaymentSent. + expect_payment_sent(&nodes[0], payment_preimage, None, true, true); +} diff --git a/lightning/src/ln/reorg_tests.rs b/lightning/src/ln/reorg_tests.rs index dac92cddc97..5b5160148d7 100644 --- a/lightning/src/ln/reorg_tests.rs +++ b/lightning/src/ln/reorg_tests.rs @@ -1,5 +1,3 @@ -#![cfg_attr(rustfmt, rustfmt_skip)] - // This file is Copyright its original authors, visible in version control // history. // @@ -12,10 +10,10 @@ //! Further functional tests which test blockchain reorganizations. use crate::chain::chaininterface::LowerBoundedFeeEstimator; -use crate::chain::channelmonitor::{ANTI_REORG_DELAY, Balance, LATENCY_GRACE_PERIOD_BLOCKS}; +use crate::chain::channelmonitor::{Balance, ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS}; use crate::chain::transaction::OutPoint; use crate::chain::Confirm; -use crate::events::{Event, ClosureReason, HTLCHandlingFailureType}; +use crate::events::{ClosureReason, Event, HTLCHandlingFailureType}; use crate::ln::msgs::{BaseMessageHandler, ChannelMessageHandler, Init, MessageSendEvent}; use crate::ln::types::ChannelId; use crate::sign::OutputSpender; @@ -23,8 +21,8 @@ use crate::types::payment::PaymentHash; use crate::types::string::UntrustedString; use crate::util::ser::Writeable; -use bitcoin::script::Builder; use bitcoin::opcodes; +use bitcoin::script::Builder; use bitcoin::secp256k1::Secp256k1; use crate::prelude::*; @@ -52,22 +50,26 @@ fn do_test_onchain_htlc_reorg(local_commitment: bool, claim: bool) { let legacy_cfg = test_legacy_channel_config(); let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, Some(legacy_cfg), None]); let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + let node_id_2 = nodes[2].node.get_our_node_id(); create_announced_chan_between_nodes(&nodes, 0, 1); let chan_2 = create_announced_chan_between_nodes(&nodes, 1, 2); // Make sure all nodes are at the same starting height - connect_blocks(&nodes[0], 2*CHAN_CONFIRM_DEPTH + 1 - nodes[0].best_block_info().1); - connect_blocks(&nodes[1], 2*CHAN_CONFIRM_DEPTH + 1 - nodes[1].best_block_info().1); - connect_blocks(&nodes[2], 2*CHAN_CONFIRM_DEPTH + 1 - nodes[2].best_block_info().1); + connect_blocks(&nodes[0], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[0].best_block_info().1); + connect_blocks(&nodes[1], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[1].best_block_info().1); + connect_blocks(&nodes[2], 2 * CHAN_CONFIRM_DEPTH + 1 - nodes[2].best_block_info().1); - let (our_payment_preimage, our_payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 1_000_000); + let (our_payment_preimage, our_payment_hash, ..) = + route_payment(&nodes[0], &[&nodes[1], &nodes[2]], 1_000_000); // Provide preimage to node 2 by claiming payment nodes[2].node.claim_funds(our_payment_preimage); expect_payment_claimed!(nodes[2], our_payment_hash, 1_000_000); check_added_monitors(&nodes[2], 1); - get_htlc_update_msgs(&nodes[2], &nodes[1].node.get_our_node_id()); + get_htlc_update_msgs(&nodes[2], &node_id_1); let claim_txn = if local_commitment { // Broadcast node 1 commitment txn to broadcast the HTLC-Timeout @@ -78,18 +80,23 @@ fn do_test_onchain_htlc_reorg(local_commitment: bool, claim: bool) { check_spends!(node_1_commitment_txn[1], node_1_commitment_txn[0]); // Give node 2 node 1's transactions and get its response (claiming the HTLC instead). - connect_block(&nodes[2], &create_dummy_block(nodes[2].best_block_hash(), 42, node_1_commitment_txn.clone())); - check_closed_broadcast!(nodes[2], true); // We should get a BroadcastChannelUpdate (and *only* a BroadcstChannelUpdate) + let block = + create_dummy_block(nodes[2].best_block_hash(), 42, node_1_commitment_txn.clone()); + connect_block(&nodes[2], &block); + check_closed_broadcast(&nodes[2], 1, true); // We should get a BroadcastChannelUpdate (and *only* a BroadcstChannelUpdate) check_added_monitors(&nodes[2], 1); - check_closed_event(&nodes[2], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 100000); - let node_2_commitment_txn = nodes[2].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); + let reason = ClosureReason::CommitmentTxConfirmed; + check_closed_event(&nodes[2], 1, reason, &[node_id_1], 100000); + let node_2_commitment_txn = nodes[2].tx_broadcaster.txn_broadcast(); assert_eq!(node_2_commitment_txn.len(), 1); // ChannelMonitor: 1 offered HTLC-Claim check_spends!(node_2_commitment_txn[0], node_1_commitment_txn[0]); // Make sure node 1's height is the same as the !local_commitment case connect_blocks(&nodes[1], 1); // Confirm node 1's commitment txn (and HTLC-Timeout) on node 1 - connect_block(&nodes[1], &create_dummy_block(nodes[1].best_block_hash(), 42, node_1_commitment_txn.clone())); + let block = + create_dummy_block(nodes[1].best_block_hash(), 42, node_1_commitment_txn.clone()); + connect_block(&nodes[1], &block); // ...but return node 1's commitment tx in case claim is set and we're preparing to reorg vec![node_1_commitment_txn[0].clone(), node_2_commitment_txn[0].clone()] @@ -113,9 +120,9 @@ fn do_test_onchain_htlc_reorg(local_commitment: bool, claim: bool) { // ...but return node 2's commitment tx (and claim) in case claim is set and we're preparing to reorg vec![node_2_commitment_txn.pop().unwrap()] }; - check_closed_broadcast!(nodes[1], true); // We should get a BroadcastChannelUpdate (and *only* a BroadcstChannelUpdate) + check_closed_broadcast(&nodes[1], 1, true); // We should get a BroadcastChannelUpdate (and *only* a BroadcstChannelUpdate) check_added_monitors(&nodes[1], 1); - check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[2].node.get_our_node_id()], 100000); + check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[node_id_2], 100000); // Connect ANTI_REORG_DELAY - 2 blocks, giving us a confirmation count of ANTI_REORG_DELAY - 1. connect_blocks(&nodes[1], ANTI_REORG_DELAY - 2); check_added_monitors(&nodes[1], 0); @@ -136,25 +143,27 @@ fn do_test_onchain_htlc_reorg(local_commitment: bool, claim: bool) { connect_block(&nodes[1], &create_dummy_block(nodes[1].best_block_hash(), 42, Vec::new())); expect_and_process_pending_htlcs_and_htlc_handling_failed( &nodes[1], - &[HTLCHandlingFailureType::Forward { node_id: Some(nodes[2].node.get_our_node_id()), channel_id: chan_2.2 }] + &[HTLCHandlingFailureType::Forward { node_id: Some(node_id_2), channel_id: chan_2.2 }], ); } check_added_monitors(&nodes[1], 1); // Which should result in an immediate claim/fail of the HTLC: - let mut htlc_updates = get_htlc_update_msgs(&nodes[1], &nodes[0].node.get_our_node_id()); + let mut htlc_updates = get_htlc_update_msgs(&nodes[1], &node_id_0); if claim { assert_eq!(htlc_updates.update_fulfill_htlcs.len(), 1); - nodes[0].node.handle_update_fulfill_htlc(nodes[1].node.get_our_node_id(), htlc_updates.update_fulfill_htlcs.remove(0)); + let update_fulfill = htlc_updates.update_fulfill_htlcs.remove(0); + nodes[0].node.handle_update_fulfill_htlc(node_id_1, update_fulfill); } else { assert_eq!(htlc_updates.update_fail_htlcs.len(), 1); - nodes[0].node.handle_update_fail_htlc(nodes[1].node.get_our_node_id(), &htlc_updates.update_fail_htlcs[0]); + nodes[0].node.handle_update_fail_htlc(node_id_1, &htlc_updates.update_fail_htlcs[0]); } do_commitment_signed_dance(&nodes[0], &nodes[1], &htlc_updates.commitment_signed, false, true); if claim { expect_payment_sent!(nodes[0], our_payment_preimage); } else { - expect_payment_failed_with_update!(nodes[0], our_payment_hash, false, chan_2.0.contents.short_channel_id, true); + let scid = chan_2.0.contents.short_channel_id; + expect_payment_failed_with_update!(nodes[0], our_payment_hash, false, scid, true); } } @@ -186,6 +195,8 @@ fn test_counterparty_revoked_reorg() { let legacy_cfg = test_legacy_channel_config(); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg), None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1_000_000, 500_000_000); @@ -196,25 +207,29 @@ fn test_counterparty_revoked_reorg() { // Now add two HTLCs in each direction, one dust and one not. route_payment(&nodes[0], &[&nodes[1]], 5_000_000); route_payment(&nodes[0], &[&nodes[1]], 5_000); - let (payment_preimage_3, payment_hash_3, ..) = route_payment(&nodes[1], &[&nodes[0]], 4_000_000); + let (payment_preimage_3, payment_hash_3, ..) = + route_payment(&nodes[1], &[&nodes[0]], 4_000_000); let payment_hash_4 = route_payment(&nodes[1], &[&nodes[0]], 4_000).1; nodes[0].node.claim_funds(payment_preimage_3); - let _ = get_htlc_update_msgs(&nodes[0], &nodes[1].node.get_our_node_id()); + let _ = get_htlc_update_msgs(&nodes[0], &node_id_1); check_added_monitors(&nodes[0], 1); expect_payment_claimed!(nodes[0], payment_hash_3, 4_000_000); let mut unrevoked_local_txn = get_local_commitment_txn!(nodes[0], chan.2); - assert_eq!(unrevoked_local_txn.len(), 3); // commitment + 2 HTLC txn + // There should be the commitment transaction and two HTLC transactions. + assert_eq!(unrevoked_local_txn.len(), 3); // Sort the unrevoked transactions in reverse order, ie commitment tx, then HTLC 1 then HTLC 3 - unrevoked_local_txn.sort_unstable_by_key(|tx| 1_000_000 - tx.output.iter().map(|outp| outp.value.to_sat()).sum::<u64>()); + unrevoked_local_txn.sort_unstable_by_key(|tx| { + 1_000_000 - tx.output.iter().map(|outp| outp.value.to_sat()).sum::<u64>() + }); // Now mine A's old commitment transaction, which should close the channel, but take no action // on any of the HTLCs, at least until we get six confirmations (which we won't get). mine_transaction(&nodes[1], &revoked_local_txn[0]); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); - check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); + check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[node_id_0], 1000000); // Connect up to one block before the revoked transaction would be considered final, then do a // reorg that disconnects the full chain and goes up to the height at which the revoked @@ -248,7 +263,10 @@ fn test_counterparty_revoked_reorg() { expect_payment_failed_conditions(&nodes[1], payment_hash_4, false, conditions) } -fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_unconfirmed: bool, connect_style: ConnectStyle) { +fn do_test_unconf_chan( + reload_node: bool, reorg_after_reload: bool, use_funding_unconfirmed: bool, + connect_style: ConnectStyle, +) { // After creating a chan between nodes, we disconnect all blocks previously seen to force a // channel close on nodes[0] side. We also use this to provide very basic testing of logic // around freeing background events which store monitor updates during block_[dis]connected. @@ -264,12 +282,15 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); *nodes[0].connect_style.borrow_mut() = connect_style; - let chan_conf_height = core::cmp::max(nodes[0].best_block_info().1 + 1, nodes[1].best_block_info().1 + 1); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + let chan_conf_height = + core::cmp::max(nodes[0].best_block_info().1 + 1, nodes[1].best_block_info().1 + 1); let chan = create_announced_chan_between_nodes(&nodes, 0, 1); { let per_peer_state = nodes[0].node.per_peer_state.read().unwrap(); - let peer_state = per_peer_state.get(&nodes[1].node.get_our_node_id()).unwrap().lock().unwrap(); + let peer_state = per_peer_state.get(&node_id_1).unwrap().lock().unwrap(); assert_eq!(peer_state.channel_by_id.len(), 1); assert_eq!(nodes[0].node.short_to_chan_info.read().unwrap().len(), 2); } @@ -306,12 +327,12 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ let relevant_txids = nodes[0].node.get_relevant_txids(); assert_eq!(relevant_txids.len(), 0); - let txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); + let txn = nodes[0].tx_broadcaster.txn_broadcast(); assert_eq!(txn.len(), 1); { let per_peer_state = nodes[0].node.per_peer_state.read().unwrap(); - let peer_state = per_peer_state.get(&nodes[1].node.get_our_node_id()).unwrap().lock().unwrap(); + let peer_state = per_peer_state.get(&node_id_1).unwrap().lock().unwrap(); assert_eq!(peer_state.channel_by_id.len(), 0); assert_eq!(nodes[0].node.short_to_chan_info.read().unwrap().len(), 0); } @@ -320,11 +341,18 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ } let expected_err = "Funding transaction was un-confirmed, originally locked at 6 confs."; + let broadcast_close_msg = + "Channel closed because of an exception: Funding transaction was un-confirmed, originally locked at 6 confs."; + let counterparty_force_closed_reason = || ClosureReason::CounterpartyForceClosed { + peer_msg: UntrustedString(format!( + "Channel closed because of an exception: {}", + expected_err + )), + }; if reload_node && !reorg_after_reload { - handle_announce_close_broadcast_events(&nodes, 0, 1, true, "Channel closed because of an exception: Funding transaction was un-confirmed, originally locked at 6 confs."); + handle_announce_close_broadcast_events(&nodes, 0, 1, true, broadcast_close_msg); check_added_monitors(&nodes[1], 1); - let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(format!("Channel closed because of an exception: {}", expected_err)) }; - check_closed_event(&nodes[1], 1, reason, &[nodes[0].node.get_our_node_id()], 100000); + check_closed_event(&nodes[1], 1, counterparty_force_closed_reason(), &[node_id_0], 100000); } if reload_node { @@ -334,10 +362,20 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ // it when we go to deserialize, and then use the ChannelManager. let nodes_0_serialized = nodes[0].node.encode(); let chan_0_monitor_serialized = get_monitor!(nodes[0], chan.2).encode(); + let current_config = nodes[0].node.get_current_config(); + let serialized_monitors = [&chan_0_monitor_serialized[..]]; - reload_node!(nodes[0], nodes[0].node.get_current_config(), &nodes_0_serialized, &[&chan_0_monitor_serialized], persister, new_chain_monitor, nodes_0_deserialized); + reload_node!( + nodes[0], + current_config, + &nodes_0_serialized, + &serialized_monitors, + persister, + new_chain_monitor, + nodes_0_deserialized + ); - nodes[1].node.peer_disconnected(nodes[0].node.get_our_node_id()); + nodes[1].node.peer_disconnected(node_id_0); if reorg_after_reload { // If we haven't yet closed the channel, reconnect the peers so that nodes[0] will @@ -381,7 +419,7 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ { let per_peer_state = nodes[0].node.per_peer_state.read().unwrap(); - let peer_state = per_peer_state.get(&nodes[1].node.get_our_node_id()).unwrap().lock().unwrap(); + let peer_state = per_peer_state.get(&node_id_1).unwrap().lock().unwrap(); assert_eq!(peer_state.channel_by_id.len(), 0); assert_eq!(nodes[0].node.short_to_chan_info.read().unwrap().len(), 0); } @@ -393,30 +431,36 @@ fn do_test_unconf_chan(reload_node: bool, reorg_after_reload: bool, use_funding_ } check_added_monitors(&nodes[0], 1); - let txn = nodes[0].tx_broadcaster.txn_broadcasted.lock().unwrap().split_off(0); + let txn = nodes[0].tx_broadcaster.txn_broadcast(); assert_eq!(txn.len(), 1); } if reorg_after_reload || !reload_node { - handle_announce_close_broadcast_events(&nodes, 0, 1, true, "Channel closed because of an exception: Funding transaction was un-confirmed, originally locked at 6 confs."); + handle_announce_close_broadcast_events(&nodes, 0, 1, true, broadcast_close_msg); check_added_monitors(&nodes[1], 1); - let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(format!("Channel closed because of an exception: {}", expected_err)) }; - check_closed_event(&nodes[1], 1, reason, &[nodes[0].node.get_our_node_id()], 100000); + check_closed_event(&nodes[1], 1, counterparty_force_closed_reason(), &[node_id_0], 100000); } - check_closed_event(&nodes[0], 1, ClosureReason::ProcessingError { err: expected_err.to_owned() }, &[nodes[1].node.get_our_node_id()], 100000); + let processing_error = ClosureReason::ProcessingError { err: expected_err.to_owned() }; + check_closed_event(&nodes[0], 1, processing_error, &[node_id_1], 100000); // Now check that we can create a new channel if reload_node && !reorg_after_reload { // If we dropped the channel before reloading the node, nodes[1] was also dropped from // nodes[0] storage, and hence not connected again on startup. We therefore need to // reconnect to the node before attempting to create a new channel. - nodes[0].node.peer_connected(nodes[1].node.get_our_node_id(), &Init { - features: nodes[1].node.init_features(), networks: None, remote_network_address: None - }, true).unwrap(); - nodes[1].node.peer_connected(nodes[0].node.get_our_node_id(), &Init { - features: nodes[0].node.init_features(), networks: None, remote_network_address: None - }, true).unwrap(); + let node_1_init = Init { + features: nodes[1].node.init_features(), + networks: None, + remote_network_address: None, + }; + let node_0_init = Init { + features: nodes[0].node.init_features(), + networks: None, + remote_network_address: None, + }; + nodes[0].node.peer_connected(node_id_1, &node_1_init, true).unwrap(); + nodes[1].node.peer_connected(node_id_0, &node_0_init, true).unwrap(); } create_announced_chan_between_nodes(&nodes, 0, 1); @@ -472,10 +516,14 @@ fn test_set_outpoints_partial_claiming() { let legacy_cfg = test_legacy_channel_config(); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg), None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); let chan = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 1000000, 59000000); - let (payment_preimage_1, payment_hash_1, ..) = route_payment(&nodes[1], &[&nodes[0]], 3_000_000); - let (payment_preimage_2, payment_hash_2, ..) = route_payment(&nodes[1], &[&nodes[0]], 3_000_000); + let (payment_preimage_1, payment_hash_1, ..) = + route_payment(&nodes[1], &[&nodes[0]], 3_000_000); + let (payment_preimage_2, payment_hash_2, ..) = + route_payment(&nodes[1], &[&nodes[0]], 3_000_000); // Remote commitment txn with 4 outputs: to_local, to_remote, 2 outgoing HTLC let remote_txn = get_local_commitment_txn!(nodes[1], chan.2); @@ -497,8 +545,8 @@ fn test_set_outpoints_partial_claiming() { // Connect blocks on node A commitment transaction mine_transaction(&nodes[0], &remote_txn[0]); - check_closed_broadcast!(nodes[0], true); - check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000); + check_closed_broadcast(&nodes[0], 1, true); + check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[node_id_1], 1000000); check_added_monitors(&nodes[0], 1); // Verify node A broadcast tx claiming both HTLCs { @@ -512,17 +560,18 @@ fn test_set_outpoints_partial_claiming() { // Connect blocks on node B connect_blocks(&nodes[1], TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS + 1); - check_closed_broadcast!(nodes[1], true); - check_closed_events(&nodes[1], &[ExpectedCloseEvent { + check_closed_broadcast(&nodes[1], 1, true); + let expected_close = ExpectedCloseEvent { channel_capacity_sats: Some(1_000_000), channel_id: Some(chan.2), - counterparty_node_id: Some(nodes[0].node.get_our_node_id()), + counterparty_node_id: Some(node_id_0), discard_funding: false, splice_failed: false, reason: None, // Could be due to either HTLC timing out, so don't bother checking channel_funding_txo: None, user_channel_id: None, - }]); + }; + check_closed_events(&nodes[1], &[expected_close]); check_added_monitors(&nodes[1], 1); // Verify node B broadcast 2 HTLC-timeout txn let partial_claim_tx = { @@ -581,6 +630,8 @@ fn do_test_to_remote_after_local_detection(style: ConnectStyle) { let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); *nodes[0].connect_style.borrow_mut() = style; *nodes[1].connect_style.borrow_mut() = style; @@ -596,14 +647,14 @@ fn do_test_to_remote_after_local_detection(style: ConnectStyle) { mine_transaction(&nodes[0], &remote_txn_a[0]); mine_transaction(&nodes[1], &remote_txn_a[0]); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); assert!(nodes[0].node.list_channels().is_empty()); check_added_monitors(&nodes[0], 1); - check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[1].node.get_our_node_id()], 1000000); - check_closed_broadcast!(nodes[1], true); + check_closed_event(&nodes[0], 1, ClosureReason::CommitmentTxConfirmed, &[node_id_1], 1000000); + check_closed_broadcast(&nodes[1], 1, true); assert!(nodes[1].node.list_channels().is_empty()); check_added_monitors(&nodes[1], 1); - check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 1000000); + check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[node_id_0], 1000000); assert!(nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty()); assert!(nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events().is_empty()); @@ -637,11 +688,25 @@ fn do_test_to_remote_after_local_detection(style: ConnectStyle) { let mut node_a_spendable = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events(); assert_eq!(node_a_spendable.len(), 1); - if let Event::SpendableOutputs { outputs, channel_id, counterparty_node_id: _ } = node_a_spendable.pop().unwrap() { + if let Event::SpendableOutputs { outputs, channel_id, counterparty_node_id: _ } = + node_a_spendable.pop().unwrap() + { assert_eq!(outputs.len(), 1); assert_eq!(channel_id, Some(chan_id)); - let spend_tx = nodes[0].keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(), - Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, None, &Secp256k1::new()).unwrap(); + let spendable_outputs = [&outputs[0]]; + let destination_script = Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(); + let spend_tx = nodes[0] + .keys_manager + .backing + .spend_spendable_outputs( + &spendable_outputs, + Vec::new(), + destination_script, + 253, + None, + &Secp256k1::new(), + ) + .unwrap(); check_spends!(spend_tx, remote_txn_b[0]); } @@ -658,11 +723,25 @@ fn do_test_to_remote_after_local_detection(style: ConnectStyle) { let mut node_b_spendable = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events(); assert_eq!(node_b_spendable.len(), 1); - if let Event::SpendableOutputs { outputs, channel_id, counterparty_node_id: _ } = node_b_spendable.pop().unwrap() { + if let Event::SpendableOutputs { outputs, channel_id, counterparty_node_id: _ } = + node_b_spendable.pop().unwrap() + { assert_eq!(outputs.len(), 1); assert_eq!(channel_id, Some(chan_id)); - let spend_tx = nodes[1].keys_manager.backing.spend_spendable_outputs(&[&outputs[0]], Vec::new(), - Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(), 253, None, &Secp256k1::new()).unwrap(); + let spendable_outputs = [&outputs[0]]; + let destination_script = Builder::new().push_opcode(opcodes::all::OP_RETURN).into_script(); + let spend_tx = nodes[1] + .keys_manager + .backing + .spend_spendable_outputs( + &spendable_outputs, + Vec::new(), + destination_script, + 253, + None, + &Secp256k1::new(), + ) + .unwrap(); check_spends!(spend_tx, remote_txn_a[0]); } } @@ -686,8 +765,10 @@ fn test_htlc_preimage_claim_holder_commitment_after_counterparty_commitment_reor let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let legacy_cfg = test_legacy_channel_config(); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg), None, None]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg), None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); let (_, _, chan_id, funding_tx) = create_announced_chan_between_nodes(&nodes, 0, 1); @@ -699,7 +780,7 @@ fn test_htlc_preimage_claim_holder_commitment_after_counterparty_commitment_reor // holder commitment. nodes[0] .node - .force_close_broadcasting_latest_txn(&chan_id, &nodes[1].node.get_our_node_id(), message.clone()) + .force_close_broadcasting_latest_txn(&chan_id, &node_id_1, message.clone()) .unwrap(); check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); @@ -707,16 +788,16 @@ fn test_htlc_preimage_claim_holder_commitment_after_counterparty_commitment_reor broadcasted_latest_txn: Some(true), message: message.clone(), }; - check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], 100000); + check_closed_event(&nodes[0], 1, reason, &[node_id_1], 100000); nodes[1] .node - .force_close_broadcasting_latest_txn(&chan_id, &nodes[0].node.get_our_node_id(), message.clone()) + .force_close_broadcasting_latest_txn(&chan_id, &node_id_0, message.clone()) .unwrap(); check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; - check_closed_event(&nodes[1], 1, reason, &[nodes[0].node.get_our_node_id()], 100000); + check_closed_event(&nodes[1], 1, reason, &[node_id_0], 100000); let mut txn = nodes[0].tx_broadcaster.txn_broadcast(); assert_eq!(txn.len(), 1); @@ -743,8 +824,11 @@ fn test_htlc_preimage_claim_holder_commitment_after_counterparty_commitment_reor // Provide the preimage now, such that we only claim from the holder commitment (since it's // currently confirmed) and not the counterparty's. get_monitor!(nodes[1], chan_id).provide_payment_preimage_unsafe_legacy( - &payment_hash, &payment_preimage, &nodes[1].tx_broadcaster, - &LowerBoundedFeeEstimator(nodes[1].fee_estimator), &nodes[1].logger + &payment_hash, + &payment_preimage, + &nodes[1].tx_broadcaster, + &LowerBoundedFeeEstimator(nodes[1].fee_estimator), + &nodes[1].logger, ); let mut txn = nodes[1].tx_broadcaster.txn_broadcast(); @@ -754,7 +838,8 @@ fn test_htlc_preimage_claim_holder_commitment_after_counterparty_commitment_reor } #[test] -fn test_htlc_preimage_claim_prev_counterparty_commitment_after_current_counterparty_commitment_reorg() { +fn test_htlc_preimage_claim_prev_counterparty_commitment_after_current_counterparty_commitment_reorg( +) { // We detect a counterparty commitment confirm onchain, followed by a reorg and a // confirmation of the previous (still unrevoked) counterparty commitment. Then, if we learn // of the preimage for an HTLC in both commitments, test that we only claim the currently @@ -762,8 +847,10 @@ fn test_htlc_preimage_claim_prev_counterparty_commitment_after_current_counterpa let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let legacy_cfg = test_legacy_channel_config(); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg), None, None]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(legacy_cfg), None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); let (_, _, chan_id, funding_tx) = create_announced_chan_between_nodes(&nodes, 0, 1); @@ -778,26 +865,32 @@ fn test_htlc_preimage_claim_prev_counterparty_commitment_after_current_counterpa check_added_monitors(&nodes[0], 1); let mut msg_events = nodes[0].node.get_and_clear_pending_msg_events(); assert_eq!(msg_events.len(), 1); - let (update_fee, commit_sig) = if let MessageSendEvent::UpdateHTLCs { node_id, channel_id: _, mut updates } = msg_events.pop().unwrap() { - assert_eq!(node_id, nodes[1].node.get_our_node_id()); - (updates.update_fee.take().unwrap(), updates.commitment_signed) - } else { + let MessageSendEvent::UpdateHTLCs { node_id, channel_id: _, mut updates } = + msg_events.pop().unwrap() + else { panic!("Unexpected message send event"); }; + assert_eq!(node_id, node_id_1); + let update_fee = updates.update_fee.take().unwrap(); + let commit_sig = updates.commitment_signed; // Handle the fee update on the other side, but don't send the last RAA such that the previous // commitment is still valid (unrevoked). - nodes[1].node().handle_update_fee(nodes[0].node.get_our_node_id(), &update_fee); - let _last_revoke_and_ack = commitment_signed_dance_return_raa(&nodes[1], &nodes[0], &commit_sig, false); + nodes[1].node().handle_update_fee(node_id_0, &update_fee); + let _last_revoke_and_ack = + commitment_signed_dance_return_raa(&nodes[1], &nodes[0], &commit_sig, false); let message = "Channel force-closed".to_owned(); // Force close with the latest commitment, confirm it, and reorg it with the previous commitment. - nodes[0].node.force_close_broadcasting_latest_txn(&chan_id, &nodes[1].node.get_our_node_id(), message.clone()).unwrap(); + nodes[0] + .node + .force_close_broadcasting_latest_txn(&chan_id, &node_id_1, message.clone()) + .unwrap(); check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; - check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], 100000); + check_closed_event(&nodes[0], 1, reason, &[node_id_1], 100000); let mut txn = nodes[0].tx_broadcaster.txn_broadcast(); assert_eq!(txn.len(), 1); @@ -810,7 +903,7 @@ fn test_htlc_preimage_claim_prev_counterparty_commitment_after_current_counterpa check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); - check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[nodes[0].node.get_our_node_id()], 100000); + check_closed_event(&nodes[1], 1, ClosureReason::CommitmentTxConfirmed, &[node_id_0], 100000); disconnect_blocks(&nodes[0], 1); disconnect_blocks(&nodes[1], 1); @@ -821,8 +914,11 @@ fn test_htlc_preimage_claim_prev_counterparty_commitment_after_current_counterpa // Provide the preimage now, such that we only claim from the previous commitment (since it's // currently confirmed) and not the latest. get_monitor!(nodes[1], chan_id).provide_payment_preimage_unsafe_legacy( - &payment_hash, &payment_preimage, &nodes[1].tx_broadcaster, - &LowerBoundedFeeEstimator(nodes[1].fee_estimator), &nodes[1].logger + &payment_hash, + &payment_preimage, + &nodes[1].tx_broadcaster, + &LowerBoundedFeeEstimator(nodes[1].fee_estimator), + &nodes[1].logger, ); let mut txn = nodes[1].tx_broadcaster.txn_broadcast(); @@ -831,10 +927,15 @@ fn test_htlc_preimage_claim_prev_counterparty_commitment_after_current_counterpa check_spends!(htlc_preimage_tx, prev_commitment_a); // Make sure it was indeed a preimage claim and not a revocation claim since the previous // commitment (still unrevoked) is the currently confirmed closing transaction. - assert_eq!(htlc_preimage_tx.input[0].witness.second_to_last().unwrap(), &payment_preimage.0[..]); + assert_eq!( + htlc_preimage_tx.input[0].witness.second_to_last().unwrap(), + &payment_preimage.0[..] + ); } -fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a_anchor: bool, revoked_counterparty_commitment: bool) { +fn do_test_retries_own_commitment_broadcast_after_reorg( + keyed_anchors: bool, p2a_anchor: bool, revoked_counterparty_commitment: bool, +) { // Tests that a node will retry broadcasting its own commitment after seeing a confirmed // counterparty commitment be reorged out. let mut chanmon_cfgs = create_chanmon_cfgs(2); @@ -847,9 +948,12 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = p2a_anchor; let persister; let new_chain_monitor; - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); let nodes_1_deserialized; let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); let coinbase_tx = provide_anchor_reserves(&nodes); @@ -862,11 +966,18 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a // Trigger a new commitment by routing a dummy HTLC. We will have B broadcast the previous commitment. let serialized_node = nodes[1].node.encode(); let serialized_monitor = get_monitor!(nodes[1], chan_id).encode(); + let serialized_monitors = [&serialized_monitor[..]]; let _ = route_payment(&nodes[0], &[&nodes[1]], 1000); reload_node!( - nodes[1], config, &serialized_node, &[&serialized_monitor], persister, new_chain_monitor, nodes_1_deserialized + nodes[1], + config, + &serialized_node, + &serialized_monitors, + persister, + new_chain_monitor, + nodes_1_deserialized ); } @@ -875,7 +986,7 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::HTLCsTimedOut { payment_hash: Some(payment_hash) }; - check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], 100_000); + check_closed_event(&nodes[0], 1, reason, &[node_id_1], 100_000); if keyed_anchors || p2a_anchor { handle_bump_close_event(&nodes[0]); } @@ -905,12 +1016,12 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a let message = "Channel force-closed".to_owned(); nodes[1] .node - .force_close_broadcasting_latest_txn(&chan_id, &nodes[0].node.get_our_node_id(), message.clone()) + .force_close_broadcasting_latest_txn(&chan_id, &node_id_0, message.clone()) .unwrap(); check_closed_broadcast(&nodes[1], 1, !revoked_counterparty_commitment); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; - check_closed_event(&nodes[1], 1, reason, &[nodes[0].node.get_our_node_id()], 100_000); + check_closed_event(&nodes[1], 1, reason, &[node_id_0], 100_000); if keyed_anchors || p2a_anchor { handle_bump_close_event(&nodes[1]); } @@ -925,7 +1036,6 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a // Confirm B's commitment, A should now broadcast an HTLC timeout for commitment B. mine_transactions(&nodes[0], &[&tx, &anchor_tx]); tx - } else { let mut txn = nodes[1].tx_broadcaster.txn_broadcast(); assert_eq!(txn.len(), 1); @@ -946,7 +1056,7 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a assert_eq!(txn.len(), 3); check_spends!(txn[0], commitment_b); check_spends!(txn[1], funding_tx); - check_spends!(txn[2], txn[1], coinbase_tx); // Anchor output spend transaction. + check_spends!(txn[2], txn[1], coinbase_tx); // Anchor output spend transaction. } else { let mut txn = nodes[0].tx_broadcaster.txn_broadcast(); assert_eq!(txn.len(), 2); @@ -971,7 +1081,7 @@ fn do_test_retries_own_commitment_broadcast_after_reorg(keyed_anchors: bool, p2a if keyed_anchors || p2a_anchor { assert_eq!(txn.len(), 2); check_spends!(txn[0], funding_tx); - check_spends!(txn[1], txn[0], coinbase_tx); // Anchor output spend. + check_spends!(txn[1], txn[0], coinbase_tx); // Anchor output spend. } else { assert_eq!(txn.len(), 2); check_spends!(txn[0], txn[1]); // HTLC timeout A @@ -1009,8 +1119,8 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool, p2a let coinbase_tx = provide_anchor_reserves(&nodes); - let node_a_id = nodes[0].node.get_our_node_id(); - let node_b_id = nodes[1].node.get_our_node_id(); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); let (_, _, chan_id, funding_tx) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 10_000_000, 0); @@ -1028,8 +1138,8 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool, p2a }; // First disconnect peers so that we don't have to deal with messages: - nodes[0].node.peer_disconnected(node_b_id); - nodes[1].node.peer_disconnected(node_a_id); + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); // Give node B preimages so that it will claim the first two HTLCs on-chain. nodes[1].node.claim_funds(preimage_a); @@ -1042,12 +1152,12 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool, p2a // Force-close and fetch node B's commitment transaction and the transaction claiming the first // two HTLCs. - nodes[1].node.force_close_broadcasting_latest_txn(&chan_id, &node_a_id, err).unwrap(); + nodes[1].node.force_close_broadcasting_latest_txn(&chan_id, &node_id_0, err).unwrap(); check_closed_broadcast(&nodes[1], 1, false); check_added_monitors(&nodes[1], 1); let message = "Channel force-closed".to_owned(); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; - check_closed_event(&nodes[1], 1, reason, &[node_a_id], 10_000_000); + check_closed_event(&nodes[1], 1, reason, &[node_id_0], 10_000_000); handle_bump_close_event(&nodes[1]); let mut txn = nodes[1].tx_broadcaster.txn_broadcast(); @@ -1072,7 +1182,7 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool, p2a } check_closed_broadcast(&nodes[0], 1, false); let reason = ClosureReason::CommitmentTxConfirmed; - check_closed_event(&nodes[0], 1, reason, &[node_b_id], 10_000_000); + check_closed_event(&nodes[0], 1, reason, &[node_id_1], 10_000_000); check_added_monitors(&nodes[0], 1); if let Some(ref a_tx) = anchor_tx { @@ -1087,10 +1197,10 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool, p2a assert_eq!(txn.len(), 3, "{txn:?}"); if p2a_anchor { check_spends!(txn[0], funding_tx); - check_spends!(txn[1], txn[0], anchor_tx.as_ref().unwrap()); // Anchor output spend. + check_spends!(txn[1], txn[0], anchor_tx.as_ref().unwrap()); // Anchor output spend. } else { check_spends!(txn[0], funding_tx); - check_spends!(txn[1], txn[0], coinbase_tx); // Anchor output spend. + check_spends!(txn[1], txn[0], coinbase_tx); // Anchor output spend. } } else { assert_eq!(txn.len(), 1, "{txn:?}"); @@ -1123,7 +1233,8 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool, p2a let mut found_expected_events = [false, false, false, false]; for event in sent_events { match event { - Event::PaymentSent { payment_hash, .. }|Event::PaymentPathSuccessful { payment_hash: Some(payment_hash), .. } => { + Event::PaymentSent { payment_hash, .. } + | Event::PaymentPathSuccessful { payment_hash: Some(payment_hash), .. } => { let path_success = matches!(event, Event::PaymentPathSuccessful { .. }); if payment_hash == payment_hash_a { found_expected_events[0 + if path_success { 1 } else { 0 }] = true; @@ -1214,7 +1325,8 @@ fn do_test_split_htlc_expiry_tracking(use_third_htlc: bool, reorg_out: bool, p2a let mut found_expected_events = [false, false]; for event in failed_events { match event { - Event::PaymentFailed { payment_hash: Some(payment_hash), .. }|Event::PaymentPathFailed { payment_hash, .. } => { + Event::PaymentFailed { payment_hash: Some(payment_hash), .. } + | Event::PaymentPathFailed { payment_hash, .. } => { let path_failed = matches!(event, Event::PaymentPathFailed { .. }); if payment_hash == payment_hash_c { found_expected_events[if path_failed { 1 } else { 0 }] = true; diff --git a/lightning/src/ln/script.rs b/lightning/src/ln/script.rs index 5258b8f3283..44a7cc1778c 100644 --- a/lightning/src/ln/script.rs +++ b/lightning/src/ln/script.rs @@ -56,7 +56,7 @@ impl Readable for ShutdownScript { } } -impl_writeable_tlv_based_enum_legacy!(ShutdownScriptImpl, ; +impl_ser_tlv_based_enum_legacy!(ShutdownScriptImpl, ; (0, Legacy), (1, Bolt2), ); diff --git a/lightning/src/ln/shutdown_tests.rs b/lightning/src/ln/shutdown_tests.rs index 50c8f72f9be..d70b240e4e4 100644 --- a/lightning/src/ln/shutdown_tests.rs +++ b/lightning/src/ln/shutdown_tests.rs @@ -71,9 +71,9 @@ fn pre_funding_lock_shutdown_test() { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); assert!(nodes[0].node.list_channels().is_empty()); @@ -122,9 +122,9 @@ fn expect_channel_shutdown_state() { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); assert!(nodes[0].node.list_channels().is_empty()); @@ -216,9 +216,9 @@ fn expect_channel_shutdown_state_with_htlc() { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); let reason_a = ClosureReason::LocallyInitiatedCooperativeClosure; check_closed_event(&nodes[0], 1, reason_a, &[node_b_id], 100000); @@ -284,9 +284,9 @@ fn test_lnd_bug_6039() { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); let reason_a = ClosureReason::LocallyInitiatedCooperativeClosure; @@ -361,7 +361,7 @@ fn expect_channel_shutdown_state_with_force_closure() { .node .force_close_broadcasting_latest_txn(&chan_1.2, &node_a_id, message.clone()) .unwrap(); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); expect_channel_shutdown_state!(nodes[0], chan_1.2, ChannelShutdownState::NotShuttingDown); @@ -371,7 +371,7 @@ fn expect_channel_shutdown_state_with_force_closure() { assert_eq!(node_txn.len(), 1); check_spends!(node_txn[0], chan_1.3); mine_transaction(&nodes[0], &node_txn[0]); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); assert!(nodes[0].node.list_channels().is_empty()); @@ -410,7 +410,7 @@ fn updates_shutdown_wait() { assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); - let (_, payment_hash, payment_secret) = get_payment_preimage_hash!(nodes[0]); + let (_, payment_hash, payment_secret) = get_payment_preimage_hash(&nodes[0], None, None); let payment_params_1 = PaymentParameters::from_node_id(node_b_id, TEST_FINAL_CLTV) .with_bolt11_features(nodes[1].node.bolt11_invoice_features()) @@ -443,12 +443,12 @@ fn updates_shutdown_wait() { ) .unwrap(); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let id = PaymentId(payment_hash.0); let res = nodes[0].node.send_payment_with_route(route_1, payment_hash, onion, id); unwrap_send_err!(nodes[0], res, true, APIError::ChannelUnavailable { .. }, {}); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 100_000); let res = nodes[1].node.send_payment_with_route(route_2, payment_hash, onion, id); unwrap_send_err!(nodes[1], res, true, APIError::ChannelUnavailable { .. }, {}); @@ -483,9 +483,9 @@ fn updates_shutdown_wait() { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); let reason_a = ClosureReason::LocallyInitiatedCooperativeClosure; @@ -544,7 +544,7 @@ fn do_htlc_fail_async_shutdown(blinded_recipient: bool) { amt_msat, ) }; - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, amt_msat); let id = PaymentId(our_payment_hash.0); nodes[0] .node @@ -618,9 +618,9 @@ fn do_htlc_fail_async_shutdown(blinded_recipient: bool) { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); assert!(nodes[0].node.list_channels().is_empty()); @@ -750,8 +750,7 @@ fn do_test_shutdown_rebroadcast(recv_count: u8) { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = - get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); assert!(node_0_2nd_closing_signed.is_some()); } @@ -799,10 +798,9 @@ fn do_test_shutdown_rebroadcast(recv_count: u8) { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_2nd_closing_signed) = - get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); let reason = ClosureReason::LocallyInitiatedCooperativeClosure; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); @@ -834,9 +832,13 @@ fn do_test_shutdown_rebroadcast(recv_count: u8) { // get_closing_signed_broadcast usually eats the BroadcastChannelUpdate for us and // checks it, but in this case nodes[1] didn't ever get a chance to receive a // closing_signed so we do it ourselves - check_closed_broadcast!(nodes[1], false); + check_closed_broadcast(&nodes[1], 1, false); check_added_monitors(&nodes[1], 1); - let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(format!("Got a message for a channel from the wrong node! No such channel for the passed counterparty_node_id {}", &node_b_id)) }; + let peer_msg = format!( + "Got a message for a channel from the wrong node! No such channel_id {} for the passed counterparty_node_id {}", + chan_1.2, node_b_id + ); + let reason = ClosureReason::CounterpartyForceClosed { peer_msg: UntrustedString(peer_msg) }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); } @@ -1384,7 +1386,7 @@ fn do_test_closing_signed_reinit_timeout(timeout_step: TimeoutStep) { let node_1_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let node_0_2nd_closing_signed = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let node_0_2nd_closing_signed = get_closing_signed_broadcast(&nodes[0], node_b_id); if timeout_step == TimeoutStep::NoTimeout { nodes[1].node.handle_closing_signed(node_a_id, &node_0_2nd_closing_signed.1.unwrap()); let reason_b = ClosureReason::CounterpartyInitiatedCooperativeClosure; @@ -1414,7 +1416,7 @@ fn do_test_closing_signed_reinit_timeout(timeout_step: TimeoutStep) { || (txn[0].output[1].script_pubkey.is_p2wpkh() && txn[0].output[0].script_pubkey.is_p2wsh()) ); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::ProcessingError { err: "closing_signed negotiation failed to finish within two timer ticks".to_string(), @@ -1476,11 +1478,11 @@ fn do_simple_legacy_shutdown_test(high_initiator_fee: bool) { } nodes[1].node.handle_closing_signed(node_a_id, &node_0_closing_signed); - let (_, mut node_1_closing_signed) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, mut node_1_closing_signed) = get_closing_signed_broadcast(&nodes[1], node_a_id); node_1_closing_signed.as_mut().unwrap().fee_range = None; nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed.unwrap()); - let (_, node_0_none) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_none) = get_closing_signed_broadcast(&nodes[0], node_b_id); assert!(node_0_none.is_none()); let reason_a = ClosureReason::LocallyInitiatedCooperativeClosure; check_closed_event(&nodes[0], 1, reason_a, &[node_b_id], 100000); @@ -1524,7 +1526,7 @@ fn simple_target_feerate_shutdown() { let node_0_closing_signed = get_event_msg!(nodes[0], MessageSendEvent::SendClosingSigned, node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &node_0_closing_signed); - let (_, node_1_closing_signed_opt) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_closing_signed_opt) = get_closing_signed_broadcast(&nodes[1], node_a_id); let node_1_closing_signed = node_1_closing_signed_opt.unwrap(); // nodes[1] was passed a target which was larger than the current channel feerate, which it @@ -1554,7 +1556,7 @@ fn simple_target_feerate_shutdown() { assert_eq!(node_0_closing_signed.fee_satoshis, node_1_closing_signed.fee_satoshis); nodes[0].node.handle_closing_signed(node_b_id, &node_1_closing_signed); - let (_, node_0_none) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, node_0_none) = get_closing_signed_broadcast(&nodes[0], node_b_id); assert!(node_0_none.is_none()); let reason_a = ClosureReason::LocallyInitiatedCooperativeClosure; check_closed_event(&nodes[0], 1, reason_a, &[node_b_id], 100000); @@ -1656,9 +1658,9 @@ fn do_outbound_update_no_early_closing_signed(use_htlc: bool) { let bs_closing_signed = get_event_msg!(nodes[1], MessageSendEvent::SendClosingSigned, node_a_id); nodes[0].node.handle_closing_signed(node_b_id, &bs_closing_signed); - let (_, as_2nd_closing_signed) = get_closing_signed_broadcast!(nodes[0].node, node_b_id); + let (_, as_2nd_closing_signed) = get_closing_signed_broadcast(&nodes[0], node_b_id); nodes[1].node.handle_closing_signed(node_a_id, &as_2nd_closing_signed.unwrap()); - let (_, node_1_none) = get_closing_signed_broadcast!(nodes[1].node, node_a_id); + let (_, node_1_none) = get_closing_signed_broadcast(&nodes[1], node_a_id); assert!(node_1_none.is_none()); let reason_a = ClosureReason::LocallyInitiatedCooperativeClosure; @@ -1901,7 +1903,7 @@ fn test_pending_htlcs_arent_lost_on_mon_delay() { // moment `cs_last_raa` is received by B. let (route_b, payment_hash_b, _preimage, payment_secret_b) = get_route_and_payment_hash!(&nodes[0], nodes[2], 900_000); - let onion = RecipientOnionFields::secret_only(payment_secret_b); + let onion = RecipientOnionFields::secret_only(payment_secret_b, 900_000); let id = PaymentId(payment_hash_b.0); nodes[0].node.send_payment_with_route(route_b, payment_hash_b, onion, id).unwrap(); check_added_monitors(&nodes[0], 1); diff --git a/lightning/src/ln/splicing_tests.rs b/lightning/src/ln/splicing_tests.rs index ace1783327d..c762ca062b2 100644 --- a/lightning/src/ln/splicing_tests.rs +++ b/lightning/src/ln/splicing_tests.rs @@ -9,28 +9,48 @@ #![cfg_attr(not(test), allow(unused_imports))] -use crate::chain::chaininterface::{TransactionType, FEERATE_FLOOR_SATS_PER_KW}; +use crate::chain::chaininterface::{FundingPurpose, TransactionType, FEERATE_FLOOR_SATS_PER_KW}; use crate::chain::channelmonitor::{ANTI_REORG_DELAY, LATENCY_GRACE_PERIOD_BLOCKS}; use crate::chain::transaction::OutPoint; use crate::chain::ChannelMonitorUpdateStatus; -use crate::events::bump_transaction::sync::WalletSourceSync; -use crate::events::{ClosureReason, Event, FundingInfo, HTLCHandlingFailureType}; +use crate::events::{ + ClosureReason, Event, FundingInfo, HTLCHandlingFailureType, NegotiationFailureReason, +}; use crate::ln::chan_utils; -use crate::ln::channel::CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY; +use crate::ln::channel::{ + ANCHOR_OUTPUT_VALUE_SATOSHI, CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY, + DISCONNECT_PEER_AWAITING_RESPONSE_TICKS, FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, + MIN_CHANNEL_VALUE_SATOSHIS, +}; +use crate::ln::channel_state::{SpliceCandidateDetails, SpliceCandidateStatus, SpliceDetails}; use crate::ln::channelmanager::{provided_init_features, PaymentId, BREAKDOWN_TIMEOUT}; use crate::ln::functional_test_utils::*; -use crate::ln::funding::{FundingTxInput, SpliceContribution}; +use crate::ln::funding::{FundingContribution, FundingContributionError, FundingTemplate}; use crate::ln::msgs::{self, BaseMessageHandler, ChannelMessageHandler, MessageSendEvent}; use crate::ln::outbound_payment::RecipientOnionFields; use crate::ln::types::ChannelId; use crate::routing::router::{PaymentParameters, RouteParameters}; +use crate::types::features::ChannelTypeFeatures; +use crate::types::string::UntrustedString; +use crate::util::config::UserConfig; use crate::util::errors::APIError; use crate::util::ser::Writeable; +use crate::util::test_channel_signer::SignerOp; +use crate::util::wallet_utils::{ + CoinSelection, CoinSelectionSourceSync, ConfirmedUtxo, Input, WalletSourceSync, WalletSync, +}; + +use crate::sync::Arc; use bitcoin::hashes::Hash; use bitcoin::secp256k1::ecdsa::Signature; -use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint as BitcoinOutPoint, ScriptBuf, Transaction, TxOut, WPubkeyHash}; +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; +use bitcoin::transaction::Version; +use bitcoin::SignedAmount; +use bitcoin::{ + Amount, FeeRate, OutPoint as BitcoinOutPoint, Psbt, ScriptBuf, Transaction, TxOut, Txid, + WPubkeyHash, WScriptHash, +}; #[test] fn test_splicing_not_supported_api_error() { @@ -47,15 +67,7 @@ fn test_splicing_not_supported_api_error() { let (_, _, channel_id, _) = create_announced_chan_between_nodes(&nodes, 0, 1); - let bs_contribution = SpliceContribution::splice_in(Amount::ZERO, Vec::new(), None); - - let res = nodes[1].node.splice_channel( - &channel_id, - &node_id_0, - bs_contribution.clone(), - 0, // funding_feerate_per_kw, - None, // locktime - ); + let res = nodes[1].node.splice_channel(&channel_id, &node_id_0); match res { Err(APIError::ChannelUnavailable { err }) => { assert!(err.contains("Peer does not support splicing")) @@ -76,13 +88,7 @@ fn test_splicing_not_supported_api_error() { reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - let res = nodes[1].node.splice_channel( - &channel_id, - &node_id_0, - bs_contribution, - 0, // funding_feerate_per_kw, - None, // locktime - ); + let res = nodes[1].node.splice_channel(&channel_id, &node_id_0); match res { Err(APIError::ChannelUnavailable { err }) => { assert!(err.contains("Peer does not support quiescence, a splicing prerequisite")) @@ -102,64 +108,251 @@ fn test_v1_splice_in_negative_insufficient_inputs() { create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); // Amount being added to the channel through the splice-in - let splice_in_sats = 20_000; + let splice_in_value = Amount::from_sat(20_000); // Create additional inputs, but insufficient - let extra_splice_funding_input_sats = splice_in_sats - 1; - let funding_inputs = - create_dual_funding_utxos_with_prev_txs(&nodes[0], &[extra_splice_funding_input_sats]); + let extra_splice_funding_input = splice_in_value - Amount::ONE_SAT; - let contribution = - SpliceContribution::splice_in(Amount::from_sat(splice_in_sats), funding_inputs, None); + provide_utxo_reserves(&nodes, 1, extra_splice_funding_input); + + let feerate = FeeRate::from_sat_per_kwu(1024); // Initiate splice-in, with insufficient input contribution - let res = nodes[0].node.splice_channel( - &channel_id, - &nodes[1].node.get_our_node_id(), - contribution, - 1024, // funding_feerate_per_kw, - None, // locktime - ); - match res { - Err(APIError::APIMisuseError { err }) => { - assert!(err.contains("Need more inputs")) - }, - _ => panic!("Wrong error {:?}", res.err().unwrap()), + let funding_template = + nodes[0].node.splice_channel(&channel_id, &nodes[1].node.get_our_node_id()).unwrap(); + + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + assert!(funding_template + .splice_in_sync(splice_in_value, feerate, FeeRate::MAX, &wallet) + .is_err()); +} + +/// A mock wallet that returns a pre-configured [`CoinSelection`] with a single input and change +/// output. Used to test edge cases where the input value is tight relative to the fee estimate. +#[cfg(test)] +struct TightBudgetWallet { + utxo_value: Amount, + change_value: Amount, +} + +#[cfg(test)] +impl CoinSelectionSourceSync for TightBudgetWallet { + fn select_confirmed_utxos( + &self, _claim_id: Option<crate::chain::ClaimId>, _must_spend: Vec<Input>, + _must_pay_to: &[TxOut], _target_feerate_sat_per_1000_weight: u32, _max_tx_weight: u64, + ) -> Result<CoinSelection, ()> { + let prevout = TxOut { + value: self.utxo_value, + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }; + let prevtx = Transaction { + input: vec![], + output: vec![prevout], + version: Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + }; + let utxo = ConfirmedUtxo::new_p2wpkh(prevtx, 0).unwrap(); + + let change_output = TxOut { + value: self.change_value, + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }; + + Ok(CoinSelection { confirmed_utxos: vec![utxo], change_output: Some(change_output) }) + } + + fn sign_psbt(&self, _psbt: Psbt) -> Result<Transaction, ()> { + unreachable!("should not reach signing") } } +#[cfg(test)] +fn config_with_min_funding_satoshis(min_funding_satoshis: u64) -> UserConfig { + let mut config = test_default_channel_config(); + config.channel_handshake_limits.min_funding_satoshis = min_funding_satoshis; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + config +} + +#[cfg(test)] +fn assert_min_funding_error<'a, 'b, 'c>( + node: &Node<'a, 'b, 'c>, recipient: PublicKey, min_funding_satoshis: u64, +) { + let msg = get_event_msg!(node, MessageSendEvent::SendTxAbort, recipient); + let data = tx_abort_data(&msg); + assert!( + data.contains(&format!("configured min_funding_satoshis {min_funding_satoshis}")), + "unexpected tx_abort: {}", + data + ); +} + +#[cfg(test)] +fn tx_abort_data(msg: &msgs::TxAbort) -> String { + String::from_utf8(msg.data.clone()).expect("tx_abort data should be valid UTF-8") +} + pub fn negotiate_splice_tx<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, - initiator_contribution: SpliceContribution, + funding_contribution: FundingContribution, ) { - let new_funding_script = - complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone()); + let new_funding_script = complete_splice_handshake(initiator, acceptor); + complete_interactive_funding_negotiation( initiator, acceptor, channel_id, - initiator_contribution, + funding_contribution, new_funding_script, ); } -pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( +pub fn initiate_splice_in<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, - initiator_contribution: SpliceContribution, -) -> ScriptBuf { - let node_id_initiator = initiator.node.get_our_node_id(); + value_added: Amount, +) -> FundingContribution { + do_initiate_splice_in(initiator, acceptor, channel_id, value_added) +} + +pub fn do_initiate_splice_in<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + value_added: Amount, +) -> FundingContribution { let node_id_acceptor = acceptor.node.get_our_node_id(); + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = initiator.node.splice_channel(&channel_id, &node_id_acceptor).unwrap(); + let feerate = funding_template.min_rbf_feerate().unwrap_or(floor_feerate); + let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger); + let funding_contribution = + funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap(); + initiator + .node + .funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None) + .unwrap(); + funding_contribution +} + +pub fn do_initiate_rbf_splice_in<'a, 'b, 'c, 'd>( + node: &'a Node<'b, 'c, 'd>, counterparty: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + feerate: FeeRate, +) -> FundingContribution { + let node_id_counterparty = counterparty.node.get_our_node_id(); + let funding_template = node.node.splice_channel(&channel_id, &node_id_counterparty).unwrap(); + let funding_contribution = + funding_template.with_prior_contribution(feerate, FeeRate::MAX).build().unwrap(); + node.node + .funding_contributed(&channel_id, &node_id_counterparty, funding_contribution.clone(), None) + .unwrap(); + funding_contribution +} + +pub fn do_initiate_rbf_splice_in_and_out<'a, 'b, 'c, 'd>( + node: &'a Node<'b, 'c, 'd>, counterparty: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + outputs: Vec<TxOut>, feerate: FeeRate, +) -> FundingContribution { + let node_id_counterparty = counterparty.node.get_our_node_id(); + let funding_template = node.node.splice_channel(&channel_id, &node_id_counterparty).unwrap(); + let funding_contribution = funding_template + .with_prior_contribution(feerate, FeeRate::MAX) + .add_outputs(outputs) + .build() + .unwrap(); + node.node + .funding_contributed(&channel_id, &node_id_counterparty, funding_contribution.clone(), None) + .unwrap(); + funding_contribution +} +pub fn do_initiate_splice_in_at_feerate<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + value_added: Amount, feerate: FeeRate, +) -> FundingContribution { + let node_id_acceptor = acceptor.node.get_our_node_id(); + let funding_template = initiator.node.splice_channel(&channel_id, &node_id_acceptor).unwrap(); + let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger); + let funding_contribution = + funding_template.splice_in_sync(value_added, feerate, FeeRate::MAX, &wallet).unwrap(); initiator .node - .splice_channel( - &channel_id, - &node_id_acceptor, - initiator_contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, - ) + .funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None) + .unwrap(); + funding_contribution +} + +pub fn initiate_splice_out<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + outputs: Vec<TxOut>, +) -> Result<FundingContribution, APIError> { + let node_id_acceptor = acceptor.node.get_our_node_id(); + let funding_contribution = + build_splice_out_contribution(initiator, acceptor, channel_id, outputs).unwrap(); + match initiator.node.funding_contributed( + &channel_id, + &node_id_acceptor, + funding_contribution.clone(), + None, + ) { + Ok(()) => Ok(funding_contribution), + Err(e) => { + expect_splice_failed_events( + initiator, + &channel_id, + funding_contribution, + NegotiationFailureReason::ContributionInvalid, + ); + Err(e) + }, + } +} + +pub fn build_splice_out_contribution<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + outputs: Vec<TxOut>, +) -> Result<FundingContribution, FundingContributionError> { + let node_id_acceptor = acceptor.node.get_our_node_id(); + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = initiator.node.splice_channel(&channel_id, &node_id_acceptor).unwrap(); + let feerate = funding_template.min_rbf_feerate().unwrap_or(floor_feerate); + funding_template.splice_out(outputs, feerate, FeeRate::MAX) +} + +pub fn initiate_splice_in_and_out<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + value_added: Amount, outputs: Vec<TxOut>, +) -> FundingContribution { + do_initiate_splice_in_and_out(initiator, acceptor, channel_id, value_added, outputs) +} + +pub fn do_initiate_splice_in_and_out<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + value_added: Amount, outputs: Vec<TxOut>, +) -> FundingContribution { + let node_id_acceptor = acceptor.node.get_our_node_id(); + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = initiator.node.splice_channel(&channel_id, &node_id_acceptor).unwrap(); + let feerate = funding_template.min_rbf_feerate().unwrap_or(floor_feerate); + let wallet = WalletSync::new(Arc::clone(&initiator.wallet_source), initiator.logger); + let funding_contribution = funding_template + .without_prior_contribution(feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(value_added) + .unwrap() + .add_outputs(outputs) + .build() + .unwrap(); + initiator + .node + .funding_contributed(&channel_id, &node_id_acceptor, funding_contribution.clone(), None) .unwrap(); + funding_contribution +} + +pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, +) -> ScriptBuf { + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); acceptor.node.handle_stfu(node_id_initiator, &stfu_init); @@ -180,91 +373,249 @@ pub fn complete_splice_handshake<'a, 'b, 'c, 'd>( new_funding_script } +pub fn complete_rbf_handshake<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, +) -> msgs::TxAckRbf { + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + let tx_init_rbf = get_event_msg!(initiator, MessageSendEvent::SendTxInitRbf, node_id_acceptor); + acceptor.node.handle_tx_init_rbf(node_id_initiator, &tx_init_rbf); + let tx_ack_rbf = get_event_msg!(acceptor, MessageSendEvent::SendTxAckRbf, node_id_initiator); + initiator.node.handle_tx_ack_rbf(node_id_acceptor, &tx_ack_rbf); + + tx_ack_rbf +} + pub fn complete_interactive_funding_negotiation<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, - initiator_contribution: SpliceContribution, new_funding_script: ScriptBuf, + initiator_contribution: FundingContribution, new_funding_script: ScriptBuf, +) { + complete_interactive_funding_negotiation_for_both( + initiator, + acceptor, + channel_id, + initiator_contribution, + None, + 0, + new_funding_script, + ); +} + +pub fn complete_interactive_funding_negotiation_for_both<'a, 'b, 'c, 'd>( + initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, + initiator_contribution: FundingContribution, + acceptor_contribution: Option<FundingContribution>, acceptor_funding_satoshis: i64, + new_funding_script: ScriptBuf, ) { let node_id_initiator = initiator.node.get_our_node_id(); let node_id_acceptor = acceptor.node.get_our_node_id(); - let funding_outpoint = initiator + let (funding_outpoint, channel_value_satoshis) = initiator .node .list_channels() .iter() .find(|channel| { channel.counterparty.node_id == node_id_acceptor && channel.channel_id == channel_id }) - .map(|channel| channel.funding_txo.unwrap()) + .map(|channel| (channel.funding_txo.unwrap(), channel.channel_value_satoshis)) .unwrap(); - let (initiator_inputs, initiator_outputs, initiator_change_script) = + let new_channel_value = Amount::from_sat( + channel_value_satoshis + .checked_add_signed(initiator_contribution.net_value().to_sat()) + .unwrap() + .checked_add_signed(acceptor_funding_satoshis) + .unwrap(), + ); + let (initiator_funding_tx_inputs, mut expected_initiator_outputs) = initiator_contribution.into_tx_parts(); - let mut expected_initiator_inputs = initiator_inputs + let mut expected_initiator_inputs = initiator_funding_tx_inputs .iter() .map(|input| input.utxo.outpoint) .chain(core::iter::once(funding_outpoint.into_bitcoin_outpoint())) .collect::<Vec<_>>(); - let mut expected_initiator_scripts = initiator_outputs - .into_iter() - .map(|output| output.script_pubkey) - .chain(core::iter::once(new_funding_script)) - .chain(initiator_change_script.into_iter()) - .collect::<Vec<_>>(); + expected_initiator_outputs + .push(TxOut { script_pubkey: new_funding_script, value: new_channel_value }); + + let (mut expected_acceptor_inputs, mut expected_acceptor_scripts) = + if let Some(acceptor_contribution) = acceptor_contribution { + let (acceptor_inputs, acceptor_outputs) = acceptor_contribution.into_tx_parts(); + let expected_acceptor_inputs = + acceptor_inputs.iter().map(|input| input.utxo.outpoint).collect::<Vec<_>>(); + let expected_acceptor_scripts = + acceptor_outputs.into_iter().map(|output| output.script_pubkey).collect::<Vec<_>>(); + (expected_acceptor_inputs, expected_acceptor_scripts) + } else { + (Vec::new(), Vec::new()) + }; + let mut initiator_sent_tx_complete; let mut acceptor_sent_tx_complete = false; loop { - if !expected_initiator_inputs.is_empty() { - let tx_add_input = - get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); - let input_prevout = BitcoinOutPoint { - txid: tx_add_input - .prevtx - .as_ref() - .map(|prevtx| prevtx.compute_txid()) - .or(tx_add_input.shared_input_txid) - .unwrap(), - vout: tx_add_input.prevtx_out, - }; - expected_initiator_inputs.remove( - expected_initiator_inputs.iter().position(|input| *input == input_prevout).unwrap(), - ); - acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); - } else if !expected_initiator_scripts.is_empty() { - let tx_add_output = - get_event_msg!(initiator, MessageSendEvent::SendTxAddOutput, node_id_acceptor); - expected_initiator_scripts.remove( - expected_initiator_scripts - .iter() - .position(|script| *script == tx_add_output.script) - .unwrap(), - ); - acceptor.node.handle_tx_add_output(node_id_initiator, &tx_add_output); - } else { - let msg_events = initiator.node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - if let MessageSendEvent::SendTxComplete { ref msg, .. } = &msg_events[0] { + // Initiator's turn: send TxAddInput, TxAddOutput, or TxComplete + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + match &msg_events[0] { + MessageSendEvent::SendTxAddInput { msg, .. } => { + let input_prevout = BitcoinOutPoint { + txid: msg + .prevtx + .as_ref() + .map(|prevtx| prevtx.compute_txid()) + .or(msg.shared_input_txid) + .unwrap(), + vout: msg.prevtx_out, + }; + expected_initiator_inputs.remove( + expected_initiator_inputs + .iter() + .position(|input| *input == input_prevout) + .unwrap(), + ); + acceptor.node.handle_tx_add_input(node_id_initiator, msg); + initiator_sent_tx_complete = false; + }, + MessageSendEvent::SendTxAddOutput { msg, .. } => { + expected_initiator_outputs.remove( + expected_initiator_outputs + .iter() + .position(|output| { + *output.script_pubkey == msg.script && output.value.to_sat() == msg.sats + }) + .unwrap(), + ); + acceptor.node.handle_tx_add_output(node_id_initiator, msg); + initiator_sent_tx_complete = false; + }, + MessageSendEvent::SendTxComplete { msg, .. } => { acceptor.node.handle_tx_complete(node_id_initiator, msg); - } else { - panic!(); - } - if acceptor_sent_tx_complete { - break; - } + initiator_sent_tx_complete = true; + if acceptor_sent_tx_complete { + break; + } + }, + _ => panic!("Unexpected message event: {:?}", msg_events[0]), } - let mut msg_events = acceptor.node.get_and_clear_pending_msg_events(); + // Acceptor's turn: send TxAddInput, TxAddOutput, or TxComplete + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - if let MessageSendEvent::SendTxComplete { ref msg, .. } = msg_events.remove(0) { - initiator.node.handle_tx_complete(node_id_acceptor, msg); - } else { - panic!(); + match &msg_events[0] { + MessageSendEvent::SendTxAddInput { msg, .. } => { + let input_prevout = BitcoinOutPoint { + txid: msg + .prevtx + .as_ref() + .map(|prevtx| prevtx.compute_txid()) + .or(msg.shared_input_txid) + .unwrap(), + vout: msg.prevtx_out, + }; + expected_acceptor_inputs.remove( + expected_acceptor_inputs + .iter() + .position(|input| *input == input_prevout) + .unwrap(), + ); + initiator.node.handle_tx_add_input(node_id_acceptor, msg); + acceptor_sent_tx_complete = false; + }, + MessageSendEvent::SendTxAddOutput { msg, .. } => { + expected_acceptor_scripts.remove( + expected_acceptor_scripts + .iter() + .position(|script| *script == msg.script) + .unwrap(), + ); + initiator.node.handle_tx_add_output(node_id_acceptor, msg); + acceptor_sent_tx_complete = false; + }, + MessageSendEvent::SendTxComplete { msg, .. } => { + initiator.node.handle_tx_complete(node_id_acceptor, msg); + acceptor_sent_tx_complete = true; + if initiator_sent_tx_complete { + break; + } + }, + _ => panic!("Unexpected message event: {:?}", msg_events[0]), + } + } + + assert!(expected_initiator_inputs.is_empty(), "Not all initiator inputs were sent"); + assert!(expected_initiator_outputs.is_empty(), "Not all initiator outputs were sent"); + assert!(expected_acceptor_inputs.is_empty(), "Not all acceptor inputs were sent"); + assert!(expected_acceptor_scripts.is_empty(), "Not all acceptor outputs were sent"); +} + +/// Arguments for [`sign_interactive_funding_tx`]. [`SignInteractiveFundingTxArgs::new`] defaults to a +/// first-attempt splice on a confirmed channel where only the initiator contributes; augment with the +/// builder methods as the scenario requires. +pub struct SignInteractiveFundingTxArgs<'a, 'b, 'c, 'd> { + initiator: &'a Node<'b, 'c, 'd>, + acceptor: &'a Node<'b, 'c, 'd>, + is_0conf: bool, + acceptor_has_contribution: bool, + expected_replaced_txid: Option<Txid>, + unconfirmed_funding_txid: Option<Txid>, +} + +impl<'a, 'b, 'c, 'd> SignInteractiveFundingTxArgs<'a, 'b, 'c, 'd> { + pub fn new(initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>) -> Self { + Self { + initiator, + acceptor, + is_0conf: false, + acceptor_has_contribution: false, + expected_replaced_txid: None, + unconfirmed_funding_txid: None, } - acceptor_sent_tx_complete = true; + } + + /// The channel is zero-conf, so `splice_locked` is exchanged at signing and the initiator's + /// `splice_locked` is returned. + pub fn zero_conf(mut self) -> Self { + self.is_0conf = true; + self + } + + /// The acceptor contributed inputs and so must also sign the funding transaction. + pub fn with_acceptor_contribution(mut self) -> Self { + self.acceptor_has_contribution = true; + self + } + + /// This is an RBF replacing the negotiated candidate `prior_txid`, expected as the prior candidate + /// in the `TransactionType::InteractiveFunding` broadcast. + pub fn replacing(mut self, prior_txid: Txid) -> Self { + self.expected_replaced_txid = Some(prior_txid); + self + } + + /// The channel's funding transaction, identified by `unconfirmed_funding_txid`, is still + /// unconfirmed, so signing also (re-)broadcasts it; the helper asserts it is broadcast alongside + /// the splice. + pub fn with_unconfirmed_funding(mut self, unconfirmed_funding_txid: Txid) -> Self { + self.unconfirmed_funding_txid = Some(unconfirmed_funding_txid); + self } } pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( - initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, is_0conf: bool, + args: SignInteractiveFundingTxArgs<'a, 'b, 'c, 'd>, ) -> (Transaction, Option<(msgs::SpliceLocked, PublicKey)>) { + let SignInteractiveFundingTxArgs { + initiator, + acceptor, + is_0conf, + acceptor_has_contribution, + expected_replaced_txid, + unconfirmed_funding_txid, + } = args; let node_id_initiator = initiator.node.get_our_node_id(); let node_id_acceptor = acceptor.node.get_our_node_id(); @@ -297,6 +648,29 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( }; acceptor.node.handle_commitment_signed(node_id_initiator, &initial_commit_sig_for_acceptor); + if acceptor_has_contribution { + // When the acceptor contributed inputs, it needs to sign as well. The counterparty's + // commitment_signed is buffered until the acceptor signs. + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + let event = get_event!(acceptor, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + let partially_signed_tx = acceptor.wallet_source.sign_tx(unsigned_transaction).unwrap(); + acceptor + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); + } else { + panic!(); + } + } + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); assert_eq!(msg_events.len(), 2, "{msg_events:?}"); if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { @@ -333,6 +707,19 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( let tx = { let mut initiator_txn = initiator.tx_broadcaster.txn_broadcast_with_types(); + if let Some(unconfirmed_funding_txid) = unconfirmed_funding_txid { + // The initiator (re-)broadcasts its still-unconfirmed funding alongside the splice; + // remove it so only the splice (InteractiveFunding) remains to compare against the acceptor. + assert_eq!(initiator_txn.len(), 2); + let pos = initiator_txn + .iter() + .position(|(tx, tx_type)| { + tx.compute_txid() == unconfirmed_funding_txid + && matches!(tx_type, TransactionType::Funding { .. }) + }) + .expect("the unconfirmed funding should be (re-)broadcast"); + initiator_txn.remove(pos); + } assert_eq!(initiator_txn.len(), 1); let mut acceptor_txn = acceptor.tx_broadcaster.txn_broadcast_with_types(); assert_eq!(acceptor_txn.len(), 1); @@ -340,17 +727,29 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( assert_eq!(initiator_txn[0].0, acceptor_txn[0].0); let (tx, initiator_tx_type) = initiator_txn.remove(0); let (_, acceptor_tx_type) = acceptor_txn.remove(0); - // Verify transaction types are Splice for both nodes - assert!( - matches!(initiator_tx_type, TransactionType::Splice { .. }), - "Expected TransactionType::Splice, got {:?}", - initiator_tx_type - ); - assert!( - matches!(acceptor_tx_type, TransactionType::Splice { .. }), - "Expected TransactionType::Splice, got {:?}", - acceptor_tx_type - ); + // Verify transaction types are InteractiveFunding for both nodes. The initiator always + // contributes; the acceptor contributes iff the flag says so. Both parties must observe + // the same prior candidate txid as the caller declares. + let assert_broadcast = + |label: &str, tx_type: &TransactionType, contribution_expected: bool| { + let candidates = match tx_type { + TransactionType::InteractiveFunding { candidates } => candidates, + other => panic!("Expected TransactionType::InteractiveFunding, got {other:?}"), + }; + let last = candidates.last().expect("at least one candidate"); + assert_eq!(last.txid, tx.compute_txid(), "{label} last candidate txid mismatch"); + let last_channel = last.channels.first().expect("at least one channel"); + assert!(matches!(last_channel.purpose, FundingPurpose::Splice)); + assert_eq!( + last_channel.contribution.is_some(), + contribution_expected, + "{label} contribution presence mismatch", + ); + let prior_txid = candidates.len().checked_sub(2).map(|i| candidates[i].txid); + assert_eq!(prior_txid, expected_replaced_txid, "{label} replaced_txid mismatch"); + }; + assert_broadcast("initiator", &initiator_tx_type, true); + assert_broadcast("acceptor", &acceptor_tx_type, acceptor_has_contribution); tx }; (tx, splice_locked) @@ -358,59 +757,87 @@ pub fn sign_interactive_funding_tx<'a, 'b, 'c, 'd>( pub fn splice_channel<'a, 'b, 'c, 'd>( initiator: &'a Node<'b, 'c, 'd>, acceptor: &'a Node<'b, 'c, 'd>, channel_id: ChannelId, - initiator_contribution: SpliceContribution, -) -> Transaction { - let node_id_initiator = initiator.node.get_our_node_id(); + funding_contribution: FundingContribution, +) -> (Transaction, ScriptBuf) { let node_id_acceptor = acceptor.node.get_our_node_id(); - let new_funding_script = - complete_splice_handshake(initiator, acceptor, channel_id, initiator_contribution.clone()); + let new_funding_script = complete_splice_handshake(initiator, acceptor); complete_interactive_funding_negotiation( initiator, acceptor, channel_id, - initiator_contribution, - new_funding_script, + funding_contribution, + new_funding_script.clone(), ); - let (splice_tx, splice_locked) = sign_interactive_funding_tx(initiator, acceptor, false); + let (splice_tx, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(initiator, acceptor)); assert!(splice_locked.is_none()); expect_splice_pending_event(initiator, &node_id_acceptor); - expect_splice_pending_event(acceptor, &node_id_initiator); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); + + (splice_tx, new_funding_script) +} - splice_tx +pub struct SpliceLockedResult { + pub stfu: Option<MessageSendEvent>, + pub node_a_discarded: Vec<(Vec<bitcoin::OutPoint>, Vec<ScriptBuf>)>, + pub node_b_discarded: Vec<(Vec<bitcoin::OutPoint>, Vec<ScriptBuf>)>, } pub fn lock_splice_after_blocks<'a, 'b, 'c, 'd>( node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, num_blocks: u32, -) { +) -> SpliceLockedResult { connect_blocks(node_a, num_blocks); connect_blocks(node_b, num_blocks); let node_id_b = node_b.node.get_our_node_id(); let splice_locked_for_node_b = get_event_msg!(node_a, MessageSendEvent::SendSpliceLocked, node_id_b); - lock_splice(node_a, node_b, &splice_locked_for_node_b, false); + lock_splice(node_a, node_b, &splice_locked_for_node_b, false, &[]) } pub fn lock_splice<'a, 'b, 'c, 'd>( node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, - splice_locked_for_node_b: &msgs::SpliceLocked, is_0conf: bool, -) { - let (prev_funding_outpoint, prev_funding_script) = node_a + splice_locked_for_node_b: &msgs::SpliceLocked, is_0conf: bool, expected_discard_txids: &[Txid], +) -> SpliceLockedResult { + let prev_funding_txid = node_a .chain_monitor .chain_monitor .get_monitor(splice_locked_for_node_b.channel_id) - .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) + .map(|monitor| monitor.get_funding_txo().txid) .unwrap(); + complete_splice_locked_exchange( + node_a, + node_b, + splice_locked_for_node_b, + is_0conf, + expected_discard_txids, + prev_funding_txid, + ) +} +fn complete_splice_locked_exchange<'a, 'b, 'c, 'd>( + node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, + splice_locked_for_node_b: &msgs::SpliceLocked, is_0conf: bool, expected_discard_txids: &[Txid], + prev_funding_txid: Txid, +) -> SpliceLockedResult { let node_id_a = node_a.node.get_our_node_id(); let node_id_b = node_b.node.get_our_node_id(); node_b.node.handle_splice_locked(node_id_a, splice_locked_for_node_b); let mut msg_events = node_b.node.get_and_clear_pending_msg_events(); + + // If the acceptor had a pending QuiescentAction, return the stfu message so that it can be used + // for the next splice attempt. + let node_b_stfu = msg_events + .last() + .filter(|event| matches!(event, MessageSendEvent::SendStfu { .. })) + .is_some() + .then(|| msg_events.pop().unwrap()); + assert_eq!(msg_events.len(), if is_0conf { 1 } else { 2 }, "{msg_events:?}"); if let MessageSendEvent::SendSpliceLocked { msg, .. } = msg_events.remove(0) { node_a.node.handle_splice_locked(node_id_b, &msg); @@ -425,13 +852,37 @@ pub fn lock_splice<'a, 'b, 'c, 'd>( } } - expect_channel_ready_event(&node_a, &node_id_b); - check_added_monitors(&node_a, 1); - expect_channel_ready_event(&node_b, &node_id_a); - check_added_monitors(&node_b, 1); + let mut node_a_discarded = Vec::new(); + let mut node_b_discarded = Vec::new(); + for (idx, node) in [node_a, node_b].into_iter().enumerate() { + let events = node.node.get_and_clear_pending_events(); + assert!(!events.is_empty(), "Expected at least ChannelReady, got {events:?}"); + assert!(matches!(events[0], Event::ChannelReady { .. })); + let discarded = if idx == 0 { &mut node_a_discarded } else { &mut node_b_discarded }; + for event in &events[1..] { + match event { + Event::DiscardFunding { + funding_info: FundingInfo::Contribution { inputs, outputs }, + .. + } => { + discarded.push((inputs.clone(), outputs.clone())); + }, + other => panic!("Expected DiscardFunding with Contribution, got {:?}", other), + } + } + check_added_monitors(node, 1); + } + let mut node_a_stfu = None; if !is_0conf { let mut msg_events = node_a.node.get_and_clear_pending_msg_events(); + + // If node_a had a pending QuiescentAction, filter out the stfu message. + node_a_stfu = msg_events + .iter() + .position(|event| matches!(event, MessageSendEvent::SendStfu { .. })) + .map(|i| msg_events.remove(i)); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); if let MessageSendEvent::SendAnnouncementSignatures { msg, .. } = msg_events.remove(0) { node_b.node.handle_announcement_signatures(node_id_a, &msg); @@ -453,10 +904,30 @@ pub fn lock_splice<'a, 'b, 'c, 'd>( // Remove the corresponding outputs and transactions the chain source is watching for the // old funding as it is no longer being tracked. - node_a - .chain_source - .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); - node_b.chain_source.remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); + for node in [node_a, node_b] { + node.chain_source.remove_watched_by_txid(prev_funding_txid); + for txid in expected_discard_txids { + node.chain_source.remove_watched_by_txid(*txid); + } + } + + SpliceLockedResult { stfu: node_a_stfu.or(node_b_stfu), node_a_discarded, node_b_discarded } +} + +pub fn lock_rbf_splice_after_blocks<'a, 'b, 'c, 'd>( + node_a: &'a Node<'b, 'c, 'd>, node_b: &'a Node<'b, 'c, 'd>, tx: &Transaction, num_blocks: u32, + expected_discard_txids: &[Txid], +) -> SpliceLockedResult { + mine_transaction(node_a, tx); + mine_transaction(node_b, tx); + + connect_blocks(node_a, num_blocks); + connect_blocks(node_b, num_blocks); + + let node_id_b = node_b.node.get_our_node_id(); + let splice_locked_for_node_b = + get_event_msg!(node_a, MessageSendEvent::SendSpliceLocked, node_id_b); + lock_splice(node_a, node_b, &splice_locked_for_node_b, false, expected_discard_txids) } #[test] @@ -501,20 +972,12 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); - let contribution = SpliceContribution::splice_out(vec![TxOut { + let outputs = vec![TxOut { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + }]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()).unwrap(); // Attempt a splice negotiation that only goes up to receiving `splice_init`. Reconnecting // should implicitly abort the negotiation and reset the splice state such that we're able to @@ -552,23 +1015,20 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { nodes[1].node.peer_disconnected(node_id_0); } - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::PeerDisconnected, + ); let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); reconnect_args.send_channel_ready = (true, true); reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()).unwrap(); // Attempt a splice negotiation that ends mid-construction of the funding transaction. // Reconnecting should implicitly abort the negotiation and reset the splice state such that @@ -611,23 +1071,20 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { nodes[1].node.peer_disconnected(node_id_0); } - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::PeerDisconnected, + ); let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); reconnect_args.send_channel_ready = (true, true); reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()).unwrap(); // Attempt a splice negotiation that ends before the initial `commitment_signed` messages are // exchanged. The node missing the other's `commitment_signed` upon reconnecting should @@ -701,11 +1158,23 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { let tx_abort = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1); nodes[1].node.handle_tx_abort(node_id_0, &tx_abort); - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::CounterpartyAborted { + msg: UntrustedString( + "Signing was not completed for this funding transaction; it may be forgotten." + .to_string(), + ), + }, + ); // Attempt a splice negotiation that completes, (i.e. `tx_signatures` are exchanged). Reconnecting // should not abort the negotiation or reset the splice state. - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); if reload { let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode(); @@ -740,6 +1209,70 @@ fn do_test_splice_state_reset_on_disconnect(reload: bool) { lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); } +#[test] +fn test_reload_resets_splice_negotiation_without_dropping_candidates() { + // A reload should abort an in-flight RBF negotiation, but it must not drop the previously + // negotiated splice candidate that the monitor is still tracking. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister_0, chain_monitor_0); + let node_0; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution.clone()); + + let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.min_rbf_feerate(), Some(rbf_feerate)); + assert!(funding_template.prior_contribution().is_some()); + + let rbf_contribution = + funding_template.with_prior_contribution(rbf_feerate, FeeRate::MAX).build().unwrap(); + nodes[0].node.funding_contributed(&channel_id, &node_id_1, rbf_contribution, None).unwrap(); + complete_rbf_handshake(&nodes[0], &nodes[1]); + + let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode(); + reload_node!( + nodes[0], + nodes[0].node.encode(), + &[&encoded_monitor_0], + persister_0, + chain_monitor_0, + node_0 + ); + let _ = get_event!(&nodes[0], Event::SpliceNegotiationFailed); + + // The reload dropped the in-flight RBF round (a `ConstructingTransaction` state does not persist), + // but the previously negotiated candidate survives as the sole candidate, with its contribution. + let details = nodes[0] + .node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + .unwrap(); + assert_eq!(details.candidates.len(), 1); + assert!(matches!(details.candidates[0].status, SpliceCandidateStatus::Negotiated { .. })); + assert_eq!(details.candidates[0].contribution, Some(funding_contribution.clone())); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.min_rbf_feerate(), Some(rbf_feerate)); + assert_eq!(funding_template.prior_contribution().unwrap(), &funding_contribution); +} + #[test] fn test_config_reject_inbound_splices() { // Tests that nodes with `reject_inbound_splices` properly reject inbound splices but still @@ -757,20 +1290,12 @@ fn test_config_reject_inbound_splices() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); - let contribution = SpliceContribution::splice_out(vec![TxOut { + let outputs = vec![TxOut { value: Amount::from_sat(1_000), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + }]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs.clone()).unwrap(); let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); nodes[1].node.handle_stfu(node_id_0, &stfu); @@ -791,14 +1316,21 @@ fn test_config_reject_inbound_splices() { nodes[0].node.peer_disconnected(node_id_1); nodes[1].node.peer_disconnected(node_id_0); - let _event = get_event!(nodes[0], Event::SpliceFailed); + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::PeerDisconnected, + ); let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); reconnect_args.send_channel_ready = (true, true); reconnect_args.send_announcement_sigs = (true, true); reconnect_nodes(reconnect_args); - let _ = splice_channel(&nodes[1], &nodes[0], channel_id, contribution); + let funding_contribution = + initiate_splice_out(&nodes[1], &nodes[0], channel_id, outputs).unwrap(); + let _ = splice_channel(&nodes[1], &nodes[0], channel_id, funding_contribution); } #[test] @@ -806,7 +1338,8 @@ fn test_splice_in() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let mut config = test_default_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -816,26 +1349,24 @@ fn test_splice_in() { let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); - let added_value = Amount::from_sat(initial_channel_value_sat * 2); - let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); - let fees = Amount::from_sat(321); + let utxo_value = added_value * 3 / 4; + let fees = Amount::from_sat(322); - let initiator_contribution = SpliceContribution::splice_in( - added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], - Some(change_script.clone()), - ); + provide_utxo_reserves(&nodes, 2, utxo_value); + + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); - let expected_change = Amount::ONE_BTC * 2 - added_value - fees; + let (splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + let expected_change = utxo_value * 2 - added_value - fees; assert_eq!( - splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, + splice_tx + .output + .iter() + .find(|txout| txout.script_pubkey != new_funding_script) + .unwrap() + .value, expected_change, ); @@ -853,12 +1384,275 @@ fn test_splice_in() { let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); } +#[test] +fn test_min_funding_satoshis_allows_splice_init_with_positive_counterparty_contribution() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let min_funding_satoshis = 150_000; + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + nodes[1].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis)); + + let added_value = Amount::from_sat(10_000); + provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000)); + let _funding_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + assert!(splice_init.funding_contribution_satoshis > 0); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let _splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); +} + +#[test] +fn test_min_funding_satoshis_rejects_splice_init_with_negative_counterparty_contribution() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let min_funding_satoshis = 150_000; + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + nodes[1].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis)); + + let outputs = vec![TxOut { + value: Amount::from_sat(10_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let _funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + assert!(splice_init.funding_contribution_satoshis < 0); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + assert_min_funding_error(&nodes[1], node_id_0, min_funding_satoshis); +} + +#[test] +fn test_min_funding_satoshis_allows_outbound_splice_ack_with_negative_counterparty_contribution() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let min_funding_satoshis = 150_000; + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + initial_channel_value_sat, + 50_000_000, + ); + nodes[0].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis)); + + let added_value = Amount::from_sat(10_000); + provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000)); + let _node_0_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let outputs = vec![TxOut { + value: Amount::from_sat(10_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]; + let _node_1_contribution = + initiate_splice_out(&nodes[1], &nodes[0], channel_id, outputs).unwrap(); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert!(splice_ack.funding_contribution_satoshis < 0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert!(!msg_events.is_empty(), "{msg_events:?}"); + assert!( + !msg_events.iter().any(|event| matches!(event, MessageSendEvent::HandleError { .. })), + "{msg_events:?}" + ); +} + +#[test] +fn test_min_funding_satoshis_rejects_splice_ack_with_negative_counterparty_contribution() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let min_funding_satoshis = 150_000; + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( + &nodes, + 1, + 0, + initial_channel_value_sat, + 50_000_000, + ); + nodes[0].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis)); + + let added_value = Amount::from_sat(10_000); + provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000)); + let _node_0_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + + let outputs = vec![TxOut { + value: Amount::from_sat(10_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]; + let _node_1_contribution = + initiate_splice_out(&nodes[1], &nodes[0], channel_id, outputs).unwrap(); + + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + assert!(!stfu_ack.initiator); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert!(splice_ack.funding_contribution_satoshis < 0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + assert_min_funding_error(&nodes[0], node_id_1, min_funding_satoshis); +} + +#[test] +fn test_min_funding_satoshis_rejects_tx_init_rbf_with_negative_counterparty_contribution() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let min_funding_satoshis = 150_000; + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + nodes[1].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis)); + + let added_value = Amount::from_sat(10_000); + provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000)); + let first_contribution = initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + let (_first_splice_tx, _) = + splice_channel(&nodes[1], &nodes[0], channel_id, first_contribution); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let outputs = vec![TxOut { + value: Amount::from_sat(10_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let rbf_contribution = funding_template.splice_out(outputs, rbf_feerate, FeeRate::MAX).unwrap(); + nodes[0].node.funding_contributed(&channel_id, &node_id_1, rbf_contribution, None).unwrap(); + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert!(tx_init_rbf.funding_output_contribution.unwrap() < 0); + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + assert_min_funding_error(&nodes[1], node_id_0, min_funding_satoshis); +} + +#[test] +fn test_min_funding_satoshis_rejects_tx_ack_rbf_with_negative_counterparty_contribution() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let min_funding_satoshis = 150_000; + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( + &nodes, + 1, + 0, + initial_channel_value_sat, + 50_000_000, + ); + nodes[0].node.set_current_config(config_with_min_funding_satoshis(min_funding_satoshis)); + + let added_value = Amount::from_sat(10_000); + provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000)); + let first_contribution = initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_first_splice_tx, _) = + splice_channel(&nodes[0], &nodes[1], channel_id, first_contribution); + + let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let rbf_feerate = funding_template_0.min_rbf_feerate().unwrap(); + let node_0_contribution = + funding_template_0.with_prior_contribution(rbf_feerate, FeeRate::MAX).build().unwrap(); + nodes[0].node.funding_contributed(&channel_id, &node_id_1, node_0_contribution, None).unwrap(); + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + + let outputs = vec![TxOut { + value: Amount::from_sat(10_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]; + let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let node_1_contribution = + funding_template_1.splice_out(outputs, rbf_feerate, FeeRate::MAX).unwrap(); + nodes[1].node.funding_contributed(&channel_id, &node_id_0, node_1_contribution, None).unwrap(); + + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + assert!(!stfu_ack.initiator); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); + assert!(tx_ack_rbf.funding_output_contribution.unwrap() < 0); + nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf); + assert_min_funding_error(&nodes[0], node_id_1, min_funding_satoshis); +} + #[test] fn test_splice_out() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let mut config = test_default_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -868,7 +1662,7 @@ fn test_splice_out() { let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); - let initiator_contribution = SpliceContribution::splice_out(vec![ + let outputs = vec![ TxOut { value: Amount::from_sat(initial_channel_value_sat / 4), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), @@ -877,9 +1671,11 @@ fn test_splice_out() { value: Amount::from_sat(initial_channel_value_sat / 4), script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), }, - ]); + ]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); mine_transaction(&nodes[0], &splice_tx); mine_transaction(&nodes[1], &splice_tx); @@ -895,11 +1691,12 @@ fn test_splice_out() { } #[test] -fn test_splice_in_and_out() { +fn test_splice_in_and_out_funds_outputs_from_inputs() { let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let mut config = test_default_channel_config(); - config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); @@ -907,167 +1704,548 @@ fn test_splice_in_and_out() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - let _ = send_payment(&nodes[0], &[&nodes[1]], 100_000); - - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); + let value_added = Amount::from_sat(20_000); + let utxo_value = Amount::from_sat(50_000); + let outputs = vec![ + TxOut { + value: Amount::from_sat(20_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(20_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + provide_utxo_reserves(&nodes, 2, utxo_value); + + let funding_contribution = + initiate_splice_in_and_out(&nodes[0], &nodes[1], channel_id, value_added, outputs); + let fees = Amount::from_sat(385); + let total_output_value: Amount = + funding_contribution.outputs().iter().map(|output| output.value).sum(); + let expected_change = utxo_value * 2 - value_added - total_output_value - fees; + assert_eq!(funding_contribution.change_output().unwrap().value, expected_change); + assert!(funding_contribution.net_value() >= value_added.to_signed().unwrap()); + + let (splice_tx, _) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution.clone()); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); - // Contribute a net negative value, with fees taken from the contributed inputs and the - // remaining value sent to change - let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - let added_value = Amount::from_sat(htlc_limit_msat / 1000); - let removed_value = added_value * 2; - let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); - let fees = if cfg!(feature = "grind_signatures") { - Amount::from_sat(383) - } else { - Amount::from_sat(384) - }; + let channel = &nodes[0].node.list_channels()[0]; + assert_eq!( + channel.channel_value_satoshis, + initial_channel_value_sat + funding_contribution.net_value().to_sat() as u64, + ); +} - assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); +#[test] +fn test_fails_initiating_concurrent_splices() { + fails_initiating_concurrent_splices(true); + fails_initiating_concurrent_splices(false); +} - let initiator_contribution = SpliceContribution::splice_in_and_out( - added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], - vec![ - TxOut { - value: removed_value / 2, - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }, - TxOut { - value: removed_value / 2, - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }, - ], - Some(change_script.clone()), - ); +#[cfg(test)] +fn fails_initiating_concurrent_splices(reconnect: bool) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = test_default_channel_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); - let expected_change = Amount::ONE_BTC * 2 - added_value - fees; - assert_eq!( - splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, - expected_change, - ); + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + let node_0_id = nodes[0].node.get_our_node_id(); + let node_1_id = nodes[1].node.get_our_node_id(); - mine_transaction(&nodes[0], &splice_tx); - mine_transaction(&nodes[1], &splice_tx); + send_payment(&nodes[0], &[&nodes[1]], 1_000); + provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC); - let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert!(htlc_limit_msat < added_value.to_sat() * 1000); - let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + let outputs = vec![TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); - lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_1_id).unwrap(); + let funding_contribution = + funding_template.splice_out(outputs.clone(), feerate, FeeRate::MAX).unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_1_id, funding_contribution.clone(), None) + .unwrap(); - let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert!(htlc_limit_msat < added_value.to_sat() * 1000); - let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + assert_eq!( + nodes[0].node.splice_channel(&channel_id, &node_1_id), + Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced as one is waiting to be negotiated", + channel_id + ), + }), + ); - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); + let new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]); - // Contribute a net positive value, with fees taken from the contributed inputs and the - // remaining value sent to change - let added_value = Amount::from_sat(initial_channel_value_sat * 2); - let removed_value = added_value / 2; - let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); - let fees = if cfg!(feature = "grind_signatures") { - Amount::from_sat(383) - } else { - Amount::from_sat(384) - }; + assert_eq!( + nodes[0].node.splice_channel(&channel_id, &node_1_id), + Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced as one is currently being negotiated", + channel_id + ), + }), + ); - let initiator_contribution = SpliceContribution::splice_in_and_out( - added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], - vec![ - TxOut { - value: removed_value / 2, - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }, - TxOut { - value: removed_value / 2, - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }, - ], - Some(change_script.clone()), + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + funding_contribution, + new_funding_script, ); - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); - let expected_change = Amount::ONE_BTC * 2 - added_value - fees; assert_eq!( - splice_tx.output.iter().find(|txout| txout.script_pubkey == change_script).unwrap().value, - expected_change, + nodes[0].node.splice_channel(&channel_id, &node_1_id), + Err(APIError::APIMisuseError { + err: format!( + "Channel {} cannot be spliced as one is currently being negotiated", + channel_id + ), + }), ); + let (splice_tx, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1])); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_1_id); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // Now that the splice is pending, another splice may be initiated. + assert!(nodes[0].node.splice_channel(&channel_id, &node_1_id).is_ok()); + + if reconnect { + nodes[0].node.peer_disconnected(node_1_id); + nodes[1].node.peer_disconnected(node_0_id); + reconnect_nodes(ReconnectArgs::new(&nodes[0], &nodes[1])); + } + + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + mine_transaction(&nodes[0], &splice_tx); mine_transaction(&nodes[1], &splice_tx); + let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1).stfu; + // Node 0 had called splice_channel (line above) but never funding_contributed, so no stfu + // is expected from node 0 at this point. + assert!(stfu.is_none()); +} - let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert_eq!(htlc_limit_msat, 0); +#[test] +fn test_initiating_splice_holds_stfu_with_pending_splice() { + // Test that a splice can be completed and locked successfully. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC); - let htlc_limit_msat = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; - assert!(htlc_limit_msat > initial_channel_value_sat / 2 * 1000); - let _ = send_payment(&nodes[0], &[&nodes[1]], htlc_limit_msat); + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - let coinbase_tx1 = provide_anchor_reserves(&nodes); - let coinbase_tx2 = provide_anchor_reserves(&nodes); + // Node 0 initiates a splice, completing the full flow. + let value_added = Amount::from_sat(10_000); + let funding_contribution_0 = initiate_splice_in(&nodes[0], &nodes[1], channel_id, value_added); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution_0); - // Fail adding a net contribution value of zero - let added_value = Amount::from_sat(initial_channel_value_sat * 2); - let removed_value = added_value; - let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()); + // Mine and lock the splice. + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], 5).stfu; + assert!(stfu.is_none()); +} - let initiator_contribution = SpliceContribution::splice_in_and_out( - added_value, - vec![ - FundingTxInput::new_p2wpkh(coinbase_tx1, 0).unwrap(), - FundingTxInput::new_p2wpkh(coinbase_tx2, 0).unwrap(), - ], - vec![ - TxOut { - value: removed_value / 2, - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }, - TxOut { - value: removed_value / 2, - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }, - ], - Some(change_script), - ); +#[test] +fn test_splice_both_contribute_tiebreak() { + // Same feerate: the acceptor's change increases because is_initiator=false has lower weight. + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + do_test_splice_tiebreak(feerate, feerate, Amount::from_sat(50_000), true); +} - assert_eq!( - nodes[0].node.splice_channel( - &channel_id, - &nodes[1].node.get_our_node_id(), - initiator_contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, - ), - Err(APIError::APIMisuseError { - err: format!("Channel {} cannot be spliced; contribution cannot be zero", channel_id), - }), +#[test] +fn test_splice_tiebreak_higher_feerate() { + // Node 0 (winner) uses a higher feerate than node 1 (loser). Node 1's change output is + // adjusted (reduced) to accommodate the higher feerate. Negotiation succeeds. + let feerate = FEERATE_FLOOR_SATS_PER_KW as u64; + do_test_splice_tiebreak( + FeeRate::from_sat_per_kwu(feerate * 3), + FeeRate::from_sat_per_kwu(feerate), + Amount::from_sat(50_000), + true, ); } -#[cfg(test)] -#[derive(PartialEq)] -enum SpliceStatus { - Unconfirmed, - Confirmed, - Locked, +#[test] +fn test_splice_tiebreak_lower_feerate() { + // Node 0 (winner) uses a lower feerate than node 1 (loser). Since the initiator's feerate + // is below node 1's minimum, node 1 proceeds without contribution and retries as initiator. + let feerate = FEERATE_FLOOR_SATS_PER_KW as u64; + do_test_splice_tiebreak( + FeeRate::from_sat_per_kwu(feerate), + FeeRate::from_sat_per_kwu(feerate * 3), + Amount::from_sat(50_000), + false, + ); } #[test] -fn test_splice_commitment_broadcast() { +fn test_splice_tiebreak_feerate_too_high() { + // Node 0 (winner) uses a high feerate (20,000 sat/kwu). Node 1 splices in 95,000 sats from + // a 100,000 sat UTXO, leaving too little budget for fees. Node 1 proceeds without its + // contribution and retries as initiator. + let feerate = FEERATE_FLOOR_SATS_PER_KW as u64; + do_test_splice_tiebreak( + FeeRate::from_sat_per_kwu(20_000), + FeeRate::from_sat_per_kwu(feerate), + Amount::from_sat(95_000), + false, + ); +} + +/// Runs the splice tie-breaker test with the given per-node feerates and node 1's splice value. +/// +/// Both nodes call splice_channel + splice_in_sync + funding_contributed, both send STFU, +/// node 0 wins the tie-break. If `expect_acceptor_contributes` is true, node 1 contributes +/// to the splice; otherwise, node 1 proceeds without contribution and retries as initiator. +#[cfg(test)] +fn do_test_splice_tiebreak( + node_0_feerate: FeeRate, node_1_feerate: FeeRate, node_1_splice_value: Amount, + expect_acceptor_contributes: bool, +) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); + + // Node 0 calls splice_channel + splice_in_sync + funding_contributed. + let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = funding_template_0 + .splice_in_sync(added_value, node_0_feerate, FeeRate::MAX, &wallet_0) + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + // Node 1 calls splice_channel + splice_in_sync + funding_contributed. + let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = funding_template_1 + .splice_in_sync(node_1_splice_value, node_1_feerate, FeeRate::MAX, &wallet_1) + .unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Both nodes emit STFU. + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + assert!(stfu_0.initiator); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + assert!(stfu_1.initiator); + + // Tie-break: node 1 handles node 0's STFU first — node 1 loses (not the outbound funder). + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Node 0 handles node 1's STFU — node 0 wins (outbound funder), sends SpliceInit. + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + + // Node 1 handles SpliceInit — whether it contributes depends on feerate/budget constraints. + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + let acceptor_contributes = splice_ack.funding_contribution_satoshis != 0; + assert_eq!( + acceptor_contributes, expect_acceptor_contributes, + "Expected acceptor contribution: {}, got: {}", + expect_acceptor_contributes, acceptor_contributes, + ); + + // Node 0 handles SpliceAck — starts interactive tx construction. + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + // Compute the new funding script from the splice pubkeys. + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + if acceptor_contributes { + // Capture change output values for assertions. + let node_0_change = node_0_funding_contribution + .change_output() + .expect("splice-in should have a change output") + .clone(); + let node_1_change = node_1_funding_contribution + .change_output() + .expect("splice-in should have a change output") + .clone(); + + // Complete interactive funding negotiation with both parties' inputs/outputs. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution), + splice_ack.funding_contribution_satoshis, + new_funding_script, + ); + + // Sign (acceptor has contribution) and broadcast. + let (tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).with_acceptor_contribution(), + ); + assert!(splice_locked.is_none()); + + // The initiator's change output should remain unchanged (no feerate adjustment). + let initiator_change_in_tx = tx + .output + .iter() + .find(|o| o.script_pubkey == node_0_change.script_pubkey) + .expect("Initiator's change output should be in the splice transaction"); + assert_eq!( + initiator_change_in_tx.value, node_0_change.value, + "Initiator's change output should remain unchanged", + ); + + // The acceptor's change output should be adjusted based on the feerate difference. + let acceptor_change_in_tx = tx + .output + .iter() + .find(|o| o.script_pubkey == node_1_change.script_pubkey) + .expect("Acceptor's change output should be in the splice transaction"); + if node_0_feerate <= node_1_feerate { + // Initiator's feerate <= acceptor's original: the acceptor's change increases because + // is_initiator=false has lower weight, and the feerate is the same or lower. + assert!( + acceptor_change_in_tx.value > node_1_change.value, + "Acceptor's change should increase when initiator feerate ({}) <= acceptor \ + feerate ({}): adjusted {} vs original {}", + node_0_feerate.to_sat_per_kwu(), + node_1_feerate.to_sat_per_kwu(), + acceptor_change_in_tx.value, + node_1_change.value, + ); + } else { + // Initiator's feerate > acceptor's original: the higher feerate more than compensates + // for the lower weight, so the acceptor's change decreases. + assert!( + acceptor_change_in_tx.value < node_1_change.value, + "Acceptor's change should decrease when initiator feerate ({}) > acceptor \ + feerate ({}): adjusted {} vs original {}", + node_0_feerate.to_sat_per_kwu(), + node_1_feerate.to_sat_per_kwu(), + acceptor_change_in_tx.value, + node_1_change.value, + ); + } + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + mine_transaction(&nodes[0], &tx); + mine_transaction(&nodes[1], &tx); + + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + } else { + // Acceptor does not contribute — complete with only node 0's inputs/outputs. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + None, + 0, + new_funding_script, + ); + + // Sign (no acceptor contribution) and broadcast. + let (tx, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1])); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + mine_transaction(&nodes[0], &tx); + mine_transaction(&nodes[1], &tx); + + // After splice_locked, node 1's preserved QuiescentAction triggers STFU for retry. + let node_1_stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1).stfu; + let stfu_1 = if let Some(MessageSendEvent::SendStfu { msg, .. }) = node_1_stfu { + assert!(msg.initiator); + msg + } else { + panic!("Expected SendStfu from node 1 after splice_locked"); + }; + + // === Part 2: Node 1 retries as initiator at its preferred feerate === + // TODO(splicing): Node 1 should retry contribution via RBF above instead + + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); + + nodes[0].node.handle_splice_init(node_id_1, &splice_init); + let splice_ack = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceAck, node_id_1); + + nodes[1].node.handle_splice_ack(node_id_0, &splice_ack); + + let new_funding_script_2 = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + complete_interactive_funding_negotiation( + &nodes[1], + &nodes[0], + channel_id, + node_1_funding_contribution, + new_funding_script_2, + ); + + let (new_splice_tx, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(&nodes[1], &nodes[0])); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[1], &node_id_0); + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + + mine_transaction(&nodes[1], &new_splice_tx); + mine_transaction(&nodes[0], &new_splice_tx); + + lock_splice_after_blocks(&nodes[1], &nodes[0], ANTI_REORG_DELAY - 1); + } +} + +#[test] +fn test_splice_tiebreak_feerate_too_high_rejected() { + // Node 0 (winner) proposes a feerate far above node 1's (loser) max_feerate, and node 1's + // fair fee at that feerate exceeds its budget. This triggers FeeRateAdjustmentError::TooHigh, + // causing node 1 to reject with tx_abort. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); + + // Node 0 uses an extremely high feerate (100,000 sat/kwu). Node 1 uses the floor feerate + // with a moderate splice-in (50,000 sats from a 100,000 sat UTXO) and a low max_feerate + // (3,000 sat/kwu). The target (100k) far exceeds node 1's max (3k), and the fair fee at + // 100k exceeds node 1's budget, triggering TooHigh. + let high_feerate = FeeRate::from_sat_per_kwu(100_000); + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let node_0_added_value = Amount::from_sat(50_000); + let node_1_added_value = Amount::from_sat(50_000); + let node_1_max_feerate = FeeRate::from_sat_per_kwu(3_000); + + // Node 0: very high feerate, moderate splice-in. + let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = funding_template_0 + .splice_in_sync(node_0_added_value, high_feerate, FeeRate::MAX, &wallet_0) + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + // Node 1: floor feerate, moderate splice-in, low max_feerate. + let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = funding_template_1 + .splice_in_sync(node_1_added_value, floor_feerate, node_1_max_feerate, &wallet_1) + .unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Both emit STFU. + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + // Tie-break: node 0 wins. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Node 0 sends SpliceInit at 100,000 sat/kwu. + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + + // Node 1 handles SpliceInit — TooHigh: target (100k) >> max (3k) and fair fee > budget. + // Node 1 exits quiescence upon rejecting with tx_abort, and since it has a pending + // QuiescentAction (from its own splice attempt), it immediately re-proposes quiescence. + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2); + match &msg_events[0] { + MessageSendEvent::SendTxAbort { node_id, msg } => { + assert_eq!(*node_id, node_id_0); + assert_eq!(msg.channel_id, channel_id); + }, + _ => panic!("Expected SendTxAbort, got {:?}", msg_events[0]), + }; + match &msg_events[1] { + MessageSendEvent::SendStfu { node_id, .. } => { + assert_eq!(*node_id, node_id_0); + }, + _ => panic!("Expected SendStfu, got {:?}", msg_events[1]), + }; +} + +#[cfg(test)] +#[derive(PartialEq)] +enum SpliceStatus { + Unconfirmed, + Confirmed, + Locked, +} + +#[test] +fn test_splice_commitment_broadcast() { do_test_splice_commitment_broadcast(SpliceStatus::Unconfirmed, false); do_test_splice_commitment_broadcast(SpliceStatus::Unconfirmed, true); do_test_splice_commitment_broadcast(SpliceStatus::Confirmed, false); @@ -1081,8 +2259,7 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: // Tests that we're able to enforce HTLCs onchain during the different stages of a splice. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let config = test_default_channel_config(); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_id_0 = nodes[0].node.get_our_node_id(); @@ -1092,19 +2269,20 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: let (_, _, channel_id, initial_funding_tx) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); + let coinbase_tx = provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC); // We want to have two HTLCs pending to make sure we can claim those sent before and after a // splice negotiation. let payment_amount = 1_000_000; let (preimage1, payment_hash1, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); + let splice_in_amount = initial_channel_capacity / 2; - let initiator_contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx.clone(), 0).unwrap()], - Some(nodes[0].wallet_source.get_change_script().unwrap()), - ); - let splice_tx = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let initiator_contribution = + do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, Amount::from_sat(splice_in_amount)); + let (expected_discarded_inputs, expected_discarded_outputs) = + initiator_contribution.clone().into_contributed_inputs_and_outputs(); + let (splice_tx, _) = + splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution.clone()); let (preimage2, payment_hash2, ..) = route_payment(&nodes[0], &[&nodes[1]], payment_amount); let htlc_expiry = nodes[0].best_block_info().1 + TEST_FINAL_CLTV + LATENCY_GRACE_PERIOD_BLOCKS; @@ -1155,7 +2333,7 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: message: "test".to_owned(), }; let closed_channel_capacity = if splice_status == SpliceStatus::Locked { - initial_channel_capacity + splice_in_amount + initial_channel_capacity + initiator_contribution.net_value().to_sat() as u64 } else { initial_channel_capacity }; @@ -1246,14 +2424,25 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: .chain_source .remove_watched_txn_and_outputs(funding_outpoint, txout.script_pubkey.clone()); - // `SpendableOutputs` events are also included here, but we don't care for them. let events = nodes[0].chain_monitor.chain_monitor.get_and_clear_pending_events(); assert_eq!(events.len(), if claim_htlcs { 2 } else { 4 }, "{events:?}"); if let Event::DiscardFunding { funding_info, .. } = &events[0] { - assert_eq!(*funding_info, FundingInfo::OutPoint { outpoint: funding_outpoint }); + assert_eq!( + *funding_info, + FundingInfo::Contribution { + inputs: expected_discarded_inputs, + outputs: expected_discarded_outputs, + } + ); } else { panic!(); } + assert!(matches!(&events[1], Event::SpendableOutputs { .. })); + if !claim_htlcs { + assert!(matches!(&events[2], Event::SpendableOutputs { .. })); + assert!(matches!(&events[3], Event::SpendableOutputs { .. })); + } + let events = nodes[1].chain_monitor.chain_monitor.get_and_clear_pending_events(); assert_eq!(events.len(), if claim_htlcs { 2 } else { 1 }, "{events:?}"); if let Event::DiscardFunding { funding_info, .. } = &events[0] { @@ -1261,6 +2450,9 @@ fn do_test_splice_commitment_broadcast(splice_status: SpliceStatus, claim_htlcs: } else { panic!(); } + if claim_htlcs { + assert!(matches!(&events[1], Event::SpendableOutputs { .. })); + } } } @@ -1297,7 +2489,7 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { route_payment(&nodes[0], &[&nodes[1]], 1_000_000); // Negotiate the splice up until the nodes exchange `tx_complete`. - let initiator_contribution = SpliceContribution::splice_out(vec![ + let outputs = vec![ TxOut { value: Amount::from_sat(initial_channel_value_sat / 4), script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), @@ -1306,7 +2498,9 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { value: Amount::from_sat(initial_channel_value_sat / 4), script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), }, - ]); + ]; + let initiator_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); // Node 0 should have a signing event to handle since they had a contribution in the splice. @@ -1339,6 +2533,25 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { ); // We should have another signing event generated upon reload as they're not persisted. let _ = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + + // The negotiation is awaiting signatures, so it has no negotiated candidate yet, only our + // in-flight contribution. That contribution (written under its own TLV) survives the reload. + let details = nodes[0] + .node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + .unwrap(); + assert_eq!(details.candidates.len(), 1); + assert!(matches!( + details.candidates[0].status, + SpliceCandidateStatus::AwaitingSignatures { .. } + )); + assert!(details.candidates[0].contribution.is_some()); + if async_monitor_update { persister_0a.set_update_ret(ChannelMonitorUpdateStatus::InProgress); persister_1a.set_update_ret(ChannelMonitorUpdateStatus::InProgress); @@ -1414,7 +2627,7 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { reconnect_nodes!(|reconnect_args: &mut ReconnectArgs| { reconnect_args.send_interactive_tx_sigs = (false, true); }); - expect_splice_pending_event(&nodes[1], &node_id_0); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); // Reestablish the channel again to make sure node 0 doesn't retransmit `tx_signatures` // unnecessarily as it was delivered in the previous reestablishment. @@ -1527,721 +2740,9016 @@ fn do_test_splice_reestablish(reload: bool, async_monitor_update: bool) { } #[test] -fn test_propose_splice_while_disconnected() { - do_test_propose_splice_while_disconnected(false, false); - do_test_propose_splice_while_disconnected(false, true); - do_test_propose_splice_while_disconnected(true, false); - do_test_propose_splice_while_disconnected(true, true); -} - -#[cfg(test)] -fn do_test_propose_splice_while_disconnected(reload: bool, use_0conf: bool) { - // Test that both nodes are able to propose a splice while the counterparty is disconnected, and - // whoever doesn't go first due to the quiescence tie-breaker, will retry their splice after the - // first one becomes locked. +fn test_reestablish_sends_tx_signatures_before_splice_locked() { + // If a splice confirms after `peer_connected` but before `channel_reestablish` is handled, the + // peer state is connected while the channel still has its disconnected bit set. We must not send + // `splice_locked` until the channel is reestablished. If the peer also lost our `tx_signatures`, + // we must retransmit them before `splice_locked` so it recognizes the negotiated candidate. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let (persister_0a, persister_0b, persister_1a, persister_1b); - let (chain_monitor_0a, chain_monitor_0b, chain_monitor_1a, chain_monitor_1b); - let mut config = test_default_channel_config(); - if use_0conf { - config.channel_handshake_limits.trust_own_funding_0conf = true; - } - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); - let (node_0a, node_0b, node_1a, node_1b); - let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); let node_id_0 = nodes[0].node.get_our_node_id(); let node_id_1 = nodes[1].node.get_our_node_id(); - let initial_channel_value_sat = 1_000_000; - let push_msat = initial_channel_value_sat / 2 * 1000; - let channel_id = if use_0conf { - let (funding_tx, channel_id) = open_zero_conf_channel_with_value( - &nodes[0], - &nodes[1], - None, - initial_channel_value_sat, - push_msat, - ); - mine_transaction(&nodes[0], &funding_tx); - mine_transaction(&nodes[1], &funding_tx); - channel_id + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + let prev_funding_txid = get_monitor!(nodes[0], channel_id).get_funding_txo().txid; + + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, funding_contribution); + + let event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event { + let partially_signed_tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[0] + .node + .funding_transaction_signed(&channel_id, &node_id_1, partially_signed_tx) + .unwrap(); } else { - let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( - &nodes, - 0, - 1, - initial_channel_value_sat, - push_msat, - ); - channel_id - }; + panic!("Unexpected event {event:?}"); + } + + let commitment_update_0 = get_htlc_update_msgs(&nodes[0], &node_id_1); + nodes[1].node.handle_commitment_signed(node_id_0, &commitment_update_0.commitment_signed[0]); + check_added_monitors(&nodes[1], 1); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] { + assert!(updates.update_add_htlcs.is_empty()); + assert_eq!(updates.commitment_signed.len(), 1); + nodes[0].node.handle_commitment_signed(node_id_1, &updates.commitment_signed[0]); + check_added_monitors(&nodes[0], 1); + } else { + panic!("Unexpected event {:?}", msg_events[0]); + } + if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[1] { + nodes[0].node.handle_tx_signatures(node_id_1, msg); + check_added_monitors(&nodes[0], 0); + expect_splice_pending_event(&nodes[0], &node_id_1); + } else { + panic!("Unexpected event {:?}", msg_events[1]); + } + + // Node 0 completes the exchange locally and broadcasts the splice, but its responding + // `tx_signatures` are lost. Node 1 therefore still has no negotiated candidate for the splice. + let tx_signatures_0 = get_event_msg!(nodes[0], MessageSendEvent::SendTxSignatures, node_id_1); + let splice_txid = tx_signatures_0.tx_hash; + let mut broadcast_transactions = nodes[0].tx_broadcaster.txn_broadcast(); + assert_eq!(broadcast_transactions.len(), 1, "{broadcast_transactions:?}"); + let splice_tx = broadcast_transactions.remove(0); + assert_eq!(splice_tx.compute_txid(), splice_txid); + assert!(nodes[1].tx_broadcaster.txn_broadcast().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); - // Start with the nodes disconnected, and have each one attempt a splice. nodes[0].node.peer_disconnected(node_id_1); nodes[1].node.peer_disconnected(node_id_0); - let splice_out_sat = initial_channel_value_sat / 4; - let node_0_contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(splice_out_sat), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); - nodes[0] - .node - .splice_channel( - &channel_id, - &node_id_1, - node_0_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + connect_nodes(&nodes[0], &nodes[1]); + let reestablish_0 = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelReestablish, node_id_1); + let reestablish_1 = + get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, node_id_0); + assert!(reestablish_0.next_funding.is_none()); + assert_ne!( + reestablish_0.my_current_funding_locked.as_ref().map(|funding| funding.txid), + Some(splice_txid), + ); + assert_eq!(reestablish_1.next_funding.as_ref().map(|funding| funding.txid), Some(splice_txid)); + + confirm_transaction(&nodes[0], &splice_tx); assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); - let node_1_contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(splice_out_sat), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }]); + nodes[1].node.handle_channel_reestablish(node_id_0, &reestablish_0); + let _ = get_event_msg!(nodes[1], MessageSendEvent::SendChannelUpdate, node_id_0); + + nodes[0].node.handle_channel_reestablish(node_id_1, &reestablish_1); + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 3, "{msg_events:?}"); + if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[0] { + nodes[1].node.handle_tx_signatures(node_id_0, &msg); + check_added_monitors(&nodes[1], 0); + } else { + panic!("Unexpected event {:?}", msg_events[0]); + } + let splice_locked_0 = if let MessageSendEvent::SendSpliceLocked { msg, .. } = &msg_events[1] { + msg + } else { + panic!("Unexpected event {:?}", msg_events[1]); + }; + assert!(matches!(msg_events[2], MessageSendEvent::SendChannelUpdate { .. })); + + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + let broadcast_transactions = nodes[1].tx_broadcaster.txn_broadcast(); + assert_eq!(broadcast_transactions.len(), 1, "{broadcast_transactions:?}"); + assert_eq!(broadcast_transactions[0], splice_tx); + + confirm_transaction(&nodes[1], &splice_tx); + complete_splice_locked_exchange( + &nodes[0], + &nodes[1], + &splice_locked_0, + false, + &[], + prev_funding_txid, + ); + + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn test_promoted_splice_locked_sent_after_channel_reestablish() { + // Test that a splice gets promoted for both nodes if one of the nodes sees the splice lock + // before reestablishment and the other after. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + let prev_funding_txo = get_monitor!(nodes[0], channel_id).get_funding_txo(); + + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Send a payment from node 0 to node 1 but don't fully commit it to make sure node 1 + // sends `splice_locked` first when it responds. + let payment_amount = 1_000_000; + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], payment_amount); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); + let payment_id = PaymentId(payment_hash.0); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + let update = get_htlc_update_msgs(&nodes[0], &node_id_1); + check_added_monitors(&nodes[0], 1); + + nodes[1].node.handle_update_add_htlc(node_id_0, &update.update_add_htlcs[0]); + nodes[1].node.handle_commitment_signed_batch_test(node_id_0, &update.commitment_signed); + check_added_monitors(&nodes[1], 1); + let (_dropped_raa, dropped_commitment_signed) = get_revoke_commit_msgs(&nodes[1], &node_id_0); + assert!(dropped_commitment_signed.len() > 1, "{dropped_commitment_signed:?}"); + + // Confirm the splice for node 0 first. This should result in them sending `splice_locked`, but + // node 1 should not send it back yet as it hasn't seen the confirmation. + confirm_transaction(&nodes[0], &splice_tx); + let splice_locked_0 = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Reconnect the peers. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + connect_nodes(&nodes[0], &nodes[1]); + let reestablish_0 = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelReestablish, node_id_1); + let reestablish_1 = + get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, node_id_0); + + // Before delivering the reestablish message to each other, confirm the splice for node 1. We + // should see a `ChannelReady` event for node 1 as the pending splice should have been promoted, + // but `splice_locked` should not be sent until it receives node 0's reestablish. + confirm_transaction(&nodes[1], &splice_tx); + check_added_monitors(&nodes[1], 1); + let new_funding_txo = + get_monitor!(nodes[1], channel_id).get_funding_txo().into_bitcoin_outpoint(); + let channel_ready_1 = get_event!(&nodes[1], Event::ChannelReady); + assert!(matches!( + channel_ready_1, Event::ChannelReady { funding_txo, .. } + if funding_txo == Some(new_funding_txo) + )); + + nodes[1].node.handle_channel_reestablish(node_id_0, &reestablish_0); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 5, "{msg_events:?}"); + assert!(matches!(&msg_events[0], MessageSendEvent::SendAnnouncementSignatures { .. })); + let splice_locked_1 = if let MessageSendEvent::SendSpliceLocked { msg, .. } = &msg_events[1] { + msg + } else { + panic!("Unexpected event {:?}", msg_events[1]); + }; + let revoke_and_ack = if let MessageSendEvent::SendRevokeAndACK { msg, .. } = &msg_events[2] { + msg + } else { + panic!("Unexpected event {:?}", msg_events[2]); + }; + let commit_sig = if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[3] { + assert_eq!(updates.commitment_signed.len(), 1); + updates.commitment_signed.first().unwrap() + } else { + panic!("Unexpected event {:?}", msg_events[3]); + }; + assert!(matches!(&msg_events[4], MessageSendEvent::SendChannelUpdate { .. })); + + // Deliver node 1's reestablish to node 0. Since it was generated prior to the splice + // confirmation, it should not promote the splice for node 0 yet. + nodes[0].node.handle_channel_reestablish(node_id_1, &reestablish_1); + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendChannelUpdate, node_id_1); + + // Deliver node 1's splice locked to node 0, allowing the splice to be promoted on node 0's side + // as well. + nodes[0].node.handle_splice_locked(node_id_1, splice_locked_1); + check_added_monitors(&nodes[0], 1); + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendAnnouncementSignatures, node_id_1); + let channel_ready_0 = get_event!(&nodes[0], Event::ChannelReady); + assert!(matches!( + channel_ready_0, Event::ChannelReady { funding_txo, .. } + if funding_txo == Some(new_funding_txo) + )); + + // And finally, deliver the remaining messages to fully commit the sent HTLC. + nodes[0].node.handle_revoke_and_ack(node_id_1, revoke_and_ack); + check_added_monitors(&nodes[0], 1); + nodes[0].node.handle_commitment_signed(node_id_1, commit_sig); + check_added_monitors(&nodes[0], 1); + + let revoke_and_ack = get_event_msg!(&nodes[0], MessageSendEvent::SendRevokeAndACK, node_id_1); + nodes[1].node.handle_revoke_and_ack(node_id_0, &revoke_and_ack); + check_added_monitors(&nodes[1], 1); + nodes[1].node.process_pending_htlc_forwards(); + expect_payment_claimable!(&nodes[1], payment_hash, payment_secret, payment_amount); + + // We should be able to send payments again now that the state is fully committed. + send_payment(&nodes[0], &[&nodes[1]], payment_amount); + + for node in [&nodes[0], &nodes[1]] { + node.chain_source.remove_watched_by_txid(prev_funding_txo.txid); + } +} + +#[test] +fn test_splice_reestablish_waits_for_holder_tx_signatures_before_commitment_signed() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let outputs = vec![TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let initiator_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); + + let signing_event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Drop the acceptor's initial `commitment_signed`. On reconnection, node 0's + // `channel_reestablish` should request it again, while node 1's `channel_reestablish` should + // not make node 0 retransmit a `commitment_signed` before holder transaction signatures are + // available. + let _ = get_htlc_update_msgs(&nodes[1], &node_id_0); + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_args.send_interactive_tx_commit_sig = (true, false); + reconnect_nodes(reconnect_args); + + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + let unsigned_transaction = if let Event::FundingTransactionReadyForSigning { + unsigned_transaction, + .. + } = signing_event + { + unsigned_transaction + } else { + panic!("Expected FundingTransactionReadyForSigning event"); + }; + let tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[0].node.funding_transaction_signed(&channel_id, &node_id_1, tx).unwrap(); + check_added_monitors(&nodes[0], 1); + + let initiator_commit_sig = get_htlc_update_msgs(&nodes[0], &node_id_1); nodes[1] .node - .splice_channel( - &channel_id, - &node_id_0, - node_1_contribution.clone(), - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + .handle_commitment_signed_batch_test(node_id_0, &initiator_commit_sig.commitment_signed); + check_added_monitors(&nodes[1], 1); + + let acceptor_tx_signatures = + get_event_msg!(nodes[1], MessageSendEvent::SendTxSignatures, node_id_0); + nodes[0].node.handle_tx_signatures(node_id_1, &acceptor_tx_signatures); + let initiator_tx_signatures = + get_event_msg!(nodes[0], MessageSendEvent::SendTxSignatures, node_id_1); + nodes[1].node.handle_tx_signatures(node_id_0, &initiator_tx_signatures); + + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn test_splice_reestablish_sends_commitment_signed_before_tx_signatures() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let outputs = vec![TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let initiator_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); + + let signing_event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Drop node 1's initial `commitment_signed` so node 0 requests it on reconnect. + let acceptor_commit_sig = get_htlc_update_msgs(&nodes[1], &node_id_0); + assert_eq!(acceptor_commit_sig.commitment_signed.len(), 1); + + let unsigned_transaction = if let Event::FundingTransactionReadyForSigning { + unsigned_transaction, + .. + } = signing_event + { + unsigned_transaction + } else { + panic!("Expected FundingTransactionReadyForSigning event"); + }; + let tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[0].node.funding_transaction_signed(&channel_id, &node_id_1, tx).unwrap(); + check_added_monitors(&nodes[0], 0); + + let initiator_commit_sig = get_htlc_update_msgs(&nodes[0], &node_id_1); + nodes[1] + .node + .handle_commitment_signed_batch_test(node_id_0, &initiator_commit_sig.commitment_signed); + check_added_monitors(&nodes[1], 1); + + // Drop node 1's `tx_signatures`. At this point node 0 has not received node 1's + // `commitment_signed`, while node 1 has its `tx_signatures` ready, so one + // `channel_reestablish` should trigger both retransmissions. + let _ = get_event_msg!(&nodes[1], MessageSendEvent::SendTxSignatures, node_id_0); + + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + connect_nodes(&nodes[0], &nodes[1]); + let reestablish_0 = + get_event_msg!(nodes[0], MessageSendEvent::SendChannelReestablish, node_id_1); + let _reestablish_1 = + get_event_msg!(nodes[1], MessageSendEvent::SendChannelReestablish, node_id_0); + let next_funding = reestablish_0.next_funding.as_ref().expect("next_funding should be set"); + assert!(next_funding.should_retransmit(msgs::NextFundingFlag::CommitmentSigned)); + + nodes[1].node.handle_channel_reestablish(node_id_0, &reestablish_0); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + let commitment_update_idx = msg_events + .iter() + .position(|event| { + matches!(event, MessageSendEvent::UpdateHTLCs { updates, .. } + if updates.commitment_signed.len() == 1) + }) + .expect("commitment_signed should be retransmitted"); + let tx_signatures_idx = msg_events + .iter() + .position(|event| matches!(event, MessageSendEvent::SendTxSignatures { .. })) + .expect("tx_signatures should be retransmitted"); + assert!( + commitment_update_idx < tx_signatures_idx, + "commitment_signed should be retransmitted before tx_signatures: {msg_events:?}" + ); + + let commitment_signed = + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[commitment_update_idx] { + updates.commitment_signed.clone() + } else { + panic!("Expected UpdateHTLCs"); + }; + let tx_signatures = + if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[tx_signatures_idx] { + msg.clone() + } else { + panic!("Expected SendTxSignatures"); + }; + nodes[0].node.handle_commitment_signed_batch_test(node_id_1, &commitment_signed); + check_added_monitors(&nodes[0], 1); + nodes[0].node.handle_tx_signatures(node_id_1, &tx_signatures); + let initiator_tx_signatures = + get_event_msg!(nodes[0], MessageSendEvent::SendTxSignatures, node_id_1); + nodes[1].node.handle_tx_signatures(node_id_0, &initiator_tx_signatures); + + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn test_splice_confirms_on_both_sides_while_disconnected() { + // Regression test: when a splice transaction confirms on both sides while peers are + // disconnected, each peer's `channel_reestablish` carries `my_current_funding_locked` with the + // splice txid. The receiving side must not emit `announcement_signatures` for the pre-splice + // funding in that handler — those would be verified against the post-splice channel + // announcement on the peer and force-close the channel. Instead, sigs are generated after the + // inferred `splice_locked` promotes the splice funding. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let prev_funding_outpoint = get_monitor!(nodes[0], channel_id).get_funding_txo(); + let prev_funding_script = get_monitor!(nodes[0], channel_id).get_funding_script(); + + // Capture the pre-splice scid so we can later assert the announcement_sigs each side emits + // on reconnect carry the post-splice scid, not the pre-splice one the bug would emit. + let pre_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap(); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Disconnect before either side confirms the splice. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + // Confirm the splice on both sides while disconnected. Each side's `transactions_confirmed` + // runs `check_get_splice_locked`, which sets `pending_splice.sent_funding_txid` so that + // `my_current_funding_locked` will carry the splice txid on reconnect. No `splice_locked` + // messages are queued while disconnected. + confirm_transaction(&nodes[0], &splice_tx); + confirm_transaction(&nodes[1], &splice_tx); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); - if reload { - let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode(); - reload_node!( - nodes[0], - nodes[0].node.encode(), - &[&encoded_monitor_0], - persister_0a, - chain_monitor_0a, - node_0a + // Reconnect manually so we can inspect each side's emitted `SendAnnouncementSignatures`. + // Each side's `channel_reestablish` carries `my_current_funding_locked` with the splice + // txid, triggering inferred `splice_locked` on the peer. With the fix in place, + // `announcement_signatures` are generated from the post-splice funding (via the promotion + // path) rather than the pre-splice funding (via the reestablish handler). + connect_nodes(&nodes[0], &nodes[1]); + let reestablish_0 = get_chan_reestablish_msgs!(nodes[0], nodes[1]); + let reestablish_1 = get_chan_reestablish_msgs!(nodes[1], nodes[0]); + for msg in &reestablish_0 { + nodes[1].node.handle_channel_reestablish(node_id_0, msg); + } + for msg in &reestablish_1 { + nodes[0].node.handle_channel_reestablish(node_id_1, msg); + } + check_added_monitors(&nodes[0], 1); + check_added_monitors(&nodes[1], 1); + expect_channel_ready_event(&nodes[0], &node_id_1); + expect_channel_ready_event(&nodes[1], &node_id_0); + + // Each side should emit exactly one `SendAnnouncementSignatures` (post-promotion). The + // pre-fix behavior would emit a second, stale pre-splice one — our assertion is that the + // only sigs we send carry the post-splice scid. + let take_announcement_sigs = |events: Vec<MessageSendEvent>| -> msgs::AnnouncementSignatures { + let mut sigs = events.into_iter().filter_map(|e| match e { + MessageSendEvent::SendAnnouncementSignatures { msg, .. } => Some(msg), + _ => None, + }); + let only = sigs.next().expect("expected one SendAnnouncementSignatures"); + assert!(sigs.next().is_none(), "expected only one SendAnnouncementSignatures"); + only + }; + let node_0_events = nodes[0].node.get_and_clear_pending_msg_events(); + let node_1_events = nodes[1].node.get_and_clear_pending_msg_events(); + let node_0_sigs = take_announcement_sigs(node_0_events); + let node_1_sigs = take_announcement_sigs(node_1_events); + assert_ne!(node_0_sigs.short_channel_id, pre_splice_scid); + assert_ne!(node_1_sigs.short_channel_id, pre_splice_scid); + + // Cross-deliver to complete the post-splice announcement exchange, then drain the + // resulting `BroadcastChannelAnnouncement` events on each side. + nodes[1].node.handle_announcement_signatures(node_id_0, &node_0_sigs); + nodes[0].node.handle_announcement_signatures(node_id_1, &node_1_sigs); + let _ = nodes[0].node.get_and_clear_pending_msg_events(); + let _ = nodes[1].node.get_and_clear_pending_msg_events(); + + // Channel must still be operational after reconnect — no force-close from mismatched + // announcement signatures. + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + // No stray events or messages left over. + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Clean up chain-source state for the retired pre-splice funding so end-of-test checks pass. + nodes[0] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); + nodes[1] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); +} + +#[test] +fn test_holding_cell_claim_freed_after_inferred_splice_locked() { + // If `channel_reestablish` infers a missed `splice_locked`, it must promote the splice before + // freeing holding-cell updates. If the promotion monitor update is asynchronous, holding-cell + // updates must remain held until that monitor update completes. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + let prev_funding_outpoint = get_monitor!(nodes[0], channel_id).get_funding_txo(); + let prev_funding_script = get_monitor!(nodes[0], channel_id).get_funding_script(); + let prev_scid = nodes[0].node.list_channels()[0].short_channel_id; + + let (payment_preimage, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + nodes[1].node.claim_funds(payment_preimage); + check_added_monitors(&nodes[1], 1); + expect_payment_claimed!(nodes[1], payment_hash, 1_000_000); + + confirm_transaction(&nodes[0], &splice_tx); + confirm_transaction(&nodes[1], &splice_tx); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.expect_renegotiated_funding_locked_monitor_update = (true, true); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); + + expect_channel_ready_event(&nodes[0], &node_id_1); + expect_channel_ready_event(&nodes[1], &node_id_0); + assert_ne!(prev_scid, nodes[0].node.list_channels()[0].short_channel_id); + + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + let mut commitment_update = get_htlc_update_msgs(&nodes[1], &node_id_0); + check_added_monitors(&nodes[1], 1); + nodes[0] + .node + .handle_update_fulfill_htlc(node_id_1, commitment_update.update_fulfill_htlcs.remove(0)); + do_commitment_signed_dance( + &nodes[0], + &nodes[1], + &commitment_update.commitment_signed, + false, + false, + ); + + expect_payment_sent!(nodes[0], payment_preimage); + + nodes[0] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); + nodes[1] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); +} + +#[test] +fn test_stale_monitor_pending_resends_cleared_by_reestablish() { + // A stale ChannelManager may be reloaded after a monitor update completed and released its + // messages to the peer. If a later splice-locked monitor update is in-flight while the channel + // reestablishes, completing it must not release the stale messages again. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let persister; + let chain_monitor; + let node_1_reload; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + let prev_funding_outpoint = get_monitor!(nodes[1], channel_id).get_funding_txo(); + let prev_funding_script = get_monitor!(nodes[1], channel_id).get_funding_script(); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Only let node 0 see the splice lock for now. + confirm_transaction(&nodes[0], &splice_tx); + let splice_locked_0 = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Send an HTLC from node 0 to 1 that will get fully committed to. + let payment_amount = 1_000_000; + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], payment_amount); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); + let payment_id = PaymentId(payment_hash.0); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + let htlc_update = get_htlc_update_msgs(&nodes[0], &node_id_1); + check_added_monitors(&nodes[0], 1); + + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.handle_update_add_htlc(node_id_0, &htlc_update.update_add_htlcs[0]); + nodes[1].node.handle_commitment_signed_batch_test(node_id_0, &htlc_update.commitment_signed); + check_added_monitors(&nodes[1], 1); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Persist the `ChannelManager` while node 1 is still pending to send their RAA+CS to node 0. + let stale_manager_1 = nodes[1].node.encode(); + + // Let node 1 release its RAA+CS to node 0 and process them. + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + let (raa, commitment_signed) = get_revoke_commit_msgs(&nodes[1], &node_id_0); + nodes[0].node.handle_revoke_and_ack(node_id_1, &raa); + check_added_monitors(&nodes[0], 1); + nodes[0].node.handle_commitment_signed_batch_test(node_id_1, &commitment_signed); + check_added_monitors(&nodes[0], 1); + let _dropped_raa = get_event_msg!(nodes[0], MessageSendEvent::SendRevokeAndACK, node_id_1); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Reload node 1 with the stale `ChannelManager` and confirm the splice. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + let latest_monitor_1 = get_monitor!(nodes[1], channel_id).encode(); + reload_node!( + nodes[1], + &stale_manager_1, + &[&latest_monitor_1], + persister, + chain_monitor, + node_1_reload + ); + + mine_transaction_without_consistency_checks(&nodes[1], &splice_tx); + connect_blocks(&nodes[1], 5); + persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + + // Reestablish the channel. While the `ChannelManager` should think it still owes node 0 its + // RAA+CS, it should determine from node 0's `channel_reestablish` that they were already + // delivered. + connect_nodes(&nodes[0], &nodes[1]); + check_added_monitors(&nodes[1], 1); + let reestablish_0 = get_chan_reestablish_msgs!(nodes[0], nodes[1]); + let reestablish_1 = get_chan_reestablish_msgs!(nodes[1], nodes[0]); + nodes[1].node.handle_channel_reestablish(node_id_0, reestablish_0.first().unwrap()); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert!( + msg_events.iter().all(|event| !matches!( + event, + MessageSendEvent::SendRevokeAndACK { .. } | MessageSendEvent::UpdateHTLCs { .. } + )), + "stale monitor-pending resend leaked during reestablish: {msg_events:?}" + ); + + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert!( + msg_events.iter().all(|event| !matches!( + event, + MessageSendEvent::SendRevokeAndACK { .. } | MessageSendEvent::UpdateHTLCs { .. } + )), + "stale monitor-pending resend leaked after reestablish: {msg_events:?}" + ); + expect_channel_ready_event(&nodes[1], &node_id_0); + + nodes[0].node.handle_channel_reestablish(node_id_1, reestablish_1.first().unwrap()); + check_added_monitors(&nodes[0], 1); + expect_channel_ready_event(&nodes[0], &node_id_1); + + // Finish fully committing the HTLC and make sure we can still send more payments. + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 3, "{msg_events:?}"); + if let MessageSendEvent::SendRevokeAndACK { msg, .. } = &msg_events[0] { + nodes[1].node.handle_revoke_and_ack(node_id_0, msg); + check_added_monitors(&nodes[1], 1); + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[1].node.process_pending_htlc_forwards(); + expect_payment_claimable!(&nodes[1], payment_hash, payment_secret, payment_amount); + } else { + panic!("Unexpected event {:?}", &msg_events[0]); + } + + send_payment(&nodes[0], &[&nodes[1]], payment_amount); + + for node in &[&nodes[0], &nodes[1]] { + node.chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); + } +} + +#[test] +fn test_stale_announcement_signatures_ignored_after_splice_lock() { + // Regression test: a peer may transmit `announcement_signatures` signed over a pre-splice + // `short_channel_id` (for example, a stale retransmission or a peer implementation that + // hasn't yet caught up to our post-splice promotion). Verifying those sigs against the + // post-splice `UnsignedChannelAnnouncement` will always fail the hash check, but that is not + // a protocol violation — the spec permits ignoring and the channel should stay open. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + // Use the lower-level helper so we get the signed `ChannelAnnouncement` back — the test + // needs node 1's pre-splice announcement signatures to replay later. + let chan_announcement = + create_chan_between_nodes_with_value(&nodes[0], &nodes[1], initial_channel_value_sat, 0); + let channel_id = chan_announcement.3; + update_nodes_with_chan_announce( + &nodes, + 0, + 1, + &chan_announcement.0, + &chan_announcement.1, + &chan_announcement.2, + ); + + // Extract node 1's pre-splice signatures from the ChannelAnnouncement. `UnsignedChannelAnnouncement` + // orders `node_id_1`/`node_id_2` by serialized pubkey; node 1's sigs are in slot 1 iff node 1's + // pubkey is lexicographically smaller. + let node_1_is_node_one = node_id_1.serialize() < node_id_0.serialize(); + let (stale_node_sig, stale_bitcoin_sig) = if node_1_is_node_one { + (chan_announcement.0.node_signature_1, chan_announcement.0.bitcoin_signature_1) + } else { + (chan_announcement.0.node_signature_2, chan_announcement.0.bitcoin_signature_2) + }; + + // Capture the pre-splice `short_channel_id` — this is the scid the stale sigs sign over. + let pre_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap(); + + let outputs = vec![ + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }, + TxOut { + value: Amount::from_sat(initial_channel_value_sat / 4), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }, + ]; + let funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + // The post-splice scid is now different; confirm that. + let post_splice_scid = nodes[0].node.list_channels()[0].short_channel_id.unwrap(); + assert_ne!(pre_splice_scid, post_splice_scid); + + // Replay node 1's pre-splice announcement signatures, now stale (the current scid is the + // post-splice one). This is the exact shape of message a peer would send if it retransmitted + // an old `announcement_signatures` across a splice handoff. + let stale_sigs = msgs::AnnouncementSignatures { + channel_id, + short_channel_id: pre_splice_scid, + node_signature: stale_node_sig, + bitcoin_signature: stale_bitcoin_sig, + }; + nodes[0].node.handle_announcement_signatures(node_id_1, &stale_sigs); + + // No force-close, no outbound error, no events. The channel must still be listed and usable. + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert_eq!(nodes[0].node.list_channels().len(), 1); + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn test_propose_splice_while_disconnected() { + do_test_propose_splice_while_disconnected(false); + do_test_propose_splice_while_disconnected(true); +} + +#[cfg(test)] +fn do_test_propose_splice_while_disconnected(use_0conf: bool) { + // Test that both nodes are able to propose a splice while the counterparty is disconnected, and + // whoever doesn't go first due to the quiescence tie-breaker, will have their contribution + // merged into the counterparty-initiated splice. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + if use_0conf { + config.channel_handshake_limits.trust_own_funding_0conf = true; + } + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 1_000_000; + let push_msat = initial_channel_value_sat / 2 * 1000; + let channel_id = if use_0conf { + let (funding_tx, channel_id) = open_zero_conf_channel_with_value( + &nodes[0], + &nodes[1], + None, + initial_channel_value_sat, + push_msat, + ); + mine_transaction(&nodes[0], &funding_tx); + mine_transaction(&nodes[1], &funding_tx); + channel_id + } else { + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + initial_channel_value_sat, + push_msat, + ); + channel_id + }; + + // Start with the nodes disconnected, and have each one attempt a splice. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + let splice_out_sat = initial_channel_value_sat / 4; + let node_0_outputs = vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let node_0_funding_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, node_0_outputs).unwrap(); + + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + let node_1_outputs = vec![TxOut { + value: Amount::from_sat(splice_out_sat), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]; + let node_1_funding_contribution = + initiate_splice_out(&nodes[1], &nodes[0], channel_id, node_1_outputs).unwrap(); + + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Reconnect the nodes. Both nodes should attempt quiescence as the initiator, but only one will + // be it via the tie-breaker. + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_channel_ready = (true, true); + if !use_0conf { + reconnect_args.send_announcement_sigs = (true, true); + } + reconnect_args.send_stfu = (true, true); + reconnect_nodes(reconnect_args); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + let (prev_funding_outpoint, prev_funding_script) = nodes[0] + .chain_monitor + .chain_monitor + .get_monitor(channel_id) + .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) + .unwrap(); + + // Negotiate the splice to completion. Node 1's quiescent action should be consumed by + // splice_init, so both contributions are merged into a single splice. + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!(splice_ack.funding_contribution_satoshis, 0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution), + splice_ack.funding_contribution_satoshis, + new_funding_script, + ); + let mut args = + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).with_acceptor_contribution(); + if use_0conf { + args = args.zero_conf(); + } + let (splice_tx, splice_locked) = sign_interactive_funding_tx(args); + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + let splice_locked = if use_0conf { + let (splice_locked, for_node_id) = splice_locked.unwrap(); + assert_eq!(for_node_id, node_id_1); + splice_locked + } else { + assert!(splice_locked.is_none()); + + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + // Mine enough blocks for the splice to become locked. + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); + + get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1) + }; + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked); + + // Node 1's quiescent action was consumed, so it should NOT send stfu. + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), if use_0conf { 1 } else { 2 }, "{msg_events:?}"); + if let MessageSendEvent::SendSpliceLocked { ref msg, .. } = &msg_events[0] { + nodes[0].node.handle_splice_locked(node_id_1, msg); + if use_0conf { + // TODO(splicing): Revisit splice transaction rebroadcasts. + let txn_0 = nodes[0].tx_broadcaster.txn_broadcast(); + assert_eq!(txn_0.len(), 1); + assert_eq!(&txn_0[0], &splice_tx); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + } + } else { + panic!("Unexpected event {:?}", &msg_events[0]); + } + if !use_0conf { + if let MessageSendEvent::SendAnnouncementSignatures { ref msg, .. } = &msg_events[1] { + nodes[0].node.handle_announcement_signatures(node_id_1, msg); + } else { + panic!("Unexpected event {:?}", &msg_events[1]); + } + } + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), if use_0conf { 0 } else { 2 }, "{msg_events:?}"); + if !use_0conf { + if let MessageSendEvent::SendAnnouncementSignatures { ref msg, .. } = &msg_events[0] { + nodes[1].node.handle_announcement_signatures(node_id_0, msg); + } else { + panic!("Unexpected event {:?}", &msg_events[1]); + } + assert!(matches!(&msg_events[1], MessageSendEvent::BroadcastChannelAnnouncement { .. })); + } + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), if use_0conf { 0 } else { 1 }, "{msg_events:?}"); + if !use_0conf { + assert!(matches!(&msg_events[0], MessageSendEvent::BroadcastChannelAnnouncement { .. })); + } + + expect_channel_ready_event(&nodes[0], &node_id_1); + check_added_monitors(&nodes[0], 1); + expect_channel_ready_event(&nodes[1], &node_id_0); + check_added_monitors(&nodes[1], 1); + + // Remove the corresponding outputs and transactions the chain source is watching for the + // old funding as it is no longer being tracked. + nodes[0] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); + nodes[1] + .chain_source + .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); + + // Sanity check that we can still make a test payment. + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn disconnect_on_unexpected_interactive_tx_message() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC); + + let splice_in_amount = initial_channel_capacity / 2; + let contribution = + initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount)); + + // Complete interactive-tx construction, but fail by having the acceptor send a duplicate + // tx_complete instead of commitment_signed. + negotiate_splice_tx(initiator, acceptor, channel_id, contribution); + + let _ = get_event!(initiator, Event::FundingTransactionReadyForSigning); + let _ = get_htlc_update_msgs(acceptor, &node_id_initiator); + + let tx_complete = msgs::TxComplete { channel_id }; + initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete); + + let _warning = get_warning_msg(initiator, &node_id_acceptor); +} + +#[test] +fn fail_splice_on_interactive_tx_error() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC); + + let splice_in_amount = initial_channel_capacity / 2; + + // Fail during interactive-tx construction by having the acceptor echo back tx_add_input instead + // of sending tx_complete. The failure occurs because the serial id will have the wrong parity. + let funding_contribution = + initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount)); + let _ = complete_splice_handshake(initiator, acceptor); + + // Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence. + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(initiator, acceptor, 1_000_000); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); + let payment_id = PaymentId(payment_hash.0); + initiator.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + + let tx_add_input = + get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); + acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); + + let _tx_complete = + get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); + initiator.node.handle_tx_add_input(node_id_acceptor, &tx_add_input); + + expect_splice_failed_events( + initiator, + &channel_id, + funding_contribution, + NegotiationFailureReason::NegotiationError { + msg: "Abort: Parity for `serial_id` was incorrect".to_string(), + }, + ); + + // We exit quiescence upon sending `tx_abort`, so we should see the holding cell be immediately + // freed. + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + let tx_abort = if let MessageSendEvent::SendTxAbort { msg, .. } = &msg_events[0] { + msg + } else { + panic!("Unexpected event {:?}", msg_events[0]); + }; + let update = if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[1] { + updates + } else { + panic!("Unexpected event {:?}", msg_events[1]); + }; + check_added_monitors(initiator, 1); + + acceptor.node.handle_tx_abort(node_id_initiator, tx_abort); + let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); + initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort); + + acceptor.node.handle_update_add_htlc(node_id_initiator, &update.update_add_htlcs[0]); + do_commitment_signed_dance(acceptor, initiator, &update.commitment_signed, false, false); +} + +#[test] +fn fail_splice_on_tx_abort() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC); + + let splice_in_amount = initial_channel_capacity / 2; + + // Fail during interactive-tx construction by having the acceptor send tx_abort instead of + // tx_complete. + let funding_contribution = + initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount)); + let _ = complete_splice_handshake(initiator, acceptor); + + // Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence. + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(initiator, acceptor, 1_000_000); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); + let payment_id = PaymentId(payment_hash.0); + initiator.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + + let tx_add_input = + get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); + acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); + + let _tx_complete = + get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); + + // Inject a fake `tx_abort` to the initiator to trigger the splice to be aborted. + let tx_abort = msgs::TxAbort { channel_id, data: Vec::new() }; + initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort); + + expect_splice_failed_events( + initiator, + &channel_id, + funding_contribution, + NegotiationFailureReason::CounterpartyAborted { msg: UntrustedString(String::new()) }, + ); + + // We exit quiescence upon receiving `tx_abort`, so we should see our `tx_abort` echo and the + // holding cell be immediately freed. + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + check_added_monitors(initiator, 1); + if let MessageSendEvent::SendTxAbort { msg, .. } = &msg_events[0] { + acceptor.node.handle_tx_abort(node_id_initiator, msg); + // The acceptor still tries to ack the abort by sending its own back to the initiator since + // a fake one was originally sent to it. + let _ = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); + } else { + panic!("Unexpected event {:?}", msg_events[0]); + }; + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[1] { + acceptor.node.handle_update_add_htlc(node_id_initiator, &updates.update_add_htlcs[0]); + do_commitment_signed_dance(acceptor, initiator, &updates.commitment_signed, false, false); + } else { + panic!("Unexpected event {:?}", msg_events[1]); + }; +} + +#[test] +fn acceptor_with_local_contribution_can_cancel_funding_contributed_before_funding_transaction_signed( +) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: initiator.wallet_source.get_change_script().unwrap(), + }]; + let initiator_contribution = + initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + let acceptor_contribution = initiate_splice_in( + acceptor, + initiator, + channel_id, + Amount::from_sat(initial_channel_capacity / 2), + ); + + let stfu_initiator = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + let stfu_acceptor = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + + acceptor.node.handle_stfu(node_id_initiator, &stfu_initiator); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + initiator.node.handle_stfu(node_id_acceptor, &stfu_acceptor); + + let splice_init = get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let splice_ack = get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator); + assert_ne!(splice_ack.funding_contribution_satoshis, 0); + initiator.node.handle_splice_ack(node_id_acceptor, &splice_ack); + + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + complete_interactive_funding_negotiation_for_both( + initiator, + acceptor, + channel_id, + initiator_contribution.clone(), + Some(acceptor_contribution.clone()), + splice_ack.funding_contribution_satoshis, + new_funding_script, + ); + + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); + } else { + unreachable!(); + } + + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + let initial_commit_sig = if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] { + updates.commitment_signed[0].clone() + } else { + panic!("Unexpected event {:?}", msg_events[0]); + }; + acceptor.node.handle_commitment_signed(node_id_initiator, &initial_commit_sig); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + let _signing_event = get_event!(acceptor, Event::FundingTransactionReadyForSigning); + + acceptor.node.cancel_funding_contributed(&channel_id, &node_id_initiator).unwrap(); + let events = acceptor.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2); + assert!(matches!(events[0], Event::DiscardFunding { .. })); + assert!(matches!(events[1], Event::SpliceNegotiationFailed { .. })); + let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); + + initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort); + let reason = NegotiationFailureReason::CounterpartyAborted { + msg: UntrustedString("Manually aborted funding negotiation".into()), + }; + expect_splice_failed_events(initiator, &channel_id, initiator_contribution, reason); + let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor); + acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort); +} + +#[test] +fn acceptor_can_cancel_queued_funding_contributed_during_counterparty_splice() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let acceptor = &nodes[0]; + let initiator = &nodes[1]; + + let node_id_acceptor = acceptor.node.get_our_node_id(); + let node_id_initiator = initiator.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let initiator_contribution = + do_initiate_splice_in(initiator, acceptor, channel_id, added_value); + + let stfu_initiator = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_initiator); + let stfu_acceptor = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_acceptor); + + let splice_init = get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let splice_ack = get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator); + assert_eq!(splice_ack.funding_contribution_satoshis, 0); + + let funding_template = acceptor.node.splice_channel(&channel_id, &node_id_initiator).unwrap(); + let feerate = funding_template.min_rbf_feerate().unwrap(); + let wallet = WalletSync::new(Arc::clone(&acceptor.wallet_source), acceptor.logger); + let queued_contribution = funding_template + .splice_in_sync(Amount::from_sat(25_000), feerate, FeeRate::MAX, &wallet) + .unwrap(); + acceptor + .node + .funding_contributed(&channel_id, &node_id_initiator, queued_contribution.clone(), None) + .unwrap(); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + // The acceptor is mid-negotiation on the counterparty's splice and has its own contribution + // queued behind it; both surface at once. + let details = acceptor + .node + .list_channels() + .into_iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .unwrap(); + assert_eq!(details.candidates.len(), 2); + // The counterparty's in-flight round, which we did not contribute to. + assert!(matches!( + details.candidates[0].status, + SpliceCandidateStatus::ConstructingTransaction { is_initiator: false, .. } + )); + assert_eq!(details.candidates[0].contribution, None); + // Our own contribution, queued to RBF the counterparty's round once it completes. + assert_eq!(details.candidates[1].status, SpliceCandidateStatus::WaitingOnQuiescence); + assert_eq!(details.candidates[1].contribution, Some(queued_contribution.clone())); + + acceptor.node.cancel_funding_contributed(&channel_id, &node_id_initiator).unwrap(); + let reason = NegotiationFailureReason::LocallyCanceled; + expect_splice_failed_events(acceptor, &channel_id, queued_contribution, reason); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + initiator.node.handle_splice_ack(node_id_acceptor, &splice_ack); + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + complete_interactive_funding_negotiation( + initiator, + acceptor, + channel_id, + initiator_contribution, + new_funding_script, + ); + + let (splice_tx, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(initiator, acceptor)); + assert!(splice_locked.is_none()); + expect_splice_pending_event(initiator, &node_id_acceptor); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); + + mine_transaction(initiator, &splice_tx); + mine_transaction(acceptor, &splice_tx); + assert!(lock_splice_after_blocks(initiator, acceptor, ANTI_REORG_DELAY - 1).stfu.is_none()); +} + +#[test] +fn cancel_funding_contributed_before_funding_transaction_signed() { + do_cancel_funding_contributed_before_funding_transaction_signed(0); // AwaitingQuiescence + do_cancel_funding_contributed_before_funding_transaction_signed(1); // AwaitingAck + do_cancel_funding_contributed_before_funding_transaction_signed(2); // ConstructingTransaction + do_cancel_funding_contributed_before_funding_transaction_signed(3); // AwaitingSignatures +} + +#[cfg(test)] +fn do_cancel_funding_contributed_before_funding_transaction_signed(state: u8) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: initiator.wallet_source.get_change_script().unwrap(), + }]; + let funding_contribution = + initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + + match state { + 0 => { + // Cancel after funding_contributed queues `stfu`, but before the quiescence attempt is + // delivered to the peer. + }, + 1 => { + // Deliver splice_init, but keep splice_ack queued so the initiator remains in + // FundingNegotiation::AwaitingAck while the acceptor tracks the pending splice. + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + let splice_init = + get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events[0], MessageSendEvent::SendSpliceAck { .. })); + }, + 2 => { + // Complete the splice handshake so the initiator is constructing the interactive tx. + let _new_funding_script = complete_splice_handshake(initiator, acceptor); + + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events[0], MessageSendEvent::SendTxAddInput { .. })); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + }, + 3 => { + // Complete interactive tx negotiation so the initiator is awaiting funding signatures. + let new_funding_script = complete_splice_handshake(initiator, acceptor); + complete_interactive_funding_negotiation( + initiator, + acceptor, + channel_id, + funding_contribution.clone(), + new_funding_script, + ); + + // The initiator should have a signing event to handle, while the acceptor immediately + // sends their initial commitment_signed. Deliver it before canceling to ensure it gets + // discarded with the splice. + let _signing_event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); + let acceptor_commit_sig = get_htlc_update_msgs(acceptor, &node_id_initiator); + initiator.node.handle_commitment_signed( + node_id_acceptor, + &acceptor_commit_sig.commitment_signed[0], + ); + check_added_monitors(initiator, 0); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + }, + _ => panic!("unexpected state {state}"), + } + assert!(initiator.node.get_and_clear_pending_events().is_empty()); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); + + // Queue an outgoing HTLC to the holding cell. It should be freed once we cancel the splice and + // exit quiescence. + if state != 0 { + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(initiator, acceptor, 1_000_000); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); + let payment_id = PaymentId(payment_hash.0); + initiator.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + } + + initiator.node.cancel_funding_contributed(&channel_id, &node_id_acceptor).unwrap(); + let reason = NegotiationFailureReason::LocallyCanceled; + expect_splice_failed_events(initiator, &channel_id, funding_contribution, reason); + + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if state == 0 { + // We didn't reach quiescence prior to canceling, so we should see our `stfu` followed by a + // disconnect. + if let MessageSendEvent::SendStfu { .. } = &msg_events[0] { + } else { + panic!("Unexpected event {:?}", msg_events[0]); + } + if let MessageSendEvent::HandleError { action, .. } = &msg_events[1] { + assert!(matches!(action, msgs::ErrorAction::DisconnectPeerWithWarning { .. })); + } else { + panic!("Unexpected event {:?}", msg_events[1]); + } + return; + } + + // We exit or terminate the quiescence attempt upon canceling the splice, so we should see a + // tx_abort followed by the holding cell HTLC being released immediately. + let tx_abort = if let MessageSendEvent::SendTxAbort { msg, .. } = &msg_events[0] { + msg + } else { + panic!("Unexpected event {:?}", msg_events[0]); + }; + let update = if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[1] { + updates + } else { + panic!("Unexpected event {:?}", msg_events[1]); + }; + check_added_monitors(initiator, 1); + + acceptor.node.handle_tx_abort(node_id_initiator, tx_abort); + let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); + initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort); + + acceptor.node.handle_update_add_htlc(node_id_initiator, &update.update_add_htlcs[0]); + do_commitment_signed_dance(acceptor, initiator, &update.commitment_signed, false, false); +} + +#[test] +fn cancel_funding_contributed_then_inflight_commitment_signed_does_not_close_channel() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: initiator.wallet_source.get_change_script().unwrap(), + }]; + let funding_contribution = + initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + let new_funding_script = complete_splice_handshake(initiator, acceptor); + complete_interactive_funding_negotiation( + initiator, + acceptor, + channel_id, + funding_contribution.clone(), + new_funding_script, + ); + + // Both peers completed the interactive transaction exchange. Since only the + // initiator contributed splice funds, the initiator must still surface the + // unsigned funding transaction before it may send its initial + // `commitment_signed`. + let _ = get_event!(initiator, Event::FundingTransactionReadyForSigning); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + + // The acceptor has no funding contribution, so it can send its initial + // `commitment_signed` immediately. Hold that message to model it racing with + // the local caller's decision to cancel instead of sign. + let acceptor_commit_sig = get_htlc_update_msgs(acceptor, &node_id_initiator); + assert_eq!(acceptor_commit_sig.commitment_signed.len(), 1); + + // Cancel before signing. This is a valid API flow: local contribution is + // discarded, the splice negotiation fails locally, and LDK queues a + // `tx_abort` for the peer. + initiator.node.cancel_funding_contributed(&channel_id, &node_id_acceptor).unwrap(); + let reason = NegotiationFailureReason::LocallyCanceled; + expect_splice_failed_events(initiator, &channel_id, funding_contribution, reason); + + // Keep our `tx_abort` queued. The fuzz failure has this exact ordering: our + // abort is outbound, but the acceptor's earlier `commitment_signed` reaches + // us first. + let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor); + assert_eq!(tx_abort.channel_id, channel_id); + + initiator + .node + .handle_commitment_signed(node_id_acceptor, &acceptor_commit_sig.commitment_signed[0]); + + // The delayed `commitment_signed` belonged to the splice we just aborted. It + // should not be validated against the post-abort channel state and should + // not force-close the live channel as an invalid commitment signature. + assert!(initiator.node.get_and_clear_pending_events().is_empty()); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); +} + +#[test] +fn cannot_cancel_funding_contributed_after_funding_transaction_signed() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: initiator.wallet_source.get_change_script().unwrap(), + }]; + let funding_contribution = + initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + let new_funding_script = complete_splice_handshake(initiator, acceptor); + complete_interactive_funding_negotiation( + initiator, + acceptor, + channel_id, + funding_contribution, + new_funding_script, + ); + assert!(acceptor.node.get_and_clear_pending_events().is_empty()); + let _acceptor_commit_sig = get_htlc_update_msgs(acceptor, &node_id_initiator); + + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); + } else { + unreachable!(); + } + + let res = initiator.node.cancel_funding_contributed(&channel_id, &node_id_acceptor); + match res { + Err(APIError::APIMisuseError { err }) => assert!(err.contains("already signed")), + _ => panic!("Unexpected result {res:?}"), + } + + assert!(initiator.node.get_and_clear_pending_events().is_empty()); + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert!( + msg_events.iter().all(|event| !matches!(event, MessageSendEvent::SendTxAbort { .. })), + "{msg_events:?}" + ); +} + +#[test] +fn fail_splice_on_tx_complete_error() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = test_default_channel_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[1]; + let acceptor = &nodes[0]; + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: acceptor.wallet_source.get_change_script().unwrap(), + }]; + let funding_contribution = + initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + let _ = complete_splice_handshake(initiator, acceptor); + + // Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence. + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(acceptor, initiator, 1_000_000); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); + let payment_id = PaymentId(payment_hash.0); + acceptor.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + + let tx_add_input = + get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); + acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); + let tx_complete = get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); + initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete); + + // Tamper the shared funding output such that the acceptor fails upon `tx_complete`. + let mut tx_add_output = + get_event_msg!(initiator, MessageSendEvent::SendTxAddOutput, node_id_acceptor); + if tx_add_output.script.is_p2wsh() { + tx_add_output.sats *= 2; + } + acceptor.node.handle_tx_add_output(node_id_initiator, &tx_add_output); + let tx_complete = get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); + initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete); + + let mut tx_add_output = + get_event_msg!(initiator, MessageSendEvent::SendTxAddOutput, node_id_acceptor); + if tx_add_output.script.is_p2wsh() { + tx_add_output.sats *= 2; + } + acceptor.node.handle_tx_add_output(node_id_initiator, &tx_add_output); + let tx_complete = get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); + initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete); + + let _ = get_event!(initiator, Event::FundingTransactionReadyForSigning); + let tx_complete = get_event_msg!(initiator, MessageSendEvent::SendTxComplete, node_id_acceptor); + acceptor.node.handle_tx_complete(node_id_initiator, &tx_complete); + + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + check_added_monitors(acceptor, 1); + let tx_abort = if let MessageSendEvent::SendTxAbort { msg, .. } = &msg_events[0] { + msg + } else { + panic!("Unexpected event {:?}", msg_events[0]); + }; + let update = if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[1] { + updates + } else { + panic!("Unexpected event {:?}", msg_events[1]); + }; + + initiator.node.handle_tx_abort(node_id_acceptor, tx_abort); + expect_splice_failed_events( + initiator, + &channel_id, + funding_contribution, + NegotiationFailureReason::CounterpartyAborted { + msg: UntrustedString( + "Total value of outputs exceeds total value of inputs".to_string(), + ), + }, + ); + + let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor); + acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort); + + initiator.node.handle_update_add_htlc(node_id_acceptor, &update.update_add_htlcs[0]); + do_commitment_signed_dance(initiator, acceptor, &update.commitment_signed, false, false); +} + +#[test] +fn free_holding_cell_on_tx_signatures_quiescence_exit() { + do_test_free_holding_cell_on_tx_signatures_quiescence_exit(true); + do_test_free_holding_cell_on_tx_signatures_quiescence_exit(false); +} + +#[cfg(test)] +fn do_test_free_holding_cell_on_tx_signatures_quiescence_exit(update_from_initiator: bool) { + // Test that if there's an update in the holding cell while we're quiescent, that it gets freed + // upon exiting quiescence via the `tx_signatures` exchange. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = test_default_channel_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + if !update_from_initiator { + // Give the acceptor enough balance to queue the mirrored outbound HTLC. + send_payment(initiator, &[acceptor], 2_000_000); + provide_utxo_reserves(&nodes, 2, Amount::ONE_BTC); + } + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: initiator.wallet_source.get_change_script().unwrap(), + }]; + let initiator_contribution = + initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + if update_from_initiator { + negotiate_splice_tx(initiator, acceptor, channel_id, initiator_contribution); + } else { + // Make the acceptor the second signer so receiving the initiator's `tx_signatures` causes it + // to send both its own `tx_signatures` and the commitment update held during quiescence. + let acceptor_contribution = + initiate_splice_in(acceptor, initiator, channel_id, Amount::from_sat(200_000)); + let stfu_initiator = + get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + let stfu_acceptor = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + acceptor.node.handle_stfu(node_id_initiator, &stfu_initiator); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + initiator.node.handle_stfu(node_id_acceptor, &stfu_acceptor); + + let splice_init = + get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let splice_ack = + get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator); + initiator.node.handle_splice_ack(node_id_acceptor, &splice_ack); + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + complete_interactive_funding_negotiation_for_both( + initiator, + acceptor, + channel_id, + initiator_contribution, + Some(acceptor_contribution), + splice_ack.funding_contribution_satoshis, + new_funding_script, + ); + } + + // Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence. + let (update_sender, update_recipient) = + if update_from_initiator { (initiator, acceptor) } else { (acceptor, initiator) }; + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(update_sender, update_recipient, 1_000_000); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); + let payment_id = PaymentId(payment_hash.0); + update_sender.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + assert!(update_sender.node.get_and_clear_pending_msg_events().is_empty()); + + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); + } else { + unreachable!(); + } + + let update = get_htlc_update_msgs(initiator, &node_id_acceptor); + acceptor.node.handle_commitment_signed(node_id_initiator, &update.commitment_signed[0]); + if !update_from_initiator { + // The acceptor's initial commitment_signed is buffered until it signs its contributed input. + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + let event = get_event!(acceptor, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + let partially_signed_tx = acceptor.wallet_source.sign_tx(unsigned_transaction).unwrap(); + acceptor + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); + } else { + unreachable!(); + } + } + check_added_monitors(acceptor, 1); + + let acceptor_msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!( + acceptor_msg_events.len(), + if update_from_initiator { 2 } else { 1 }, + "{acceptor_msg_events:?}" + ); + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &acceptor_msg_events[0] { + assert!(updates.update_add_htlcs.is_empty()); + assert_eq!(updates.commitment_signed.len(), 1); + let commitment_signed = &updates.commitment_signed[0]; + initiator.node.handle_commitment_signed(node_id_acceptor, commitment_signed); + check_added_monitors(&initiator, 1); + } else { + panic!("Unexpected event {:?}", &acceptor_msg_events[0]); + } + + let expect_tx_signatures_then_htlc_update = |msg_events: &[MessageSendEvent]| match msg_events { + [MessageSendEvent::SendTxSignatures { .. }, MessageSendEvent::UpdateHTLCs { updates, .. }] => + { + assert_eq!(updates.update_add_htlcs.len(), 1); + assert_eq!(updates.commitment_signed.len(), 2); + }, + _ => panic!("Unexpected events {msg_events:?}"), + }; + if update_from_initiator { + if let MessageSendEvent::SendTxSignatures { node_id, ref msg } = &acceptor_msg_events[1] { + assert_eq!(*node_id, node_id_initiator); + initiator.node.handle_tx_signatures(node_id_acceptor, msg); + } else { + panic!("Unexpected event {:?}", &acceptor_msg_events[1]); + } + + // With `tx_signatures` exchanged, we've exited quiescence and should now see the outgoing + // HTLC update be sent. + let initiator_msg_events = initiator.node.get_and_clear_pending_msg_events(); + check_added_monitors(initiator, 1); // Outgoing HTLC monitor update + expect_tx_signatures_then_htlc_update(&initiator_msg_events); + } else { + let initiator_tx_signatures = + get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, node_id_acceptor); + acceptor.node.handle_tx_signatures(node_id_initiator, &initiator_tx_signatures); + + let acceptor_msg_events = acceptor.node.get_and_clear_pending_msg_events(); + check_added_monitors(acceptor, 1); // Outgoing HTLC monitor update + expect_tx_signatures_then_htlc_update(&acceptor_msg_events); + } + + // If the messages are dropped and the peers reconnect, the `tx_signatures` need to be + // retransmitted before the freed holding-cell update so the peer can leave quiescence before + // handling normal commitment updates. + initiator.node.peer_disconnected(node_id_acceptor); + acceptor.node.peer_disconnected(node_id_initiator); + let mut reconnect_args = ReconnectArgs::new(initiator, acceptor); + if update_from_initiator { + reconnect_args.send_announcement_sigs = (true, true); + reconnect_args.send_interactive_tx_sigs = (false, true); + reconnect_args.pending_htlc_adds = (0, 1); + } else { + reconnect_args.send_interactive_tx_sigs = (true, false); + reconnect_args.pending_htlc_adds = (1, 0); + } + reconnect_nodes(reconnect_args); + + expect_splice_pending_event(initiator, &node_id_acceptor); + if !update_from_initiator { + expect_splice_pending_event(acceptor, &node_id_initiator); + } +} + +#[test] +fn fail_splice_on_channel_close() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let _node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC); + + let splice_in_amount = initial_channel_capacity / 2; + + // Close the channel before completion of interactive-tx construction. + let _ = initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount)); + let _ = complete_splice_handshake(initiator, acceptor); + let _tx_add_input = + get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); + + initiator + .node + .force_close_broadcasting_latest_txn(&channel_id, &node_id_acceptor, "test".to_owned()) + .unwrap(); + handle_bump_events(initiator, true, 0); + check_closed_events( + &nodes[0], + &[ExpectedCloseEvent { + channel_id: Some(channel_id), + discard_funding: true, + splice_failed: true, + channel_funding_txo: None, + user_channel_id: Some(42), + ..Default::default() + }], + ); + check_closed_broadcast(&nodes[0], 1, true); + check_added_monitors(&nodes[0], 1); +} + +#[test] +fn fail_quiescent_action_on_channel_close() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + + let _node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + let splice_in_amount = initial_channel_capacity / 2; + + provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC); + + // Close the channel before completion of STFU handshake. + let _ = initiate_splice_in(initiator, acceptor, channel_id, Amount::from_sat(splice_in_amount)); + + let _stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + + initiator + .node + .force_close_broadcasting_latest_txn(&channel_id, &node_id_acceptor, "test".to_owned()) + .unwrap(); + handle_bump_events(initiator, true, 0); + check_closed_events( + &nodes[0], + &[ExpectedCloseEvent { + channel_id: Some(channel_id), + discard_funding: true, + splice_failed: true, + channel_funding_txo: None, + user_channel_id: Some(42), + ..Default::default() + }], + ); + check_closed_broadcast(&nodes[0], 1, true); + check_added_monitors(&nodes[0], 1); +} + +#[test] +fn abandon_splice_quiescent_action_on_shutdown() { + do_abandon_splice_quiescent_action_on_shutdown(true, false); + do_abandon_splice_quiescent_action_on_shutdown(false, false); + do_abandon_splice_quiescent_action_on_shutdown(true, true); + do_abandon_splice_quiescent_action_on_shutdown(false, true); +} + +#[cfg(test)] +fn do_abandon_splice_quiescent_action_on_shutdown(local_shutdown: bool, pending_splice: bool) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + provide_utxo_reserves(&nodes, 1, Amount::ONE_BTC); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_capacity = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + + // When testing with a prior pending splice, complete splice A first so that + // `splice_funding_failed_for` filters against `pending_splice.contributed_inputs/outputs`. + if pending_splice { + let funding_contribution = do_initiate_splice_in( + &nodes[0], + &nodes[1], + channel_id, + Amount::from_sat(initial_channel_capacity / 2), + ); + let (_splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + } + + // Since we cannot close after having sent `stfu`, send an HTLC so that when we attempt to + // splice, the `stfu` message is held back. + let payment_amount = 1_000_000; + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(&nodes[0], &nodes[1], payment_amount); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); + let payment_id = PaymentId(payment_hash.0); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + let update = get_htlc_update_msgs(&nodes[0], &node_id_1); + check_added_monitors(&nodes[0], 1); + + nodes[1].node.handle_update_add_htlc(node_id_0, &update.update_add_htlcs[0]); + // After a splice, commitment_signed messages are batched across funding scopes. + nodes[1].node.handle_commitment_signed_batch_test(node_id_0, &update.commitment_signed); + check_added_monitors(&nodes[1], 1); + let (revoke_and_ack, _) = get_revoke_commit_msgs(&nodes[1], &node_id_0); + + nodes[0].node.handle_revoke_and_ack(node_id_1, &revoke_and_ack); + check_added_monitors(&nodes[0], 1); + + // Attempt the splice. `stfu` should not go out yet as the state machine is pending. + // When there's a prior splice, include a splice-out output with a different script_pubkey + // so the test can verify selective filtering: the change output (same script_pubkey as + // the prior splice) is filtered, while the splice-out output (different script_pubkey) + // survives. + let splice_in_amount = + if pending_splice { initial_channel_capacity / 4 } else { initial_channel_capacity / 2 }; + let splice_out_output = if pending_splice { + let script_pubkey = nodes[1].wallet_source.get_change_script().unwrap(); + Some(TxOut { value: Amount::from_sat(1_000), script_pubkey }) + } else { + None + }; + let funding_contribution = if let Some(ref output) = splice_out_output { + initiate_splice_in_and_out( + &nodes[0], + &nodes[1], + channel_id, + Amount::from_sat(splice_in_amount), + vec![output.clone()], + ) + } else { + initiate_splice_in(&nodes[0], &nodes[1], channel_id, Amount::from_sat(splice_in_amount)) + }; + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Close the channel. We should see a `SpliceNegotiationFailed` event for the pending splice + // `QuiescentAction`. + let (closer_node, closee_node) = + if local_shutdown { (&nodes[0], &nodes[1]) } else { (&nodes[1], &nodes[0]) }; + let closer_node_id = closer_node.node.get_our_node_id(); + let closee_node_id = closee_node.node.get_our_node_id(); + + closer_node.node.close_channel(&channel_id, &closee_node_id).unwrap(); + let shutdown = get_event_msg!(closer_node, MessageSendEvent::SendShutdown, closee_node_id); + closee_node.node.handle_shutdown(closer_node_id, &shutdown); + + if pending_splice { + // With a prior pending splice, contributions are filtered against committed inputs/outputs. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2, "{events:?}"); + match &events[0] { + Event::DiscardFunding { + funding_info: FundingInfo::Contribution { inputs, outputs }, + .. + } => { + // The UTXO was filtered: it's still committed to the prior splice. + assert!(inputs.is_empty(), "Expected empty inputs (filtered), got {:?}", inputs); + // The change output was filtered (same script_pubkey as the prior splice's + // change output), but the splice-out output survives (different script_pubkey). + let expected_outputs: Vec<_> = + splice_out_output.into_iter().map(|output| output.script_pubkey).collect(); + assert_eq!(*outputs, expected_outputs); + }, + other => panic!("Expected DiscardFunding with Contribution, got {:?}", other), + } + match &events[1] { + Event::SpliceNegotiationFailed { channel_id: cid, reason, contribution, .. } => { + assert_eq!(*cid, channel_id); + assert_eq!(*reason, NegotiationFailureReason::ChannelClosing); + assert!(contribution.is_some()); + }, + other => panic!("Expected SpliceNegotiationFailed, got {:?}", other), + } + } else { + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::ChannelClosing, + ); + } + let _ = get_event_msg!(closee_node, MessageSendEvent::SendShutdown, closer_node_id); +} + +#[cfg(test)] +fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forward: bool) { + // Test that we are still able to forward and resolve HTLCs while the original SCIDs contained + // in the onion packets have now changed due channel splices becoming locked. + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_config.cltv_expiry_delta = CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY as u16 * 2; + let node_chanmgrs = create_node_chanmgrs( + 3, + &node_cfgs, + &[Some(config.clone()), Some(config.clone()), Some(config)], + ); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + let node_id_2 = nodes[2].node.get_our_node_id(); + + let (_, _, channel_id_0_1, _) = create_announced_chan_between_nodes(&nodes, 0, 1); + let (chan_upd_1_2, _, channel_id_1_2, _) = create_announced_chan_between_nodes(&nodes, 1, 2); + + let node_max_height = + nodes.iter().map(|node| node.blocks.lock().unwrap().len()).max().unwrap() as u32; + connect_blocks(&nodes[0], node_max_height - nodes[0].best_block_info().1); + connect_blocks(&nodes[1], node_max_height - nodes[1].best_block_info().1); + connect_blocks(&nodes[2], node_max_height - nodes[2].best_block_info().1); + + // Send an outbound HTLC from node 0 to 2. + let payment_amount = 1_000_000; + let payment_params = + PaymentParameters::from_node_id(node_id_2, CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY * 2) + .with_bolt11_features(nodes[2].node.bolt11_invoice_features()) + .unwrap(); + let route_params = + RouteParameters::from_payment_params_and_value(payment_params, payment_amount); + let route = get_route(&nodes[0], &route_params).unwrap(); + let (_, payment_hash, payment_secret) = + get_payment_preimage_hash(&nodes[2], Some(payment_amount), None); + let onion = RecipientOnionFields::secret_only(payment_secret, payment_amount); + let id = PaymentId(payment_hash.0); + nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); + check_added_monitors(&nodes[0], 1); + + // Node 1 should now have a pending HTLC to forward to 2. + let update_add_0_1 = get_htlc_update_msgs(&nodes[0], &node_id_1); + nodes[1].node.handle_update_add_htlc(node_id_0, &update_add_0_1.update_add_htlcs[0]); + let commitment = &update_add_0_1.commitment_signed; + do_commitment_signed_dance(&nodes[1], &nodes[0], commitment, false, false); + assert!(nodes[1].node.needs_pending_htlc_processing()); + + // Splice both channels, lock them, and connect enough blocks to trigger the legacy SCID pruning + // logic while the HTLC is still pending. + let outputs_0_1 = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id_0_1, outputs_0_1).unwrap(); + let (splice_tx_0_1, _) = splice_channel(&nodes[0], &nodes[1], channel_id_0_1, contribution); + for node in &nodes { + mine_transaction(node, &splice_tx_0_1); + } + + let outputs_1_2 = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }]; + let contribution = + initiate_splice_out(&nodes[1], &nodes[2], channel_id_1_2, outputs_1_2).unwrap(); + let (splice_tx_1_2, _) = splice_channel(&nodes[1], &nodes[2], channel_id_1_2, contribution); + for node in &nodes { + mine_transaction(node, &splice_tx_1_2); + } + + for node in &nodes { + connect_blocks(node, ANTI_REORG_DELAY - 2); + } + let splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + lock_splice(&nodes[0], &nodes[1], &splice_locked, false, &[]); + + for node in &nodes { + connect_blocks(node, 1); + } + let splice_locked = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceLocked, node_id_2); + lock_splice(&nodes[1], &nodes[2], &splice_locked, false, &[]); + + if expire_scid_pre_forward { + for node in &nodes { + connect_blocks(node, CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY); + } + + // Now attempt to forward the HTLC from node 1 to 2 which will fail because the SCID is no + // longer stored and has expired. Obviously this is somewhat of an absurd case - not + // forwarding for `CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY` blocks is kinda nuts. + let fail_type = HTLCHandlingFailureType::InvalidForward { + requested_forward_scid: chan_upd_1_2.contents.short_channel_id, + }; + expect_htlc_forwarding_fails(&nodes[1], &[fail_type]); + check_added_monitors(&nodes[1], 1); + let update_fail_1_0 = get_htlc_update_msgs(&nodes[1], &node_id_0); + nodes[0].node.handle_update_fail_htlc(node_id_1, &update_fail_1_0.update_fail_htlcs[0]); + let commitment = &update_fail_1_0.commitment_signed; + do_commitment_signed_dance(&nodes[0], &nodes[1], commitment, false, false); + + let conditions = PaymentFailedConditions::new(); + expect_payment_failed_conditions(&nodes[0], payment_hash, false, conditions); + } else { + // Now attempt to forward the HTLC from node 1 to 2. + nodes[1].node.process_pending_htlc_forwards(); + check_added_monitors(&nodes[1], 1); + let update_add_1_2 = get_htlc_update_msgs(&nodes[1], &node_id_2); + nodes[2].node.handle_update_add_htlc(node_id_1, &update_add_1_2.update_add_htlcs[0]); + let commitment = &update_add_1_2.commitment_signed; + do_commitment_signed_dance(&nodes[2], &nodes[1], commitment, false, false); + assert!(nodes[2].node.needs_pending_htlc_processing()); + + // Node 2 should see the claimable payment. Fail it back to make sure we also handle the SCID + // change on the way back. + nodes[2].node.process_pending_htlc_forwards(); + expect_payment_claimable!(&nodes[2], payment_hash, payment_secret, payment_amount); + nodes[2].node.fail_htlc_backwards(&payment_hash); + let fail_type = HTLCHandlingFailureType::Receive { payment_hash }; + expect_and_process_pending_htlcs_and_htlc_handling_failed(&nodes[2], &[fail_type]); + check_added_monitors(&nodes[2], 1); + + let update_fail_1_2 = get_htlc_update_msgs(&nodes[2], &node_id_1); + nodes[1].node.handle_update_fail_htlc(node_id_2, &update_fail_1_2.update_fail_htlcs[0]); + let commitment = &update_fail_1_2.commitment_signed; + do_commitment_signed_dance(&nodes[1], &nodes[2], commitment, false, false); + let fail_type = HTLCHandlingFailureType::Forward { + node_id: Some(node_id_2), + channel_id: channel_id_1_2, + }; + expect_and_process_pending_htlcs_and_htlc_handling_failed(&nodes[1], &[fail_type]); + check_added_monitors(&nodes[1], 1); + + let update_fail_0_1 = get_htlc_update_msgs(&nodes[1], &node_id_0); + nodes[0].node.handle_update_fail_htlc(node_id_1, &update_fail_0_1.update_fail_htlcs[0]); + let commitment = &update_fail_0_1.commitment_signed; + do_commitment_signed_dance(&nodes[0], &nodes[1], commitment, false, false); + + let conditions = PaymentFailedConditions::new(); + expect_payment_failed_conditions(&nodes[0], payment_hash, true, conditions); + } +} + +#[test] +fn test_splice_with_inflight_htlc_forward_and_resolution() { + do_test_splice_with_inflight_htlc_forward_and_resolution(true); + do_test_splice_with_inflight_htlc_forward_and_resolution(false); +} + +#[test] +fn test_splice_buffer_commitment_signed_until_funding_tx_signed() { + // Test that when the counterparty sends their initial `commitment_signed` before the user has + // called `funding_transaction_signed`, we buffer the message and process it at the end of + // `funding_transaction_signed`. This allows the user to cancel the splice negotiation if + // desired without having queued an irreversible monitor update. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + // Negotiate a splice-out where only the initiator (node 0) has a contribution. + // This means node 1 will send their commitment_signed immediately after tx_complete. + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let initiator_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); + + // Node 0 (initiator with contribution) should have a signing event to handle. + let signing_event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + + // Node 1 (acceptor with no contribution) won't have a signing event and will immediately + // send their initial commitment_signed. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + let acceptor_commit_sig = get_htlc_update_msgs(&nodes[1], &node_id_0); + + // Deliver the acceptor's commitment_signed to the initiator BEFORE the initiator has called + // funding_transaction_signed. The message should be buffered, not processed. + nodes[0].node.handle_commitment_signed(node_id_1, &acceptor_commit_sig.commitment_signed[0]); + + // No monitor update should have happened since the message is buffered. + check_added_monitors(&nodes[0], 0); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Now handle the signing event and call `funding_transaction_signed`. + if let Event::FundingTransactionReadyForSigning { + channel_id: event_channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = signing_event + { + assert_eq!(event_channel_id, channel_id); + assert_eq!(counterparty_node_id, node_id_1); + + let partially_signed_tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[0] + .node + .funding_transaction_signed(&channel_id, &node_id_1, partially_signed_tx) + .unwrap(); + } else { + panic!("Expected FundingTransactionReadyForSigning event"); + } + + // After funding_transaction_signed: + // 1. The initiator should send their commitment_signed + // 2. The buffered commitment_signed from the acceptor should be processed (monitor update) + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + let initiator_commit_sig = + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { + updates.commitment_signed[0].clone() + } else { + panic!("Expected UpdateHTLCs message"); + }; + + // The buffered commitment_signed should have been processed, resulting in a monitor update. + check_added_monitors(&nodes[0], 1); + + // Complete the rest of the flow normally. + nodes[1].node.handle_commitment_signed(node_id_0, &initiator_commit_sig); + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[0] { + nodes[0].node.handle_tx_signatures(node_id_1, msg); + } else { + panic!("Expected SendTxSignatures message"); + } + check_added_monitors(&nodes[1], 1); + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[0] { + nodes[1].node.handle_tx_signatures(node_id_0, msg); + } else { + panic!("Expected SendTxSignatures message"); + } + + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // Both nodes should broadcast the splice transaction. + let splice_tx = { + let mut txn_0 = nodes[0].tx_broadcaster.txn_broadcast(); + assert_eq!(txn_0.len(), 1); + let txn_1 = nodes[1].tx_broadcaster.txn_broadcast(); + assert_eq!(txn_0, txn_1); + txn_0.remove(0) + }; + + // Verify the channel is operational by sending a payment. + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + + // Lock the splice by confirming the transaction. + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + // Verify the channel is still operational by sending another payment. + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn test_splice_buffer_invalid_commitment_signed_closes_channel() { + // Test that when the counterparty sends an invalid `commitment_signed` (with a bad signature) + // before the user has called `funding_transaction_signed`, the channel is closed with an error + // when `ChannelManager::funding_transaction_signed` processes the buffered message. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + // Negotiate a splice-out where only the initiator (node 0) has a contribution. + // This means node 1 will send their commitment_signed immediately after tx_complete. + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let initiator_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); + + // Node 0 (initiator with contribution) should have a signing event to handle. + let signing_event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + + // Node 1 (acceptor with no contribution) won't have a signing event and will immediately + // send their initial commitment_signed. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + let mut acceptor_commit_sig = get_htlc_update_msgs(&nodes[1], &node_id_0); + + // Invalidate the signature by modifying one byte. This will cause signature verification + // to fail when the buffered message is processed. + let original_sig = acceptor_commit_sig.commitment_signed[0].signature; + let mut sig_bytes = original_sig.serialize_compact(); + sig_bytes[0] ^= 0x01; // Flip a bit to corrupt the signature + acceptor_commit_sig.commitment_signed[0].signature = + Signature::from_compact(&sig_bytes).unwrap(); + + // Deliver the acceptor's invalid commitment_signed to the initiator BEFORE the initiator has + // called funding_transaction_signed. The message should be buffered, not processed. + nodes[0].node.handle_commitment_signed(node_id_1, &acceptor_commit_sig.commitment_signed[0]); + + // No monitor update should have happened since the message is buffered. + check_added_monitors(&nodes[0], 0); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Now handle the signing event and call `funding_transaction_signed`. + // This should process the buffered invalid commitment_signed and close the channel. + if let Event::FundingTransactionReadyForSigning { + channel_id: event_channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = signing_event + { + assert_eq!(event_channel_id, channel_id); + assert_eq!(counterparty_node_id, node_id_1); + + let partially_signed_tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[0] + .node + .funding_transaction_signed(&channel_id, &node_id_1, partially_signed_tx) + .unwrap(); + } else { + panic!("Expected FundingTransactionReadyForSigning event"); + } + + // After funding_transaction_signed: + // 1. The initiator sends its commitment_signed (UpdateHTLCs message). + // 2. The buffered invalid commitment_signed from the acceptor is processed, causing the + // channel to close due to the invalid signature. + // We expect 3 message events: UpdateHTLCs, BroadcastChannelUpdate, and HandleError. + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 3, "{msg_events:?}"); + match &msg_events[0] { + MessageSendEvent::UpdateHTLCs { ref updates, .. } => { + assert!(!updates.commitment_signed.is_empty()); + }, + _ => panic!("Expected UpdateHTLCs message, got {:?}", msg_events[0]), + } + match &msg_events[1] { + MessageSendEvent::HandleError { + action: msgs::ErrorAction::SendErrorMessage { ref msg }, + .. + } => { + assert!(msg.data.contains("Invalid commitment tx signature from peer")); + }, + _ => panic!("Expected HandleError with SendErrorMessage, got {:?}", msg_events[1]), + } + match &msg_events[2] { + MessageSendEvent::BroadcastChannelUpdate { ref msg, .. } => { + assert_eq!(msg.contents.channel_flags & 2, 2); + }, + _ => panic!("Expected BroadcastChannelUpdate, got {:?}", msg_events[2]), + } + + let err = "Invalid commitment tx signature from peer".to_owned(); + let reason = ClosureReason::ProcessingError { err }; + check_closed_events( + &nodes[0], + &[ExpectedCloseEvent::from_id_reason(channel_id, false, reason)], + ); + check_added_monitors(&nodes[0], 1); +} + +#[test] +fn test_splice_waits_for_initial_commitment_monitor_update_before_releasing_tx_signatures() { + do_splice_waits_for_initial_commitment_monitor_update_before_releasing_tx_signatures(false); + do_splice_waits_for_initial_commitment_monitor_update_before_releasing_tx_signatures(true); +} + +#[cfg(test)] +fn do_splice_waits_for_initial_commitment_monitor_update_before_releasing_tx_signatures( + complete_update_while_disconnected: bool, +) { + // Test that if processing the counterparty's initial `commitment_signed` returns + // `ChannelMonitorUpdateStatus::InProgress`, we do not release our `tx_signatures` when their + // `tx_signatures` is received. We should only release ours once the monitor update completes. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let initiator_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, outputs).unwrap(); + negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); + + let signing_event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id: event_channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = signing_event + { + assert_eq!(event_channel_id, channel_id); + assert_eq!(counterparty_node_id, node_id_1); + + let partially_signed_tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[0] + .node + .funding_transaction_signed(&channel_id, &node_id_1, partially_signed_tx) + .unwrap(); + } else { + panic!("Expected FundingTransactionReadyForSigning event"); + } + + let initiator_commit_sig = get_htlc_update_msgs(&nodes[0], &node_id_1); + nodes[1].node.handle_commitment_signed(node_id_0, &initiator_commit_sig.commitment_signed[0]); + check_added_monitors(&nodes[1], 1); + + // Leave the monitor update for node 0's processing of the initial `commitment_signed` pending. + chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + let counterparty_commit_sig = + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { + updates.commitment_signed[0].clone() + } else { + panic!("Expected UpdateHTLCs message"); + }; + let counterparty_tx_signatures = + if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[1] { + msg.clone() + } else { + panic!("Expected SendTxSignatures message"); + }; + + nodes[0].node.handle_commitment_signed(node_id_1, &counterparty_commit_sig); + check_added_monitors(&nodes[0], 1); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + nodes[0].node.handle_tx_signatures(node_id_1, &counterparty_tx_signatures); + + // We should not send our `tx_signatures` while the monitor update is still in progress. + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Reestablishing before the monitor update completes should still not release `tx_signatures`. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + if complete_update_while_disconnected { + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + } + + nodes[0].chain_monitor.complete_sole_pending_chan_update(&channel_id); + chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + if !complete_update_while_disconnected { + let initiator_tx_signatures = + get_event_msg!(nodes[0], MessageSendEvent::SendTxSignatures, node_id_1); + nodes[1].node.handle_tx_signatures(node_id_0, &initiator_tx_signatures); + } + + expect_splice_pending_event(&nodes[0], &node_id_1); + if !complete_update_while_disconnected { + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + } +} + +#[test] +fn test_monitor_restore_sends_tx_signatures_before_splice_locked() { + // When a 0-conf splice's RenegotiatedFunding monitor update completes asynchronously after + // the counterparty already sent its tx_signatures, restoring the channel releases both our + // tx_signatures and, with the signatures exchange now being complete, our 0-conf splice_locked. + // The tx_signatures must be sent first: the counterparty only learns the new funding txid is + // fully signed upon receiving our tx_signatures, and it will close the channel upon receiving + // splice_locked for a funding txid outside its negotiated candidates. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_limits.trust_own_funding_0conf = true; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + // The channel must be 0-conf so that the splice funding, which inherits the channel's + // minimum depth, locks as soon as tx_signatures are exchanged. + let initial_channel_value_sat = 100_000; + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0); + mine_transaction(&nodes[0], &funding_tx); + mine_transaction(&nodes[1], &funding_tx); + let prev_funding_txid = funding_tx.compute_txid(); + + // Node 1 initiates a splice-in. The shared funding input counts towards the splice + // initiator's contributed input value, so node 0 -- contributing nothing -- will send its + // tx_signatures first, making node 1 the second signer. + provide_utxo_reserves(&nodes, 1, Amount::from_sat(100_000)); + let splice_in_sat = Amount::from_sat(50_000); + let funding_contribution = initiate_splice_in(&nodes[1], &nodes[0], channel_id, splice_in_sat); + negotiate_splice_tx(&nodes[1], &nodes[0], channel_id, funding_contribution); + + // Node 1 signs its contributed inputs and sends its commitment_signed for the new funding. + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + let event = get_event!(nodes[1], Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event { + let partially_signed_tx = nodes[1].wallet_source.sign_tx(unsigned_transaction).unwrap(); + nodes[1] + .node + .funding_transaction_signed(&channel_id, &node_id_0, partially_signed_tx) + .unwrap(); + } else { + panic!(); + } + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { + nodes[0].node.handle_commitment_signed(node_id_1, &updates.commitment_signed[0]); + } else { + panic!("Unexpected event {:?}", msg_events[0]); + } + check_added_monitors(&nodes[0], 1); + + // Node 0 contributed no inputs, so it is the first signer: it sends its tx_signatures + // immediately, along with its commitment_signed. + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] { + // Node 1 processes node 0's commitment_signed while its monitor persistence is async, leaving + // the RenegotiatedFunding monitor update in flight. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + nodes[1].node.handle_commitment_signed(node_id_0, &updates.commitment_signed[0]); + check_added_monitors(&nodes[1], 1); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + } else { + panic!("Unexpected event {:?}", msg_events[0]); + } + if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[1] { + // Node 1 receives node 0's tx_signatures while the monitor update is still in flight. Its + // responding tx_signatures (and everything resulting from the completed exchange) must be + // withheld until the monitor update completes. + nodes[1].node.handle_tx_signatures(node_id_0, msg); + check_added_monitors(&nodes[1], 0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + assert!(nodes[1].tx_broadcaster.txn_broadcast().is_empty()); + } else { + panic!("Unexpected event {:?}", msg_events[1]); + } + + // Complete the monitor update. Node 1 now broadcasts the splice transaction and releases its + // tx_signatures. With both sides' signatures in hand and a 0-conf splice, it also generates + // splice_locked. + nodes[1].chain_monitor.complete_sole_pending_chan_update(&channel_id); + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + expect_splice_pending_event(&nodes[1], &node_id_0); + let txn = nodes[1].tx_broadcaster.txn_broadcast(); + assert_eq!(txn.len(), 1, "{txn:?}"); + let splice_tx = txn[0].clone(); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::SendTxSignatures { msg, .. } = &msg_events[0] { + nodes[0].node.handle_tx_signatures(node_id_1, msg); + } else { + panic!("Unexpected event {:?}", msg_events[0]); + } + if let MessageSendEvent::SendSpliceLocked { msg, .. } = &msg_events[1] { + nodes[0].node.handle_splice_locked(node_id_1, msg); + } else { + panic!("Unexpected event {:?}", msg_events[1]); + } + + // Node 0's signing session completed upon receiving node 1's tx_signatures: node 0 broadcasts + // the splice transaction and sends its own 0-conf splice_locked. Node 1's splice_locked then + // promotes the splice funding on node 0. + let txn = nodes[0].tx_broadcaster.txn_broadcast(); + assert!(!txn.is_empty()); + assert!(txn.iter().all(|tx| *tx == splice_tx), "{txn:?}"); + expect_channel_ready_event(&nodes[0], &node_id_1); + check_added_monitors(&nodes[0], 1); + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::SendSpliceLocked { ref msg, .. } = msg_events[0] { + nodes[1].node.handle_splice_locked(node_id_0, msg); + } else { + panic!("Unexpected event {:?}", msg_events[0]); + } + expect_channel_ready_event(&nodes[1], &node_id_0); + check_added_monitors(&nodes[1], 1); + let txn = nodes[1].tx_broadcaster.txn_broadcast(); + assert!(txn.iter().all(|tx| *tx == splice_tx), "{txn:?}"); + + // The old funding is no longer tracked once the splice is locked on both sides. + nodes[0].chain_source.remove_watched_by_txid(prev_funding_txid); + nodes[1].chain_source.remove_watched_by_txid(prev_funding_txid); + + // The channel remains usable over the new funding. + send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn retransmit_completed_tx_signatures_during_monitor_update_after_reestablish() { + // Test that splice `tx_signatures` owed to our peer are retransmitted on reestablish even if + // an unrelated monitor update is still in flight. The signature exchange already completed + // locally, so retransmitting the signatures does not depend on the pending monitor update and + // allows our peer to exit quiescence before the held commitment update is restored. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let config = test_default_channel_config(); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: initiator.wallet_source.get_change_script().unwrap(), + }]; + let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + negotiate_splice_tx(initiator, acceptor, channel_id, contribution); + + // Queue an outgoing HTLC to the holding cell. It should be freed once we exit quiescence. + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(initiator, acceptor, 1_000_000); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); + let payment_id = PaymentId(payment_hash.0); + initiator.node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { + channel_id, + counterparty_node_id, + unsigned_transaction, + .. + } = event + { + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &counterparty_node_id, partially_signed_tx) + .unwrap(); + } else { + unreachable!(); + } + + let update = get_htlc_update_msgs(initiator, &node_id_acceptor); + acceptor.node.handle_commitment_signed(node_id_initiator, &update.commitment_signed[0]); + check_added_monitors(&acceptor, 1); + + // The acceptor sends `tx_signatures` first since it contributed no inputs. + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { + let commitment_signed = &updates.commitment_signed[0]; + initiator.node.handle_commitment_signed(node_id_acceptor, commitment_signed); + check_added_monitors(&initiator, 1); + } else { + panic!("Unexpected event {:?}", &msg_events[0]); + } + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + + // Handle the acceptor's `tx_signatures` while the initiator's monitor persistence is async. + // This completes the exchange atomically: the initiator releases its `tx_signatures` and + // exits quiescence, freeing the holding cell HTLC, which itself results in a new monitor + // update that remains in flight. + chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + let splice_txid = + if let MessageSendEvent::SendTxSignatures { node_id, ref msg } = &msg_events[1] { + assert_eq!(*node_id, node_id_initiator); + initiator.node.handle_tx_signatures(node_id_acceptor, msg); + msg.tx_hash + } else { + panic!("Unexpected event {:?}", &msg_events[1]); + }; + check_added_monitors(&initiator, 1); + expect_splice_pending_event(initiator, &node_id_acceptor); + + // The initiator's `tx_signatures` goes out immediately, but the freed holding cell update is + // withheld while the monitor update is in flight. Drop the `tx_signatures` (lost in + // transit), such that the initiator owes the acceptor both its `tx_signatures` and a + // commitment update. + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::SendTxSignatures { node_id, ref msg } = &msg_events[0] { + assert_eq!(*node_id, node_id_acceptor); + assert_eq!(msg.tx_hash, splice_txid); + } else { + panic!("Unexpected event {:?}", &msg_events[0]); + } + + // Reconnect while the initiator's monitor update is still in flight. The acceptor's + // signing session is incomplete, so its `channel_reestablish` causes the initiator to + // retransmit its completed exchange's `tx_signatures` immediately. The normal commitment + // update remains withheld by the in-flight monitor update. + initiator.node.peer_disconnected(node_id_acceptor); + acceptor.node.peer_disconnected(node_id_initiator); + let mut reconnect_args = ReconnectArgs::new(acceptor, initiator); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_args.send_interactive_tx_sigs = (true, false); + reconnect_nodes(reconnect_args); + check_added_monitors(acceptor, 0); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + assert!(initiator.node.get_and_clear_pending_msg_events().is_empty()); + + // Once the monitor update completes, only the freed holding cell update remains to be sent. + initiator.chain_monitor.complete_sole_pending_chan_update(&channel_id); + chanmon_cfgs[0].persister.set_update_ret(ChannelMonitorUpdateStatus::Completed); + + let msg_events = initiator.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] { + acceptor.node.handle_update_add_htlc(node_id_initiator, &updates.update_add_htlcs[0]); + do_commitment_signed_dance(acceptor, initiator, &updates.commitment_signed, false, false); + } else { + panic!("Unexpected event {:?}", &msg_events[0]); + } +} + +#[test] +fn test_splice_balance_falls_below_reserve() { + // Test that we're able to proceed with a splice where the acceptor does not contribute + // anything, but the initiator does, resulting in an increased channel reserve that the + // counterparty does not meet but is still valid. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initial_channel_value_sat = 100_000; + // Push 10k sat to node 1 so it has balance to send HTLCs back. + let push_msat = 10_000_000; + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + initial_channel_value_sat, + push_msat, + ); + + let _ = provide_anchor_reserves(&nodes); + + // Create bidirectional pending HTLCs (routed but not claimed). + // Outbound HTLC from node 0 to node 1. + let (preimage_0_to_1, _hash_0_to_1, ..) = route_payment(&nodes[0], &[&nodes[1]], 1_000_000); + // Large inbound HTLC from node 1 to node 0, bringing node 1's remaining balance down to + // 2000 sat. The old reserve (1% of 100k) is 1000 sat so this is still above reserve. + let (preimage_1_to_0, _hash_1_to_0, ..) = route_payment(&nodes[1], &[&nodes[0]], 8_000_000); + + // Splice-in 200k sat. The new channel value becomes 300k sat, raising the reserve to 3000 + // sat. Node 1's remaining 2000 sat is now below the new reserve. + let initiator_contribution = + initiate_splice_in(&nodes[0], &nodes[1], channel_id, Amount::from_sat(200_000)); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, initiator_contribution); + + // Confirm and lock the splice. + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + // Claim both pending HTLCs to verify the channel is fully functional after the splice. + claim_payment(&nodes[0], &[&nodes[1]], preimage_0_to_1); + claim_payment(&nodes[1], &[&nodes[0]], preimage_1_to_0); + + // Final sanity check: send a payment using the new spliced capacity. + let _ = send_payment(&nodes[0], &[&nodes[1]], 1_000_000); +} + +#[test] +fn test_funding_contributed_counterparty_not_found() { + // Tests that calling funding_contributed with an unknown counterparty_node_id returns + // ChannelUnavailable and emits a DiscardFunding event. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = + funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap(); + + // Use a fake/unknown public key as counterparty + let fake_node_id = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap()); + + assert_eq!( + nodes[0].node.funding_contributed( + &channel_id, + &fake_node_id, + funding_contribution.clone(), + None + ), + Err(APIError::no_such_peer(&fake_node_id)), + ); + + expect_discard_funding_event(&nodes[0], &channel_id, funding_contribution); +} + +#[test] +fn test_funding_contributed_channel_not_found() { + // Tests that calling funding_contributed with an unknown channel_id returns + // ChannelUnavailable and emits a DiscardFunding event. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 50_000_000); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = + funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap(); + + // Use a random/unknown channel_id + let fake_channel_id = ChannelId::from_bytes([42; 32]); + + assert_eq!( + nodes[0].node.funding_contributed( + &fake_channel_id, + &node_id_1, + funding_contribution.clone(), + None + ), + Err(APIError::no_such_channel_for_peer(&fake_channel_id, &node_id_1)), + ); + + expect_discard_funding_event(&nodes[0], &fake_channel_id, funding_contribution); +} + +#[test] +fn test_funding_contributed_splice_already_pending() { + // Tests that calling funding_contributed when there's already a pending splice + // contribution returns Err(APIMisuseError) and emits a DiscardFunding event containing only the + // inputs/outputs that are NOT already in the existing contribution. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 2, splice_in_amount * 2); + + // Use the contribution builder with an output so we can test output filtering + let first_splice_out = TxOut { + value: Amount::from_sat(5_000), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_raw_hash(Hash::all_zeros())), + }; + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let first_contribution = funding_template + .with_prior_contribution(feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(splice_in_amount) + .unwrap() + .add_output(first_splice_out.clone()) + .build() + .unwrap(); + + // Initiate a second splice with a DIFFERENT output (different script_pubkey) to test that + // non-overlapping outputs are included in DiscardFunding (not filtered out). + let second_splice_out = TxOut { + value: Amount::from_sat(6_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }; + + // Clear UTXOs and add a LARGER one for the second contribution to ensure + // the change output will be different from the first contribution's change + nodes[0].wallet_source.clear_utxos(); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 3); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let second_contribution = funding_template + .without_prior_contribution(feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(splice_in_amount) + .unwrap() + .add_output(second_splice_out.clone()) + .build() + .unwrap(); + + // The change script should remain the same. + assert_eq!( + first_contribution.change_output().map(|output| &output.script_pubkey), + second_contribution.change_output().map(|output| &output.script_pubkey), + ); + let change_script = first_contribution.change_output().unwrap().script_pubkey.clone(); + + // First funding_contributed - this sets up the quiescent action + nodes[0].node.funding_contributed(&channel_id, &node_id_1, first_contribution, None).unwrap(); + + // Drain the pending stfu message + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Second funding_contributed with a different contribution - this should trigger + // DiscardFunding because there's already a pending quiescent action (splice contribution). + // Only inputs/outputs NOT in the existing contribution should be discarded. + let (expected_inputs, mut expected_outputs) = + second_contribution.clone().into_contributed_inputs_and_outputs(); + expected_outputs.retain(|output| *output != change_script); + + // Returns Err(APIMisuseError) and emits DiscardFunding for the non-duplicate parts of the second contribution + assert_eq!( + nodes[0].node.funding_contributed(&channel_id, &node_id_1, second_contribution, None), + Err(APIError::APIMisuseError { + err: format!("Channel {} already has a pending funding contribution", channel_id), + }) + ); + + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + match &events[0] { + Event::DiscardFunding { channel_id: event_channel_id, funding_info } => { + assert_eq!(event_channel_id, &channel_id); + if let FundingInfo::Contribution { inputs, outputs } = funding_info { + // The input is different, so it should be in the discard event + assert_eq!(*inputs, expected_inputs); + // The different output should NOT be filtered out, but the change script should as + // it is the same in both contributions. + assert_eq!(*outputs, expected_outputs); + } else { + panic!("Expected FundingInfo::Contribution"); + } + }, + _ => panic!("Expected DiscardFunding event"), + } +} + +#[test] +fn test_funding_contributed_duplicate_contribution_no_event() { + // Tests that calling funding_contributed with the exact same contribution twice + // returns Err(APIMisuseError) and emits no events on the second call (DoNothing path). + // This tests the case where all inputs/outputs in the second contribution + // are already present in the existing contribution. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let contribution = + funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap(); + + // First funding_contributed - this sets up the quiescent action + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None).unwrap(); + + // Drain the pending stfu message + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Second funding_contributed with the SAME contribution (same inputs/outputs) + // This should trigger the DoNothing path because all inputs/outputs are duplicates. + // Returns Err(APIMisuseError) and emits NO events. + assert_eq!( + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution, None), + Err(APIError::APIMisuseError { + err: format!("Duplicate funding contribution for channel {}", channel_id), + }) + ); + + // Verify no events were emitted - the duplicate contribution is silently ignored + let events = nodes[0].node.get_and_clear_pending_events(); + assert!(events.is_empty(), "Expected no events for duplicate contribution, got {:?}", events); +} + +#[test] +fn test_funding_contributed_active_funding_negotiation() { + do_test_funding_contributed_active_funding_negotiation(0); // AwaitingAck + do_test_funding_contributed_active_funding_negotiation(1); // ConstructingTransaction + do_test_funding_contributed_active_funding_negotiation(2); // AwaitingSignatures +} + +#[cfg(test)] +fn do_test_funding_contributed_active_funding_negotiation(state: u8) { + // Tests that calling funding_contributed when a splice is already being actively negotiated + // (pending_splice.funding_negotiation exists and is_initiator()) returns Err(APIMisuseError) + // and emits SpliceNegotiationFailed + DiscardFunding events for non-duplicate contributions, or + // returns Err(APIMisuseError) with no events for duplicate contributions. + // + // State 0: AwaitingAck (splice_init sent, splice_ack not yet received) + // State 1: ConstructingTransaction (splice handshake complete, interactive TX in progress) + // State 2: AwaitingSignatures (interactive TX complete, awaiting signing) + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 2, splice_in_amount * 2); + + // Build first contribution + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let first_contribution = + funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap(); + + // Build second contribution with different UTXOs and a splice-out output using a different + // script_pubkey (node 1's address) so it survives script_pubkey-based filtering. + nodes[0].wallet_source.clear_utxos(); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 3); + let splice_out_output = TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }; + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let second_contribution = funding_template + .without_prior_contribution(feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(splice_in_amount) + .unwrap() + .add_outputs(vec![splice_out_output.clone()]) + .build() + .unwrap(); + + // The change script should remain the same. + assert_eq!( + first_contribution.change_output().map(|output| &output.script_pubkey), + second_contribution.change_output().map(|output| &output.script_pubkey), + ); + let change_script = first_contribution.change_output().unwrap().script_pubkey.clone(); + + // First funding_contributed - sets up the quiescent action and queues STFU + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, first_contribution.clone(), None) + .unwrap(); + + // Complete the STFU exchange. This consumes the quiescent_action and creates + // FundingNegotiation::AwaitingAck with splice_init queued. + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + // Drain the splice_init from the initiator's pending message events + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + + if state >= 1 { + // Process splice_init/ack to move to ConstructingTransaction + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + if state == 2 { + // Complete interactive TX negotiation to move to AwaitingSignatures + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + first_contribution.clone(), + new_funding_script, + ); + + // Drain the FundingTransactionReadyForSigning event from the initiator + let _ = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + } + } + + // Call funding_contributed with the second contribution. Inputs don't overlap (different + // UTXOs) so they all survive. The splice-out output (different script_pubkey) survives + // while the change output (same script_pubkey as first contribution) is filtered. + let (expected_inputs, mut expected_outputs) = + second_contribution.clone().into_contributed_inputs_and_outputs(); + expected_outputs.retain(|output| *output != change_script); + assert_eq!( + nodes[0].node.funding_contributed(&channel_id, &node_id_1, second_contribution, None), + Err(APIError::APIMisuseError { + err: format!("Channel {} already has a pending funding contribution", channel_id), + }) + ); + + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1, "{events:?}"); + match &events[0] { + Event::DiscardFunding { channel_id: event_channel_id, funding_info } => { + assert_eq!(*event_channel_id, channel_id); + if let FundingInfo::Contribution { inputs, outputs } = funding_info { + // Inputs are unique (different UTXOs) so none are filtered. + assert_eq!(*inputs, expected_inputs); + // Only the splice-out output survives; the change output is filtered + // (same script_pubkey as first contribution's change). + assert_eq!(*outputs, vec![splice_out_output.script_pubkey]); + } else { + panic!("Expected FundingInfo::Contribution"); + } + }, + _ => panic!("Expected DiscardFunding event, got {:?}", events[1]), + } + + // Also test the DoNothing path: call funding_contributed with the same contribution + // as the existing negotiation. All inputs/outputs are duplicates, so no events. + assert_eq!( + nodes[0].node.funding_contributed(&channel_id, &node_id_1, first_contribution, None), + Err(APIError::APIMisuseError { + err: format!("Duplicate funding contribution for channel {}", channel_id), + }) + ); + + let events = nodes[0].node.get_and_clear_pending_events(); + assert!(events.is_empty(), "Expected no events for duplicate contribution, got {:?}", events); + + // Cleanup: drain leftover message events from the in-progress splice negotiation + if state == 1 { + // Initiator has its first interactive TX message queued after handle_splice_ack + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events[0], MessageSendEvent::SendTxAddInput { .. })); + } + if state == 2 { + // Acceptor (no contribution) auto-signed and sent commitment_signed + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events[0], MessageSendEvent::UpdateHTLCs { .. })); + } +} + +#[test] +fn test_funding_contributed_channel_shutdown() { + // Tests that calling funding_contributed after initiating channel shutdown returns Err(APIMisuseError) + // and emits both SpliceNegotiationFailed and DiscardFunding events. The channel is no longer usable + // after shutdown is initiated, so quiescence cannot be proposed. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = + funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap(); + + // Initiate channel shutdown - this makes is_usable() return false + nodes[0].node.close_channel(&channel_id, &node_id_1).unwrap(); + + // Drain the pending shutdown message + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendShutdown, node_id_1); + + // Now call funding_contributed - this should trigger FailSplice because + // propose_quiescence() will fail when is_usable() returns false. + // Returns Err(APIMisuseError) and emits both SpliceNegotiationFailed and DiscardFunding. + assert_eq!( + nodes[0].node.funding_contributed( + &channel_id, + &node_id_1, + funding_contribution.clone(), + None + ), + Err(APIError::APIMisuseError { + err: format!("Channel {} cannot accept funding contribution", channel_id), + }) + ); + + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::ChannelClosing, + ); +} + +#[test] +fn test_funding_contributed_unfunded_channel() { + // Tests that calling funding_contributed on an unfunded channel returns APIMisuseError + // and emits a DiscardFunding event. The channel exists but is not yet funded. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + // Create a funded channel for the splice operation + let (_, _, funded_channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + // Create an unfunded channel (after open/accept but before funding tx) + let unfunded_channel_id = exchange_open_accept_chan(&nodes[0], &nodes[1], 50_000, 0); + + // Drain the FundingGenerationReady event for the unfunded channel + let _ = get_event!(nodes[0], Event::FundingGenerationReady); + + let splice_in_amount = Amount::from_sat(20_000); + provide_utxo_reserves(&nodes, 1, splice_in_amount * 2); + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&funded_channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = + funding_template.splice_in_sync(splice_in_amount, feerate, FeeRate::MAX, &wallet).unwrap(); + + // Call funding_contributed with the unfunded channel's ID instead of the funded one. + // Returns APIMisuseError because the channel is not funded. + assert_eq!( + nodes[0].node.funding_contributed( + &unfunded_channel_id, + &node_id_1, + funding_contribution.clone(), + None + ), + Err(APIError::APIMisuseError { + err: format!( + "Channel with id {} not expecting funding contribution", + unfunded_channel_id + ), + }) + ); + + expect_discard_funding_event(&nodes[0], &unfunded_channel_id, funding_contribution); +} + +#[test] +fn test_splice_pending_htlcs() { + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + do_test_splice_pending_htlcs(config); + + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + do_test_splice_pending_htlcs(config); + + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + do_test_splice_pending_htlcs(config); +} + +#[cfg(test)] +fn do_test_splice_pending_htlcs(config: UserConfig) { + // Test balance checks for inbound and outbound splice-outs while there are pending HTLCs in the channel. + // The channel fundee requests unaffordable splice-outs in the first section, while the channel funder does so + // in the second section. + let anchors_features = ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies(); + let initial_channel_value = Amount::from_sat(100_000); + let push_amount = Amount::from_sat(10_000); + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + initial_channel_value.to_sat(), + push_amount.to_sat() * 1000, + ); + + let details = &nodes[0].node.list_channels()[0]; + let channel_type = details.channel_type.clone().unwrap(); + let feerate_per_kw = details.feerate_sat_per_1000_weight.unwrap(); + let spike_multiple = if channel_type == ChannelTypeFeatures::only_static_remote_key() { + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32 + } else { + 1 + }; + let spiked_feerate = spike_multiple * feerate_per_kw; + + // Place some pending HTLCs in the channel, in both directions. + let (preimage_1_to_0_a, _hash_1_to_0, ..) = route_payment(&nodes[1], &[&nodes[0]], 2_000_000); + let (preimage_1_to_0_b, _hash_1_to_0, ..) = route_payment(&nodes[1], &[&nodes[0]], 2_000_000); + let (preimage_1_to_0_c, _hash_1_to_0, ..) = route_payment(&nodes[1], &[&nodes[0]], 2_000_000); + let (preimage_0_to_1_a, _hash_0_to_1, ..) = route_payment(&nodes[0], &[&nodes[1]], 40_000_000); + let (preimage_0_to_1_b, _hash_0_to_1, ..) = route_payment(&nodes[0], &[&nodes[1]], 40_000_000); + + let splice_out_dance = |initiator: usize, + acceptor: usize, + // We will setup the channel such that splicing out an additional satoshi + // overdraws the initiator's balance. + splice_out: Amount, + splice_out_incl_fees: Amount, + post_splice_reserve: Amount| + -> FundingContribution { + let initiator = &nodes[initiator]; + let acceptor = &nodes[acceptor]; + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + // 1) Check that splicing out an additional satoshi fails validation on the sender's side. + + let script_pubkey = initiator.wallet_source.get_change_script().unwrap(); + let outputs = vec![TxOut { value: splice_out + Amount::ONE_SAT, script_pubkey }]; + assert!(matches!( + build_splice_out_contribution(initiator, acceptor, channel_id, outputs), + Err(FundingContributionError::InvalidSpliceValue), + )); + + // 2) Check that splicing out with the additional satoshi removed passes validation on the sender's side. + + let script_pubkey = initiator.wallet_source.get_change_script().unwrap(); + let outputs = vec![TxOut { value: splice_out, script_pubkey }]; + let contribution = + initiate_splice_out(initiator, acceptor, channel_id, outputs.clone()).unwrap(); + assert_eq!(contribution.net_value(), -splice_out_incl_fees.to_signed().unwrap()); + + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + // 3) Overwrite the splice-out message to add an additional satoshi to the splice-out, and check that it fails + // validation on the receiver's side. + + let mut splice_init = + get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + splice_init.funding_contribution_satoshis -= 1; + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + + let msg = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); + assert_eq!(msg.channel_id, channel_id); + let cannot_be_spliced_out = format!( + "Their post-splice channel balance {} is smaller than our selected v2 reserve {}", + post_splice_reserve - Amount::ONE_SAT, + post_splice_reserve + ); + assert_eq!(tx_abort_data(&msg), format!("Invalid contribution: {cannot_be_spliced_out}")); + + acceptor.node.peer_disconnected(node_id_initiator); + initiator.node.peer_disconnected(node_id_acceptor); + + let reconnect_args = ReconnectArgs::new(initiator, acceptor); + reconnect_nodes(reconnect_args); + + expect_splice_failed_events( + initiator, + &channel_id, + contribution, + NegotiationFailureReason::PeerDisconnected, + ); + + // 4) Try again with the additional satoshi removed from the splice-out message, and check that it passes + // validation on the receiver's side. + + let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + assert_eq!(contribution.net_value(), -splice_out_incl_fees.to_signed().unwrap()); + + contribution + }; + + let (preimage_1_to_0_d, node_1_splice_out_incl_fees) = { + // 0) Set the channel up such that if node 1 splices out an additional satoshi over the `splice_out` + // value, it overdraws its reserve. + + let debit_htlcs = Amount::from_sat(2_000 * 3); + let balance = push_amount - debit_htlcs; + let estimated_fees = Amount::from_sat(183); + let splice_out = Amount::from_sat(1000); + let splice_out_incl_fees = splice_out + estimated_fees; + let post_splice_reserve = (initial_channel_value - splice_out_incl_fees) / 100; + let pre_splice_balance = post_splice_reserve + splice_out_incl_fees; + let amount_msat = (balance - pre_splice_balance).to_sat() * 1000; + let (preimage_1_to_0_d, ..) = route_payment(&nodes[1], &[&nodes[0]], amount_msat); + + let contribution = + splice_out_dance(1, 0, splice_out, splice_out_incl_fees, post_splice_reserve); + let _new_funding_script = complete_splice_handshake(&nodes[1], &nodes[0]); + + // Don't complete the splice, leave node 1's balance untouched such that its + // `next_outbound_htlc_limit_msat` is exactly equal to its pre-splice balance - its pre-splice reserve. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + let reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_nodes(reconnect_args); + expect_splice_failed_events( + &nodes[1], + &channel_id, + contribution, + NegotiationFailureReason::PeerDisconnected, + ); + let details = &nodes[1].node.list_channels()[0]; + let expected_outbound_htlc_max = + (pre_splice_balance.to_sat() - details.unspendable_punishment_reserve.unwrap()) * 1000; + assert_eq!(details.next_outbound_htlc_limit_msat, expected_outbound_htlc_max); + + // At the end of the show, we'll claim the HTLC we used to setup the channel's balances above so we + // return its preimage. + // We'll also send a HTLC with the exact remaining amount available in the channel, which will match + // the balance we were about to splice out here. + (preimage_1_to_0_d, splice_out_incl_fees) + }; + + let preimage_0_to_1_d = { + // 0) Set the channel up such that if node 0 splices out an additional satoshi over the `splice_out` + // value, it overdraws its reserve. + + let debit_htlcs = Amount::from_sat(40_000 * 2); + let debit_anchors = + if channel_type == anchors_features { Amount::from_sat(330 * 2) } else { Amount::ZERO }; + let balance = initial_channel_value - push_amount - debit_htlcs - debit_anchors; + let estimated_fees = Amount::from_sat(183); + let splice_out = Amount::from_sat(1000); + let splice_out_incl_fees = splice_out + estimated_fees; + let post_splice_reserve = (initial_channel_value - splice_out_incl_fees) / 100; + // The 6 HTLCs we sent previously, the HTLC we send just below, and the fee spike buffer HTLC. + let htlc_count = 6 + 1 + 1; + let commit_tx_fee = Amount::from_sat(chan_utils::commit_tx_fee_sat( + spiked_feerate, + htlc_count, + &channel_type, + )); + let pre_splice_balance = post_splice_reserve + commit_tx_fee + splice_out_incl_fees; + let amount_msat = (balance - pre_splice_balance).to_sat() * 1000; + let (preimage_0_to_1_d, ..) = route_payment(&nodes[0], &[&nodes[1]], amount_msat); + + // Now actually follow through on the splice. + let contribution = + splice_out_dance(0, 1, splice_out, splice_out_incl_fees, post_splice_reserve); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + + // The funder's balance has exactly its reserve plus the fee for an inbound non-dust HTLC, + // so its `next_outbound_htlc_limit_msat` is exactly 0. We'll send that last inbound non-dust HTLC + // across further below to close the circle. + assert_eq!(nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat, 0); + + // Confirm and lock the splice. + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + + // Node 0 has now spliced the channel, so even though node 1 has not done anything, the max-size HTLC node 1 + // can send is now its pre-splice balance - its post-splice reserve. This matches the balance it was about to + // splice out above, but never did. + let outbound_htlc_max = nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert_eq!(outbound_htlc_max, node_1_splice_out_incl_fees.to_sat() * 1000); + + // Send the last max-size non-dust HTLC in the channel. + let _ = send_payment(&nodes[1], &[&nodes[0]], node_1_splice_out_incl_fees.to_sat() * 1000); + + // Node 1 is exactly at the V2 channel reserve, given that we just sent node 1's entire available balance + // across. + assert_eq!(nodes[1].node.list_channels()[0].next_outbound_htlc_limit_msat, 0); + + // Node 0's balance is its previous balance (ie the previous reserved fee) + the HTLC it just claimed + // - the new reserved fee (the channel reserves cancel out). + let previous_balance = chan_utils::commit_tx_fee_sat(spiked_feerate, 8, &channel_type); + let claimed_htlc = node_1_splice_out_incl_fees.to_sat(); + let commit_tx_fee = chan_utils::commit_tx_fee_sat(spiked_feerate, 9, &channel_type); + let new_balance = previous_balance + claimed_htlc - commit_tx_fee; + let outbound_htlc_max = nodes[0].node.list_channels()[0].next_outbound_htlc_limit_msat; + assert_eq!(outbound_htlc_max, new_balance * 1000); + + // Return the preimage of the HTLC used to setup the balances so we can claim the HTLC below. + preimage_0_to_1_d + }; + + // Clean up the channel. + claim_payment(&nodes[1], &[&nodes[0]], preimage_1_to_0_a); + claim_payment(&nodes[1], &[&nodes[0]], preimage_1_to_0_b); + claim_payment(&nodes[1], &[&nodes[0]], preimage_1_to_0_c); + + claim_payment(&nodes[1], &[&nodes[0]], preimage_1_to_0_d); + + claim_payment(&nodes[0], &[&nodes[1]], preimage_0_to_1_a); + claim_payment(&nodes[0], &[&nodes[1]], preimage_0_to_1_b); + + claim_payment(&nodes[0], &[&nodes[1]], preimage_0_to_1_d); + + // Check that the channel is still operational. + let _ = send_payment(&nodes[0], &[&nodes[1]], 2_000 * 1000); + let _ = send_payment(&nodes[1], &[&nodes[0]], 2_000 * 1000); +} + +// Returns after both sides are quiescent (no splice_init is generated since we use DoNothing). +pub fn reenter_quiescence<'a, 'b, 'c>( + node_a: &Node<'a, 'b, 'c>, node_b: &Node<'a, 'b, 'c>, channel_id: &ChannelId, +) { + let node_id_a = node_a.node.get_our_node_id(); + let node_id_b = node_b.node.get_our_node_id(); + + node_a.node.maybe_propose_quiescence(&node_id_b, channel_id).unwrap(); + let stfu_a = get_event_msg!(node_a, MessageSendEvent::SendStfu, node_id_b); + node_b.node.handle_stfu(node_id_a, &stfu_a); + let stfu_b = get_event_msg!(node_b, MessageSendEvent::SendStfu, node_id_a); + node_a.node.handle_stfu(node_id_b, &stfu_b); +} + +#[test] +fn test_splice_acceptor_disconnect_emits_events() { + // When both nodes contribute to a splice and the negotiation fails due to disconnect, + // both the initiator and acceptor should receive SpliceNegotiationFailed + DiscardFunding events + // so each can reclaim their UTXOs. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 1, added_value * 2); + + // Both nodes initiate splice-in (tiebreak: node 0 wins). + let node_0_funding_contribution = + do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let _node_1_funding_contribution = + do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!(splice_ack.funding_contribution_satoshis, 0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + // Disconnect mid-interactive-TX negotiation. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + // The initiator should get SpliceNegotiationFailed + DiscardFunding. + expect_splice_failed_events( + &nodes[0], + &channel_id, + node_0_funding_contribution, + NegotiationFailureReason::PeerDisconnected, + ); + + // The acceptor should also get SpliceNegotiationFailed + DiscardFunding with its contributions + // so it can reclaim its UTXOs. The contribution is feerate-adjusted by handle_splice_init, + // so we check for non-empty inputs/outputs rather than exact values. + let events = nodes[1].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2, "{events:?}"); + match &events[0] { + Event::DiscardFunding { + funding_info: FundingInfo::Contribution { inputs, outputs }, + .. + } => { + assert!(!inputs.is_empty(), "Expected acceptor inputs, got empty"); + assert!(!outputs.is_empty(), "Expected acceptor outputs, got empty"); + }, + other => panic!("Expected DiscardFunding with Contribution, got {:?}", other), + } + match &events[1] { + Event::SpliceNegotiationFailed { channel_id: cid, reason, contribution, .. } => { + assert_eq!(*cid, channel_id); + assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected); + assert!(contribution.is_some()); + }, + other => panic!("Expected SpliceNegotiationFailed, got {:?}", other), + } + + // Reconnect and verify the channel is still operational. + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_channel_ready = (true, true); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); +} + +#[test] +fn test_splice_rbf_acceptor_basic() { + // Test the full end-to-end flow for RBF of a pending splice transaction. + // Complete a splice-in, then use splice_channel API to initiate an RBF attempt + // with a higher feerate, going through the full tx_init_rbf → tx_ack_rbf → + // interactive TX → signing → mining → splice_locked flow. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 1: Complete a splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Step 2: Provide more UTXO reserves for the RBF attempt. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 3: Use splice_channel API to initiate the RBF. + // Original feerate was FEERATE_FLOOR_SATS_PER_KW (253). 253 + 25 = 278. + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); + let funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + + // Steps 4-8: STFU exchange → tx_init_rbf → tx_ack_rbf. + complete_rbf_handshake(&nodes[0], &nodes[1]); + + // Step 9: Complete interactive funding negotiation. + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + funding_contribution, + new_funding_script.clone(), + ); + + // Step 10: Sign and broadcast. The prior candidate in the broadcast's + // `TransactionType::InteractiveFunding` must point at the first splice tx it is replacing. + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(first_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // Step 11: Mine, lock, and verify DiscardFunding for the replaced splice candidate. + let result = lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx, + ANTI_REORG_DELAY - 1, + &[first_splice_tx.compute_txid()], + ); + + // The test wallet reuses the same UTXO across RBF rounds (the wallet doesn't track + // in-flight spends), so all contributed inputs are in the promoted tx. No unique + // contributions to discard. + assert!(result.node_a_discarded.is_empty()); + assert!(result.node_b_discarded.is_empty()); +} + +#[test] +fn test_splice_rbf_discard_unique_contribution() { + // Verify that DiscardFunding events contain the correct unique inputs and outputs when the + // RBF round uses different UTXOs than the initial splice. By clearing the wallet between + // rounds and providing fresh UTXOs, we force distinct inputs per round. Round 0 also + // includes a splice-out output with a unique script_pubkey not present in the RBF tx. + // When the RBF is promoted, round 0's inputs and splice-out output should appear in + // DiscardFunding. The change output is filtered because it shares a script_pubkey with the + // promoted tx's change output. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Round 0: Splice-in-and-out from node 0 with a splice-out output. + let splice_out_output = TxOut { + value: Amount::from_sat(5_000), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::all_zeros()), + }; + let funding_contribution = do_initiate_splice_in_and_out( + &nodes[0], + &nodes[1], + channel_id, + added_value, + vec![splice_out_output.clone()], + ); + let round_0_inputs: Vec<_> = funding_contribution.contributed_inputs().collect(); + assert!(!round_0_inputs.is_empty()); + + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Clear node 0's wallet so round 1 must use different UTXOs. + nodes[0].wallet_source.clear_utxos(); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Round 1: RBF with fresh UTXOs, splice-in only (no splice-out output). + let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template + .without_prior_contribution(rbf_feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(added_value) + .unwrap() + .build() + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, funding_contribution.clone(), None) + .unwrap(); + let round_1_inputs: Vec<_> = funding_contribution.contributed_inputs().collect(); + assert_ne!(round_0_inputs, round_1_inputs, "Rounds must use different UTXOs"); + + complete_rbf_handshake(&nodes[0], &nodes[1]); + + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + funding_contribution, + new_funding_script.clone(), + ); + + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(first_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + let result = lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx, + ANTI_REORG_DELAY - 1, + &[first_splice_tx.compute_txid()], + ); + + // Node 0's round 0 inputs are NOT in the promoted tx (which uses round 1's fresh UTXOs), + // so they appear as unique contributions to discard. The splice-out output also survives + // because its script_pubkey is not in the promoted tx. The change output is filtered + // because it shares a script_pubkey with the promoted tx's change output. + assert_eq!(result.node_a_discarded.len(), 1); + let (ref inputs, ref outputs) = result.node_a_discarded[0]; + assert_eq!(*inputs, round_0_inputs); + assert_eq!(*outputs, vec![splice_out_output.script_pubkey]); + + // Node 1 (non-contributing acceptor) has no contributions to discard. + assert!(result.node_b_discarded.is_empty()); +} + +#[test] +fn test_splice_rbf_at_high_feerate() { + // Test that min_rbf_feerate satisfies the spec's 25/24 rule at high feerates (above 600 + // sat/kwu, where a flat +25 increment alone would be insufficient). + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 1: Complete a splice-in at floor feerate. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Step 2: RBF to a high feerate (1000 sat/kwu, well above the 600 crossover point). + provide_utxo_reserves(&nodes, 2, added_value * 2); + let high_feerate = FeeRate::from_sat_per_kwu(1000); + let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, high_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script.clone(), + ); + let (rbf_tx_1, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(first_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // Step 3: RBF again using the template's min_rbf_feerate. The counterparty must accept it. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = { + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + funding_template.min_rbf_feerate().unwrap() + }; + let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script, + ); + let (_, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).replacing(rbf_tx_1.compute_txid()), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); +} + +#[test] +fn test_splice_rbf_insufficient_feerate() { + // Test that splice_in_sync rejects a feerate that doesn't satisfy the +25 sat/kwu rule, and that the + // acceptor also rejects tx_init_rbf with an insufficient feerate from a misbehaving peer. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Initiator-side: splice_in_sync rejects an insufficient feerate. + // Original feerate was 253. Using exactly 253 should fail since 253 * 24 < 253 * 25. + let same_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + + // Verify that the template exposes the RBF floor. + let min_rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + assert_eq!(min_rbf_feerate, expected_floor); + + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + assert!(funding_template + .splice_in_sync(added_value, same_feerate, FeeRate::MAX, &wallet) + .is_err()); + + // Verify that the floor feerate succeeds. + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(funding_template + .splice_in_sync(added_value, min_rbf_feerate, FeeRate::MAX, &wallet) + .is_ok()); + + // Acceptor-side: tx_init_rbf with an insufficient feerate is also rejected. + // Node 0 initiates a proper RBF but we tamper the feerate to be insufficient. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let mut tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + tx_init_rbf.feerate_sat_per_1000_weight = FEERATE_FLOOR_SATS_PER_KW; + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!(tx_abort.channel_id, channel_id); + + // Queue a payment while quiescent. It should go to the holding cell and be freed once + // quiescence is exited by the tx_abort exchange. + let (route, payment_hash, _payment_preimage, payment_secret) = + get_route_and_payment_hash!(nodes[0], nodes[1], 1_000_000); + let onion = RecipientOnionFields::secret_only(payment_secret, 1_000_000); + let payment_id = PaymentId(payment_hash.0); + nodes[0].node.send_payment_with_route(route, payment_hash, onion, payment_id).unwrap(); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Node 0 echoes tx_abort and exits quiescence, freeing the holding cell. + nodes[0].node.handle_tx_abort(node_id_1, &tx_abort); + + // The RBF round contributed the same inputs and outputs as the prior round, so after + // filtering against the prior round's committed UTXOs nothing remains to discard and + // `DiscardFunding` is suppressed; only `SpliceNegotiationFailed` is emitted. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1, "{events:?}"); + assert!( + matches!(&events[0], Event::SpliceNegotiationFailed { channel_id: cid, .. } if *cid == channel_id) + ); + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + let tx_abort_echo = match &msg_events[0] { + MessageSendEvent::SendTxAbort { msg, .. } => msg.clone(), + other => panic!("Expected SendTxAbort, got {:?}", other), + }; + match &msg_events[1] { + MessageSendEvent::UpdateHTLCs { updates, .. } => { + assert_eq!(updates.update_add_htlcs.len(), 1); + }, + other => panic!("Expected UpdateHTLCs, got {:?}", other), + } + + // Complete the HTLC commitment exchange so the channel is ready for the next RBF attempt. + // The holding cell free generated a monitor update for the outgoing HTLC. + check_added_monitors(&nodes[0], 1); + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[1] { + nodes[1].node.handle_update_add_htlc(node_id_0, &updates.update_add_htlcs[0]); + do_commitment_signed_dance(&nodes[1], &nodes[0], &updates.commitment_signed, false, false); + } else { + unreachable!(); + } + + // Node 1 handles the echo (no-op since it already aborted). + nodes[1].node.handle_tx_abort(node_id_0, &tx_abort_echo); + + // Acceptor-side: a counterparty feerate that only satisfies the 25/24 rule (263) is + // rejected — the spec requires max(prev + 25, prev * 25/24) = 278 at low feerates. + // Node 0 initiates another proper RBF but we tamper the feerate to the 25/24 value. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let mut tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + let rbf_feerate_25_24 = ((FEERATE_FLOOR_SATS_PER_KW as u64) * 25 / 24) as u32; + tx_init_rbf.feerate_sat_per_1000_weight = rbf_feerate_25_24; + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!(tx_abort.channel_id, channel_id); + + // Node 0 echoes tx_abort and exits quiescence. + nodes[0].node.handle_tx_abort(node_id_1, &tx_abort); + let tx_abort_echo = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1); + + // As above: nothing remains after filtering, so `DiscardFunding` is suppressed. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + assert!( + matches!(&events[0], Event::SpliceNegotiationFailed { channel_id: cid, .. } if *cid == channel_id) + ); + + nodes[1].node.handle_tx_abort(node_id_0, &tx_abort_echo); + + // Acceptor-side: prev + 25 = 278 satisfies the combined BIP125 rule and is accepted. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, FEERATE_FLOOR_SATS_PER_KW + 25); + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let _tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); +} + +#[test] +fn test_splice_rbf_insufficient_feerate_high() { + // At high feerates (above ~600 sat/kwu) the 25/24 multiplicative rule dominates the +25 + // flat increment. Verify that the counterparty validation rejects a feerate satisfying only + // the flat increment and accepts one satisfying the 25/24 rule. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in at floor feerate, then RBF to 1000 sat/kwu. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + provide_utxo_reserves(&nodes, 2, added_value * 2); + let high_feerate = FeeRate::from_sat_per_kwu(1000); + let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, high_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script, + ); + let (_, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).replacing(splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // prev=1000: flat increment gives 1000+25=1025, 25/24 rule gives 1000*25/24=1041. + // Feerate 1025 satisfies the flat increment but not 25/24 — rejected. + // Node 0 initiates another proper RBF but we tamper the feerate to 1025. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let min_rbf_feerate = FeeRate::from_sat_per_kwu(1041); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let mut tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + tx_init_rbf.feerate_sat_per_1000_weight = 1025; + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!(tx_abort.channel_id, channel_id); + + // Node 0 echoes tx_abort and exits quiescence. + nodes[0].node.handle_tx_abort(node_id_1, &tx_abort); + let tx_abort_echo = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1); + + // The RBF round's inputs and outputs are fully filtered against the prior round's + // committed UTXOs, so `DiscardFunding` is suppressed. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + assert!( + matches!(&events[0], Event::SpliceNegotiationFailed { channel_id: cid, .. } if *cid == channel_id) + ); + + nodes[1].node.handle_tx_abort(node_id_0, &tx_abort_echo); + + // Feerate 1041 satisfies both rules — accepted. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, min_rbf_feerate); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, 1041); + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let _tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); +} + +#[test] +fn test_splice_rbf_no_pending_splice() { + // Test that tx_init_rbf is rejected when there is no pending splice to RBF. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + // Re-enter quiescence without having done a splice. + reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(50_000), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!( + tx_abort_data(&tx_abort), + "Rejecting RBF attempt: No pending splice available to RBF" + ); +} + +#[test] +fn test_splice_rbf_active_negotiation() { + // Test that tx_init_rbf is rejected when a funding negotiation is already in progress. + // Start a splice but don't complete interactive TX construction, then send tx_init_rbf. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Initiate a splice but only complete the handshake (STFU + splice_init/ack), + // leaving interactive TX construction in progress. + let _funding_contribution = + do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let _new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]); + + // Now the acceptor (node 1) has a funding_negotiation in progress (ConstructingTransaction). + // Sending tx_init_rbf should be rejected. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!(tx_abort.channel_id, channel_id); + + // Clear the initiator's pending interactive TX messages from the incomplete splice handshake. + nodes[0].node.get_and_clear_pending_msg_events(); +} + +#[test] +fn test_splice_rbf_after_splice_locked() { + // Test that tx_init_rbf is rejected when the counterparty has already sent splice_locked. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Mine the splice tx on both nodes. + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + // Connect enough blocks on node 0 only so it sends splice_locked. + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + + let splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + + // Deliver splice_locked to node 1. Since node 1 hasn't confirmed enough blocks, + // it won't send its own splice_locked back, but it will set received_funding_txid. + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked); + + // Node 1 shouldn't have any messages to send (no splice_locked since it hasn't confirmed). + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert!(msg_events.is_empty(), "Expected no messages, got {:?}", msg_events); + + // Re-enter quiescence (node 0 initiates). + reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + + // Node 0 sends tx_init_rbf, but node 0 already sent splice_locked, so it should be rejected. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!(tx_abort_data(&tx_abort), "Rejecting RBF attempt: Already received splice_locked"); +} + +#[test] +fn test_splice_rbf_stfu_after_splice_locked() { + // Test that we don't send tx_init_rbf when we've already sent splice_locked. + // + // Scenario: node 0 initiates an RBF and sends STFU, but before receiving the counterparty's + // STFU response, it mines enough blocks to send splice_locked (setting sent_funding_txid). + // When node 1's STFU arrives, the stfu() handler should detect that RBF is no longer valid + // and return WarnAndDisconnect instead of sending tx_init_rbf. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Mine the splice tx on both nodes (not enough for splice_locked yet). + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + + // Provide more UTXOs for the RBF attempt. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Initiate RBF from node 0 with fresh inputs so the RBF round has a unique input that + // survives filtering when the failure cleanup runs. + let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template + .without_prior_contribution(rbf_feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(added_value) + .unwrap() + .build() + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, funding_contribution.clone(), None) + .unwrap(); + + // Node 0 sends STFU (can_initiate_rbf passes since no splice_locked yet). + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Deliver STFU to node 1; extract node 1's STFU response but don't deliver it yet. + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + // Mine enough blocks on node 0 so it sends splice_locked (sets sent_funding_txid). + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + let _splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + + // Now deliver node 1's STFU to node 0. The stfu() handler should detect that RBF is no + // longer valid (we already sent splice_locked) and return WarnAndDisconnect. + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + match &msg_events[0] { + MessageSendEvent::HandleError { action, .. } => { + assert_eq!( + *action, + msgs::ErrorAction::DisconnectPeerWithWarning { + msg: msgs::WarningMessage { + channel_id, + data: format!( + "Channel {} already sent splice_locked, cannot RBF", + channel_id, + ), + }, + } + ); + }, + _ => panic!("Expected HandleError, got {:?}", msg_events[0]), + } + + // Node 0 should emit DiscardFunding + SpliceNegotiationFailed for the RBF contribution. + // The change output is filtered (same script_pubkey as the first splice's change output), + // but the input survives because it's a different UTXO from the first splice. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2, "{events:?}"); + match &events[0] { + Event::DiscardFunding { + funding_info: FundingInfo::Contribution { inputs, outputs }, + .. + } => { + assert!(!inputs.is_empty()); + assert!(outputs.is_empty()); + }, + other => panic!("Expected DiscardFunding, got {:?}", other), + } + match &events[1] { + Event::SpliceNegotiationFailed { channel_id: cid, reason, .. } => { + assert_eq!(*cid, channel_id); + assert_eq!(*reason, NegotiationFailureReason::CannotInitiateRbf); + }, + other => panic!("Expected SpliceNegotiationFailed, got {:?}", other), + } +} + +#[test] +fn test_splice_zeroconf_no_rbf_feerate() { + // Test that splice_channel returns a FundingTemplate with min_rbf_feerate = None for a + // zero-conf channel, even when a splice negotiation is in progress. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_limits.trust_own_funding_0conf = true; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0); + mine_transaction(&nodes[0], &funding_tx); + mine_transaction(&nodes[1], &funding_tx); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 1, added_value * 2); + + // Initiate a splice (node 0) and complete the handshake so a funding negotiation is in + // progress. + let _funding_contribution = + do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let _new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]); + + // The acceptor (node 1) calling splice_channel should return no RBF feerate since + // zero-conf channels cannot RBF. + let funding_template = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + assert!(funding_template.min_rbf_feerate().is_none()); + + // Drain pending interactive tx messages from the splice handshake. + nodes[0].node.get_and_clear_pending_msg_events(); +} + +#[test] +fn test_splice_rbf_zeroconf_rejected() { + // Test that tx_init_rbf is rejected when option_zeroconf is negotiated. + // The zero-conf check happens before the pending_splice check, so we don't need to complete + // a splice — just enter quiescence and send tx_init_rbf. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_limits.trust_own_funding_0conf = true; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0); + mine_transaction(&nodes[0], &funding_tx); + mine_transaction(&nodes[1], &funding_tx); + + // Enter quiescence (node 0 initiates). + reenter_quiescence(&nodes[0], &nodes[1], &channel_id); + + // Node 0 sends tx_init_rbf, but the channel has option_zeroconf, so it should be rejected. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(50_000), + }; + + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!( + tx_abort_data(&tx_abort), + format!("Rejecting RBF attempt: Channel {} has option_zeroconf, cannot RBF", channel_id) + ); +} + +#[test] +fn test_splice_rbf_not_quiescence_initiator() { + // Test that tx_init_rbf from the non-quiescence-initiator is rejected because the + // quiescence initiator's RBF flow has already set funding_negotiation to AwaitingAck. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Provide more UTXO reserves for the RBF attempt. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Initiate RBF from node 0 (quiescence initiator). + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); + let _funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + + // STFU exchange: node 0 initiates quiescence. + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + // Node 0 sends tx_init_rbf as the quiescence initiator — grab and discard. + let _tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + + // Now craft a competing tx_init_rbf from node 1 (the non-initiator). + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: 500, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + + nodes[0].node.handle_tx_init_rbf(node_id_1, &tx_init_rbf); + + let tx_abort = get_event_msg!(nodes[0], MessageSendEvent::SendTxAbort, node_id_1); + assert_eq!(tx_abort.channel_id, channel_id); +} + +#[test] +fn test_splice_rbf_both_contribute_tiebreak() { + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let feerate = FeeRate::from_sat_per_kwu(min_rbf_feerate); + let added_value = Amount::from_sat(50_000); + do_test_splice_rbf_tiebreak(feerate, feerate, added_value, true); +} + +#[test] +fn test_splice_rbf_tiebreak_higher_feerate() { + // Node 0 (winner) uses a higher feerate than node 1 (loser). Node 1's change output is + // adjusted (reduced) to accommodate the higher feerate. Negotiation succeeds. + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + do_test_splice_rbf_tiebreak( + FeeRate::from_sat_per_kwu(min_rbf_feerate * 3), + FeeRate::from_sat_per_kwu(min_rbf_feerate), + Amount::from_sat(50_000), + true, + ); +} + +#[test] +fn test_splice_rbf_tiebreak_lower_feerate() { + // Node 0 (winner) uses a lower feerate than node 1 (loser). Since the initiator's feerate + // is below node 1's minimum, node 1 proceeds without contribution and will retry via a new + // splice at its preferred feerate after the RBF locks. + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + do_test_splice_rbf_tiebreak( + FeeRate::from_sat_per_kwu(min_rbf_feerate), + FeeRate::from_sat_per_kwu(min_rbf_feerate * 3), + Amount::from_sat(50_000), + false, + ); +} + +#[test] +fn test_splice_rbf_tiebreak_feerate_too_high() { + // Node 0 (winner) uses a feerate high enough that node 1's (loser) contribution cannot + // cover the fees. Node 1 proceeds without its contribution (QuiescentAction is preserved + // for a future splice). The RBF completes with only node 0's inputs/outputs. + let min_rbf_feerate = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + do_test_splice_rbf_tiebreak( + FeeRate::from_sat_per_kwu(20_000), + FeeRate::from_sat_per_kwu(min_rbf_feerate), + Amount::from_sat(95_000), + false, + ); +} + +/// Runs the tie-breaker test with the given per-node feerates and node 1's splice value. +/// +/// Both nodes call `splice_channel` + `funding_contributed`, both send STFU, and node 0 (the outbound +/// channel funder) wins the quiescence tie-break. The loser (node 1) becomes the acceptor. Whether +/// node 1 contributes to the RBF transaction depends on the feerate and budget constraints. +/// +/// `expect_acceptor_contributes` asserts the expected outcome: whether node 1's `tx_ack_rbf` +/// includes a funding output contribution. +pub fn do_test_splice_rbf_tiebreak( + rbf_feerate_0: FeeRate, rbf_feerate_1: FeeRate, node_1_splice_value: Amount, + expect_acceptor_contributes: bool, +) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + // Complete an initial splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (first_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Provide more UTXOs for both nodes' RBF attempts. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Node 0 calls splice_channel + funding_contributed. + let node_0_funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_0); + + // Node 1 calls splice_channel + funding_contributed. + let node_1_funding_contribution = do_initiate_splice_in_at_feerate( + &nodes[1], + &nodes[0], + channel_id, + node_1_splice_value, + rbf_feerate_1, + ); + + // Both nodes sent STFU (both have awaiting_quiescence set). + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + assert!(stfu_0.initiator); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + assert!(stfu_1.initiator); + + // Exchange STFUs. Node 0 is the outbound channel funder and wins the tie-break. + // Node 1 handles node 0's STFU first — it already sent its own STFU (local_stfu_sent is set), + // so this goes through the tie-break path. Node 1 loses (is_outbound = false) and becomes the + // acceptor. Its quiescent_action is preserved for the tx_init_rbf handler. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + + // Node 0 handles node 1's STFU — it already sent its own STFU, so tie-break again. + // Node 0 wins (is_outbound = true), consumes its quiescent_action, and sends tx_init_rbf. + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Node 0 sends tx_init_rbf. + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.channel_id, channel_id); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, rbf_feerate_0.to_sat_per_kwu() as u32); + + // Node 1 handles tx_init_rbf — its quiescent_action is consumed, adjusting its contribution + // for node 0's feerate. Whether it contributes depends on the feerate and budget constraints. + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); + assert_eq!(tx_ack_rbf.channel_id, channel_id); + + // Node 0 handles tx_ack_rbf. + let acceptor_contributes = tx_ack_rbf.funding_output_contribution.is_some(); + assert_eq!( + acceptor_contributes, expect_acceptor_contributes, + "Expected acceptor contribution: {}, got: {}", + expect_acceptor_contributes, acceptor_contributes, + ); + nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf); + + if acceptor_contributes { + // Capture change output values for assertions. + let node_0_change = node_0_funding_contribution + .change_output() + .expect("splice-in should have a change output") + .clone(); + let node_1_change = node_1_funding_contribution + .change_output() + .expect("splice-in should have a change output") + .clone(); + + // Complete interactive funding negotiation with both parties' inputs/outputs. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution), + tx_ack_rbf.funding_output_contribution.unwrap(), + new_funding_script.clone(), + ); + + // Sign (acceptor has contribution) and broadcast. + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .with_acceptor_contribution() + .replacing(first_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + + // The initiator's change output should remain unchanged (no feerate adjustment). + let initiator_change_in_tx = rbf_tx + .output + .iter() + .find(|o| o.script_pubkey == node_0_change.script_pubkey) + .expect("Initiator's change output should be in the RBF transaction"); + assert_eq!( + initiator_change_in_tx.value, node_0_change.value, + "Initiator's change output should remain unchanged", + ); + + // The acceptor's change output should be adjusted based on the feerate difference. + let acceptor_change_in_tx = rbf_tx + .output + .iter() + .find(|o| o.script_pubkey == node_1_change.script_pubkey) + .expect("Acceptor's change output should be in the RBF transaction"); + if rbf_feerate_0 <= rbf_feerate_1 { + // Initiator's feerate <= acceptor's original: the acceptor's change increases because + // is_initiator=false has lower weight, and the feerate is the same or lower. + assert!( + acceptor_change_in_tx.value > node_1_change.value, + "Acceptor's change should increase when initiator feerate ({}) <= acceptor \ + feerate ({}): adjusted {} vs original {}", + rbf_feerate_0.to_sat_per_kwu(), + rbf_feerate_1.to_sat_per_kwu(), + acceptor_change_in_tx.value, + node_1_change.value, + ); + } else { + // Initiator's feerate > acceptor's original: the higher feerate more than compensates + // for the lower weight, so the acceptor's change decreases. + assert!( + acceptor_change_in_tx.value < node_1_change.value, + "Acceptor's change should decrease when initiator feerate ({}) > acceptor \ + feerate ({}): adjusted {} vs original {}", + rbf_feerate_0.to_sat_per_kwu(), + rbf_feerate_1.to_sat_per_kwu(), + acceptor_change_in_tx.value, + node_1_change.value, + ); + } + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Mine, lock, and verify DiscardFunding for the replaced splice candidate. + let result = lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx, + ANTI_REORG_DELAY - 1, + &[first_splice_tx.compute_txid()], + ); + + // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs + // are in the promoted tx and nothing is unique to discard. + assert!(result.node_a_discarded.is_empty()); + assert!(result.node_b_discarded.is_empty()); + } else { + // Acceptor does not contribute — complete with only node 0's inputs/outputs. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + None, + 0, + new_funding_script.clone(), + ); + + // Sign (acceptor has no contribution) and broadcast. + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(first_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // Mine, lock, and verify DiscardFunding for the replaced splice candidate. + // Node 1's QuiescentAction was preserved, so after splice_locked it re-initiates + // quiescence to retry its contribution in a future splice. + let result = lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx, + ANTI_REORG_DELAY - 1, + &[first_splice_tx.compute_txid()], + ); + let stfu_1 = if let Some(MessageSendEvent::SendStfu { msg, .. }) = result.stfu { + msg + } else { + panic!("Expected SendStfu from node 1"); + }; + assert!(stfu_1.initiator); + + // === Part 2: Node 1's preserved QuiescentAction leads to a new splice === + // + // After splice_locked, pending_splice is None. So when stfu() consumes the + // QuiescentAction, it sends SpliceInit (not TxInitRbf), starting a brand new splice. + + // Node 0 receives node 1's STFU and responds with its own STFU. + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Node 1 receives STFU → quiescence established → node 1 is the initiator → + // sends SpliceInit. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); + + // Node 0 handles SpliceInit → sends SpliceAck. + nodes[0].node.handle_splice_init(node_id_1, &splice_init); + let splice_ack = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceAck, node_id_1); + + // Node 1 handles SpliceAck → starts interactive tx construction. + nodes[1].node.handle_splice_ack(node_id_0, &splice_ack); + + // Compute the new funding script from the splice pubkeys. + let new_funding_script_2 = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + // Complete interactive funding negotiation with node 1 as initiator (only node 1 + // contributes). + complete_interactive_funding_negotiation( + &nodes[1], + &nodes[0], + channel_id, + node_1_funding_contribution, + new_funding_script_2, + ); + + // Sign (no acceptor contribution) and broadcast. + let (new_splice_tx, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(&nodes[1], &nodes[0])); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[1], &node_id_0); + assert!(nodes[0].node.get_and_clear_pending_events().is_empty()); + + // Mine and lock. + mine_transaction(&nodes[1], &new_splice_tx); + mine_transaction(&nodes[0], &new_splice_tx); + + lock_splice_after_blocks(&nodes[1], &nodes[0], ANTI_REORG_DELAY - 1); + } +} + +#[test] +fn test_splice_rbf_tiebreak_feerate_too_high_rejected() { + // Node 0 (winner) proposes an RBF feerate far above node 1's (loser) max_feerate, and + // node 1's fair fee at that feerate exceeds its budget. This triggers + // FeeRateAdjustmentError::TooHigh in the queued contribution path, causing node 1 to + // reject with tx_abort. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete an initial splice-in from node 0. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_first_splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Provide more UTXOs for both nodes' RBF attempts. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Node 0 uses an extremely high feerate (100,000 sat/kwu). Node 1 uses the minimum RBF + // feerate with a moderate splice-in (50,000 sats) and a low max_feerate (3,000 sat/kwu). + // The target (100k) far exceeds node 1's max (3k), and the fair fee at 100k exceeds + // node 1's budget, triggering TooHigh. + let high_feerate = FeeRate::from_sat_per_kwu(100_000); + let min_rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let min_rbf_feerate = FeeRate::from_sat_per_kwu(min_rbf_feerate_sat_per_kwu); + let node_1_max_feerate = FeeRate::from_sat_per_kwu(3_000); + + let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = funding_template_0 + .splice_in_sync(added_value, high_feerate, FeeRate::MAX, &wallet_0) + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = funding_template_1 + .splice_in_sync(added_value, min_rbf_feerate, node_1_max_feerate, &wallet_1) + .unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Both sent STFU. + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + // Tie-break: node 0 wins. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Node 0 sends tx_init_rbf at 100,000 sat/kwu. + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, high_feerate.to_sat_per_kwu() as u32); + + // Node 1 handles tx_init_rbf — TooHigh: target (100k) >> max (3k) and fair fee > budget. + // Node 1 exits quiescence upon rejecting with tx_abort, and since it has a pending + // QuiescentAction (from its own splice RBF attempt), it immediately re-proposes quiescence. + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2); + match &msg_events[0] { + MessageSendEvent::SendTxAbort { node_id, msg } => { + assert_eq!(*node_id, node_id_0); + assert_eq!(msg.channel_id, channel_id); + }, + _ => panic!("Expected SendTxAbort, got {:?}", msg_events[0]), + }; + match &msg_events[1] { + MessageSendEvent::SendStfu { node_id, .. } => { + assert_eq!(*node_id, node_id_0); + }, + _ => panic!("Expected SendStfu, got {:?}", msg_events[1]), + }; +} + +#[test] +fn test_splice_rbf_acceptor_recontributes() { + // When the counterparty RBFs a splice and we have no pending QuiescentAction, + // our prior contribution should be automatically re-used. This tests the scenario: + // 1. Both nodes contribute to a splice (tiebreak: node 0 wins). + // 2. Only node 0 initiates an RBF — node 1 has no QuiescentAction. + // 3. Node 1 should re-contribute its prior inputs/outputs via our_prior_contribution. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); + + // Step 1: Both nodes initiate a splice at floor feerate. + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + + let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = + funding_template_0.splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_0).unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = + funding_template_1.splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_1).unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Step 2: Both send STFU; tiebreak: node 0 wins. + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Step 3: Node 0 sends SpliceInit, node 1 handles as acceptor (QuiescentAction consumed). + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!(splice_ack.funding_contribution_satoshis, 0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + // Complete interactive funding with both contributions. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution.clone()), + splice_ack.funding_contribution_satoshis, + new_funding_script.clone(), + ); + + let (first_splice_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).with_acceptor_contribution(), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Step 4: Provide new UTXOs for node 0's RBF (node 1 does NOT initiate RBF). + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Step 5: Only node 0 calls splice_channel + funding_contributed. + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); + let rbf_funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + + // Steps 6-9: STFU exchange → tx_init_rbf → tx_ack_rbf. + // Node 1 should re-contribute via our_prior_contribution. + let tx_ack_rbf = complete_rbf_handshake(&nodes[0], &nodes[1]); + assert!( + tx_ack_rbf.funding_output_contribution.is_some(), + "Acceptor should re-contribute via our_prior_contribution" + ); + + // Step 10: Complete interactive funding with both contributions. + // Node 1's prior contribution is re-used — pass a clone for matching. + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + rbf_funding_contribution, + Some(node_1_funding_contribution), + tx_ack_rbf.funding_output_contribution.unwrap(), + new_funding_script.clone(), + ); + + // Step 11: Sign (acceptor has contribution) and broadcast. + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .with_acceptor_contribution() + .replacing(first_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Step 12: Mine, lock, and verify DiscardFunding for the replaced splice candidate. + let result = lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx, + ANTI_REORG_DELAY - 1, + &[first_splice_tx.compute_txid()], + ); + + // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs + // are in the promoted tx and nothing is unique to discard. + assert!(result.node_a_discarded.is_empty()); + assert!(result.node_b_discarded.is_empty()); +} + +#[test] +fn test_splice_rbf_after_counterparty_rbf_aborted() { + // When a counterparty-initiated RBF is aborted, the acceptor's prior contribution is + // restored to the original feerate (before adjustment). Initiating our own RBF afterward + // uses this restored contribution. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); + + // Step 1: Both nodes initiate a splice at floor feerate. + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + + let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = + funding_template_0.splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_0).unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = + funding_template_1.splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_1).unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Step 2: Tiebreak — node 0 wins, both contribute to initial splice. + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution), + splice_ack.funding_contribution_satoshis, + new_funding_script, + ); + + let (_first_splice_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).with_acceptor_contribution(), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Step 3: Node 0 initiates RBF. Node 1 has no QuiescentAction, so its prior contribution + // is adjusted to the RBF feerate via for_acceptor_at_feerate. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + let _rbf_funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + + let tx_ack_rbf = complete_rbf_handshake(&nodes[0], &nodes[1]); + assert!(tx_ack_rbf.funding_output_contribution.is_some()); + + // Step 4: Abort the RBF. Node 0 sends tx_abort; node 1's prior contribution is restored + // to the original feerate (the RBF round's adjusted entry is popped from contributions). + // Drain node 0's pending TxAddInput from the interactive tx negotiation start. + nodes[0].node.get_and_clear_pending_msg_events(); + + let tx_abort = msgs::TxAbort { channel_id, data: vec![] }; + nodes[1].node.handle_tx_abort(node_id_0, &tx_abort); + + let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); + assert!(!msg_events.is_empty()); + let tx_abort_echo = match &msg_events[0] { + MessageSendEvent::SendTxAbort { msg, .. } => msg.clone(), + other => panic!("Expected SendTxAbort, got {:?}", other), + }; + + nodes[0].node.handle_tx_abort(node_id_1, &tx_abort_echo); + nodes[0].node.get_and_clear_pending_msg_events(); + nodes[0].node.get_and_clear_pending_events(); + nodes[1].node.get_and_clear_pending_events(); + + // Step 5: Node 1 initiates its own RBF via splice_channel → + // rbf_prior_contribution_sync. + // The prior contribution's feerate is restored to the original floor feerate, not the + // RBF-adjusted feerate. + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let funding_template = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + assert!(funding_template.min_rbf_feerate().is_some()); + assert_eq!( + funding_template.prior_contribution().unwrap().feerate(), + feerate, + "Prior contribution should have the original feerate, not the RBF-adjusted one", + ); + + let wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let rbf_contribution = + funding_template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet); + assert!(rbf_contribution.is_ok()); +} + +#[test] +fn test_splice_rbf_recontributes_feerate_too_high() { + // When the counterparty RBFs at a feerate too high for our prior contribution, + // we should reject the RBF rather than proceeding without our contribution. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); + + // Step 1: Both nodes initiate a splice. Node 0 at floor feerate, node 1 splices in 95k + // from a 100k UTXO (tight budget: ~5k for change/fees). + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + + let funding_template_0 = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_funding_contribution = funding_template_0 + .splice_in_sync(Amount::from_sat(50_000), floor_feerate, FeeRate::MAX, &wallet_0) + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, node_0_funding_contribution.clone(), None) + .unwrap(); + + let node_1_added_value = Amount::from_sat(95_000); + let funding_template_1 = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_funding_contribution = funding_template_1 + .splice_in_sync(node_1_added_value, floor_feerate, FeeRate::MAX, &wallet_1) + .unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_funding_contribution.clone(), None) + .unwrap(); + + // Step 2: Both send STFU; tiebreak: node 0 wins. + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Step 3: Complete the initial splice with both contributing. + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!(splice_ack.funding_contribution_satoshis, 0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution), + splice_ack.funding_contribution_satoshis, + new_funding_script.clone(), + ); + + let (_first_splice_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).with_acceptor_contribution(), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // Step 4: Provide new UTXOs. Node 0 initiates RBF at 20,000 sat/kwu. + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); + + let high_feerate = FeeRate::from_sat_per_kwu(20_000); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let rbf_funding_contribution = funding_template + .splice_in_sync(Amount::from_sat(50_000), high_feerate, FeeRate::MAX, &wallet) + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, rbf_funding_contribution.clone(), None) + .unwrap(); + + // Step 5: STFU exchange. + let stfu_a = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_a); + let stfu_b = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_b); + + // Step 6: Node 0 sends tx_init_rbf at 20,000 sat/kwu. + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + assert_eq!(tx_init_rbf.feerate_sat_per_1000_weight, high_feerate.to_sat_per_kwu() as u32); + + // Step 7: Node 1's prior contribution (95k from 100k UTXO) can't cover fees at 20k sat/kwu. + // Should reject with tx_abort rather than proceeding without contribution. + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + + let tx_abort = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + assert_eq!(tx_abort.channel_id, channel_id); +} + +#[test] +fn test_splice_rbf_sequential() { + // Three consecutive RBF rounds on the same splice (initial → RBF #1 → RBF #2). + // Node 0 is the quiescence initiator; node 1 is the acceptor with no contribution. + // Verifies: + // - Each round satisfies the +25 sat/kwu feerate rule + // - DiscardFunding events reference the correct txids from previous rounds + // - The final RBF can be mined and splice_locked successfully + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // --- Round 0: Initial splice-in from node 0 at floor feerate (253). --- + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx_0, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Feerate progression: 253 → 253+25 = 278 → 278+25 = 303 + let feerate_1_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; // 278 + let feerate_2_sat_per_kwu = feerate_1_sat_per_kwu + 25; + + // --- Round 1: RBF #1 at feerate 278. --- + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let rbf_feerate_1 = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu); + let funding_contribution_1 = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_1); + complete_rbf_handshake(&nodes[0], &nodes[1]); + + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + funding_contribution_1, + new_funding_script.clone(), + ); + let (splice_tx_1, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(splice_tx_0.compute_txid()), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // --- Round 2: RBF #2 at feerate 303. --- + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let rbf_feerate_2 = FeeRate::from_sat_per_kwu(feerate_2_sat_per_kwu); + let funding_contribution_2 = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_2); + complete_rbf_handshake(&nodes[0], &nodes[1]); + + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + funding_contribution_2, + new_funding_script.clone(), + ); + let (rbf_tx_final, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(splice_tx_1.compute_txid()), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // --- Mine and lock the final RBF, verifying DiscardFunding for both replaced candidates. --- + let splice_tx_0_txid = splice_tx_0.compute_txid(); + let splice_tx_1_txid = splice_tx_1.compute_txid(); + let result = lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx_final, + ANTI_REORG_DELAY - 1, + &[splice_tx_0_txid, splice_tx_1_txid], + ); + + // The test wallet reuses the same UTXOs across RBF rounds, so all contributed inputs + // are in the promoted tx and nothing is unique to discard. + assert!(result.node_a_discarded.is_empty()); + assert!(result.node_b_discarded.is_empty()); +} + +#[test] +fn test_splice_rbf_amends_prior_net_positive_contribution_request() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let initial_added_value = Amount::from_sat(100_000); + let half_added_value = Amount::from_sat(initial_added_value.to_sat() / 2); + provide_utxo_reserves(&nodes, 1, Amount::from_sat(250_000)); + + let initial_contribution = + do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, initial_added_value); + let (initial_inputs, _) = initial_contribution.clone().into_contributed_inputs_and_outputs(); + let (splice_tx_0, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, initial_contribution.clone()); + + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let first_output = TxOut { + value: Amount::from_sat(10_000), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_raw_hash(Hash::all_zeros())), + }; + let second_output = TxOut { + value: Amount::from_sat(15_000), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::all_zeros()), + }; + + let run_rbf_round = |contribution: FundingContribution, replaced_txid: Txid| { + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, contribution.clone(), None) + .unwrap(); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script.clone(), + ); + let (tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).replacing(replaced_txid), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + tx + }; + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(funding_template.prior_contribution().unwrap().outputs().is_empty()); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let contribution_1 = funding_template + .splice_out(vec![first_output.clone(), second_output.clone()], rbf_feerate, FeeRate::MAX) + .unwrap(); + let (inputs_1, _) = contribution_1.clone().into_contributed_inputs_and_outputs(); + assert_eq!(inputs_1, initial_inputs); + assert_eq!(contribution_1.outputs(), &[first_output.clone(), second_output.clone()]); + assert_eq!(contribution_1.net_value(), initial_contribution.net_value()); + assert!( + contribution_1.change_output().unwrap().value + < initial_contribution.change_output().unwrap().value + ); + let splice_tx_1 = run_rbf_round(contribution_1.clone(), splice_tx_0.compute_txid()); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_1.outputs()); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let contribution_2 = funding_template + .with_prior_contribution(rbf_feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .remove_value(half_added_value) + .unwrap() + .build() + .unwrap(); + let (inputs_2, _) = contribution_2.clone().into_contributed_inputs_and_outputs(); + assert_eq!(inputs_2, initial_inputs); + assert_eq!(contribution_2.outputs(), contribution_1.outputs()); + assert!(contribution_2.net_value() < contribution_1.net_value()); + let splice_tx_2 = run_rbf_round(contribution_2.clone(), splice_tx_1.compute_txid()); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_2.outputs()); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let contribution_3 = funding_template + .with_prior_contribution(rbf_feerate, FeeRate::MAX) + .remove_outputs(&first_output.script_pubkey) + .build() + .unwrap(); + let (inputs_3, _) = contribution_3.clone().into_contributed_inputs_and_outputs(); + assert_eq!(inputs_3, initial_inputs); + assert_eq!(contribution_3.outputs(), std::slice::from_ref(&second_output)); + assert_eq!(contribution_3.net_value(), contribution_2.net_value()); + assert!( + contribution_3.change_output().unwrap().value + > contribution_2.change_output().unwrap().value + ); + let splice_tx_3 = run_rbf_round(contribution_3.clone(), splice_tx_2.compute_txid()); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_3.outputs()); + let contribution_4 = + funding_template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet).unwrap(); + let (inputs_4, _) = contribution_4.clone().into_contributed_inputs_and_outputs(); + assert_eq!(inputs_4, initial_inputs); + assert_eq!(contribution_4.outputs(), contribution_3.outputs()); + assert_eq!(contribution_4.net_value(), contribution_3.net_value()); + assert!( + contribution_4.change_output().unwrap().value + < contribution_3.change_output().unwrap().value + ); + let rbf_tx_final = run_rbf_round(contribution_4, splice_tx_3.compute_txid()); + + lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx_final, + ANTI_REORG_DELAY - 1, + &[ + splice_tx_0.compute_txid(), + splice_tx_1.compute_txid(), + splice_tx_2.compute_txid(), + splice_tx_3.compute_txid(), + ], + ); +} + +#[test] +fn test_splice_rbf_amends_prior_net_negative_contribution_request() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, 100_000, 0); + + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let first_output = TxOut { + value: Amount::from_sat(10_000), + script_pubkey: ScriptBuf::new_p2wpkh(&WPubkeyHash::from_raw_hash(Hash::all_zeros())), + }; + let second_output = TxOut { + value: Amount::from_sat(15_000), + script_pubkey: ScriptBuf::new_p2wsh(&WScriptHash::all_zeros()), + }; + + let initial_contribution = + initiate_splice_out(&nodes[0], &nodes[1], channel_id, vec![first_output.clone()]).unwrap(); + let (initial_inputs, _) = initial_contribution.clone().into_contributed_inputs_and_outputs(); + assert!(initial_inputs.is_empty()); + let (splice_tx_0, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, initial_contribution.clone()); + let manual_input_pair_tx = provide_utxo_reserves(&nodes, 2, Amount::from_sat(20_000)); + let manual_input_single_tx = provide_utxo_reserves(&nodes, 1, Amount::from_sat(10_000)); + let manual_input_0 = ConfirmedUtxo::new_p2wpkh(manual_input_pair_tx.clone(), 0).unwrap(); + let manual_input_1 = ConfirmedUtxo::new_p2wpkh(manual_input_pair_tx, 1).unwrap(); + let manual_input_2 = ConfirmedUtxo::new_p2wpkh(manual_input_single_tx, 0).unwrap(); + + let run_rbf_round = |contribution: FundingContribution, replaced_txid: Txid| { + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, contribution.clone(), None) + .unwrap(); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script.clone(), + ); + let (tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).replacing(replaced_txid), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + tx + }; + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!( + funding_template.prior_contribution().unwrap().outputs(), + std::slice::from_ref(&first_output), + ); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let contribution_1 = funding_template + .splice_out(vec![second_output.clone()], rbf_feerate, FeeRate::MAX) + .unwrap(); + let (inputs_1, _) = contribution_1.clone().into_contributed_inputs_and_outputs(); + assert!(inputs_1.is_empty()); + assert_eq!(contribution_1.outputs(), &[first_output.clone(), second_output.clone()]); + assert!(contribution_1.net_value() < initial_contribution.net_value()); + let splice_tx_1 = run_rbf_round(contribution_1.clone(), splice_tx_0.compute_txid()); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_1.outputs()); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let contribution_2 = funding_template + .with_prior_contribution(rbf_feerate, FeeRate::MAX) + .remove_outputs(&first_output.script_pubkey) + .build() + .unwrap(); + let (inputs_2, _) = contribution_2.clone().into_contributed_inputs_and_outputs(); + assert!(inputs_2.is_empty()); + assert_eq!(contribution_2.outputs(), std::slice::from_ref(&second_output)); + assert!(contribution_2.net_value() > contribution_1.net_value()); + let splice_tx_2 = run_rbf_round(contribution_2.clone(), splice_tx_1.compute_txid()); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_2.outputs()); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let contribution_3 = funding_template + .with_prior_contribution(rbf_feerate, FeeRate::MAX) + .add_inputs(vec![manual_input_0.clone(), manual_input_1.clone()]) + .unwrap() + .build() + .unwrap(); + let (inputs_3, _) = contribution_3.clone().into_contributed_inputs_and_outputs(); + assert_eq!(inputs_3, vec![manual_input_0.utxo.outpoint, manual_input_1.utxo.outpoint],); + assert_eq!(contribution_3.outputs(), contribution_2.outputs()); + assert!(contribution_3.net_value() > SignedAmount::ZERO); + assert!(contribution_3.change_output().is_none()); + let splice_tx_3 = run_rbf_round(contribution_3.clone(), splice_tx_2.compute_txid()); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_3.outputs()); + let prior_inputs = funding_template + .prior_contribution() + .unwrap() + .clone() + .into_contributed_inputs_and_outputs() + .0; + assert_eq!(prior_inputs, vec![manual_input_0.utxo.outpoint, manual_input_1.utxo.outpoint],); + let rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + let contribution_4 = funding_template + .with_prior_contribution(rbf_feerate, FeeRate::MAX) + .add_input(manual_input_2.clone()) + .unwrap() + .remove_input(&manual_input_0.utxo.outpoint) + .unwrap() + .remove_input(&manual_input_1.utxo.outpoint) + .unwrap() + .build() + .unwrap(); + let (inputs_4, _) = contribution_4.clone().into_contributed_inputs_and_outputs(); + assert_eq!(inputs_4, vec![manual_input_2.utxo.outpoint]); + assert_eq!(contribution_4.outputs(), contribution_3.outputs()); + assert!(contribution_4.net_value() < SignedAmount::ZERO); + assert!(contribution_4.net_value() < contribution_3.net_value()); + assert!(contribution_4.change_output().is_none()); + let splice_tx_4 = run_rbf_round(contribution_4.clone(), splice_tx_3.compute_txid()); + + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert_eq!(funding_template.prior_contribution().unwrap().outputs(), contribution_4.outputs()); + let contribution_5 = + funding_template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet).unwrap(); + let (inputs_5, _) = contribution_5.clone().into_contributed_inputs_and_outputs(); + assert_eq!(inputs_5, vec![manual_input_2.utxo.outpoint]); + assert_eq!(contribution_5.outputs(), contribution_4.outputs()); + assert!(contribution_5.net_value() < SignedAmount::ZERO); + assert!(contribution_5.net_value() < contribution_4.net_value()); + assert!(contribution_5.change_output().is_none()); + let rbf_tx_final = run_rbf_round(contribution_5, splice_tx_4.compute_txid()); + + lock_rbf_splice_after_blocks( + &nodes[0], + &nodes[1], + &rbf_tx_final, + ANTI_REORG_DELAY - 1, + &[ + splice_tx_0.compute_txid(), + splice_tx_1.compute_txid(), + splice_tx_2.compute_txid(), + splice_tx_3.compute_txid(), + splice_tx_4.compute_txid(), + ], + ); +} + +#[test] +fn test_splice_rbf_acceptor_contributes_then_disconnects() { + // When both nodes contribute to a splice and the initiator RBFs (with the acceptor + // re-contributing via prior contribution), disconnecting mid-interactive-TX should emit + // SpliceNegotiationFailed + DiscardFunding for both nodes so each can reclaim their UTXOs. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); + + // --- Round 0: Both nodes initiate splice-in (tiebreak: node 0 wins). --- + let node_0_funding_contribution = + do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let node_1_funding_contribution = + do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!(splice_ack.funding_contribution_satoshis, 0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + node_0_funding_contribution, + Some(node_1_funding_contribution.clone()), + splice_ack.funding_contribution_satoshis, + new_funding_script.clone(), + ); + + let (_first_splice_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).with_acceptor_contribution(), + ); + assert!(splice_locked.is_none()); + + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); + + // --- Round 1: Node 0 initiates RBF; node 1 re-contributes via prior. --- + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); + let _rbf_funding_contribution = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + + let tx_ack_rbf = complete_rbf_handshake(&nodes[0], &nodes[1]); + assert!( + tx_ack_rbf.funding_output_contribution.is_some(), + "Acceptor should re-contribute via prior contribution" + ); + + // Disconnect mid-interactive-TX negotiation. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + // The initiator re-used the same UTXOs as round 0. Since those UTXOs are still committed + // to round 0's splice, they are filtered and no DiscardFunding is emitted. + let _ = get_event!(&nodes[0], Event::SpliceNegotiationFailed); + + // The acceptor re-contributed the same UTXOs as round 0 (via prior contribution + // adjustment). Since those UTXOs are still committed to round 0's splice, they are + // filtered and no DiscardFunding is emitted. The contribution still fails and needs a + // SpliceNegotiationFailed event so the wallet can resume funding. + let _ = get_event!(&nodes[1], Event::SpliceNegotiationFailed); + + // Reconnect. + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); +} + +#[test] +fn test_splice_rbf_disconnect_filters_prior_contributions() { + // When disconnecting during an RBF round that reuses the same UTXOs as a prior round, + // the SpliceFundingFailed event should filter out inputs/outputs still committed to the prior + // round. This exercises the `reset_pending_splice_state` → `maybe_create_splice_funding_failed` + // macro path. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + // Provide exactly 1 UTXO per node so coin selection is deterministic. + provide_utxo_reserves(&nodes, 1, added_value * 2); + + // --- Round 0: Initial splice-in at floor feerate (253). --- + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx_0, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // --- Round 1: RBF at higher feerate without providing new UTXOs. --- + // The wallet reselects the same UTXO since the splice tx hasn't been mined. + // Include a splice-out output with a different script_pubkey so the test can verify + // selective filtering: the change output (same script_pubkey as round 0) is filtered, + // while the splice-out output (different script_pubkey) survives. + let feerate_1_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let rbf_feerate = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu); + let splice_out_output = TxOut { + value: Amount::from_sat(1_000), + script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), + }; + let _funding_contribution_1 = do_initiate_rbf_splice_in_and_out( + &nodes[0], + &nodes[1], + channel_id, + vec![splice_out_output.clone()], + rbf_feerate, + ); + + // STFU exchange + RBF handshake to start interactive TX. + complete_rbf_handshake(&nodes[0], &nodes[1]); + + // Disconnect mid-negotiation. Stale interactive TX messages are cleared by peer_disconnected. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + // The initiator should get DiscardFunding + SpliceNegotiationFailed with filtered contributions. + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 2, "{events:?}"); + match &events[0] { + Event::DiscardFunding { + funding_info: FundingInfo::Contribution { inputs, outputs }, + .. + } => { + // The UTXO was filtered out: it's still committed to round 0's splice. + assert!(inputs.is_empty(), "Expected empty inputs (filtered), got {:?}", inputs); + // The change output was filtered (same script_pubkey as round 0's change output), + // but the splice-out output survives (different script_pubkey). + assert_eq!(*outputs, vec![splice_out_output.script_pubkey.clone()]); + }, + other => panic!("Expected DiscardFunding with Contribution, got {:?}", other), + } + match &events[1] { + Event::SpliceNegotiationFailed { channel_id: cid, reason, contribution, .. } => { + assert_eq!(*cid, channel_id); + assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected); + assert!(contribution.is_some()); + }, + other => panic!("Expected SpliceNegotiationFailed, got {:?}", other), + } + + // Reconnect. After a completed splice, channel_ready is not re-sent. + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); + + // --- Round 2: RBF at the same feerate as the failed round 1 (278). --- + // This should succeed because the failed round never updated the feerate floor, which + // remains at round 0's rate (253), and 278 >= 253 + 25. + provide_utxo_reserves(&nodes, 1, added_value * 2); + + let rbf_feerate_2 = FeeRate::from_sat_per_kwu(feerate_1_sat_per_kwu); + let _funding_contribution_2 = + do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate_2); + complete_rbf_handshake(&nodes[0], &nodes[1]); + + // Disconnect again to clean up the in-progress interactive TX negotiation. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1, "{events:?}"); + match &events[0] { + Event::SpliceNegotiationFailed { channel_id: cid, reason, contribution, .. } => { + assert_eq!(*cid, channel_id); + assert_eq!(*reason, NegotiationFailureReason::PeerDisconnected); + assert!(contribution.is_some()); + }, + other => panic!("Expected SpliceNegotiationFailed, got {:?}", other), + } + + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); +} + +#[test] +fn test_splice_channel_with_pending_splice_includes_rbf_floor() { + // Test that splice_channel includes the RBF floor when a pending splice exists with + // negotiated candidates. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Fresh splice — no pending splice, so no prior contribution or minimum RBF feerate. + { + let template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(template.min_rbf_feerate().is_none()); + assert!(template.prior_contribution().is_none()); + } + + // Complete a splice-in at floor feerate. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Call splice_channel again — the pending splice should cause min_rbf_feerate to be set + // and the prior contribution to be available. + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + assert_eq!(funding_template.min_rbf_feerate(), Some(expected_floor)); + assert!(funding_template.prior_contribution().is_some()); + + // rbf_prior_contribution_sync returns the adjusted prior contribution directly. + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + assert!(funding_template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet).is_ok()); +} + +#[test] +fn test_funding_contributed_adjusts_feerate_for_rbf() { + // Test that funding_contributed adjusts the contribution's feerate to the minimum RBF feerate + // when a pending splice appears between splice_channel and funding_contributed. + // + // Node 0 calls splice_channel (no pending splice → min_rbf_feerate = None) and builds a + // contribution at floor feerate. Node 1 then initiates and completes a splice. When node 0 + // calls funding_contributed, the contribution is adjusted to the minimum RBF feerate and STFU + // is sent immediately. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 4, added_value * 2); + + // Node 0 calls splice_channel before any pending splice exists. + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(funding_template.min_rbf_feerate().is_none()); + + // Build contribution at floor feerate with high max_feerate to allow adjustment. + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let contribution = + funding_template.splice_in_sync(added_value, floor_feerate, FeeRate::MAX, &wallet).unwrap(); + + // Node 1 initiates and completes a splice, creating pending_splice with negotiated candidates. + let node_1_contribution = do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + let (_first_splice_tx, _new_funding_script) = + splice_channel(&nodes[1], &nodes[0], channel_id, node_1_contribution); + + // Node 0 calls funding_contributed. The contribution's feerate (floor) is below the RBF + // floor (floor + 25 sat/kwu), but funding_contributed adjusts it upward. + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None).unwrap(); + + // STFU should be sent immediately (the adjusted feerate satisfies the RBF check). + let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu); + let stfu_resp = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_resp); + + // Verify the RBF handshake proceeds. + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + let rbf_feerate = FeeRate::from_sat_per_kwu(tx_init_rbf.feerate_sat_per_1000_weight as u64); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + assert!(rbf_feerate >= expected_floor); +} + +#[test] +fn test_funding_contributed_rbf_adjustment_exceeds_max_feerate() { + // Test that when the minimum RBF feerate exceeds max_feerate, the adjustment in + // funding_contributed fails gracefully and the contribution keeps its original feerate. The + // splice still proceeds (STFU is sent) and the RBF negotiation handles the feerate mismatch. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 4, added_value * 2); + + // Node 0 calls splice_channel and builds contribution with max_feerate = floor_feerate. + // This means the minimum RBF feerate (floor + 25 sat/kwu) will exceed max_feerate, preventing adjustment. + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let contribution = funding_template + .splice_in_sync(added_value, floor_feerate, floor_feerate, &wallet) + .unwrap(); + + // Node 1 initiates and completes a splice. + let node_1_contribution = do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + let (_splice_tx, _) = splice_channel(&nodes[1], &nodes[0], channel_id, node_1_contribution); + + // Node 0 calls funding_contributed. The adjustment fails (minimum RBF feerate > max_feerate), + // but funding_contributed still succeeds — the contribution keeps its original feerate. + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution, None).unwrap(); + + // STFU is NOT sent — the feerate is below the minimum RBF feerate so try_send_stfu delays. + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Mine and lock the pending splice → pending_splice is cleared. + mine_transaction(&nodes[0], &_splice_tx); + mine_transaction(&nodes[1], &_splice_tx); + let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1).stfu; + + // STFU is sent during lock — the splice proceeds as a fresh splice (not RBF). + let stfu = match stfu { + Some(MessageSendEvent::SendStfu { msg, .. }) => { + assert!(msg.initiator); + msg + }, + other => panic!("Expected SendStfu, got {:?}", other), + }; + + // Complete the fresh splice and verify it uses the original floor feerate. + nodes[1].node.handle_stfu(node_id_0, &stfu); + let stfu_resp = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_resp); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + assert_eq!(splice_init.funding_feerate_per_kw, FEERATE_FLOOR_SATS_PER_KW); +} + +#[test] +fn test_peer_initiated_stfu_skips_local_rbf_feerate_check() { + // Test that a local low-fee splice RBF attempt does not prevent us from responding to a + // counterparty-initiated quiescence attempt. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 4, added_value * 2); + + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let node_0_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let node_0_contribution = + node_0_template.splice_in_sync(added_value, floor_feerate, floor_feerate, &wallet).unwrap(); + + // Node 1 creates a pending splice before node 0 submits its contribution. Node 0's + // contribution cannot be adjusted up to the pending splice's minimum RBF feerate, so it must + // not send its own stfu yet. + let node_1_contribution = do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + let (_first_splice_tx, _) = + splice_channel(&nodes[1], &nodes[0], channel_id, node_1_contribution); + nodes[0].node.funding_contributed(&channel_id, &node_id_1, node_0_contribution, None).unwrap(); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Node 1 can still initiate quiescence for its own RBF attempt. Node 0 should reply as the + // non-initiator instead of applying its local splice RBF feerate check to the response. + let min_rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + let _node_1_rbf_contribution = + do_initiate_rbf_splice_in(&nodes[1], &nodes[0], channel_id, min_rbf_feerate); + let stfu_init = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + assert!(stfu_init.initiator); + + nodes[0].node.handle_stfu(node_id_1, &stfu_init); + let stfu_response = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + assert!(!stfu_response.initiator); +} + +#[test] +fn test_funding_contributed_rbf_adjustment_insufficient_budget() { + // Test that when the change output can't absorb the fee increase needed for the minimum RBF feerate + // (even though max_feerate allows it), the adjustment fails gracefully and the splice + // proceeds with the original feerate. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 4, added_value * 2); + + // Node 0 calls splice_channel before any pending splice exists. + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + + // Build node 0's contribution at floor feerate with a tight budget. + let wallet = TightBudgetWallet { + utxo_value: added_value + Amount::from_sat(3000), + change_value: Amount::from_sat(300), + }; + let contribution = + funding_template.splice_in_sync(added_value, floor_feerate, FeeRate::MAX, &wallet).unwrap(); + + // Node 1 initiates a splice at a HIGH feerate (10,000 sat/kwu). The minimum RBF feerate will be + // max(10,000 + 25, 10,000 * 25/24) = 10,416 sat/kwu — far above what node 0's tight + // budget can handle. + let high_feerate = FeeRate::from_sat_per_kwu(10_000); + let node_1_template = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let node_1_wallet = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let node_1_contribution = node_1_template + .splice_in_sync(added_value, high_feerate, FeeRate::MAX, &node_1_wallet) + .unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, node_1_contribution.clone(), None) + .unwrap(); + let (_splice_tx, _) = splice_channel(&nodes[1], &nodes[0], channel_id, node_1_contribution); + + // Node 0 calls funding_contributed. Adjustment fails (insufficient fee buffer), so the + // contribution keeps its original feerate. + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution, None).unwrap(); + + // STFU is NOT sent — the feerate is below the minimum RBF feerate so try_send_stfu delays. + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + + // Mine and lock the pending splice → pending_splice is cleared. + mine_transaction(&nodes[0], &_splice_tx); + mine_transaction(&nodes[1], &_splice_tx); + let stfu = lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1).stfu; + + // STFU is sent during lock — the splice proceeds as a fresh splice (not RBF). + let stfu = match stfu { + Some(MessageSendEvent::SendStfu { msg, .. }) => { + assert!(msg.initiator); + msg + }, + other => panic!("Expected SendStfu, got {:?}", other), + }; + + // Complete the fresh splice and verify it uses the original floor feerate. + nodes[1].node.handle_stfu(node_id_0, &stfu); + let stfu_resp = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_resp); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + assert_eq!(splice_init.funding_feerate_per_kw, FEERATE_FLOOR_SATS_PER_KW); +} + +#[test] +fn test_prior_contribution_unadjusted_when_max_feerate_too_low() { + // Test that rbf_prior_contribution_sync re-runs coin selection when the prior + // contribution's max_feerate is too low to accommodate the minimum RBF feerate. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice with max_feerate = floor_feerate. This means the prior contribution + // stored in pending_splice.contributions will have a tight max_feerate. + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let funding_contribution = funding_template + .splice_in_sync(added_value, floor_feerate, floor_feerate, &wallet) + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, funding_contribution.clone(), None) + .unwrap(); + let (_splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Call splice_channel again — the minimum RBF feerate (floor + 25 sat/kwu) exceeds the prior + // contribution's max_feerate (floor), so adjustment fails. + // rbf_prior_contribution_sync re-runs coin selection with the caller's max_feerate. + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(funding_template.min_rbf_feerate().is_some()); + assert!(funding_template.prior_contribution().is_some()); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + assert!(funding_template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet).is_ok()); +} + +#[test] +fn test_splice_channel_during_negotiation_includes_rbf_feerate() { + // Test that splice_channel returns min_rbf_feerate derived from the in-progress + // negotiation's feerate when the acceptor calls it during active negotiation. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Node 1 initiates a splice. Perform stfu exchange and splice_init handling, which creates + // a pending_splice with funding_negotiation on node 0 (the acceptor). + let _funding_contribution = + do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + let stfu_init = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_init); + let stfu_ack = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_ack); + + let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); + nodes[0].node.handle_splice_init(node_id_1, &splice_init); + let _splice_ack = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceAck, node_id_1); + + // Node 0 (acceptor) calls splice_channel while the negotiation is in progress. + // min_rbf_feerate should be derived from the in-progress negotiation's feerate. + let template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let expected_floor = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + assert_eq!(template.min_rbf_feerate(), Some(expected_floor)); + + // No prior contribution since there are no negotiated candidates yet, so RBF is rejected. + assert!(template.prior_contribution().is_none()); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + assert!(matches!( + template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet), + Err(FundingContributionError::NotRbfScenario) + )); +} + +#[test] +fn test_rbf_sync_returns_err_when_no_min_rbf_feerate() { + // Test that rbf_prior_contribution_sync returns `NotRbfScenario` when there is no pending + // splice (min_rbf_feerate is None). + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Fresh splice — no pending splice, so min_rbf_feerate is None. + let template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + assert!(template.min_rbf_feerate().is_none()); + assert!(template.prior_contribution().is_none()); + + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + assert!(matches!( + template.rbf_prior_contribution_sync(None, FeeRate::MAX, &wallet), + Err(crate::ln::funding::FundingContributionError::NotRbfScenario), + )); +} + +#[test] +fn test_rbf_sync_returns_err_when_max_feerate_below_min_rbf() { + // Test that rbf_prior_contribution_sync returns an error when the caller's max_feerate is + // below the minimum RBF feerate. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice to create a pending splice. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Call splice_channel again to get the RBF template. + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let min_rbf_feerate = funding_template.min_rbf_feerate().unwrap(); + + // Use a max_feerate that is 1 sat/kwu below the minimum RBF feerate. + let too_low_feerate = + FeeRate::from_sat_per_kwu(min_rbf_feerate.to_sat_per_kwu().saturating_sub(1)); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + assert!(matches!( + funding_template.rbf_prior_contribution_sync(None, too_low_feerate, &wallet), + Err(crate::ln::funding::FundingContributionError::FeeRateExceedsMaximum { .. }), + )); +} + +#[test] +fn test_splice_revalidation_at_quiescence() { + // When an outbound HTLC is committed between funding_contributed and quiescence, the + // holder's balance decreases. If the splice-out was marginal at funding_contributed time, + // the re-validation at quiescence should fail and emit SpliceNegotiationFailed + DiscardFunding. + // + // Flow: + // 1. Send payment #1 (update_add + CS) → node 0 awaits RAA + // 2. funding_contributed with splice-out → passes, stfu delayed (awaiting RAA) + // 3. Process node 1's RAA → node 0 free to send + // 4. Send payment #2 (update_add + CS) → balance reduced + // 5. Process node 1's CS → node 0 sends RAA, stfu delayed (payment #2 pending) + // 6. Complete payment #2's exchange → stfu fires + // 7. stfu exchange → quiescence → re-validation fails + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let _ = provide_anchor_reserves(&nodes); + + // Step 1: Send payment #1 (update_add + CS). Node 0 awaits RAA. + let payment_1_msat = 20_000_000; + let (route_1, payment_hash_1, _, payment_secret_1) = + get_route_and_payment_hash!(nodes[0], nodes[1], payment_1_msat); + nodes[0] + .node + .send_payment_with_route( + route_1, + payment_hash_1, + RecipientOnionFields::secret_only(payment_secret_1, payment_1_msat), + PaymentId(payment_hash_1.0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + let payment_1_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + + // Step 2: funding_contributed with splice-out. Passes because the balance floor only + // includes payment #1. stfu is delayed — awaiting RAA. + let outputs = vec![TxOut { + value: Amount::from_sat(70_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let contribution = funding_template.splice_out(outputs, feerate, FeeRate::MAX).unwrap(); + + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution.clone(), None).unwrap(); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty(), "stfu should be delayed"); + + // Step 3: Deliver payment #1 to node 1 and process RAA. + let payment_1_event = SendEvent::from_event(payment_1_msgs.into_iter().next().unwrap()); + nodes[1].node.handle_update_add_htlc(node_id_0, &payment_1_event.msgs[0]); + nodes[1].node.handle_commitment_signed_batch_test(node_id_0, &payment_1_event.commitment_msg); + check_added_monitors(&nodes[1], 1); + let (raa, cs) = get_revoke_commit_msgs(&nodes[1], &node_id_0); + + // Process node 1's RAA. After this, node 0 is free to send new HTLCs. + nodes[0].node.handle_revoke_and_ack(node_id_1, &raa); + check_added_monitors(&nodes[0], 1); + + // Step 4: Send payment #2 in the window between RAA and CS processing. + let payment_2_msat = 20_000_000; + let (route_2, payment_hash_2, _, payment_secret_2) = + get_route_and_payment_hash!(nodes[0], nodes[1], payment_2_msat); + nodes[0] + .node + .send_payment_with_route( + route_2, + payment_hash_2, + RecipientOnionFields::secret_only(payment_secret_2, payment_2_msat), + PaymentId(payment_hash_2.0), + ) + .unwrap(); + check_added_monitors(&nodes[0], 1); + let payment_2_msgs = nodes[0].node.get_and_clear_pending_msg_events(); + + // Step 5: Process node 1's CS. Node 0 sends RAA but stfu is delayed (payment #2 pending). + nodes[0].node.handle_commitment_signed_batch_test(node_id_1, &cs); + check_added_monitors(&nodes[0], 1); + let raa_0 = get_event_msg!(nodes[0], MessageSendEvent::SendRevokeAndACK, node_id_1); + nodes[1].node.handle_revoke_and_ack(node_id_0, &raa_0); + check_added_monitors(&nodes[1], 1); + + // Step 6: Complete payment #2's commitment exchange. stfu fires afterward. + let payment_2_event = SendEvent::from_event(payment_2_msgs.into_iter().next().unwrap()); + nodes[1].node.handle_update_add_htlc(node_id_0, &payment_2_event.msgs[0]); + nodes[1].node.handle_commitment_signed_batch_test(node_id_0, &payment_2_event.commitment_msg); + check_added_monitors(&nodes[1], 1); + let (raa_1b, cs_1b) = get_revoke_commit_msgs(&nodes[1], &node_id_0); + nodes[0].node.handle_revoke_and_ack(node_id_1, &raa_1b); + check_added_monitors(&nodes[0], 1); + nodes[0].node.handle_commitment_signed_batch_test(node_id_1, &cs_1b); + check_added_monitors(&nodes[0], 1); + + // RAA and stfu sent together. + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 2, "{msg_events:?}"); + let raa_0b = match &msg_events[0] { + MessageSendEvent::SendRevokeAndACK { msg, .. } => msg.clone(), + other => panic!("Expected SendRevokeAndACK, got {:?}", other), + }; + let stfu_0 = match &msg_events[1] { + MessageSendEvent::SendStfu { msg, .. } => msg.clone(), + other => panic!("Expected SendStfu, got {:?}", other), + }; + + nodes[1].node.handle_revoke_and_ack(node_id_0, &raa_0b); + check_added_monitors(&nodes[1], 1); + + // Step 7: stfu exchange → quiescence → re-validation fails → disconnect. + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // handle_stfu returns WarnAndDisconnect (triggering disconnect) alongside the + // QuiescentError containing the failed contribution's events. + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + assert!(matches!(msg_events[0], MessageSendEvent::HandleError { .. })); + + expect_splice_failed_events( + &nodes[0], + &channel_id, + contribution, + NegotiationFailureReason::ContributionInvalid, + ); +} + +#[test] +fn test_splice_init_before_quiescence_sends_warning() { + // A misbehaving peer sends splice_init before quiescence is established. The receiver + // should send a warning and disconnect. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + // Node 0 initiates quiescence. + nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap(); + let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Misbehaving node 1 sends splice_init before completing the STFU handshake. + let funding_pubkey = + PublicKey::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&[42; 32]).unwrap()); + let splice_init = msgs::SpliceInit { + channel_id, + funding_contribution_satoshis: 50_000, + funding_feerate_per_kw: FEERATE_FLOOR_SATS_PER_KW, + locktime: 0, + funding_pubkey, + require_confirmed_inputs: None, + }; + nodes[0].node.handle_splice_init(node_id_1, &splice_init); + + // Node 0 should send a warning and disconnect. + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1), + other => panic!("Expected HandleError, got {:?}", other), + } +} + +#[test] +fn test_tx_init_rbf_before_quiescence_sends_warning() { + // A misbehaving peer sends tx_init_rbf before quiescence is established. The receiver + // should send a warning and disconnect. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Complete a splice-in so there's a pending splice to RBF. + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (_splice_tx, _new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Node 0 initiates quiescence. + nodes[0].node.maybe_propose_quiescence(&node_id_1, &channel_id).unwrap(); + let _stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + + // Misbehaving node 1 sends tx_init_rbf before completing the STFU handshake. + let tx_init_rbf = msgs::TxInitRbf { + channel_id, + locktime: 0, + feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW + 25, + funding_output_contribution: Some(added_value.to_sat() as i64), + }; + nodes[0].node.handle_tx_init_rbf(node_id_1, &tx_init_rbf); + + // Node 0 should send a warning and disconnect. + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1); + match &msg_events[0] { + MessageSendEvent::HandleError { node_id, .. } => assert_eq!(*node_id, node_id_1), + other => panic!("Expected HandleError, got {:?}", other), + } + + // Clean up events from the splice setup. + nodes[0].node.get_and_clear_pending_events(); + nodes[1].node.get_and_clear_pending_events(); +} + +#[test] +fn test_splice_rbf_rejects_low_feerate_after_several_attempts() { + // After several RBF attempts, the counterparty's RBF feerate must be high enough to + // confirm (per the fee estimator). Early attempts at low feerates are accepted, but + // once the threshold is crossed and the fee estimator expects a higher feerate, the + // attempt is rejected. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Round 0: Initial splice-in at floor feerate (253). + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (mut prev_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Bump the fee estimator on node 1 (the RBF receiver) early so the feerate check + // would reject once the threshold is crossed. + let high_feerate = 10_000; + *chanmon_cfgs[1].fee_estimator.sat_per_kw.lock().unwrap() = high_feerate; + + // Rounds 1-10: RBF at minimum bump. Accepted (at or below threshold). + let mut prev_feerate = FEERATE_FLOOR_SATS_PER_KW as u64; + for _ in 0..10 { + let feerate = prev_feerate + 25; + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = FeeRate::from_sat_per_kwu(feerate); + let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script.clone(), + ); + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(prev_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + prev_feerate = feerate; + prev_splice_tx = rbf_tx; + } + + // Round 11: RBF at minimum bump. Should be rejected because feerate < fee estimator. + let next_feerate = prev_feerate + 25; + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = FeeRate::from_sat_per_kwu(next_feerate); + let _contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + // Node 0 sends tx_init_rbf. Node 1 rejects the low feerate after the threshold. + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); +} + +#[test] +fn test_splice_rbf_rejects_own_low_feerate_after_several_attempts() { + // Same as test_splice_rbf_rejects_low_feerate_after_several_attempts, but for our own + // initiated RBF. The spec requires: "MUST set a high enough feerate to ensure quick + // confirmation." After several attempts, funding_contributed should reject our contribution + // if the feerate is below the fee estimator's target. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Round 0: Initial splice-in at floor feerate (253). + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (mut prev_splice_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, funding_contribution); + + // Bump node 0's fee estimator early so the feerate check would reject once the + // threshold is crossed. + let high_feerate = 10_000; + *chanmon_cfgs[0].fee_estimator.sat_per_kw.lock().unwrap() = high_feerate; + + // Rounds 1-10: RBF at minimum bump. Accepted (at or below threshold). + let mut prev_feerate = FEERATE_FLOOR_SATS_PER_KW as u64; + for _ in 0..10 { + let feerate = prev_feerate + 25; + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = FeeRate::from_sat_per_kwu(feerate); + let contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script.clone(), + ); + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(prev_splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + prev_feerate = feerate; + prev_splice_tx = rbf_tx; + } + + // Round 11: Our own RBF at minimum bump. funding_contributed should reject it. + let next_feerate = prev_feerate + 25; + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = FeeRate::from_sat_per_kwu(next_feerate); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let contribution = funding_template + .without_prior_contribution(rbf_feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(added_value) + .unwrap() + .build() + .unwrap(); + let result = nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution, None); + assert!(result.is_err(), "Expected rejection for low feerate: {:?}", result); + + // SpliceNegotiationFailed is emitted. DiscardFunding is not emitted because all inputs/outputs + // are filtered out (same UTXOs reused for RBF, still committed to the prior splice tx). + let events = nodes[0].node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1, "{events:?}"); + match &events[0] { + Event::SpliceNegotiationFailed { channel_id: cid, reason, contribution, .. } => { + assert_eq!(*cid, channel_id); + assert_eq!(*reason, NegotiationFailureReason::FeeRateTooLow); + assert!(contribution.is_some()); + }, + other => panic!("Expected SpliceNegotiationFailed, got {:?}", other), + } +} + +#[test] +fn test_no_disconnect_after_splice_completes() { + // Test that the disconnect timer is cleared when exiting quiescence after a successful splice + // negotiation. Previously, `on_tx_signatures_exchange` cleared the quiescent state but not the + // disconnect timer, causing a spurious disconnect after the splice completed. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]); + + // Fire a tick while quiescent to arm the disconnect timer. + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + + // Complete the splice negotiation, which should clear the timer when exiting quiescence. + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + funding_contribution, + new_funding_script, + ); + let (_, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1])); + assert!(splice_locked.is_none()); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + expect_splice_pending_event(&nodes[0], &node_id_1); + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // Fire enough ticks to trigger a disconnect if the timer wasn't properly cleared. + for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS { + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + } + + let has_disconnect = |events: &[MessageSendEvent]| { + events.iter().any(|event| { + matches!( + event, + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + ) + }) + }; + assert!(!has_disconnect(&nodes[0].node.get_and_clear_pending_msg_events())); + assert!(!has_disconnect(&nodes[1].node.get_and_clear_pending_msg_events())); +} + +#[test] +fn test_no_disconnect_after_splice_aborted() { + // Test that the disconnect timer is cleared when exiting quiescence after a splice negotiation + // is aborted via tx_abort. Previously, `reset_pending_splice_state` cleared the quiescent + // state but not the disconnect timer, causing a spurious disconnect after the abort. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + complete_splice_handshake(&nodes[0], &nodes[1]); + + // Fire a tick while quiescent to arm the disconnect timer. + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + + // Abort the splice, which should clear the timer when exiting quiescence. + nodes[0].node.cancel_funding_contributed(&channel_id, &node_id_1).unwrap(); + + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::LocallyCanceled, + ); + + let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); + let tx_abort = msg_events + .iter() + .find_map(|event| { + if let MessageSendEvent::SendTxAbort { msg, .. } = event { + Some(msg.clone()) + } else { + None + } + }) + .expect("Expected SendTxAbort"); + + nodes[1].node.handle_tx_abort(node_id_0, &tx_abort); + let tx_abort_echo = get_event_msg!(nodes[1], MessageSendEvent::SendTxAbort, node_id_0); + nodes[1].node.get_and_clear_pending_events(); + + nodes[0].node.handle_tx_abort(node_id_1, &tx_abort_echo); + + // Fire enough ticks to trigger a disconnect if the timer wasn't properly cleared. + for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS { + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + } + + let has_disconnect = |events: &[MessageSendEvent]| { + events.iter().any(|event| { + matches!( + event, + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + ) + }) + }; + assert!(!has_disconnect(&nodes[0].node.get_and_clear_pending_msg_events())); + assert!(!has_disconnect(&nodes[1].node.get_and_clear_pending_msg_events())); +} + +#[test] +fn test_no_disconnect_after_quiescence_on_reconnect() { + // Test that there is no spurious disconnect after reconnecting from a quiescent state. The + // disconnect timer is cleared by `remove_uncommitted_htlcs_and_mark_paused` during + // disconnection and by `exit_quiescence` during reconnection. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); + + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + let funding_contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + complete_splice_handshake(&nodes[0], &nodes[1]); + + // Fire a tick while quiescent to arm the disconnect timer. + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + + // Disconnect and reconnect. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); + + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::PeerDisconnected, + ); + + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_channel_ready = (true, true); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); + + // Fire enough ticks to trigger a disconnect if the timer wasn't properly cleared. + for _ in 0..DISCONNECT_PEER_AWAITING_RESPONSE_TICKS { + nodes[0].node.timer_tick_occurred(); + nodes[1].node.timer_tick_occurred(); + } + + let has_disconnect = |events: &[MessageSendEvent]| { + events.iter().any(|event| { + matches!( + event, + MessageSendEvent::HandleError { + action: msgs::ErrorAction::DisconnectPeerWithWarning { .. }, + .. + } + ) + }) + }; + assert!(!has_disconnect(&nodes[0].node.get_and_clear_pending_msg_events())); + assert!(!has_disconnect(&nodes[1].node.get_and_clear_pending_msg_events())); +} + +#[test] +fn test_0reserve_splice() { + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let a = do_test_0reserve_splice_holder_validation(false, false, false, config.clone()); + let _b = do_test_0reserve_splice_holder_validation(true, false, false, config.clone()); + let _c = do_test_0reserve_splice_holder_validation(false, true, false, config.clone()); + let _d = do_test_0reserve_splice_holder_validation(true, true, false, config.clone()); + + let _e = do_test_0reserve_splice_holder_validation(false, false, true, config.clone()); + let _f = do_test_0reserve_splice_holder_validation(true, false, true, config.clone()); + let _g = do_test_0reserve_splice_holder_validation(false, true, true, config.clone()); + let _h = do_test_0reserve_splice_holder_validation(true, true, true, config.clone()); + + assert_eq!(a, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + let a = do_test_0reserve_splice_holder_validation(false, false, false, config.clone()); + let _b = do_test_0reserve_splice_holder_validation(true, false, false, config.clone()); + let _c = do_test_0reserve_splice_holder_validation(false, true, false, config.clone()); + let _d = do_test_0reserve_splice_holder_validation(true, true, false, config.clone()); + + let _e = do_test_0reserve_splice_holder_validation(false, false, true, config.clone()); + let _f = do_test_0reserve_splice_holder_validation(true, false, true, config.clone()); + let _g = do_test_0reserve_splice_holder_validation(false, true, true, config.clone()); + let _h = do_test_0reserve_splice_holder_validation(true, true, true, config.clone()); + + assert_eq!(a, ChannelTypeFeatures::anchors_zero_fee_commitments()); + + let mut config = test_default_channel_config(); + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = false; + let a = do_test_0reserve_splice_counterparty_validation(false, false, false, config.clone()); + let _b = do_test_0reserve_splice_counterparty_validation(true, false, false, config.clone()); + let _c = do_test_0reserve_splice_counterparty_validation(false, true, false, config.clone()); + let _d = do_test_0reserve_splice_counterparty_validation(true, true, false, config.clone()); + + let _e = do_test_0reserve_splice_counterparty_validation(false, false, true, config.clone()); + let _f = do_test_0reserve_splice_counterparty_validation(true, false, true, config.clone()); + let _g = do_test_0reserve_splice_counterparty_validation(false, true, true, config.clone()); + let _h = do_test_0reserve_splice_counterparty_validation(true, true, true, config.clone()); + + assert_eq!(a, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = false; + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + let a = do_test_0reserve_splice_counterparty_validation(false, false, false, config.clone()); + let _b = do_test_0reserve_splice_counterparty_validation(true, false, false, config.clone()); + let _c = do_test_0reserve_splice_counterparty_validation(false, true, false, config.clone()); + let _d = do_test_0reserve_splice_counterparty_validation(true, true, false, config.clone()); + + let _e = do_test_0reserve_splice_counterparty_validation(false, false, true, config.clone()); + let _f = do_test_0reserve_splice_counterparty_validation(true, false, true, config.clone()); + let _g = do_test_0reserve_splice_counterparty_validation(false, true, true, config.clone()); + let _h = do_test_0reserve_splice_counterparty_validation(true, true, true, config.clone()); + + assert_eq!(a, ChannelTypeFeatures::anchors_zero_fee_commitments()); +} + +#[cfg(test)] +fn do_test_0reserve_splice_holder_validation( + splice_passes: bool, counterparty_has_output: bool, node_0_is_initiator: bool, + mut config: UserConfig, +) -> ChannelTypeFeatures { + use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let _node_id_1 = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + // Some dust limit, does not matter + let dust_limit_satoshis = 546; + + let (channel_id, _tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + let details = &nodes[0].node.list_channels()[0]; + let channel_type = details.channel_type.clone().unwrap(); + + let feerate = + if channel_type == ChannelTypeFeatures::anchors_zero_fee_commitments() { 0 } else { 253 }; + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + ANCHOR_OUTPUT_VALUE_SATOSHI * 2 + } else { + 0 + }; + + let initiator_value_to_self_sat = if counterparty_has_output { + send_payment(&nodes[0], &[&nodes[1]], channel_value_sat / 2 * 1_000); + channel_value_sat / 2 + } else if !node_0_is_initiator { + let tx_fee_msat = chan_utils::commit_tx_fee_sat(feerate, 2, &channel_type) * 1000; + let node_0_details = &nodes[0].node.list_channels()[0]; + let outbound_capacity_msat = node_0_details.outbound_capacity_msat; + let available_capacity_msat = node_0_details.next_outbound_htlc_limit_msat; + assert_eq!(outbound_capacity_msat, (channel_value_sat - anchors_sat) * 1000); + assert_eq!(available_capacity_msat, outbound_capacity_msat - tx_fee_msat); + send_payment(&nodes[0], &[&nodes[1]], available_capacity_msat); + + // Make sure node 0 has no output on the commitment at this point + let node_0_to_local_output_msat = channel_value_sat * 1000 + - available_capacity_msat + - anchors_sat * 1000 + - chan_utils::commit_tx_fee_sat(feerate, 0, &channel_type) * 1000; + assert!(node_0_to_local_output_msat / 1000 < dust_limit_satoshis); + let commit_tx = &get_local_commitment_txn!(nodes[0], channel_id)[0]; + assert_eq!( + commit_tx.output.len(), + if channel_type == ChannelTypeFeatures::only_static_remote_key() { 1 } else { 2 } + ); + assert_eq!( + commit_tx.output.last().unwrap().value, + Amount::from_sat(available_capacity_msat / 1000) + ); + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + assert_eq!(commit_tx.output[0].value, Amount::from_sat(330)); + } else if channel_type == ChannelTypeFeatures::anchors_zero_fee_commitments() { + assert_eq!(commit_tx.output[0].value, Amount::ZERO); + } + + available_capacity_msat / 1000 + } else { + channel_value_sat + }; + + // The estimated fees to splice out a single output at 253sat/kw + let estimated_fees_sat = 183; + let mut splice_out_max_value = if counterparty_has_output && node_0_is_initiator { + let commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(feerate, 1, &channel_type); + Amount::from_sat( + initiator_value_to_self_sat - commit_tx_fee_sat - anchors_sat - estimated_fees_sat, + ) + } else if !counterparty_has_output && node_0_is_initiator { + let commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(feerate, 0, &channel_type); + Amount::from_sat( + initiator_value_to_self_sat + - commit_tx_fee_sat + - anchors_sat - estimated_fees_sat + - dust_limit_satoshis, + ) + } else if counterparty_has_output && !node_0_is_initiator { + Amount::from_sat(initiator_value_to_self_sat - estimated_fees_sat) + } else if !counterparty_has_output && !node_0_is_initiator { + Amount::from_sat(initiator_value_to_self_sat - estimated_fees_sat - dust_limit_satoshis) + } else { + panic!("unexpected case!"); + }; + + if channel_value_sat + < splice_out_max_value.to_sat() + estimated_fees_sat + MIN_CHANNEL_VALUE_SATOSHIS + { + splice_out_max_value = Amount::from_sat( + channel_value_sat.saturating_sub(estimated_fees_sat + MIN_CHANNEL_VALUE_SATOSHIS), + ); + } + + let outputs = vec![TxOut { + value: splice_out_max_value + if splice_passes { Amount::ZERO } else { Amount::ONE_SAT }, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + + let (initiator, acceptor) = + if node_0_is_initiator { (&nodes[0], &nodes[1]) } else { (&nodes[1], &nodes[0]) }; + + let initiator_details = &initiator.node.list_channels()[0]; + assert_eq!( + initiator_details.next_splice_out_maximum_sat, + splice_out_max_value.to_sat() + estimated_fees_sat + ); + + if splice_passes { + let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + + let (splice_tx, _) = splice_channel(initiator, acceptor, channel_id, contribution); + mine_transaction(initiator, &splice_tx); + mine_transaction(acceptor, &splice_tx); + lock_splice_after_blocks(initiator, acceptor, ANTI_REORG_DELAY - 1); + } else { + assert!(matches!( + build_splice_out_contribution(initiator, acceptor, channel_id, outputs), + Err(FundingContributionError::InvalidSpliceValue), + )); + } + + channel_type +} + +#[cfg(test)] +fn do_test_0reserve_splice_counterparty_validation( + splice_passes: bool, counterparty_has_output: bool, node_0_is_initiator: bool, + mut config: UserConfig, +) -> ChannelTypeFeatures { + use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _node_id_0 = nodes[0].node.get_our_node_id(); + let _node_id_1 = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + // Some dust limit, does not matter + let dust_limit_satoshis = 546; + + let (channel_id, _tx) = + setup_0reserve_no_outputs_channels(&nodes, channel_value_sat, dust_limit_satoshis); + let details = &nodes[0].node.list_channels()[0]; + let channel_type = details.channel_type.clone().unwrap(); + + let feerate = + if channel_type == ChannelTypeFeatures::anchors_zero_fee_commitments() { 0 } else { 253 }; + let anchors_sat = + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + ANCHOR_OUTPUT_VALUE_SATOSHI * 2 + } else { + 0 + }; + + let initiator_value_to_self_sat = if counterparty_has_output { + send_payment(&nodes[0], &[&nodes[1]], channel_value_sat / 2 * 1_000); + channel_value_sat / 2 + } else if !node_0_is_initiator { + let tx_fee_msat = chan_utils::commit_tx_fee_sat(feerate, 2, &channel_type) * 1000; + let node_0_details = &nodes[0].node.list_channels()[0]; + let outbound_capacity_msat = node_0_details.outbound_capacity_msat; + let available_capacity_msat = node_0_details.next_outbound_htlc_limit_msat; + assert_eq!(outbound_capacity_msat, (channel_value_sat - anchors_sat) * 1000); + assert_eq!(available_capacity_msat, outbound_capacity_msat - tx_fee_msat); + send_payment(&nodes[0], &[&nodes[1]], available_capacity_msat); + + // Make sure node 0 has no output on the commitment at this point + let node_0_to_local_output_msat = channel_value_sat * 1000 + - available_capacity_msat + - anchors_sat * 1000 + - chan_utils::commit_tx_fee_sat(feerate, 0, &channel_type) * 1000; + assert!(node_0_to_local_output_msat / 1000 < dust_limit_satoshis); + let commit_tx = &get_local_commitment_txn!(nodes[0], channel_id)[0]; + assert_eq!( + commit_tx.output.len(), + if channel_type == ChannelTypeFeatures::only_static_remote_key() { 1 } else { 2 } ); - let encoded_monitor_1 = get_monitor!(nodes[1], channel_id).encode(); - reload_node!( - nodes[1], - nodes[1].node.encode(), - &[&encoded_monitor_1], - persister_1a, - chain_monitor_1a, - node_1a + assert_eq!( + commit_tx.output.last().unwrap().value, + Amount::from_sat(available_capacity_msat / 1000) ); + if channel_type == ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies() { + assert_eq!(commit_tx.output[0].value, Amount::from_sat(330)); + } else if channel_type == ChannelTypeFeatures::anchors_zero_fee_commitments() { + assert_eq!(commit_tx.output[0].value, Amount::ZERO); + } + + available_capacity_msat / 1000 + } else { + channel_value_sat + }; + + let mut splice_out_value_incl_fees = if counterparty_has_output && node_0_is_initiator { + let commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(feerate, 1, &channel_type); + Amount::from_sat(initiator_value_to_self_sat - commit_tx_fee_sat - anchors_sat) + } else if !counterparty_has_output && node_0_is_initiator { + let commit_tx_fee_sat = chan_utils::commit_tx_fee_sat(feerate, 0, &channel_type); + Amount::from_sat( + initiator_value_to_self_sat - commit_tx_fee_sat - anchors_sat - dust_limit_satoshis, + ) + } else if counterparty_has_output && !node_0_is_initiator { + Amount::from_sat(initiator_value_to_self_sat) + } else if !counterparty_has_output && !node_0_is_initiator { + Amount::from_sat(initiator_value_to_self_sat - dust_limit_satoshis) + } else { + panic!("unexpected case!"); + }; + + if channel_value_sat < splice_out_value_incl_fees.to_sat() + MIN_CHANNEL_VALUE_SATOSHIS { + splice_out_value_incl_fees = + Amount::from_sat(channel_value_sat.saturating_sub(MIN_CHANNEL_VALUE_SATOSHIS)); } + let (initiator, acceptor) = + if node_0_is_initiator { (&nodes[0], &nodes[1]) } else { (&nodes[1], &nodes[0]) }; - // Reconnect the nodes. Both nodes should attempt quiescence as the initiator, but only one will - // be it via the tie-breaker. - let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); - reconnect_args.send_channel_ready = (true, true); - if !use_0conf { - reconnect_args.send_announcement_sigs = (true, true); + let initiator_details = &initiator.node.list_channels()[0]; + assert_eq!(initiator_details.next_splice_out_maximum_sat, splice_out_value_incl_fees.to_sat()); + + let funding_contribution_sat = + -(splice_out_value_incl_fees.to_sat() as i64) - if splice_passes { 0 } else { 1 }; + let post_channel_value_sat = + channel_value_sat.checked_add_signed(funding_contribution_sat).unwrap(); + + let outputs = vec![TxOut { + // Splice out some dummy amount to get past the initiator's validation, + // we'll modify the message in-flight. + value: Amount::from_sat(1_000), + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let _contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + let mut splice_init = + get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + // Make the modification here + splice_init.funding_contribution_satoshis = funding_contribution_sat; + + if splice_passes { + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let _splice_ack = + get_event_msg!(acceptor, MessageSendEvent::SendSpliceAck, node_id_initiator); + } else { + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let msg = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); + assert_eq!(msg.channel_id, channel_id); + let cannot_splice_out = if u64::try_from(funding_contribution_sat.abs()).unwrap() + > initiator_value_to_self_sat + { + // They obviously can't afford their contribution, so we fail before even + // querying `TxBuilder` + format!( + "Their contribution candidate {funding_contribution_sat}sat \ + is greater than their total balance in the channel {initiator_value_to_self_sat}sat" + ) + } else if post_channel_value_sat < MIN_CHANNEL_VALUE_SATOSHIS { + // We require all spliced channels to have a value of at least 1000 satoshis after the splice + format!( + "Spliced channel value must be at least {MIN_CHANNEL_VALUE_SATOSHIS} satoshis. \ + It would be {post_channel_value_sat}" + ) + } else { + // Last but not least, `TxBuilder` decides whether all parties can afford + // HTLCs, anchors, and transaction fees while retaining at least one + // output on the commitments + "Balance exhausted on local commitment".to_string() + }; + assert_eq!(tx_abort_data(&msg), format!("Invalid contribution: {cannot_splice_out}")); + acceptor.logger.assert_log( + "lightning::ln::channelmanager", + format!("Got non-closing error: Invalid contribution: {cannot_splice_out}"), + 1, + ); } - reconnect_args.send_stfu = (true, true); - reconnect_nodes(reconnect_args); - let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); - assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); - let (prev_funding_outpoint, prev_funding_script) = nodes[0] - .chain_monitor - .chain_monitor - .get_monitor(channel_id) - .map(|monitor| (monitor.get_funding_txo(), monitor.get_funding_script())) - .unwrap(); + channel_type +} - // Negotiate the first splice to completion. - nodes[1].node.handle_splice_init(node_id_0, &splice_init); - let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); - nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); - let new_funding_script = chan_utils::make_funding_redeemscript( - &splice_init.funding_pubkey, - &splice_ack.funding_pubkey, - ) - .to_p2wsh(); - complete_interactive_funding_negotiation( - &nodes[0], - &nodes[1], - channel_id, - node_0_contribution, - new_funding_script, +/// We previously allowed a splice initiator to splice out funds past their channel reserve if the +/// the acceptor had no balance in the channel, and there were no HTLCs in the channel +#[cfg(test)] +enum AcceptorBalance { + NoBalance, + BalanceInHTLC, + SettledBalance, +} + +#[cfg(test)] +enum ValidationCase { + Passes, + FailsAtHolder, + FailsAtCounterparty, +} + +#[test] +fn test_splice_out_initiator_reserve_breach_zero_fee_commitments() { + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::NoBalance, + ValidationCase::Passes, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::BalanceInHTLC, + ValidationCase::Passes, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::SettledBalance, + ValidationCase::Passes, ); - let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[0], &nodes[1], use_0conf); - expect_splice_pending_event(&nodes[0], &node_id_1); - expect_splice_pending_event(&nodes[1], &node_id_0); - let splice_locked = if use_0conf { - let (splice_locked, for_node_id) = splice_locked.unwrap(); - assert_eq!(for_node_id, node_id_1); - splice_locked - } else { - assert!(splice_locked.is_none()); + // We used to fail this case here + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::NoBalance, + ValidationCase::FailsAtHolder, + ); - mine_transaction(&nodes[0], &splice_tx); - mine_transaction(&nodes[1], &splice_tx); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::BalanceInHTLC, + ValidationCase::FailsAtHolder, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::SettledBalance, + ValidationCase::FailsAtHolder, + ); - // Mine enough blocks for the first splice to become locked. - connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); - connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); + // We used to fail this case here + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::NoBalance, + ValidationCase::FailsAtCounterparty, + ); - get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1) + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::BalanceInHTLC, + ValidationCase::FailsAtCounterparty, + ); + do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + AcceptorBalance::SettledBalance, + ValidationCase::FailsAtCounterparty, + ); +} + +#[cfg(test)] +fn do_test_splice_out_initiator_reserve_breach_zero_fee_commitments( + acceptor_balance: AcceptorBalance, validation_case: ValidationCase, +) { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + // This reserve breach was only possible in 0FC channels + config.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + config.channel_handshake_config.our_htlc_minimum_msat = 1; + let node_chanmgrs = + create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config.clone())]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + // Node 0 is initiator, node 1 is acceptor + let _node_id_0 = nodes[0].node.get_our_node_id(); + let _node_id_1 = nodes[1].node.get_our_node_id(); + + let channel_value_sat = 100_000; + let node_1_settled_balance_msat = + if matches!(acceptor_balance, AcceptorBalance::SettledBalance) { 1 } else { 0 }; + let node_1_htlc_balance_msat = + if matches!(acceptor_balance, AcceptorBalance::BalanceInHTLC) { 1 } else { 0 }; + let node_0_balance_msat = + channel_value_sat * 1000 - node_1_settled_balance_msat - node_1_htlc_balance_msat; + + // Bump initiator's dust limit to the highest value we allow in anchor channels + let high_dust_limit_satoshis = 10_000; + + let (_, _, channel_id, _tx) = create_announced_chan_between_nodes_with_value( + &nodes, + 0, + 1, + channel_value_sat, + node_1_settled_balance_msat, + ); + + if matches!(acceptor_balance, AcceptorBalance::BalanceInHTLC) { + let _ = route_payment(&nodes[0], &[&nodes[1]], node_1_htlc_balance_msat); + } + + let initiator = &nodes[0]; + let acceptor = &nodes[1]; + let node_id_initiator = initiator.node.get_our_node_id(); + let node_id_acceptor = acceptor.node.get_our_node_id(); + + // We use a stale funding template to get around the enforcement of + // [`FundingTemplate::spliceable_balance`]. + let stale_funding_template = + nodes[0].node.splice_channel(&channel_id, &node_id_acceptor).unwrap(); + let splice_out = |funding_template: FundingTemplate, outputs: Vec<TxOut>| { + let floor_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let feerate = funding_template.min_rbf_feerate().unwrap_or(floor_feerate); + let funding_contribution = + funding_template.splice_out(outputs, feerate, FeeRate::MAX).unwrap(); + match nodes[0].node.funding_contributed( + &channel_id, + &node_id_acceptor, + funding_contribution.clone(), + None, + ) { + Ok(()) => Ok(funding_contribution), + Err(e) => { + expect_splice_failed_events( + &nodes[0], + &channel_id, + funding_contribution, + NegotiationFailureReason::ContributionInvalid, + ); + Err(e) + }, + } }; - nodes[1].node.handle_splice_locked(node_id_0, &splice_locked); - // We should see the node which lost the tie-breaker attempt their splice now by first - // negotiating quiescence, but their `stfu` won't be sent until after another reconnection. - let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), if use_0conf { 2 } else { 3 }, "{msg_events:?}"); - if let MessageSendEvent::SendSpliceLocked { ref msg, .. } = &msg_events[0] { - nodes[0].node.handle_splice_locked(node_id_1, msg); - if use_0conf { - // TODO(splicing): Revisit splice transaction rebroadcasts. - let txn_0 = nodes[0].tx_broadcaster.txn_broadcast(); - assert_eq!(txn_0.len(), 1); - assert_eq!(&txn_0[0], &splice_tx); - mine_transaction(&nodes[0], &splice_tx); - mine_transaction(&nodes[1], &splice_tx); + { + let per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[0], nodes[1], per_peer_lock, peer_state_lock, channel_id); + if let Some(chan) = channel.as_funded_mut() { + chan.context.holder_dust_limit_satoshis = high_dust_limit_satoshis; + } else { + panic!("Unexpected Channel phase"); } - } else { - panic!("Unexpected event {:?}", &msg_events[0]); } - if !use_0conf { - if let MessageSendEvent::SendAnnouncementSignatures { ref msg, .. } = &msg_events[1] { - nodes[0].node.handle_announcement_signatures(node_id_1, msg); + + { + let per_peer_lock; + let mut peer_state_lock; + let channel = + get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, channel_id); + if let Some(chan) = channel.as_funded_mut() { + chan.context.counterparty_dust_limit_satoshis = high_dust_limit_satoshis; } else { - panic!("Unexpected event {:?}", &msg_events[1]); + panic!("Unexpected Channel phase"); } } - assert!(matches!( - &msg_events[if use_0conf { 1 } else { 2 }], - MessageSendEvent::SendStfu { .. } - )); - let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), if use_0conf { 0 } else { 2 }, "{msg_events:?}"); - if !use_0conf { - if let MessageSendEvent::SendAnnouncementSignatures { ref msg, .. } = &msg_events[0] { - nodes[1].node.handle_announcement_signatures(node_id_0, msg); + if matches!(validation_case, ValidationCase::Passes) { + let node_0_balance_leftover_amount = Amount::from_sat(high_dust_limit_satoshis); + // Estimated fees of a splice_out at 253sat/kw + let estimated_fees = 183; + // Note in 0FC we've got no fee spike buffer, no commit tx fee, no anchors + let splice_out_output_sat = + node_0_balance_msat / 1000 - node_0_balance_leftover_amount.to_sat() - estimated_fees; + let splice_out_output_amount = Amount::from_sat(splice_out_output_sat); + let outputs = vec![TxOut { + value: splice_out_output_amount, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let contribution = splice_out(stale_funding_template, outputs).unwrap(); + + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + mine_transaction(&nodes[0], &splice_tx); + mine_transaction(&nodes[1], &splice_tx); + lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + } else { + let node_0_balance_leftover_amount = Amount::from_sat(high_dust_limit_satoshis - 1); + // Note in 0FC we've got no fee spike buffer, no commit tx fee, no anchors + let funding_contribution_sat = + -((node_0_balance_msat / 1000 - node_0_balance_leftover_amount.to_sat()) as i64); + let value = if matches!(validation_case, ValidationCase::FailsAtHolder) { + Amount::from_sat(funding_contribution_sat.unsigned_abs() - 183) + } else if matches!(validation_case, ValidationCase::FailsAtCounterparty) { + // Splice out some dummy amount to get past the initiator's validation, + // we'll modify the message in-flight. + Amount::from_sat(1000) } else { - panic!("Unexpected event {:?}", &msg_events[1]); + panic!("Unexpected test case"); + }; + let outputs = vec![TxOut { + value, + script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), + }]; + let contribution = splice_out(stale_funding_template, outputs); + + if matches!(validation_case, ValidationCase::FailsAtHolder) { + assert_eq!( + contribution.unwrap_err(), + APIError::APIMisuseError { + err: format!("Channel {channel_id} cannot accept funding contribution"), + } + ); + let splice_out_value = value + Amount::from_sat(183); + let splice_out_max = splice_out_value - Amount::ONE_SAT; + let cannot_splice_out = format!( + "Channel {channel_id} cannot be funded: \ + Our splice-out value of {splice_out_value} is greater than the \ + maximum {splice_out_max}" + ); + nodes[0].logger.assert_log("lightning::ln::channel", cannot_splice_out, 1); + return; } - assert!(matches!(&msg_events[1], MessageSendEvent::BroadcastChannelAnnouncement { .. })); + + // The dummy contribution should have passed the holder's validation + assert!(contribution.is_ok()); + + // When acceptor has no balance, the reserve the initiator should keep should remain + // clamped at its dust limit. We previously allowed the initiator to withdraw past + // this point. + let v2_channel_reserve = Amount::from_sat(high_dust_limit_satoshis); + + let stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + acceptor.node.handle_stfu(node_id_initiator, &stfu_init); + let stfu_ack = get_event_msg!(acceptor, MessageSendEvent::SendStfu, node_id_initiator); + initiator.node.handle_stfu(node_id_acceptor, &stfu_ack); + + let mut splice_init = + get_event_msg!(initiator, MessageSendEvent::SendSpliceInit, node_id_acceptor); + // Make the modification here, acceptor should now complain. If the acceptor has no + // balance, we previously would not complain. + splice_init.funding_contribution_satoshis = funding_contribution_sat; + acceptor.node.handle_splice_init(node_id_initiator, &splice_init); + let msg = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); + assert_eq!(msg.channel_id, channel_id); + let post_splice_channel_value_sat = node_0_balance_leftover_amount.to_sat(); + let cannot_splice_out = if matches!(acceptor_balance, AcceptorBalance::NoBalance) { + format!( + "The post-splice channel value {post_splice_channel_value_sat} \ + is smaller than their dust limit {high_dust_limit_satoshis}" + ) + } else { + // As soon as we've pushed any sats out of our balance, the channel value + // is now at the dust limit, so we don't complain when determining the new + // dust limits, but later when we check the balances against those new + // dust limits + assert_eq!( + channel_value_sat.checked_add_signed(funding_contribution_sat).unwrap(), + high_dust_limit_satoshis + ); + format!( + "Their post-splice channel balance \ + {node_0_balance_leftover_amount} is smaller than our selected v2 reserve \ + {v2_channel_reserve}" + ) + }; + assert_eq!(tx_abort_data(&msg), format!("Invalid contribution: {cannot_splice_out}")); + acceptor.logger.assert_log( + "lightning::ln::channelmanager", + format!("Got non-closing error: Invalid contribution: {cannot_splice_out}"), + 1, + ); + } +} + +#[test] +fn test_splice_out_maximum_on_both_commitments_dust_on_fundee_commitment() { + use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + const CHANNEL_VALUE_SAT: u64 = 100_000; + const FEERATE: u32 = 253; + const TOTAL_ANCHORS_SAT: u64 = 2 * 330; + const NODE_0_DUST_LIMIT_SAT: u64 = 354; + const NODE_1_DUST_LIMIT_SAT: u64 = 10_000; + + let (channel_id, _transaction) = + setup_0reserve_no_outputs_channels(&nodes, CHANNEL_VALUE_SAT, NODE_0_DUST_LIMIT_SAT); + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().counterparty_dust_limit_satoshis = NODE_1_DUST_LIMIT_SAT; + assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_0_DUST_LIMIT_SAT); + assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0); + assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0)); + } + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().holder_dust_limit_satoshis = NODE_1_DUST_LIMIT_SAT; + assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_0_DUST_LIMIT_SAT); + assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0); + assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0)); + } + + let details = &nodes[0].node.list_channels()[0]; + let channel_type = details.channel_type.clone().unwrap(); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + // This HTLC is only present on node 0's commitment + const SNEAKY_HTLC_SAT: u64 = 5_000; + + let (_, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], SNEAKY_HTLC_SAT * 1000); + + let node_0_details = &nodes[0].node.list_channels()[0]; + let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 0, &channel_type); + let expected_next_splice_out_maximum_sat = CHANNEL_VALUE_SAT + - SNEAKY_HTLC_SAT + - TOTAL_ANCHORS_SAT + - reserved_fee_sat + - NODE_1_DUST_LIMIT_SAT; + assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat); + let node_1_details = &nodes[1].node.list_channels()[0]; + assert_eq!(node_1_details.next_splice_out_maximum_sat, 0); + + fail_payment(&nodes[0], &[&nodes[1]], payment_hash); + + let details = &nodes[0].node.list_channels()[0]; + let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 2, &channel_type); + let expected_available_capacity_sat = CHANNEL_VALUE_SAT - TOTAL_ANCHORS_SAT - reserved_fee_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, expected_available_capacity_sat * 1000); + let node_0_payment_sat = expected_available_capacity_sat; + send_payment(&nodes[0], &[&nodes[1]], node_0_payment_sat * 1000); + + // Make sure the local output is now gone from node 1's commitment + assert!(TOTAL_ANCHORS_SAT + reserved_fee_sat < NODE_1_DUST_LIMIT_SAT); + + let details = &nodes[1].node.list_channels()[0]; + let expected_next_splice_out_maximum_sat = node_0_payment_sat - NODE_1_DUST_LIMIT_SAT; + assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat); + + let details = &nodes[0].node.list_channels()[0]; + let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 1, &channel_type); + let expected_next_splice_out_maximum_sat = + CHANNEL_VALUE_SAT - node_0_payment_sat - TOTAL_ANCHORS_SAT - reserved_fee_sat; + assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat); +} + +#[test] +fn test_splice_out_maximum_on_both_commitments_dust_on_funder_commitment() { + use crate::ln::htlc_reserve_unit_tests::setup_0reserve_no_outputs_channels; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let mut config = test_default_channel_config(); + config.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + const CHANNEL_VALUE_SAT: u64 = 100_000; + const FEERATE: u32 = 253; + const TOTAL_ANCHORS_SAT: u64 = 2 * 330; + const NODE_0_DUST_LIMIT_SAT: u64 = 10_000; + const NODE_1_DUST_LIMIT_SAT: u64 = 354; + + let (channel_id, _transaction) = + setup_0reserve_no_outputs_channels(&nodes, CHANNEL_VALUE_SAT, NODE_1_DUST_LIMIT_SAT); + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[0], nodes[1], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().holder_dust_limit_satoshis = NODE_0_DUST_LIMIT_SAT; + assert_eq!(chan.context().counterparty_dust_limit_satoshis, NODE_1_DUST_LIMIT_SAT); + assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0); + assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0)); + } + + { + let per_peer_state_lock; + let mut peer_state_lock; + let chan = + get_channel_ref!(nodes[1], nodes[0], per_peer_state_lock, peer_state_lock, channel_id); + chan.context_mut().counterparty_dust_limit_satoshis = NODE_0_DUST_LIMIT_SAT; + assert_eq!(chan.context().holder_dust_limit_satoshis, NODE_1_DUST_LIMIT_SAT); + assert_eq!(chan.funding().holder_selected_channel_reserve_satoshis, 0); + assert_eq!(chan.funding().counterparty_selected_channel_reserve_satoshis, Some(0)); + } + + let details = &nodes[0].node.list_channels()[0]; + let channel_type = details.channel_type.clone().unwrap(); + assert_eq!(channel_type, ChannelTypeFeatures::anchors_zero_htlc_fee_and_dependencies()); + + // This HTLC is only present on node 1's commitment + const SNEAKY_HTLC_SAT: u64 = 5_000; + + let (_, payment_hash, ..) = route_payment(&nodes[0], &[&nodes[1]], SNEAKY_HTLC_SAT * 1000); + + let node_0_details = &nodes[0].node.list_channels()[0]; + let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 0, &channel_type); + let expected_next_splice_out_maximum_sat = CHANNEL_VALUE_SAT + - SNEAKY_HTLC_SAT + - TOTAL_ANCHORS_SAT + - reserved_fee_sat + - NODE_0_DUST_LIMIT_SAT; + assert_eq!(node_0_details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat); + let node_1_details = &nodes[1].node.list_channels()[0]; + assert_eq!(node_1_details.next_splice_out_maximum_sat, 0); + + fail_payment(&nodes[0], &[&nodes[1]], payment_hash); + + let details = &nodes[0].node.list_channels()[0]; + let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 2, &channel_type); + let expected_available_capacity_sat = CHANNEL_VALUE_SAT - TOTAL_ANCHORS_SAT - reserved_fee_sat; + assert_eq!(details.next_outbound_htlc_limit_msat, expected_available_capacity_sat * 1000); + let node_0_payment_sat = expected_available_capacity_sat; + send_payment(&nodes[0], &[&nodes[1]], node_0_payment_sat * 1000); + + // Make sure the local output is now gone from node 0's commitment + assert!(TOTAL_ANCHORS_SAT + reserved_fee_sat < NODE_0_DUST_LIMIT_SAT); + + let details = &nodes[1].node.list_channels()[0]; + let expected_next_splice_out_maximum_sat = node_0_payment_sat - NODE_0_DUST_LIMIT_SAT; + assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat); + + let details = &nodes[0].node.list_channels()[0]; + let reserved_fee_sat = chan_utils::commit_tx_fee_sat(FEERATE, 1, &channel_type); + let expected_next_splice_out_maximum_sat = + CHANNEL_VALUE_SAT - node_0_payment_sat - TOTAL_ANCHORS_SAT - reserved_fee_sat; + assert_eq!(details.next_splice_out_maximum_sat, expected_next_splice_out_maximum_sat); +} + +// When we advertise the next splice out maximum, we include any HTLCs in the state +// `InboundHTLCState::LocalRemoved(Fulfill { .. })` in our balance; by the time we clear this update +// and splice the channel, our settled balance will include it. +#[test] +fn test_splice_out_maximum_includes_pending_claimed_inbound_htlc() { + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + const CHANNEL_VALUE_MSAT: u64 = 100_000_000; + const PENDING_CLAIMED_INBOUND_HTLC_MSAT: u64 = 10_000_000; + + let node_id_0 = nodes[0].node.get_our_node_id(); + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, CHANNEL_VALUE_MSAT / 1000, 0); + + let (payment_preimage, payment_hash, ..) = + route_payment(&nodes[0], &[&nodes[1]], PENDING_CLAIMED_INBOUND_HTLC_MSAT); + + nodes[1].node.claim_funds(payment_preimage); + check_added_monitors(&nodes[1], 1); + expect_payment_claimed!(nodes[1], payment_hash, PENDING_CLAIMED_INBOUND_HTLC_MSAT); + + let updates = get_htlc_update_msgs(&nodes[1], &node_id_0); + assert!(updates.update_add_htlcs.is_empty()); + assert_eq!(updates.update_fulfill_htlcs.len(), 1); + assert!(updates.update_fail_htlcs.is_empty()); + assert!(updates.update_fail_malformed_htlcs.is_empty()); + assert!(updates.update_fee.is_none()); + assert_eq!(updates.commitment_signed.len(), 1); + + let node_1_details = &nodes[1].node.list_channels()[0]; + let local_balance_before_fee_sat = PENDING_CLAIMED_INBOUND_HTLC_MSAT / 1000; + let dividend_sat = local_balance_before_fee_sat * 100 + 100 - CHANNEL_VALUE_MSAT / 1000; + let expected_splice_out_max = (dividend_sat - 1) / 99; + assert_eq!(node_1_details.next_splice_out_maximum_sat, expected_splice_out_max); + + assert!(nodes[1].node.splice_channel(&channel_id, &node_id_0).is_ok()); +} + +#[test] +fn test_async_splice_receives_tx_signatures_while_unrelated_monitor_update_pending() { + let chanmon_cfgs = create_chanmon_cfgs(3); + let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(3, &node_cfgs, &[None, None, None]); + let nodes = create_network(3, &node_cfgs, &node_chanmgrs); + + let channel_id = create_announced_chan_between_nodes(&nodes, 0, 1).2; + create_announced_chan_between_nodes(&nodes, 1, 2); + + let (initiator, acceptor) = (&nodes[0], &nodes[1]); + let initiator_node_id = initiator.node.get_our_node_id(); + let acceptor_node_id = acceptor.node.get_our_node_id(); + let final_node_id = nodes[2].node.get_our_node_id(); + + // Leave a forwarded HTLC across A-B and B-C. Later, C will reveal the + // preimage so B has to persist an unrelated preimage update on A-B while the + // delayed splice `tx_signatures` are still in flight. + let (payment_preimage, payment_hash, ..) = + route_payment(initiator, &[acceptor, &nodes[2]], 1_000_000); + + // Keep the A-B splice from completing immediately at B. The disabled + // counterparty-commitment signer forces B to wait for both the signer and + // the splice monitor update before it can send `tx_signatures`. + acceptor.disable_channel_signer_op( + &initiator_node_id, + &channel_id, + SignerOp::SignCounterpartyCommitment, + ); + + let outputs = vec![TxOut { + value: Amount::from_sat(1_000), + script_pubkey: initiator.wallet_source.get_change_script().unwrap(), + }]; + let contribution = initiate_splice_out(initiator, acceptor, channel_id, outputs).unwrap(); + negotiate_splice_tx(initiator, acceptor, channel_id, contribution); + + let event = get_event!(initiator, Event::FundingTransactionReadyForSigning); + if let Event::FundingTransactionReadyForSigning { unsigned_transaction, .. } = event { + let partially_signed_tx = initiator.wallet_source.sign_tx(unsigned_transaction).unwrap(); + initiator + .node + .funding_transaction_signed(&channel_id, &acceptor_node_id, partially_signed_tx) + .unwrap(); + } + + let initiator_commit_sig = get_htlc_update_msgs(initiator, &acceptor_node_id); + + // B accepts A's splice commitment, but the monitor update remains pending. + // This is the async-signing window that normally guards emission of B's + // `tx_signatures`. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + acceptor + .node + .handle_commitment_signed(initiator_node_id, &initiator_commit_sig.commitment_signed[0]); + check_added_monitors(acceptor, 1); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + // Unblock only the signer side first. B can now produce its splice + // `commitment_signed`, but still must not send `tx_signatures` until the + // monitor update above completes. + acceptor.enable_channel_signer_op( + &initiator_node_id, + &channel_id, + SignerOp::SignCounterpartyCommitment, + ); + acceptor.node.signer_unblocked(None); + + let msg_events = acceptor.node.get_and_clear_pending_msg_events(); + assert_eq!(msg_events.len(), 1, "{msg_events:?}"); + if let MessageSendEvent::UpdateHTLCs { updates, .. } = &msg_events[0] { + initiator.node.handle_commitment_signed(acceptor_node_id, &updates.commitment_signed[0]); + check_added_monitors(initiator, 1); + } else { + panic!("Unexpected event"); + } + + // Completing B's splice monitor update releases B's `tx_signatures`. This + // is the update for which `monitor_pending_tx_signatures` is expected to be + // set. + acceptor.chain_monitor.complete_sole_pending_chan_update(&channel_id); + + let acceptor_tx_signatures = + get_event_msg!(acceptor, MessageSendEvent::SendTxSignatures, initiator_node_id); + initiator.node.handle_tx_signatures(acceptor_node_id, &acceptor_tx_signatures); + + // A can now fully sign and broadcast the splice transaction. Save A's + // reciprocal `tx_signatures` instead of delivering them to B, so B later + // sees an old splice message after another monitor update has started. + let delayed_initiator_tx_signatures = + get_event_msg!(initiator, MessageSendEvent::SendTxSignatures, acceptor_node_id); + let mut broadcasted = initiator.tx_broadcaster.txn_broadcast(); + assert_eq!(broadcasted.len(), 1, "{broadcasted:?}"); + let splice_tx = broadcasted.pop().unwrap(); + + // Confirm the splice on both A and B before B receives A's delayed + // `tx_signatures`. This mirrors the fuzz timeline where one side's + // broadcast can reach chain before the reciprocal message reaches its peer. + mine_transaction(initiator, &splice_tx); + mine_transaction(acceptor, &splice_tx); + let _ = get_event!(initiator, Event::SpliceNegotiated); + + // Claiming the forwarded payment at C creates an HTLC fulfill that B must + // propagate backward over the same A-B channel that is being spliced. + nodes[2].node.claim_funds(payment_preimage); + check_added_monitors(&nodes[2], 1); + expect_payment_claimed!(nodes[2], payment_hash, 1_000_000); + + let mut commitment_update = get_htlc_update_msgs(&nodes[2], &acceptor_node_id); + assert_eq!(commitment_update.update_fulfill_htlcs.len(), 1); + // Deliver only the fulfill to B and make B's A-B monitor update stay + // in-flight. This monitor update is unrelated to splice tx signatures: it + // durably records the payment preimage so B can safely settle the incoming + // HTLC from A. + chanmon_cfgs[1].persister.set_update_ret(ChannelMonitorUpdateStatus::InProgress); + acceptor.node.handle_update_fulfill_htlc( + final_node_id, + commitment_update.update_fulfill_htlcs.remove(0), + ); + check_added_monitors(acceptor, 1); + assert!(acceptor.node.get_and_clear_pending_msg_events().is_empty()); + + // Deliver A's delayed splice `tx_signatures` while B is waiting on the unrelated HTLC-preimage + // monitor update. B's `tx_signatures` was already released, so there's no message to send and + // we should expect the splice negotiation to complete. + acceptor.node.handle_tx_signatures(initiator_node_id, &delayed_initiator_tx_signatures); + + // Finally, drive the state machines to completion. + acceptor.chain_monitor.complete_sole_pending_chan_update(&channel_id); + let mut update_fulfill = get_htlc_update_msgs(acceptor, &initiator_node_id); + check_added_monitors(acceptor, 1); + let payment_forwarded = get_event!(acceptor, Event::PaymentForwarded); + expect_payment_forwarded( + payment_forwarded, + acceptor, + initiator, + &nodes[2], + Some(1000), + None, + false, + false, + false, + ); + + do_commitment_signed_dance( + acceptor, + &nodes[2], + &commitment_update.commitment_signed, + false, + false, + ); + + initiator.node.handle_update_fulfill_htlc( + acceptor_node_id, + update_fulfill.update_fulfill_htlcs.remove(0), + ); + do_commitment_signed_dance( + initiator, + acceptor, + &update_fulfill.commitment_signed, + false, + false, + ); + expect_payment_sent(initiator, payment_preimage, None, true, true); +} + +/// Returns the txid carried by a candidate's status, panicking for statuses that have none. +#[cfg(test)] +fn candidate_txid(candidate: &SpliceCandidateDetails) -> Txid { + match candidate.status { + SpliceCandidateStatus::AwaitingSignatures { txid, .. } + | SpliceCandidateStatus::Negotiated { txid, .. } => txid, + ref other => panic!("candidate status carries no txid: {other:?}"), } +} - let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), if use_0conf { 0 } else { 1 }, "{msg_events:?}"); - if !use_0conf { - assert!(matches!(&msg_events[0], MessageSendEvent::BroadcastChannelAnnouncement { .. })); +/// Returns the new channel value carried by a candidate's status, panicking for statuses that have +/// none. +#[cfg(test)] +fn candidate_value(candidate: &SpliceCandidateDetails) -> u64 { + match candidate.status { + SpliceCandidateStatus::ConstructingTransaction { new_channel_value_satoshis, .. } + | SpliceCandidateStatus::AwaitingSignatures { new_channel_value_satoshis, .. } + | SpliceCandidateStatus::Negotiated { new_channel_value_satoshis, .. } => { + new_channel_value_satoshis + }, + ref other => panic!("candidate status carries no value: {other:?}"), } +} - expect_channel_ready_event(&nodes[0], &node_id_1); - check_added_monitors(&nodes[0], 1); - expect_channel_ready_event(&nodes[1], &node_id_0); - check_added_monitors(&nodes[1], 1); +#[test] +fn test_channel_details_pending_splice() { + // Test that `ChannelDetails::splice_details` reflects pending splice state throughout + // negotiation, signing, RBF, restarts, and locking. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let (persister_0, persister_1); + let (chain_monitor_0, chain_monitor_1); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let (node_0, node_1); + let mut nodes = create_network(2, &node_cfgs, &node_chanmgrs); - // Remove the corresponding outputs and transactions the chain source is watching for the - // old funding as it is no longer being tracked. - nodes[0] - .chain_source - .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script.clone()); - nodes[1] - .chain_source - .remove_watched_txn_and_outputs(prev_funding_outpoint, prev_funding_script); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); - // Reconnect the nodes. This should trigger the node which lost the tie-breaker to resend `stfu` - // for their splice attempt. - if reload { - let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode(); - reload_node!( - nodes[0], - nodes[0].node.encode(), - &[&encoded_monitor_0], - persister_0b, - chain_monitor_0b, - node_0b - ); - let encoded_monitor_1 = get_monitor!(nodes[1], channel_id).encode(); - reload_node!( - nodes[1], - nodes[1].node.encode(), - &[&encoded_monitor_1], - persister_1b, - chain_monitor_1b, - node_1b - ); - } else { - nodes[0].node.peer_disconnected(node_id_1); - nodes[1].node.peer_disconnected(node_id_0); - } - let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); - if !use_0conf { - reconnect_args.send_announcement_sigs = (true, true); - } - reconnect_args.send_stfu = (true, false); - reconnect_nodes(reconnect_args); + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - // Drive the second splice to completion. - let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - if let MessageSendEvent::SendStfu { ref msg, .. } = msg_events[0] { - nodes[1].node.handle_stfu(node_id_0, msg); - } else { - panic!("Unexpected event {:?}", &msg_events[0]); - } + let splice_details = |node: &Node<'_, '_, '_>| { + node.node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + }; + + // No splice is pending yet. + assert_eq!(splice_details(&nodes[0]), None); + assert_eq!(splice_details(&nodes[1]), None); + + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Contributing funds queues the contribution but does not start the negotiation; that begins + // once the channel becomes quiescent and splice_init is sent. Until then it surfaces as a single + // candidate awaiting quiescence, carrying our contribution. + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + assert_eq!( + splice_details(&nodes[0]), + Some(SpliceDetails { + candidates: vec![SpliceCandidateDetails { + contribution: Some(contribution.clone()), + status: SpliceCandidateStatus::WaitingOnQuiescence, + }], + confirmed_candidate: None, + received_splice_locked_txid: None, + }), + ); + assert_eq!(splice_details(&nodes[1]), None); + + let new_channel_value_sat = + (initial_channel_value_sat as i64 + contribution.net_value().to_sat()) as u64; + + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + // Once quiescent, the initiator sends splice_init and awaits the counterparty's splice_ack. The + // new channel value and txid are not yet known, so the AwaitingAck status carries neither. + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 1); + assert_eq!( + details.candidates[0].status, + SpliceCandidateStatus::AwaitingAck { + is_initiator: true, + funding_feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW, + }, + ); + assert_eq!(details.candidates[0].contribution, Some(contribution.clone())); + assert_eq!(splice_details(&nodes[1]), None); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + + // The acceptor starts constructing the transaction upon receiving splice_init, at which + // point both contributions are known. + let details = splice_details(&nodes[1]).unwrap(); + assert_eq!(details.candidates.len(), 1); + assert_eq!( + details.candidates[0].status, + SpliceCandidateStatus::ConstructingTransaction { + is_initiator: false, + funding_feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW, + new_channel_value_satoshis: new_channel_value_sat, + }, + ); + assert_eq!(details.candidates[0].contribution, None); + + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 1); + assert_eq!( + details.candidates[0].status, + SpliceCandidateStatus::ConstructingTransaction { + is_initiator: true, + funding_feerate_sat_per_1000_weight: FEERATE_FLOOR_SATS_PER_KW, + new_channel_value_satoshis: new_channel_value_sat, + }, + ); - let splice_init = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceInit, node_id_0); - nodes[0].node.handle_splice_init(node_id_1, &splice_init); - let splice_ack = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceAck, node_id_1); - nodes[1].node.handle_splice_ack(node_id_0, &splice_ack); let new_funding_script = chan_utils::make_funding_redeemscript( &splice_init.funding_pubkey, &splice_ack.funding_pubkey, ) .to_p2wsh(); + complete_interactive_funding_negotiation( - &nodes[1], &nodes[0], + &nodes[1], channel_id, - node_1_contribution, - new_funding_script, + contribution.clone(), + new_funding_script.clone(), ); - let (splice_tx, splice_locked) = sign_interactive_funding_tx(&nodes[1], &nodes[0], use_0conf); - expect_splice_pending_event(&nodes[0], &node_id_1); - expect_splice_pending_event(&nodes[1], &node_id_0); - if use_0conf { - let (splice_locked, for_node_id) = splice_locked.unwrap(); - assert_eq!(for_node_id, node_id_0); - lock_splice(&nodes[1], &nodes[0], &splice_locked, true); - } else { - assert!(splice_locked.is_none()); - mine_transaction(&nodes[0], &splice_tx); - mine_transaction(&nodes[1], &splice_tx); - lock_splice_after_blocks(&nodes[1], &nodes[0], ANTI_REORG_DELAY - 1); - } + // Once construction completes, the negotiation awaits signatures and the txid is known. + let details_0 = splice_details(&nodes[0]).unwrap(); + let details_1 = splice_details(&nodes[1]).unwrap(); + assert_eq!(details_0.candidates.len(), 1); + assert_eq!(details_1.candidates.len(), 1); + assert!(matches!( + details_0.candidates[0].status, + SpliceCandidateStatus::AwaitingSignatures { is_initiator: true, .. } + )); + assert!(matches!( + details_1.candidates[0].status, + SpliceCandidateStatus::AwaitingSignatures { is_initiator: false, .. } + )); + assert_eq!(candidate_txid(&details_0.candidates[0]), candidate_txid(&details_1.candidates[0])); + assert_eq!(candidate_value(&details_0.candidates[0]), new_channel_value_sat); + assert_eq!(details_0.candidates[0].contribution, Some(contribution.clone())); + assert_eq!(details_1.candidates[0].contribution, None); - // Sanity check that we can still make a test payment. - send_payment(&nodes[0], &[&nodes[1]], 1_000_000); -} + let (splice_tx, splice_locked) = + sign_interactive_funding_tx(SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1])); + assert!(splice_locked.is_none()); + assert_eq!(candidate_txid(&details_0.candidates[0]), splice_tx.compute_txid()); -#[test] -fn disconnect_on_unexpected_interactive_tx_message() { - let chanmon_cfgs = create_chanmon_cfgs(2); - let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let config = test_default_channel_config(); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); - let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + expect_splice_pending_event(&nodes[0], &node_id_1); + // The acceptor did not contribute, so it gets no `SpliceNegotiated` event. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); - let initiator = &nodes[0]; - let acceptor = &nodes[1]; + // With signatures exchanged, the negotiated splice is a candidate awaiting confirmations. + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 1); + assert!(matches!(details.candidates[0].status, SpliceCandidateStatus::Negotiated { .. })); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!(candidate_value(&details.candidates[0]), new_channel_value_sat); + assert_eq!(details.candidates[0].contribution, Some(contribution.clone())); + assert_eq!(details.confirmed_candidate, None); + assert_eq!(details.received_splice_locked_txid, None); + + // The acceptor did not contribute to the splice. + let details = splice_details(&nodes[1]).unwrap(); + assert_eq!(details.candidates.len(), 1); + assert!(matches!(details.candidates[0].status, SpliceCandidateStatus::Negotiated { .. })); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!(details.candidates[0].contribution, None); + + // Initiate an RBF attempt at a higher feerate. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate_sat_per_kwu = FEERATE_FLOOR_SATS_PER_KW as u64 + 25; + let rbf_feerate = FeeRate::from_sat_per_kwu(rbf_feerate_sat_per_kwu); + let rbf_contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + + // The RBF contribution is queued behind the still-pending original candidate until quiescence + // is re-reached; until then it surfaces as a second candidate awaiting negotiation, alongside the + // original candidate. + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 2); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!(details.candidates[1].status, SpliceCandidateStatus::WaitingOnQuiescence); + assert_eq!(details.candidates[1].contribution, Some(rbf_contribution.clone())); + + // Reaching quiescence turns the queued RBF contribution into a negotiation. The initiator sends + // tx_init_rbf and awaits tx_ack_rbf, so the RBF round is reported as AwaitingAck alongside the + // still-pending original candidate. + let stfu_init = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_init); + let stfu_ack = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_ack); + + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 2); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!( + details.candidates[1].status, + SpliceCandidateStatus::AwaitingAck { + is_initiator: true, + funding_feerate_sat_per_1000_weight: rbf_feerate_sat_per_kwu as u32, + }, + ); + assert_eq!(details.candidates[1].contribution, Some(rbf_contribution.clone())); + + let tx_init_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxInitRbf, node_id_1); + nodes[1].node.handle_tx_init_rbf(node_id_0, &tx_init_rbf); + let tx_ack_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxAckRbf, node_id_0); + nodes[0].node.handle_tx_ack_rbf(node_id_1, &tx_ack_rbf); + + // The RBF negotiation then moves to constructing the transaction, still alongside the original + // candidate. + let rbf_channel_value_sat = + (initial_channel_value_sat as i64 + rbf_contribution.net_value().to_sat()) as u64; + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 2); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!(details.candidates[0].contribution, Some(contribution.clone())); + assert_eq!( + details.candidates[1].status, + SpliceCandidateStatus::ConstructingTransaction { + is_initiator: true, + funding_feerate_sat_per_1000_weight: rbf_feerate_sat_per_kwu as u32, + new_channel_value_satoshis: rbf_channel_value_sat, + }, + ); + assert_eq!(details.candidates[1].contribution, Some(rbf_contribution.clone())); - let node_id_initiator = initiator.node.get_our_node_id(); - let node_id_acceptor = acceptor.node.get_our_node_id(); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + rbf_contribution.clone(), + new_funding_script, + ); + let (rbf_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).replacing(splice_tx.compute_txid()), + ); + assert!(splice_locked.is_none()); - let initial_channel_capacity = 100_000; - let (_, _, channel_id, _) = - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + expect_splice_pending_event(&nodes[0], &node_id_1); + // The acceptor did not contribute, so it gets no `SpliceNegotiated` event. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); - let coinbase_tx = provide_anchor_reserves(&nodes); - let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - Some(nodes[0].wallet_source.get_change_script().unwrap()), + // Both the original splice and its RBF replacement are candidates, in negotiation order. + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 2); + assert!(details + .candidates + .iter() + .all(|c| matches!(c.status, SpliceCandidateStatus::Negotiated { .. }))); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!(details.candidates[0].contribution, Some(contribution.clone())); + assert_eq!(candidate_txid(&details.candidates[1]), rbf_tx.compute_txid()); + assert_eq!(candidate_value(&details.candidates[1]), rbf_channel_value_sat); + assert_eq!(details.candidates[1].contribution, Some(rbf_contribution.clone())); + + let details = splice_details(&nodes[1]).unwrap(); + assert_eq!(details.candidates.len(), 2); + assert_eq!(details.candidates[1].contribution, None); + + // Pending splice state, including per-candidate contributions, survives a restart. + let encoded_monitor_0 = get_monitor!(nodes[0], channel_id).encode(); + reload_node!( + nodes[0], + &nodes[0].node.encode(), + &[&encoded_monitor_0], + persister_0, + chain_monitor_0, + node_0 + ); + let encoded_monitor_1 = get_monitor!(nodes[1], channel_id).encode(); + reload_node!( + nodes[1], + &nodes[1].node.encode(), + &[&encoded_monitor_1], + persister_1, + chain_monitor_1, + node_1 ); - // Complete interactive-tx construction, but fail by having the acceptor send a duplicate - // tx_complete instead of commitment_signed. - negotiate_splice_tx(initiator, acceptor, channel_id, contribution.clone()); + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.candidates.len(), 2); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!(details.candidates[0].contribution, Some(contribution)); + assert_eq!(candidate_txid(&details.candidates[1]), rbf_tx.compute_txid()); + assert_eq!(details.candidates[1].contribution, Some(rbf_contribution)); - let _ = get_event!(initiator, Event::FundingTransactionReadyForSigning); - let _ = get_htlc_update_msgs(acceptor, &node_id_initiator); + let details = splice_details(&nodes[1]).unwrap(); + assert_eq!(details.candidates.len(), 2); + assert!(details.candidates.iter().all(|candidate| candidate.contribution.is_none())); - let tx_complete = msgs::TxComplete { channel_id }; - initiator.node.handle_tx_complete(node_id_acceptor, &tx_complete); + let mut reconnect_args = ReconnectArgs::new(&nodes[0], &nodes[1]); + reconnect_args.send_announcement_sigs = (true, true); + reconnect_nodes(reconnect_args); - let _warning = get_warning_msg(initiator, &node_id_acceptor); + // Mine the RBF transaction; only its candidate confirms, identified by its index. + mine_transaction(&nodes[0], &rbf_tx); + mine_transaction(&nodes[1], &rbf_tx); + + let details = splice_details(&nodes[0]).unwrap(); + let confirmed = details.confirmed_candidate.unwrap(); + assert_eq!(confirmed.txid, rbf_tx.compute_txid()); + assert_eq!(confirmed.confirmations, 1); + assert_eq!(confirmed.confirmations_required, 6); + // Not yet at the required depth, so we have not sent `splice_locked` for it. + assert!(!confirmed.splice_locked_sent); + + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); + + // Once sufficiently confirmed, the splice_locked we sent is reflected in the details until + // the counterparty's splice_locked is received and the splice is promoted. + let splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.received_splice_locked_txid, None); + let confirmed = details.confirmed_candidate.unwrap(); + assert_eq!(confirmed.txid, rbf_tx.compute_txid()); + assert!(confirmed.splice_locked_sent); + assert_eq!(confirmed.confirmations, ANTI_REORG_DELAY); + + lock_splice(&nodes[0], &nodes[1], &splice_locked, false, &[splice_tx.compute_txid()]); + + // The splice is no longer pending once promoted. + assert_eq!(splice_details(&nodes[0]), None); + assert_eq!(splice_details(&nodes[1]), None); } #[test] -fn fail_splice_on_interactive_tx_error() { +fn test_channel_details_first_contribution_on_rbf() { + // When the counterparty's splice did not include a contribution from us and our first + // contribution comes in an RBF round we initiate, the in-flight contribution must not be + // attributed to the negotiated counterparty-only candidate. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let config = test_default_channel_config(); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let initiator = &nodes[0]; - let acceptor = &nodes[1]; - - let node_id_initiator = initiator.node.get_our_node_id(); - let node_id_acceptor = acceptor.node.get_our_node_id(); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); - let initial_channel_capacity = 100_000; + let initial_channel_value_sat = 100_000; let (_, _, channel_id, _) = - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); - - let coinbase_tx = provide_anchor_reserves(&nodes); - let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - Some(nodes[0].wallet_source.get_change_script().unwrap()), - ); - - // Fail during interactive-tx construction by having the acceptor echo back tx_add_input instead - // of sending tx_complete. The failure occurs because the serial id will have the wrong parity. - let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); - - let tx_add_input = - get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); - acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - let _tx_complete = - get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); - initiator.node.handle_tx_add_input(node_id_acceptor, &tx_add_input); + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + + // Splice initiated by node 1; node 0 does not contribute. + let contribution = do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, added_value); + let (splice_tx, _) = splice_channel(&nodes[1], &nodes[0], channel_id, contribution); + + // Node 0 initiates an RBF, contributing for the first time. + let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + let funding_template = nodes[0].node.splice_channel(&channel_id, &node_id_1).unwrap(); + let wallet = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let rbf_contribution = funding_template + .without_prior_contribution(rbf_feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet) + .add_value(added_value) + .unwrap() + .build() + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, rbf_contribution.clone(), None) + .unwrap(); + complete_rbf_handshake(&nodes[0], &nodes[1]); + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendTxAddInput, node_id_1); + + // While the RBF is being negotiated, node 0's contribution belongs to the negotiation, not + // to the negotiated counterparty-only candidate. + let channels = nodes[0].node.list_channels(); + let details = channels[0].splice_details.as_ref().unwrap(); + assert_eq!(details.candidates.len(), 2); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert_eq!(details.candidates[0].contribution, None); + assert!(matches!( + details.candidates[1].status, + SpliceCandidateStatus::ConstructingTransaction { is_initiator: true, .. } + )); + assert_eq!(details.candidates[1].contribution, Some(rbf_contribution.clone())); - let event = get_event!(initiator, Event::SpliceFailed); - match event { - Event::SpliceFailed { contributed_inputs, .. } => { - assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); - }, - _ => panic!("Expected Event::SpliceFailed"), - } + // Node 1 adjusted its prior contribution for the RBF round; the negotiated candidate keeps + // its original contribution. Node 1 did not initiate this round, so `is_initiator` is + // `Some(false)` even though it carries a contribution into it. + let channels = nodes[1].node.list_channels(); + let details = channels[0].splice_details.as_ref().unwrap(); + assert!(matches!( + details.candidates[1].status, + SpliceCandidateStatus::ConstructingTransaction { is_initiator: false, .. } + )); + assert!(details.candidates[1].contribution.is_some()); + assert!(details.candidates[0].contribution.is_some()); - let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor); - acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort); + // Abort the negotiation via disconnect. + nodes[0].node.peer_disconnected(node_id_1); + nodes[1].node.peer_disconnected(node_id_0); - let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); - initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort); + expect_splice_failed_events( + &nodes[0], + &channel_id, + rbf_contribution, + NegotiationFailureReason::PeerDisconnected, + ); + // Node 1's contribution to the RBF round (the prior round's contribution adjusted to the new + // feerate) has no inputs or outputs unique from the prior round, so nothing is discarded, but + // it still gets a `SpliceNegotiationFailed` so the wallet can resume funding. + let _ = get_event!(&nodes[1], Event::SpliceNegotiationFailed); + + // After the reset, the contribution alignment is restored on both nodes. + let channels = nodes[0].node.list_channels(); + let details = channels[0].splice_details.as_ref().unwrap(); + assert_eq!(details.candidates.len(), 1); + assert_eq!(details.candidates[0].contribution, None); + let channels = nodes[1].node.list_channels(); + let details = channels[0].splice_details.as_ref().unwrap(); + assert_eq!(details.candidates.len(), 1); + assert!(details.candidates[0].contribution.is_some()); } #[test] -fn fail_splice_on_tx_abort() { +fn test_channel_details_zero_conf_splice() { + // On a zero-conf channel the splice is locked (we send `splice_locked`) before it has any + // confirmations, so `ChannelDetails::splice_details` must still report the locked candidate as + // the confirmed candidate at zero confirmations. Once both sides exchange `splice_locked` the + // splice is promoted to the channel funding and is no longer reported as pending. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let config = test_default_channel_config(); + let mut config = test_default_channel_config(); + config.channel_handshake_limits.trust_own_funding_0conf = true; let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let initiator = &nodes[0]; - let acceptor = &nodes[1]; + let node_id_1 = nodes[1].node.get_our_node_id(); - let node_id_initiator = initiator.node.get_our_node_id(); - let node_id_acceptor = acceptor.node.get_our_node_id(); + let initial_channel_value_sat = 100_000; + // Leave the original funding unconfirmed -- a zero-conf channel is usable without it -- so the + // test stays focused on the zero-conf splice. + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0); - let initial_channel_capacity = 100_000; - let (_, _, channel_id, _) = - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 1, added_value * 2); - let coinbase_tx = provide_anchor_reserves(&nodes); - let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - Some(nodes[0].wallet_source.get_change_script().unwrap()), + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script, ); - // Fail during interactive-tx construction by having the acceptor send tx_abort instead of - // tx_complete. - let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); - - let tx_add_input = - get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); - acceptor.node.handle_tx_add_input(node_id_initiator, &tx_add_input); - - let _tx_complete = - get_event_msg!(acceptor, MessageSendEvent::SendTxComplete, node_id_initiator); - - acceptor.node.abandon_splice(&channel_id, &node_id_initiator).unwrap(); - let tx_abort = get_event_msg!(acceptor, MessageSendEvent::SendTxAbort, node_id_initiator); - initiator.node.handle_tx_abort(node_id_acceptor, &tx_abort); - - let event = get_event!(initiator, Event::SpliceFailed); - match event { - Event::SpliceFailed { contributed_inputs, .. } => { - assert_eq!(contributed_inputs.len(), 1); - assert_eq!(contributed_inputs[0], contribution.inputs()[0].outpoint()); - }, - _ => panic!("Expected Event::SpliceFailed"), - } + // Sign the splice. The original funding is still unconfirmed, so signing also (re-)broadcasts it + // alongside the splice; the helper asserts that and returns the splice transaction. We leave node 0 + // without the counterparty's `splice_locked`, so the splice stays pending on node 0. + let (splice_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .zero_conf() + .with_unconfirmed_funding(funding_tx.compute_txid()), + ); - let tx_abort = get_event_msg!(initiator, MessageSendEvent::SendTxAbort, node_id_acceptor); - acceptor.node.handle_tx_abort(node_id_initiator, &tx_abort); + expect_splice_pending_event(&nodes[0], &node_id_1); + // The acceptor did not contribute, so it gets no `SpliceNegotiated` event. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + + // Node 0 has sent `splice_locked` but has not yet received the counterparty's, so the splice is + // still pending. The candidate we locked is reported as the confirmed candidate even though it + // has zero confirmations. + let details = nodes[0] + .node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + .unwrap(); + let confirmed = + details.confirmed_candidate.expect("the locked zero-conf candidate should be reported"); + assert_eq!(confirmed.txid, splice_tx.compute_txid()); + assert_eq!(confirmed.confirmations, 0); + assert_eq!(confirmed.confirmations_required, 0); + assert!(confirmed.splice_locked_sent); + assert_eq!(details.received_splice_locked_txid, None); + + // Exchange both sides' `splice_locked` to lock the splice in. Node 0 sent its at signing (above); + // `lock_splice` delivers it to node 1 and brings node 1's back, promoting the splice to the + // channel funding on both sides. + let (splice_locked_for_node_1, _) = + splice_locked.expect("a zero-conf splice sends splice_locked at signing"); + lock_splice(&nodes[0], &nodes[1], &splice_locked_for_node_1, true, &[]); + + // With the splice promoted, it is no longer reported as a pending splice. + let splice_details = |node: &Node<'_, '_, '_>| { + node.node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + }; + assert_eq!(splice_details(&nodes[0]), None); + assert_eq!(splice_details(&nodes[1]), None); } #[test] -fn fail_splice_on_channel_close() { +fn test_channel_details_waiting_on_lock_zero_conf() { + // On a zero-conf channel a committed contribution can never RBF the pending candidate (RBF is + // incompatible with zero-conf), so it is reported as `WaitingOnLock` — waiting for the candidate + // to lock before it can be spliced. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let config = test_default_channel_config(); + let mut config = test_default_channel_config(); + config.channel_handshake_limits.trust_own_funding_0conf = true; let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let initiator = &nodes[0]; - let acceptor = &nodes[1]; + let node_id_1 = nodes[1].node.get_our_node_id(); - let _node_id_initiator = initiator.node.get_our_node_id(); - let node_id_acceptor = acceptor.node.get_our_node_id(); + let initial_channel_value_sat = 100_000; + let (funding_tx, channel_id) = + open_zero_conf_channel_with_value(&nodes[0], &nodes[1], None, initial_channel_value_sat, 0); - let initial_channel_capacity = 100_000; - let (_, _, channel_id, _) = - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); - let coinbase_tx = provide_anchor_reserves(&nodes); - let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - Some(nodes[0].wallet_source.get_change_script().unwrap()), + // Complete a first splice; on a zero-conf channel node 0 sends `splice_locked` at signing, but the + // splice stays pending until the counterparty's `splice_locked` arrives. + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let new_funding_script = complete_splice_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + contribution, + new_funding_script, ); + let _ = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .zero_conf() + .with_unconfirmed_funding(funding_tx.compute_txid()), + ); + expect_splice_pending_event(&nodes[0], &node_id_1); + // The acceptor did not contribute, so it gets no `SpliceNegotiated` event. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); + nodes[0].node.get_and_clear_pending_msg_events(); - // Close the channel before completion of interactive-tx construction. - let _ = complete_splice_handshake(initiator, acceptor, channel_id, contribution.clone()); - let _tx_add_input = - get_event_msg!(initiator, MessageSendEvent::SendTxAddInput, node_id_acceptor); - - initiator + // Commit a further contribution; it cannot RBF the pending candidate, so no `stfu` is sent and it + // is reported as awaiting the lock. + let queued = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + let details = nodes[0] .node - .force_close_broadcasting_latest_txn(&channel_id, &node_id_acceptor, "test".to_owned()) + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() .unwrap(); - handle_bump_events(initiator, true, 0); - check_closed_events( - &nodes[0], - &[ExpectedCloseEvent { - channel_id: Some(channel_id), - discard_funding: false, - splice_failed: true, - channel_funding_txo: None, - user_channel_id: Some(42), - ..Default::default() - }], - ); - check_closed_broadcast(&nodes[0], 1, true); - check_added_monitors(&nodes[0], 1); + assert_eq!(details.candidates.len(), 2); + assert_eq!(details.candidates[1].status, SpliceCandidateStatus::WaitingOnLock); + assert_eq!(details.candidates[1].contribution, Some(queued)); + + // This test does not lock the splice in; drain the un-exchanged `splice_locked` messages so the + // nodes tear down cleanly. + nodes[0].node.get_and_clear_pending_msg_events(); + nodes[1].node.get_and_clear_pending_msg_events(); } #[test] -fn fail_quiescent_action_on_channel_close() { +fn test_channel_details_received_splice_locked() { + // `received_splice_locked_txid` reports the candidate the counterparty considers locked. Confirm + // the splice on only one node so it sends `splice_locked` while the other has not confirmed: the + // recipient records the received txid while the splice is still pending and unconfirmed for it. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); - let config = test_default_channel_config(); - let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(config.clone()), Some(config)]); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let initiator = &nodes[0]; - let acceptor = &nodes[1]; - - let _node_id_initiator = initiator.node.get_our_node_id(); - let node_id_acceptor = acceptor.node.get_our_node_id(); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); - let initial_channel_capacity = 100_000; + let initial_channel_value_sat = 100_000; let (_, _, channel_id, _) = - create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_capacity, 0); + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - let coinbase_tx = provide_anchor_reserves(&nodes); - let splice_in_amount = initial_channel_capacity / 2; - let contribution = SpliceContribution::splice_in( - Amount::from_sat(splice_in_amount), - vec![FundingTxInput::new_p2wpkh(coinbase_tx, 0).unwrap()], - Some(nodes[0].wallet_source.get_change_script().unwrap()), - ); + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); - // Close the channel before completion of STFU handshake. - initiator - .node - .splice_channel( - &channel_id, - &node_id_acceptor, - contribution, - FEERATE_FLOOR_SATS_PER_KW, - None, - ) - .unwrap(); + // Confirm the splice on node 0 only, so it sends `splice_locked` while node 1 has not confirmed. + mine_transaction(&nodes[0], &splice_tx); + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + let splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); - let _stfu_init = get_event_msg!(initiator, MessageSendEvent::SendStfu, node_id_acceptor); + nodes[1].node.handle_splice_locked(node_id_0, &splice_locked); - initiator + // Node 1 records the counterparty's locked candidate, but has not confirmed it itself, so it has + // no confirmed candidate of its own and the splice remains pending. + let details = nodes[1] .node - .force_close_broadcasting_latest_txn(&channel_id, &node_id_acceptor, "test".to_owned()) + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() .unwrap(); - handle_bump_events(initiator, true, 0); - check_closed_events( - &nodes[0], - &[ExpectedCloseEvent { - channel_id: Some(channel_id), - discard_funding: false, - splice_failed: true, - channel_funding_txo: None, - user_channel_id: Some(42), - ..Default::default() - }], - ); - check_closed_broadcast(&nodes[0], 1, true); - check_added_monitors(&nodes[0], 1); + assert_eq!(details.received_splice_locked_txid, Some(splice_tx.compute_txid())); + assert_eq!(details.confirmed_candidate, None); + assert_eq!(details.candidates.len(), 1); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + + // Committing a further contribution while the candidate is locking (we received its + // `splice_locked`) cannot RBF that candidate, so the queued contribution waits for the lock. This + // holds even though its feerate would satisfy the RBF minimum: the locking check takes priority. + nodes[1].node.get_and_clear_pending_msg_events(); + let queued = do_initiate_splice_in(&nodes[1], &nodes[0], channel_id, Amount::from_sat(25_000)); + assert!(nodes[1].node.get_and_clear_pending_msg_events().is_empty()); + let details = nodes[1] + .node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + .unwrap(); + assert_eq!(details.candidates.len(), 2); + assert_eq!(details.candidates[1].status, SpliceCandidateStatus::WaitingOnLock); + assert_eq!(details.candidates[1].contribution, Some(queued)); } -#[cfg(test)] -fn do_test_splice_with_inflight_htlc_forward_and_resolution(expire_scid_pre_forward: bool) { - // Test that we are still able to forward and resolve HTLCs while the original SCIDs contained - // in the onion packets have now changed due channel splices becoming locked. - let chanmon_cfgs = create_chanmon_cfgs(3); - let node_cfgs = create_node_cfgs(3, &chanmon_cfgs); - let mut config = test_default_channel_config(); - config.channel_config.cltv_expiry_delta = CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY as u16 * 2; - let node_chanmgrs = create_node_chanmgrs( - 3, - &node_cfgs, - &[Some(config.clone()), Some(config.clone()), Some(config)], - ); - let nodes = create_network(3, &node_cfgs, &node_chanmgrs); +#[test] +fn test_channel_details_splice_reorg_clears_confirmed_candidate() { + // A confirmed splice candidate we have locked is reported as the confirmed candidate; a reorg + // that unconfirms it clears the confirmed candidate, including the splice_locked we sent. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let node_id_0 = nodes[0].node.get_our_node_id(); let node_id_1 = nodes[1].node.get_our_node_id(); - let node_id_2 = nodes[2].node.get_our_node_id(); - - let (_, _, channel_id_0_1, _) = create_announced_chan_between_nodes(&nodes, 0, 1); - let (chan_upd_1_2, _, channel_id_1_2, _) = create_announced_chan_between_nodes(&nodes, 1, 2); - let node_max_height = - nodes.iter().map(|node| node.blocks.lock().unwrap().len()).max().unwrap() as u32; - connect_blocks(&nodes[0], node_max_height - nodes[0].best_block_info().1); - connect_blocks(&nodes[1], node_max_height - nodes[1].best_block_info().1); - connect_blocks(&nodes[2], node_max_height - nodes[2].best_block_info().1); - - // Send an outbound HTLC from node 0 to 2. - let payment_amount = 1_000_000; - let payment_params = - PaymentParameters::from_node_id(node_id_2, CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY * 2) - .with_bolt11_features(nodes[2].node.bolt11_invoice_features()) - .unwrap(); - let route_params = - RouteParameters::from_payment_params_and_value(payment_params, payment_amount); - let route = get_route(&nodes[0], &route_params).unwrap(); - let (_, payment_hash, payment_secret) = - get_payment_preimage_hash(&nodes[2], Some(payment_amount), None); - let onion = RecipientOnionFields::secret_only(payment_secret); - let id = PaymentId(payment_hash.0); - nodes[0].node.send_payment_with_route(route.clone(), payment_hash, onion, id).unwrap(); - check_added_monitors(&nodes[0], 1); - - // Node 1 should now have a pending HTLC to forward to 2. - let update_add_0_1 = get_htlc_update_msgs(&nodes[0], &node_id_1); - nodes[1].node.handle_update_add_htlc(node_id_0, &update_add_0_1.update_add_htlcs[0]); - let commitment = &update_add_0_1.commitment_signed; - do_commitment_signed_dance(&nodes[1], &nodes[0], commitment, false, false); - assert!(nodes[1].node.needs_pending_htlc_processing()); + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - // Splice both channels, lock them, and connect enough blocks to trigger the legacy SCID pruning - // logic while the HTLC is still pending. - let contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); - let splice_tx_0_1 = splice_channel(&nodes[0], &nodes[1], channel_id_0_1, contribution); - for node in &nodes { - mine_transaction(node, &splice_tx_0_1); - } + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); - let contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[1].wallet_source.get_change_script().unwrap(), - }]); - let splice_tx_1_2 = splice_channel(&nodes[1], &nodes[2], channel_id_1_2, contribution); - for node in &nodes { - mine_transaction(node, &splice_tx_1_2); - } + let splice_details = |node: &Node<'_, '_, '_>| { + node.node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + }; - for node in &nodes { - connect_blocks(node, ANTI_REORG_DELAY - 2); - } - let splice_locked = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); - lock_splice(&nodes[0], &nodes[1], &splice_locked, false); + // Confirm the splice on node 0 so it sends splice_locked and reports the confirmed candidate. + mine_transaction(&nodes[0], &splice_tx); + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); - for node in &nodes { - connect_blocks(node, 1); - } - let splice_locked = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceLocked, node_id_2); - lock_splice(&nodes[1], &nodes[2], &splice_locked, false); + let confirmed = splice_details(&nodes[0]).unwrap().confirmed_candidate.unwrap(); + assert_eq!(confirmed.txid, splice_tx.compute_txid()); + assert!(confirmed.splice_locked_sent); - if expire_scid_pre_forward { - for node in &nodes { - connect_blocks(node, CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY); - } + // Reorg out the blocks that confirmed the splice. The confirmed candidate is cleared, along with + // the splice_locked we sent for it; the candidate itself remains pending. + disconnect_blocks(&nodes[0], ANTI_REORG_DELAY); - // Now attempt to forward the HTLC from node 1 to 2 which will fail because the SCID is no - // longer stored and has expired. Obviously this is somewhat of an absurd case - not - // forwarding for `CHANNEL_ANNOUNCEMENT_PROPAGATION_DELAY` blocks is kinda nuts. - let fail_type = HTLCHandlingFailureType::InvalidForward { - requested_forward_scid: chan_upd_1_2.contents.short_channel_id, - }; - expect_htlc_forwarding_fails(&nodes[1], &[fail_type]); - check_added_monitors(&nodes[1], 1); - let update_fail_1_0 = get_htlc_update_msgs(&nodes[1], &node_id_0); - nodes[0].node.handle_update_fail_htlc(node_id_1, &update_fail_1_0.update_fail_htlcs[0]); - let commitment = &update_fail_1_0.commitment_signed; - do_commitment_signed_dance(&nodes[0], &nodes[1], commitment, false, false); + let details = splice_details(&nodes[0]).unwrap(); + assert_eq!(details.confirmed_candidate, None); + assert_eq!(details.candidates.len(), 1); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); +} - let conditions = PaymentFailedConditions::new(); - expect_payment_failed_conditions(&nodes[0], payment_hash, false, conditions); - } else { - // Now attempt to forward the HTLC from node 1 to 2. - nodes[1].node.process_pending_htlc_forwards(); - check_added_monitors(&nodes[1], 1); - let update_add_1_2 = get_htlc_update_msgs(&nodes[1], &node_id_2); - nodes[2].node.handle_update_add_htlc(node_id_1, &update_add_1_2.update_add_htlcs[0]); - let commitment = &update_add_1_2.commitment_signed; - do_commitment_signed_dance(&nodes[2], &nodes[1], commitment, false, false); - assert!(nodes[2].node.needs_pending_htlc_processing()); +#[test] +fn test_channel_details_received_splice_locked_diverges_from_confirmed() { + // `confirmed_candidate` and `received_splice_locked_txid` can name different candidates: across a + // reorg the two sides may each see a different RBF candidate confirm. Here node 0 confirms (and + // locks) the RBF candidate while node 1 confirms (and locks) the original, so node 0 ends up with + // a `received_splice_locked_txid` that differs from its own `confirmed_candidate`. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - // Node 2 should see the claimable payment. Fail it back to make sure we also handle the SCID - // change on the way back. - nodes[2].node.process_pending_htlc_forwards(); - expect_payment_claimable!(&nodes[2], payment_hash, payment_secret, payment_amount); - nodes[2].node.fail_htlc_backwards(&payment_hash); - let fail_type = HTLCHandlingFailureType::Receive { payment_hash }; - expect_and_process_pending_htlcs_and_htlc_handling_failed(&nodes[2], &[fail_type]); - check_added_monitors(&nodes[2], 1); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); - let update_fail_1_2 = get_htlc_update_msgs(&nodes[2], &node_id_1); - nodes[1].node.handle_update_fail_htlc(node_id_2, &update_fail_1_2.update_fail_htlcs[0]); - let commitment = &update_fail_1_2.commitment_signed; - do_commitment_signed_dance(&nodes[1], &nodes[2], commitment, false, false); - let fail_type = HTLCHandlingFailureType::Forward { - node_id: Some(node_id_2), - channel_id: channel_id_1_2, - }; - expect_and_process_pending_htlcs_and_htlc_handling_failed(&nodes[1], &[fail_type]); - check_added_monitors(&nodes[1], 1); + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - let update_fail_0_1 = get_htlc_update_msgs(&nodes[1], &node_id_0); - nodes[0].node.handle_update_fail_htlc(node_id_1, &update_fail_0_1.update_fail_htlcs[0]); - let commitment = &update_fail_0_1.commitment_signed; - do_commitment_signed_dance(&nodes[0], &nodes[1], commitment, false, false); + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, added_value * 2); + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (original_tx, new_funding_script) = + splice_channel(&nodes[0], &nodes[1], channel_id, contribution); + + // RBF the splice, producing a second candidate that double-spends the original. + provide_utxo_reserves(&nodes, 2, added_value * 2); + let rbf_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 + 25); + let rbf_contribution = do_initiate_rbf_splice_in(&nodes[0], &nodes[1], channel_id, rbf_feerate); + complete_rbf_handshake(&nodes[0], &nodes[1]); + complete_interactive_funding_negotiation( + &nodes[0], + &nodes[1], + channel_id, + rbf_contribution, + new_funding_script, + ); + let (rbf_tx, _) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]) + .replacing(original_tx.compute_txid()), + ); + expect_splice_pending_event(&nodes[0], &node_id_1); + // The acceptor did not contribute, so it gets no `SpliceNegotiated` event. + assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); - let conditions = PaymentFailedConditions::new(); - expect_payment_failed_conditions(&nodes[0], payment_hash, true, conditions); - } -} + let splice_details = |node: &Node<'_, '_, '_>| { + node.node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + }; -#[test] -fn test_splice_with_inflight_htlc_forward_and_resolution() { - do_test_splice_with_inflight_htlc_forward_and_resolution(true); - do_test_splice_with_inflight_htlc_forward_and_resolution(false); + // Node 0's chain confirms the RBF candidate, so it sends `splice_locked` for it. + mine_transaction(&nodes[0], &rbf_tx); + connect_blocks(&nodes[0], ANTI_REORG_DELAY - 1); + let _ = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceLocked, node_id_1); + + // Node 1's chain instead confirms the original candidate, so it sends `splice_locked` for that. + mine_transaction(&nodes[1], &original_tx); + connect_blocks(&nodes[1], ANTI_REORG_DELAY - 1); + let splice_locked_from_1 = + get_event_msg!(nodes[1], MessageSendEvent::SendSpliceLocked, node_id_0); + + // Node 0 records the counterparty's locked candidate (the original), which differs from the RBF + // candidate node 0 itself confirmed. The splice is not promoted, as the two sides disagree. + nodes[0].node.handle_splice_locked(node_id_1, &splice_locked_from_1); + + let details = splice_details(&nodes[0]).unwrap(); + let confirmed = details.confirmed_candidate.unwrap(); + assert_eq!(confirmed.txid, rbf_tx.compute_txid()); + assert!(confirmed.splice_locked_sent); + assert_eq!(details.received_splice_locked_txid, Some(original_tx.compute_txid())); + assert_ne!(Some(confirmed.txid), details.received_splice_locked_txid); } #[test] -fn test_splice_buffer_commitment_signed_until_funding_tx_signed() { - // Test that when the counterparty sends their initial `commitment_signed` before the user has - // called `funding_transaction_signed`, we buffer the message and process it at the end of - // `funding_transaction_signed`. This allows the user to cancel the splice negotiation if - // desired without having queued an irreversible monitor update. +fn test_channel_details_acceptor_contribution_with_queued_rbf() { + // An acceptor that contributes to the counterparty's round (its committed contribution merging + // into that round via the quiescence tie-break) can also queue a further contribution for a + // future RBF. Both surface together as candidates: the in-flight counterparty round carries our + // part of it, alongside a separate candidate for the contribution we queued for the next round. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); @@ -2254,114 +11762,190 @@ fn test_splice_buffer_commitment_signed_until_funding_tx_signed() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - // Negotiate a splice-out where only the initiator (node 0) has a contribution. - // This means node 1 will send their commitment_signed immediately after tx_complete. - let initiator_contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); - negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); - // Node 0 (initiator with contribution) should have a signing event to handle. - let signing_event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); + // Both nodes commit a contribution and propose a splice. The tie-break makes node 0 (the funder) + // the initiator; node 1 becomes the acceptor and its contribution merges into node 0's round. + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let contribution_0 = nodes[0] + .node + .splice_channel(&channel_id, &node_id_1) + .unwrap() + .splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_0) + .unwrap(); + nodes[0].node.funding_contributed(&channel_id, &node_id_1, contribution_0, None).unwrap(); - // Node 1 (acceptor with no contribution) won't have a signing event and will immediately - // send their initial commitment_signed. - assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); - assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); - let acceptor_commit_sig = get_htlc_update_msgs(&nodes[1], &node_id_0); + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let contribution_1 = nodes[1] + .node + .splice_channel(&channel_id, &node_id_0) + .unwrap() + .splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_1) + .unwrap(); + nodes[1].node.funding_contributed(&channel_id, &node_id_0, contribution_1, None).unwrap(); - // Deliver the acceptor's commitment_signed to the initiator BEFORE the initiator has called - // funding_transaction_signed. The message should be buffered, not processed. - nodes[0].node.handle_commitment_signed(node_id_1, &acceptor_commit_sig.commitment_signed[0]); + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); - // No monitor update should have happened since the message is buffered. - check_added_monitors(&nodes[0], 0); - assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!( + splice_ack.funding_contribution_satoshis, 0, + "the acceptor should contribute to the counterparty's round", + ); - // Now handle the signing event and call `funding_transaction_signed`. - if let Event::FundingTransactionReadyForSigning { - channel_id: event_channel_id, - counterparty_node_id, - unsigned_transaction, - .. - } = signing_event - { - assert_eq!(event_channel_id, channel_id); - assert_eq!(counterparty_node_id, node_id_1); + // Node 1 queues a further contribution for a future RBF while node 0's round is still in flight. + let rbf_template = nodes[1].node.splice_channel(&channel_id, &node_id_0).unwrap(); + let rbf_feerate = rbf_template.min_rbf_feerate().unwrap(); + let queued = rbf_template + .splice_in_sync(Amount::from_sat(25_000), rbf_feerate, FeeRate::MAX, &wallet_1) + .unwrap(); + nodes[1].node.funding_contributed(&channel_id, &node_id_0, queued.clone(), None).unwrap(); - let partially_signed_tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); - nodes[0] - .node - .funding_transaction_signed(&channel_id, &node_id_1, partially_signed_tx) - .unwrap(); - } else { - panic!("Expected FundingTransactionReadyForSigning event"); - } + // Node 1's view: it contributed to node 0's (counterparty) round AND has its own RBF queued. + let details = nodes[1] + .node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + .unwrap(); + assert_eq!(details.candidates.len(), 2); + // Our part of node 0's in-flight round, which we did not initiate. + assert!(matches!( + details.candidates[0].status, + SpliceCandidateStatus::ConstructingTransaction { is_initiator: false, .. } + )); + assert!(details.candidates[0].contribution.is_some()); + // Our further contribution, queued to RBF that round once it completes. + assert_eq!(details.candidates[1].status, SpliceCandidateStatus::WaitingOnQuiescence); + assert_eq!(details.candidates[1].contribution, Some(queued)); +} - // After funding_transaction_signed: - // 1. The initiator should send their commitment_signed - // 2. The buffered commitment_signed from the acceptor should be processed (monitor update) - let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - let initiator_commit_sig = - if let MessageSendEvent::UpdateHTLCs { ref updates, .. } = &msg_events[0] { - updates.commitment_signed[0].clone() - } else { - panic!("Expected UpdateHTLCs message"); - }; +#[test] +fn test_channel_details_acceptor_contribution_reaches_signing() { + // An acceptor that contributes to a counterparty-initiated round is reported with + // `is_initiator: false` and its own contribution present, through the awaiting-signatures stage + // and into the negotiated candidate. + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - // The buffered commitment_signed should have been processed, resulting in a monitor update. - check_added_monitors(&nodes[0], 1); + let node_id_0 = nodes[0].node.get_our_node_id(); + let node_id_1 = nodes[1].node.get_our_node_id(); - // Complete the rest of the flow normally. - nodes[1].node.handle_commitment_signed(node_id_0, &initiator_commit_sig); - let msg_events = nodes[1].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[0] { - nodes[0].node.handle_tx_signatures(node_id_1, msg); - } else { - panic!("Expected SendTxSignatures message"); - } - check_added_monitors(&nodes[1], 1); + let initial_channel_value_sat = 100_000; + let (_, _, channel_id, _) = + create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), 1, "{msg_events:?}"); - if let MessageSendEvent::SendTxSignatures { ref msg, .. } = &msg_events[0] { - nodes[1].node.handle_tx_signatures(node_id_0, msg); - } else { - panic!("Expected SendTxSignatures message"); - } + let added_value = Amount::from_sat(50_000); + provide_utxo_reserves(&nodes, 2, Amount::from_sat(100_000)); - expect_splice_pending_event(&nodes[0], &node_id_1); - expect_splice_pending_event(&nodes[1], &node_id_0); + // Both nodes commit a contribution at the same feerate; node 0 (the funder) wins the tie-break + // and initiates, node 1 becomes the acceptor and its contribution merges into node 0's round. + let feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64); + let wallet_0 = WalletSync::new(Arc::clone(&nodes[0].wallet_source), nodes[0].logger); + let contribution_0 = nodes[0] + .node + .splice_channel(&channel_id, &node_id_1) + .unwrap() + .splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_0) + .unwrap(); + nodes[0] + .node + .funding_contributed(&channel_id, &node_id_1, contribution_0.clone(), None) + .unwrap(); - // Both nodes should broadcast the splice transaction. - let splice_tx = { - let mut txn_0 = nodes[0].tx_broadcaster.txn_broadcast(); - assert_eq!(txn_0.len(), 1); - let txn_1 = nodes[1].tx_broadcaster.txn_broadcast(); - assert_eq!(txn_0, txn_1); - txn_0.remove(0) + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let contribution_1 = nodes[1] + .node + .splice_channel(&channel_id, &node_id_0) + .unwrap() + .splice_in_sync(added_value, feerate, FeeRate::MAX, &wallet_1) + .unwrap(); + nodes[1] + .node + .funding_contributed(&channel_id, &node_id_0, contribution_1.clone(), None) + .unwrap(); + + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + + let splice_init = get_event_msg!(nodes[0], MessageSendEvent::SendSpliceInit, node_id_1); + nodes[1].node.handle_splice_init(node_id_0, &splice_init); + let splice_ack = get_event_msg!(nodes[1], MessageSendEvent::SendSpliceAck, node_id_0); + assert_ne!( + splice_ack.funding_contribution_satoshis, 0, + "the acceptor should contribute to the counterparty's round", + ); + nodes[0].node.handle_splice_ack(node_id_1, &splice_ack); + + let new_funding_script = chan_utils::make_funding_redeemscript( + &splice_init.funding_pubkey, + &splice_ack.funding_pubkey, + ) + .to_p2wsh(); + + complete_interactive_funding_negotiation_for_both( + &nodes[0], + &nodes[1], + channel_id, + contribution_0, + Some(contribution_1), + splice_ack.funding_contribution_satoshis, + new_funding_script, + ); + + let splice_details = |node: &Node<'_, '_, '_>| { + node.node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + .unwrap() }; - // Verify the channel is operational by sending a payment. - send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + // The acceptor's in-flight round awaits signatures, carrying its own (adjusted) contribution. + let details = splice_details(&nodes[1]); + assert_eq!(details.candidates.len(), 1); + assert!(matches!( + details.candidates[0].status, + SpliceCandidateStatus::AwaitingSignatures { is_initiator: false, .. } + )); + assert!(details.candidates[0].contribution.is_some()); - // Lock the splice by confirming the transaction. - mine_transaction(&nodes[0], &splice_tx); - mine_transaction(&nodes[1], &splice_tx); - lock_splice_after_blocks(&nodes[0], &nodes[1], ANTI_REORG_DELAY - 1); + let (_splice_tx, splice_locked) = sign_interactive_funding_tx( + SignInteractiveFundingTxArgs::new(&nodes[0], &nodes[1]).with_acceptor_contribution(), + ); + assert!(splice_locked.is_none()); + expect_splice_pending_event(&nodes[0], &node_id_1); + expect_splice_pending_event(&nodes[1], &node_id_0); - // Verify the channel is still operational by sending another payment. - send_payment(&nodes[0], &[&nodes[1]], 1_000_000); + // Once signed, the acceptor's negotiated candidate still carries its contribution. + let details = splice_details(&nodes[1]); + assert_eq!(details.candidates.len(), 1); + assert!(matches!(details.candidates[0].status, SpliceCandidateStatus::Negotiated { .. })); + assert!(details.candidates[0].contribution.is_some()); } #[test] -fn test_splice_buffer_invalid_commitment_signed_closes_channel() { - // Test that when the counterparty sends an invalid `commitment_signed` (with a bad signature) - // before the user has called `funding_transaction_signed`, the channel is closed with an error - // when `ChannelManager::funding_transaction_signed` processes the buffered message. +fn test_channel_details_waiting_on_lock_below_rbf_feerate() { + // A committed contribution whose feerate is below the RBF minimum of the round currently in + // flight cannot replace it, so it is reported as `WaitingOnLock`. This exercises the feerate + // branch of the classification (the zero-conf and locking checks do not apply here) and produces + // the full negotiated -> in-flight -> queued three-candidate ordering. let chanmon_cfgs = create_chanmon_cfgs(2); let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); @@ -2374,94 +11958,66 @@ fn test_splice_buffer_invalid_commitment_signed_closes_channel() { let (_, _, channel_id, _) = create_announced_chan_between_nodes_with_value(&nodes, 0, 1, initial_channel_value_sat, 0); - // Negotiate a splice-out where only the initiator (node 0) has a contribution. - // This means node 1 will send their commitment_signed immediately after tx_complete. - let initiator_contribution = SpliceContribution::splice_out(vec![TxOut { - value: Amount::from_sat(1_000), - script_pubkey: nodes[0].wallet_source.get_change_script().unwrap(), - }]); - negotiate_splice_tx(&nodes[0], &nodes[1], channel_id, initiator_contribution); - - // Node 0 (initiator with contribution) should have a signing event to handle. - let signing_event = get_event!(nodes[0], Event::FundingTransactionReadyForSigning); - - // Node 1 (acceptor with no contribution) won't have a signing event and will immediately - // send their initial commitment_signed. - assert!(nodes[1].node.get_and_clear_pending_events().is_empty()); - assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); - let mut acceptor_commit_sig = get_htlc_update_msgs(&nodes[1], &node_id_0); - - // Invalidate the signature by modifying one byte. This will cause signature verification - // to fail when the buffered message is processed. - let original_sig = acceptor_commit_sig.commitment_signed[0].signature; - let mut sig_bytes = original_sig.serialize_compact(); - sig_bytes[0] ^= 0x01; // Flip a bit to corrupt the signature - acceptor_commit_sig.commitment_signed[0].signature = - Signature::from_compact(&sig_bytes).unwrap(); + let added_value = Amount::from_sat(50_000); - // Deliver the acceptor's invalid commitment_signed to the initiator BEFORE the initiator has - // called funding_transaction_signed. The message should be buffered, not processed. - nodes[0].node.handle_commitment_signed(node_id_1, &acceptor_commit_sig.commitment_signed[0]); + // Complete a first splice at the floor feerate, leaving a negotiated candidate. + provide_utxo_reserves(&nodes, 1, added_value * 2); + let contribution = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); + let (splice_tx, _) = splice_channel(&nodes[0], &nodes[1], channel_id, contribution); - // No monitor update should have happened since the message is buffered. - check_added_monitors(&nodes[0], 0); + // The counterparty (node 1) initiates an RBF at a much higher feerate; we drive it in flight on + // node 0 (node 1 wins quiescence, as node 0 has nothing of its own queued yet). + provide_utxo_reserves(&nodes, 1, added_value * 2); + let high_feerate = FeeRate::from_sat_per_kwu(FEERATE_FLOOR_SATS_PER_KW as u64 * 4); + // Node 1 did not contribute to the original splice, so it RBFs with a first contribution. + let wallet_1 = WalletSync::new(Arc::clone(&nodes[1].wallet_source), nodes[1].logger); + let rbf_contribution = nodes[1] + .node + .splice_channel(&channel_id, &node_id_0) + .unwrap() + .without_prior_contribution(high_feerate, FeeRate::MAX) + .with_coin_selection_source_sync(&wallet_1) + .add_value(added_value) + .unwrap() + .build() + .unwrap(); + nodes[1].node.funding_contributed(&channel_id, &node_id_0, rbf_contribution, None).unwrap(); + let stfu_1 = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_id_0); + nodes[0].node.handle_stfu(node_id_1, &stfu_1); + let stfu_0 = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_id_1); + nodes[1].node.handle_stfu(node_id_0, &stfu_0); + let tx_init_rbf = get_event_msg!(nodes[1], MessageSendEvent::SendTxInitRbf, node_id_0); + nodes[0].node.handle_tx_init_rbf(node_id_1, &tx_init_rbf); + let _tx_ack_rbf = get_event_msg!(nodes[0], MessageSendEvent::SendTxAckRbf, node_id_1); + + // Node 0 commits its own contribution at the floor RBF feerate. That is enough to replace the + // original candidate, but not the higher-feerate round now in flight, so it waits for the lock. + provide_utxo_reserves(&nodes, 1, added_value * 2); + let queued = do_initiate_splice_in(&nodes[0], &nodes[1], channel_id, added_value); assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); - // Now handle the signing event and call `funding_transaction_signed`. - // This should process the buffered invalid commitment_signed and close the channel. - if let Event::FundingTransactionReadyForSigning { - channel_id: event_channel_id, - counterparty_node_id, - unsigned_transaction, - .. - } = signing_event - { - assert_eq!(event_channel_id, channel_id); - assert_eq!(counterparty_node_id, node_id_1); - - let partially_signed_tx = nodes[0].wallet_source.sign_tx(unsigned_transaction).unwrap(); - nodes[0] - .node - .funding_transaction_signed(&channel_id, &node_id_1, partially_signed_tx) - .unwrap(); - } else { - panic!("Expected FundingTransactionReadyForSigning event"); - } - - // After funding_transaction_signed: - // 1. The initiator sends its commitment_signed (UpdateHTLCs message). - // 2. The buffered invalid commitment_signed from the acceptor is processed, causing the - // channel to close due to the invalid signature. - // We expect 3 message events: UpdateHTLCs, BroadcastChannelUpdate, and HandleError. - let msg_events = nodes[0].node.get_and_clear_pending_msg_events(); - assert_eq!(msg_events.len(), 3, "{msg_events:?}"); - match &msg_events[0] { - MessageSendEvent::UpdateHTLCs { ref updates, .. } => { - assert!(!updates.commitment_signed.is_empty()); - }, - _ => panic!("Expected UpdateHTLCs message, got {:?}", msg_events[0]), - } - match &msg_events[1] { - MessageSendEvent::HandleError { - action: msgs::ErrorAction::SendErrorMessage { ref msg }, - .. - } => { - assert!(msg.data.contains("Invalid commitment tx signature from peer")); - }, - _ => panic!("Expected HandleError with SendErrorMessage, got {:?}", msg_events[1]), - } - match &msg_events[2] { - MessageSendEvent::BroadcastChannelUpdate { ref msg, .. } => { - assert_eq!(msg.contents.channel_flags & 2, 2); - }, - _ => panic!("Expected BroadcastChannelUpdate, got {:?}", msg_events[2]), - } + let details = nodes[0] + .node + .list_channels() + .iter() + .find(|channel| channel.channel_id == channel_id) + .unwrap() + .splice_details + .clone() + .unwrap(); + // Negotiated original, the counterparty's in-flight higher-feerate RBF, then our queued + // contribution awaiting the lock. + assert_eq!(details.candidates.len(), 3); + assert!(matches!(details.candidates[0].status, SpliceCandidateStatus::Negotiated { .. })); + assert_eq!(candidate_txid(&details.candidates[0]), splice_tx.compute_txid()); + assert!(matches!( + details.candidates[1].status, + SpliceCandidateStatus::ConstructingTransaction { is_initiator: false, .. } + )); + assert_eq!(details.candidates[2].status, SpliceCandidateStatus::WaitingOnLock); + assert_eq!(details.candidates[2].contribution, Some(queued)); - let err = "Invalid commitment tx signature from peer".to_owned(); - let reason = ClosureReason::ProcessingError { err }; - check_closed_events( - &nodes[0], - &[ExpectedCloseEvent::from_id_reason(channel_id, false, reason)], - ); - check_added_monitors(&nodes[0], 1); + // This test leaves an RBF round in flight; drain the un-exchanged messages for a clean teardown. + nodes[0].node.get_and_clear_pending_msg_events(); + nodes[1].node.get_and_clear_pending_msg_events(); } diff --git a/lightning/src/ln/trampoline_forward_tests.rs b/lightning/src/ln/trampoline_forward_tests.rs new file mode 100644 index 00000000000..c2c4f698399 --- /dev/null +++ b/lightning/src/ln/trampoline_forward_tests.rs @@ -0,0 +1,205 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Tests for trampoline MPP accumulation and forwarding validation in +//! [`ChannelManager::handle_trampoline_htlc`]. + +use crate::chain::transaction::OutPoint; +use crate::events::HTLCHandlingFailureReason; +use crate::ln::channelmanager::{HTLCPreviousHopData, MppPart, MIN_CLTV_EXPIRY_DELTA}; +use crate::ln::functional_test_utils::*; +use crate::ln::msgs; +use crate::ln::onion_utils::LocalHTLCFailureReason; +use crate::ln::outbound_payment::{NextTrampolineHopInfo, RecipientOnionFields}; +use crate::ln::types::ChannelId; +use crate::types::payment::{PaymentHash, PaymentSecret}; + +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + +fn test_prev_hop_data(htlc_id: u64) -> HTLCPreviousHopData { + HTLCPreviousHopData { + prev_outbound_scid_alias: 0, + user_channel_id: None, + amount_msat: None, + htlc_id, + incoming_packet_shared_secret: [0; 32], + phantom_shared_secret: None, + trampoline_shared_secret: Some([0; 32]), + blinded_failure: None, + channel_id: ChannelId::from_bytes([0; 32]), + outpoint: OutPoint { txid: bitcoin::Txid::all_zeros(), index: 0 }, + counterparty_node_id: None, + cltv_expiry: None, + } +} + +fn test_trampoline_onion_packet() -> msgs::TrampolineOnionPacket { + let secp = Secp256k1::new(); + let test_secret = SecretKey::from_slice(&[42; 32]).unwrap(); + msgs::TrampolineOnionPacket { + version: 0, + public_key: PublicKey::from_secret_key(&secp, &test_secret), + hop_data: vec![0; 650], + hmac: [0; 32], + } +} + +fn test_onion_fields(total_msat: u64) -> RecipientOnionFields { + RecipientOnionFields { + payment_secret: Some(PaymentSecret([0; 32])), + total_mpp_amount_msat: total_msat, + payment_metadata: None, + custom_tlvs: Vec::new(), + } +} + +enum TrampolineMppValidationTestCase { + FeeInsufficient, + CltvInsufficient, + CltvDeltaBelowMinimum, + TrampolineAmountExceedsReceived, + TrampolineCLTVExceedsReceived, + MismatchedPaymentSecret, +} + +/// Sends two MPP parts through [`ChannelManager::handle_trampoline_htlc`], testing various MPP +/// validation steps with a base case that succeeds. +fn do_test_trampoline_mpp_validation(test_case: Option<TrampolineMppValidationTestCase>) { + let update_add_value: u64 = 500_000; // Actual amount we received in update_add_htlc. + let update_add_cltv: u32 = 500; // Actual CLTV we received in update_add_htlc. + let sender_intended_incoming_value: u64 = 500_000; // Amount we expect for one HTLC, outer onion. + let incoming_mpp_total: u64 = 1_000_000; // Total we expect to receive across MPP parts, outer onion. + let mut next_trampoline_amount: u64 = 750_000; // Total next trampoline expects, inner onion. + let mut next_trampoline_cltv: u32 = 100; // CLTV next trampoline expects, inner onion. + + // By default, set our forwarding fee and CLTV delta to exactly what we're being offered + // for this trampoline forward, so that we can force failures by just adding one. + let mut forwarding_fee_base_msat = incoming_mpp_total - next_trampoline_amount; + let mut cltv_delta = update_add_cltv - next_trampoline_cltv; + let mut mismatch_payment_secret = false; + + let expected = match test_case { + Some(TrampolineMppValidationTestCase::FeeInsufficient) => { + forwarding_fee_base_msat += 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::CltvInsufficient) => { + cltv_delta += 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::CltvDeltaBelowMinimum) => { + // A node operator may configure a `cltv_expiry_delta` below + // `MIN_CLTV_EXPIRY_DELTA` (the raw config field isn't floored on the way in), + // but we must still require at least the minimum when forwarding. Offer a delta + // that sits *between* the too-low configured value and the minimum. + cltv_delta = (MIN_CLTV_EXPIRY_DELTA / 2) as u32; + next_trampoline_cltv = update_add_cltv - (MIN_CLTV_EXPIRY_DELTA as u32 - 1); + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::TrampolineAmountExceedsReceived) => { + next_trampoline_amount = incoming_mpp_total + 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::TrampolineCLTVExceedsReceived) => { + next_trampoline_cltv = update_add_cltv + 1; + LocalHTLCFailureReason::TrampolineFeeOrExpiryInsufficient + }, + Some(TrampolineMppValidationTestCase::MismatchedPaymentSecret) => { + mismatch_payment_secret = true; + LocalHTLCFailureReason::InvalidTrampolineForward + }, + // We currently reject trampoline forwards once accumulated. + None => LocalHTLCFailureReason::TemporaryTrampolineFailure, + }; + + let chanmon_cfgs = create_chanmon_cfgs(1); + let node_cfgs = create_node_cfgs(1, &chanmon_cfgs); + let mut cfg = test_default_channel_config(); + cfg.channel_config.forwarding_fee_base_msat = forwarding_fee_base_msat as u32; + cfg.channel_config.forwarding_fee_proportional_millionths = 0; + cfg.channel_config.cltv_expiry_delta = cltv_delta as u16; + let node_chanmgrs = create_node_chanmgrs(1, &node_cfgs, &[Some(cfg)]); + let nodes = create_network(1, &node_cfgs, &node_chanmgrs); + + let payment_hash = PaymentHash([1; 32]); + + let secp = Secp256k1::new(); + let test_secret = SecretKey::from_slice(&[2; 32]).unwrap(); + let next_trampoline = PublicKey::from_secret_key(&secp, &test_secret); + let next_hop_info = NextTrampolineHopInfo { + onion_packet: test_trampoline_onion_packet(), + blinding_point: None, + amount_msat: next_trampoline_amount, + cltv_expiry_height: next_trampoline_cltv, + }; + + let htlc1 = MppPart::new( + test_prev_hop_data(0), + update_add_value, + sender_intended_incoming_value, + update_add_cltv, + ); + assert!(nodes[0] + .node + .test_handle_trampoline_htlc( + htlc1, + test_onion_fields(incoming_mpp_total), + payment_hash, + next_hop_info.clone(), + next_trampoline, + ) + .is_ok()); + + let htlc2 = MppPart::new( + test_prev_hop_data(1), + update_add_value, + sender_intended_incoming_value, + update_add_cltv, + ); + let onion2 = if mismatch_payment_secret { + RecipientOnionFields { + payment_secret: Some(PaymentSecret([1; 32])), + total_mpp_amount_msat: incoming_mpp_total, + payment_metadata: None, + custom_tlvs: Vec::new(), + } + } else { + test_onion_fields(incoming_mpp_total) + }; + let result = nodes[0].node.test_handle_trampoline_htlc( + htlc2, + onion2, + payment_hash, + next_hop_info, + next_trampoline, + ); + + assert_eq!( + HTLCHandlingFailureReason::from(&result.expect_err("expect trampoline failure").1), + HTLCHandlingFailureReason::Local { reason: expected }, + ); +} + +#[test] +fn test_trampoline_mpp_validation() { + do_test_trampoline_mpp_validation(Some(TrampolineMppValidationTestCase::FeeInsufficient)); + do_test_trampoline_mpp_validation(Some(TrampolineMppValidationTestCase::CltvInsufficient)); + do_test_trampoline_mpp_validation(Some(TrampolineMppValidationTestCase::CltvDeltaBelowMinimum)); + do_test_trampoline_mpp_validation(Some( + TrampolineMppValidationTestCase::TrampolineAmountExceedsReceived, + )); + do_test_trampoline_mpp_validation(Some( + TrampolineMppValidationTestCase::TrampolineCLTVExceedsReceived, + )); + do_test_trampoline_mpp_validation(Some( + TrampolineMppValidationTestCase::MismatchedPaymentSecret, + )); + do_test_trampoline_mpp_validation(None); +} diff --git a/lightning/src/ln/types.rs b/lightning/src/ln/types.rs index fd8ccbae382..62ce89bb8d5 100644 --- a/lightning/src/ln/types.rs +++ b/lightning/src/ln/types.rs @@ -20,10 +20,11 @@ use crate::util::ser::{Readable, Writeable, Writer}; #[allow(unused_imports)] use crate::prelude::*; -use bitcoin::hashes::{sha256::Hash as Sha256, Hash as _, HashEngine as _}; +use bitcoin::hashes::{sha256::Hash as Sha256, Hash as CryptoHash, HashEngine as _}; use bitcoin::hex::display::impl_fmt_traits; use core::borrow::Borrow; +use core::hash::{Hash, Hasher}; /// A unique 32-byte identifier for a channel. /// Depending on how the ID is generated, several varieties are distinguished @@ -33,7 +34,7 @@ use core::borrow::Borrow; /// A _temporary_ ID is generated randomly. /// (Later revocation-point-based _v2_ is a possibility.) /// The variety (context) is not stored, it is relevant only at creation. -#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] pub struct ChannelId(pub [u8; 32]); impl ChannelId { @@ -93,7 +94,8 @@ impl ChannelId { our_revocation_basepoint: &RevocationBasepoint, ) -> Self { let our_revocation_point_bytes = our_revocation_basepoint.0.serialize(); - Self(Sha256::hash(&[[0u8; 33], our_revocation_point_bytes].concat()).to_byte_array()) + let hash_input = &[[0u8; 33], our_revocation_point_bytes].concat(); + Self(<Sha256 as CryptoHash>::hash(hash_input).to_byte_array()) } /// Indicates whether this is a V2 channel ID for the given local and remote revocation basepoints. @@ -123,6 +125,13 @@ impl Borrow<[u8]> for ChannelId { } } +impl Hash for ChannelId { + fn hash<H: Hasher>(&self, state: &mut H) { + let slice: &[u8] = self.borrow(); + Hash::hash(slice, state); + } +} + impl_fmt_traits! { impl fmt_traits for ChannelId { const LENGTH: usize = 32; diff --git a/lightning/src/ln/update_fee_tests.rs b/lightning/src/ln/update_fee_tests.rs index 24ae8525450..1cb04f13a33 100644 --- a/lightning/src/ln/update_fee_tests.rs +++ b/lightning/src/ln/update_fee_tests.rs @@ -16,6 +16,7 @@ use crate::ln::msgs::{ }; use crate::ln::outbound_payment::RecipientOnionFields; use crate::sign::ecdsa::EcdsaChannelSigner; +use crate::sign::ChannelSigner; use crate::types::features::ChannelTypeFeatures; use crate::util::config::UserConfig; use crate::util::errors::APIError; @@ -80,7 +81,7 @@ pub fn test_async_inbound_update_fee() { // ...but before it's delivered, nodes[1] starts to send a payment back to nodes[0]... let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 40000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 40000); let id = PaymentId(our_payment_hash.0); nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -181,7 +182,7 @@ pub fn test_update_fee_unordered_raa() { // ...but before it's delivered, nodes[1] starts to send a payment back to nodes[0]... let (route, our_payment_hash, _, our_payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 40000); - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 40000); let id = PaymentId(our_payment_hash.0); nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -408,7 +409,8 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann ); let channel_id = chan.2; let secp_ctx = Secp256k1::new(); - let bs_channel_reserve_sats = get_holder_selected_channel_reserve_satoshis(channel_value, &cfg); + let bs_channel_reserve_sats = + get_holder_selected_channel_reserve_satoshis(channel_value, 0, &cfg, false).unwrap(); let (anchor_outputs_value_sats, outputs_num_no_htlcs) = if channel_type_features.supports_anchors_zero_fee_htlc_tx() { (ANCHOR_OUTPUT_VALUE_SATOSHI * 2, 4) @@ -471,7 +473,7 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann let channel = get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, chan.2); let chan_signer = channel.as_funded().unwrap().get_signer(); let point_number = INITIAL_COMMITMENT_NUMBER - 1; - chan_signer.as_ref().get_per_commitment_point(point_number, &secp_ctx).unwrap() + chan_signer.get_per_commitment_point(point_number, &secp_ctx).unwrap() }; let res = { @@ -497,8 +499,6 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann ); let params = &local_chan.funding().channel_transaction_parameters; local_chan_signer - .as_ecdsa() - .unwrap() .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) .unwrap() }; @@ -508,8 +508,6 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann signature: res.0, htlc_signatures: res.1, funding_txid: None, - #[cfg(taproot)] - partial_signature_with_nonce: None, }; let update_fee = msgs::UpdateFee { channel_id: chan.2, feerate_per_kw: non_buffer_feerate + 4 }; @@ -523,7 +521,7 @@ pub fn do_test_update_fee_that_funder_cannot_afford(channel_type_features: Chann let err = "Funding remote cannot afford proposed new fee"; nodes[1].logger.assert_log_contains("lightning::ln::channelmanager", err, 3); check_added_monitors(&nodes[1], 1); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); let reason = ClosureReason::ProcessingError { err: err.to_string() }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], channel_value); } @@ -572,7 +570,7 @@ pub fn test_update_fee_that_saturates_subs() { let channel = get_channel_ref!(nodes[1], nodes[0], per_peer_lock, peer_state_lock, chan_id); let chan_signer = channel.as_funded().unwrap().get_signer(); - chan_signer.as_ref().get_per_commitment_point(INITIAL_COMMITMENT_NUMBER, &secp_ctx).unwrap() + chan_signer.get_per_commitment_point(INITIAL_COMMITMENT_NUMBER, &secp_ctx).unwrap() }; let res = { @@ -597,8 +595,6 @@ pub fn test_update_fee_that_saturates_subs() { ); let params = &local_chan.funding().channel_transaction_parameters; local_chan_signer - .as_ecdsa() - .unwrap() .sign_counterparty_commitment(params, &commitment_tx, Vec::new(), Vec::new(), &secp_ctx) .unwrap() }; @@ -608,8 +604,6 @@ pub fn test_update_fee_that_saturates_subs() { signature: res.0, htlc_signatures: res.1, funding_txid: None, - #[cfg(taproot)] - partial_signature_with_nonce: None, }; let update_fee = msgs::UpdateFee { channel_id: chan_id, feerate_per_kw: FEERATE }; @@ -620,7 +614,7 @@ pub fn test_update_fee_that_saturates_subs() { let err = "Funding remote cannot afford proposed new fee"; nodes[1].logger.assert_log_contains("lightning::ln::channelmanager", err, 3); check_added_monitors(&nodes[1], 1); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); let reason = ClosureReason::ProcessingError { err: err.to_string() }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 10_000); } @@ -665,7 +659,7 @@ pub fn test_update_fee_with_fundee_update_add_htlc() { get_route_and_payment_hash!(nodes[1], nodes[0], 800000); // nothing happens since node[1] is in AwaitingRemoteRevoke - let onion = RecipientOnionFields::secret_only(our_payment_secret); + let onion = RecipientOnionFields::secret_only(our_payment_secret, 800000); let id = PaymentId(our_payment_hash.0); nodes[1].node.send_payment_with_route(route, our_payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 0); @@ -882,12 +876,19 @@ pub fn test_chan_init_feerate_unaffordability() { MIN_AFFORDABLE_HTLC_COUNT as u64, &channel_type_features, ); - assert_eq!(nodes[0].node.create_channel(node_b_id, 100_000, push_amt + 1, 42, None, None).unwrap_err(), - APIError::APIMisuseError { err: "Funding amount (356) can't even pay fee for initial commitment transaction fee of 357.".to_string() }); + assert_eq!( + nodes[0].node.create_channel(node_b_id, 100_000, push_amt + 1, 42, None, None).unwrap_err(), + APIError::APIMisuseError { + err: "Funding amount (356) can't even pay fee for initial commitment transaction." + .to_string() + } + ); // During open, we don't have a "counterparty channel reserve" to check against, so that // requirement only comes into play on the open_channel handling side. - push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, &default_config) * 1000; + push_amt -= get_holder_selected_channel_reserve_satoshis(100_000, 0, &default_config, false) + .unwrap() + * 1000; nodes[0].node.create_channel(node_b_id, 100_000, push_amt, 42, None, None).unwrap(); let mut open_channel_msg = get_event_msg!(nodes[0], MessageSendEvent::SendOpenChannel, node_b_id); @@ -1002,7 +1003,7 @@ pub fn accept_busted_but_better_fee() { required_feerate_sat_per_kw: 5000, }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], 100000); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); }, _ => panic!("Unexpected event"), @@ -1031,7 +1032,8 @@ pub fn do_cannot_afford_on_holding_cell_release( let chanmon_cfgs = create_chanmon_cfgs(2); let mut cfg = test_legacy_channel_config(); - cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + cfg.channel_handshake_config.announced_channel_max_inbound_htlc_value_in_flight_percentage = + 100; if channel_type_features.supports_anchors_zero_fee_htlc_tx() { cfg.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = true; } @@ -1089,15 +1091,19 @@ pub fn do_cannot_afford_on_holding_cell_release( *feerate_lock = target_feerate; } - // Put the update fee into the holding cell of node 0 - - nodes[0].node.maybe_update_chan_fees(); + // Put the update fee into the holding cell of node 0. We use quiescence as an easy way to force + // the update into the holding cell. + nodes[0].node.maybe_propose_quiescence(&node_b_id, &chan_id).unwrap(); + let stfu = get_event_msg!(nodes[0], MessageSendEvent::SendStfu, node_b_id); + nodes[0].node.timer_tick_occurred(); + assert!(nodes[0].node.get_and_clear_pending_msg_events().is_empty()); + check_added_monitors(&nodes[0], 0); // While the update_fee is in the holding cell, add an inbound HTLC let (route, payment_hash, _, payment_secret) = get_route_and_payment_hash!(nodes[1], nodes[0], 5000 * 1000); - let onion = RecipientOnionFields::secret_only(payment_secret); + let onion = RecipientOnionFields::secret_only(payment_secret, 5000 * 1000); let id = PaymentId(payment_hash.0); nodes[1].node.send_payment_with_route(route, payment_hash, onion, id).unwrap(); check_added_monitors(&nodes[1], 1); @@ -1132,11 +1138,17 @@ pub fn do_cannot_afford_on_holding_cell_release( panic!(); } - // Release the update_fee from its holding cell + // Release the update_fee from its holding cell by completing the quiescence handshake. + nodes[1].node.handle_stfu(node_a_id, &stfu); + let stfu = get_event_msg!(nodes[1], MessageSendEvent::SendStfu, node_a_id); + nodes[0].node.handle_stfu(node_b_id, &stfu); + let _ = nodes[0].node.exit_quiescence(&node_b_id, &chan_id); + let _ = nodes[1].node.exit_quiescence(&node_a_id, &chan_id); let mut events = nodes[0].node.get_and_clear_pending_msg_events(); if can_afford { // We could afford the update_fee, sanity check everything assert_eq!(events.len(), 1); + check_added_monitors(&nodes[0], 1); if let MessageSendEvent::UpdateHTLCs { node_id, channel_id, updates } = events.pop().unwrap() { @@ -1215,7 +1227,9 @@ pub fn do_can_afford_given_trimmed_htlcs(inequality_regions: core::cmp::Ordering let chanmon_cfgs = create_chanmon_cfgs(2); let mut legacy_cfg = test_legacy_channel_config(); - legacy_cfg.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = 100; + legacy_cfg + .channel_handshake_config + .announced_channel_max_inbound_htlc_value_in_flight_percentage = 100; let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let node_chanmgrs = diff --git a/lightning/src/ln/zero_fee_commitment_tests.rs b/lightning/src/ln/zero_fee_commitment_tests.rs index d287b6e3de1..61c3e1063d0 100644 --- a/lightning/src/ln/zero_fee_commitment_tests.rs +++ b/lightning/src/ln/zero_fee_commitment_tests.rs @@ -129,7 +129,7 @@ fn test_htlc_claim_chunking() { let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &configs); let nodes = create_network(2, &node_cfgs, &node_chanmgrs); - let coinbase_tx = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500)); + let coinbase_tx = provide_utxo_reserves(&nodes, 50, Amount::from_sat(500)); const CHAN_CAPACITY: u64 = 10_000_000; let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value( @@ -185,12 +185,12 @@ fn test_htlc_claim_chunking() { assert_eq!(htlc_claims[1].input.len(), 34); assert_eq!(htlc_claims[1].output.len(), 24); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[0], 1, reason, &[nodes[1].node.get_our_node_id()], CHAN_CAPACITY); assert!(nodes[0].node.list_channels().is_empty()); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); check_added_monitors(&nodes[1], 1); let reason = ClosureReason::CommitmentTxConfirmed; check_closed_event(&nodes[1], 1, reason, &[nodes[0].node.get_our_node_id()], CHAN_CAPACITY); @@ -319,7 +319,7 @@ fn test_anchor_tx_too_big() { let node_a_id = nodes[0].node.get_our_node_id(); - let _coinbase_tx_a = provide_anchor_utxo_reserves(&nodes, 50, Amount::from_sat(500)); + let _coinbase_tx_a = provide_utxo_reserves(&nodes, 50, Amount::from_sat(500)); const CHAN_CAPACITY: u64 = 10_000_000; let (_, _, chan_id, _funding_tx) = create_announced_chan_between_nodes_with_value( @@ -346,7 +346,7 @@ fn test_anchor_tx_too_big() { .force_close_broadcasting_latest_txn(&chan_id, &node_a_id, message.clone()) .unwrap(); check_added_monitors(&nodes[1], 1); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[1], 1, reason, &[node_a_id], CHAN_CAPACITY); @@ -368,7 +368,7 @@ fn test_anchor_tx_too_big() { - EMPTY_WITNESS_WEIGHT - P2WSH_TXOUT_WEIGHT; nodes[1].logger.assert_log( - "lightning::events::bump_transaction", + "lightning::util::wallet_utils", format!( "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", FEERATE, max_coin_selection_weight @@ -402,7 +402,7 @@ fn test_anchor_tx_too_big() { assert_eq!(txns[1].input.len(), 2); assert_eq!(txns[1].output.len(), 1); nodes[1].logger.assert_log( - "lightning::events::bump_transaction", + "lightning::util::wallet_utils", format!( "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", FEERATE, max_coin_selection_weight diff --git a/lightning/src/offers/async_receive_offer_cache.rs b/lightning/src/offers/async_receive_offer_cache.rs index c4442b4dd8f..367cdb68fc8 100644 --- a/lightning/src/offers/async_receive_offer_cache.rs +++ b/lightning/src/offers/async_receive_offer_cache.rs @@ -76,7 +76,7 @@ impl AsyncReceiveOffer { } } -impl_writeable_tlv_based_enum!(OfferStatus, +impl_ser_tlv_based_enum!(OfferStatus, (0, Used) => { (0, invoice_created_at, required), }, @@ -86,7 +86,7 @@ impl_writeable_tlv_based_enum!(OfferStatus, (2, Pending) => {}, ); -impl_writeable_tlv_based!(AsyncReceiveOffer, { +impl_ser_tlv_based!(AsyncReceiveOffer, { (0, offer, required), (2, offer_nonce, required), (4, status, required), @@ -491,7 +491,7 @@ impl AsyncReceiveOfferCache { match offer.status { OfferStatus::Used { invoice_created_at: ref mut inv_created_at } | OfferStatus::Ready { invoice_created_at: ref mut inv_created_at } => { - *inv_created_at = core::cmp::min(invoice_created_at, *inv_created_at); + *inv_created_at = core::cmp::max(invoice_created_at, *inv_created_at); }, OfferStatus::Pending => offer.status = OfferStatus::Ready { invoice_created_at }, } diff --git a/lightning/src/offers/flow.rs b/lightning/src/offers/flow.rs index 0bb98777227..ade684e5be1 100644 --- a/lightning/src/offers/flow.rs +++ b/lightning/src/offers/flow.rs @@ -10,6 +10,8 @@ //! Provides data structures and functions for creating and managing Offers messages, //! facilitating communication, and handling BOLT12 messages and payments. +use alloc::collections::BTreeMap; + use core::sync::atomic::{AtomicUsize, Ordering}; use core::time::Duration; @@ -29,7 +31,7 @@ use crate::chain::channelmonitor::LATENCY_GRACE_PERIOD_BLOCKS; #[allow(unused_imports)] use crate::prelude::*; -use crate::chain::BestBlock; +use crate::chain::BlockLocator; use crate::ln::channel_state::ChannelDetails; use crate::ln::channelmanager::{InterceptId, PaymentId, CLTV_FAR_FAR_AWAY}; use crate::ln::inbound_payment; @@ -62,12 +64,6 @@ use crate::types::payment::{PaymentHash, PaymentSecret}; use crate::util::logger::Logger; use crate::util::ser::Writeable; -#[cfg(feature = "dnssec")] -use { - crate::blinded_path::message::DNSResolverContext, - crate::onion_message::dns_resolution::{DNSResolverMessage, DNSSECQuery, OMNameResolver}, -}; - /// A BOLT12 offers code and flow utility provider, which facilitates /// BOLT12 builder generation and onion message handling. /// @@ -75,7 +71,7 @@ use { /// for finding message paths when initiating and retrying onion messages. pub struct OffersMessageFlow<MR: MessageRouter, L: Logger> { chain_hash: ChainHash, - best_block: RwLock<BestBlock>, + best_block: RwLock<BlockLocator>, our_network_pubkey: PublicKey, highest_seen_timestamp: AtomicUsize, @@ -94,18 +90,13 @@ pub struct OffersMessageFlow<MR: MessageRouter, L: Logger> { pending_async_payments_messages: Mutex<Vec<(AsyncPaymentsMessage, MessageSendInstructions)>>, async_receive_offer_cache: Mutex<AsyncReceiveOfferCache>, - #[cfg(feature = "dnssec")] - pub(crate) hrn_resolver: OMNameResolver, - #[cfg(feature = "dnssec")] - pending_dns_onion_messages: Mutex<Vec<(DNSResolverMessage, MessageSendInstructions)>>, - logger: L, } impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { /// Creates a new [`OffersMessageFlow`] pub fn new( - chain_hash: ChainHash, best_block: BestBlock, our_network_pubkey: PublicKey, + chain_hash: ChainHash, best_block: BlockLocator, our_network_pubkey: PublicKey, current_timestamp: u32, inbound_payment_key: inbound_payment::ExpandedKey, receive_auth_key: ReceiveAuthKey, secp_ctx: Secp256k1<secp256k1::All>, message_router: MR, logger: L, @@ -126,11 +117,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { pending_offers_messages: Mutex::new(Vec::new()), pending_async_payments_messages: Mutex::new(Vec::new()), - #[cfg(feature = "dnssec")] - hrn_resolver: OMNameResolver::new(current_timestamp, best_block.height), - #[cfg(feature = "dnssec")] - pending_dns_onion_messages: Mutex::new(Vec::new()), - async_receive_offer_cache: Mutex::new(AsyncReceiveOfferCache::new()), logger, @@ -183,9 +169,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { } fn duration_since_epoch(&self) -> Duration { - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let now = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let now = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH"); @@ -199,10 +185,14 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { /// /// Must be called whenever a new chain tip becomes available. May be skipped /// for intermediary blocks. - pub fn best_block_updated(&self, header: &Header, _height: u32) { + pub fn best_block_updated(&self, header: &Header, height: u32) { let timestamp = &self.highest_seen_timestamp; let block_time = header.time as usize; + // Note that we deliberately don't use `update_for_new_tip` as we dont rely on receiving + // disconnection information instead expecting to simply "jump" to the new tip. + *self.best_block.write().unwrap() = BlockLocator::new(header.block_hash(), height); + loop { // Update timestamp to be the max of its current value and the block // timestamp. This should keep us close to the current time without relying on @@ -220,12 +210,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { break; } } - - #[cfg(feature = "dnssec")] - { - let updated_time = timestamp.load(Ordering::Acquire) as u32; - self.hrn_resolver.new_best_block(_height, updated_time); - } } } @@ -286,6 +270,39 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { self.create_blinded_paths(peers, context) } + fn blinded_paths_for_phantom_offer( + &self, per_node_peers: Vec<(PublicKey, Vec<MessageForwardNode>)>, path_count_limit: usize, + context: MessageContext, + ) -> Result<Vec<BlindedMessagePath>, ()> { + let receive_key = ReceiveAuthKey(self.inbound_payment_key.phantom_node_blinded_path_key); + let secp_ctx = &self.secp_ctx; + + let mut per_node_paths: Vec<_> = per_node_peers + .into_iter() + .filter_map(|(recipient, peers)| { + self.message_router + .create_blinded_paths(recipient, receive_key, context.clone(), peers, secp_ctx) + .ok() + }) + .collect(); + + let mut res = Vec::new(); + while res.len() < path_count_limit && !per_node_paths.is_empty() { + for node_paths in per_node_paths.iter_mut() { + if let Some(path) = node_paths.pop() { + res.push(path); + } + } + per_node_paths.retain(|node_paths| !node_paths.is_empty()); + } + + if res.is_empty() { + Err(()) + } else { + Ok(res) + } + } + /// Creates a collection of blinded paths by delegating to /// [`MessageRouter::create_blinded_paths`]. /// @@ -437,7 +454,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { let nonce = match context { None if invoice_request.metadata().is_some() => None, - Some(OffersContext::InvoiceRequest { nonce }) => Some(nonce), + Some(OffersContext::InvoiceRequest { nonce, payment_metadata: _ }) => Some(nonce), Some(OffersContext::StaticInvoiceRequested { recipient_id, invoice_slot, @@ -467,14 +484,14 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { Ok(InvreqResponseInstructions::SendInvoice(invoice_request)) } - /// Verifies a [`Bolt12Invoice`] using the provided [`OffersContext`] or the invoice's payer - /// metadata, returning the corresponding [`PaymentId`] if successful. + /// Verifies a [`Bolt12Invoice`] using the invoice's payer metadata, returning the + /// corresponding [`PaymentId`] if successful. /// /// - If an [`OffersContext::OutboundPaymentForOffer`] or - /// [`OffersContext::OutboundPaymentForRefund`] with a `nonce` is provided, verification is - /// performed using this to form the payer metadata. - /// - If no context is provided and the invoice corresponds to a [`Refund`] without blinded paths, - /// verification is performed using the [`Bolt12Invoice::payer_metadata`]. + /// [`OffersContext::OutboundPaymentForRefund`] is provided, the extracted [`PaymentId`] must + /// also match the context's `payment_id`. + /// - If no context is provided, the invoice must correspond to a [`Refund`] without blinded + /// paths. /// - If neither condition is met, verification fails. pub fn verify_bolt12_invoice( &self, invoice: &Bolt12Invoice, context: Option<&OffersContext>, @@ -486,16 +503,20 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { None if invoice.is_for_refund_without_paths() => { invoice.verify_using_metadata(expanded_key, secp_ctx) }, - Some(&OffersContext::OutboundPaymentForOffer { payment_id, nonce, .. }) => { + Some(&OffersContext::OutboundPaymentForOffer { payment_id }) => { if invoice.is_for_offer() { - invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + invoice.verify_using_metadata(expanded_key, secp_ctx).and_then(|extracted| { + (extracted == payment_id).then(|| payment_id).ok_or(()) + }) } else { Err(()) } }, - Some(&OffersContext::OutboundPaymentForRefund { payment_id, nonce, .. }) => { + Some(&OffersContext::OutboundPaymentForRefund { payment_id }) => { if invoice.is_for_refund() { - invoice.verify_using_payer_data(payment_id, nonce, expanded_key, secp_ctx) + invoice.verify_using_metadata(expanded_key, secp_ctx).and_then(|extracted| { + (extracted == payment_id).then(|| payment_id).ok_or(()) + }) } else { Err(()) } @@ -544,7 +565,8 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { let secp_ctx = &self.secp_ctx; let nonce = Nonce::from_entropy_source(entropy); - let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce }); + let context = + MessageContext::Offers(OffersContext::InvoiceRequest { nonce, payment_metadata: None }); let mut builder = OfferBuilder::deriving_signing_pubkey(node_id, expanded_key, nonce, secp_ctx) @@ -559,8 +581,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by the /// [`OffersMessageFlow`], and any corresponding [`InvoiceRequest`] can be verified using - /// [`Self::verify_invoice_request`]. The offer will expire at `absolute_expiry` if `Some`, - /// or will not expire if `None`. + /// [`Self::verify_invoice_request`]. /// /// # Privacy /// @@ -634,6 +655,25 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { }) } + /// Creates an [`OfferBuilder`] such that the [`Offer`] it builds is recognized by any + /// [`OffersMessageFlow`] using the same [`ExpandedKey`] (provided in the constructor as + /// `inbound_payment_key`), and any corresponding [`InvoiceRequest`] can be verified using + /// [`Self::verify_invoice_request`]. + /// + /// See [`Self::create_offer_builder`] for more details on privacy and limitations. + /// + /// [`ExpandedKey`]: inbound_payment::ExpandedKey + pub fn create_phantom_offer_builder<ES: EntropySource>( + &self, entropy_source: ES, per_node_peers: Vec<(PublicKey, Vec<MessageForwardNode>)>, + path_count_limit: usize, + ) -> Result<OfferBuilder<'_, DerivedMetadata, secp256k1::All>, Bolt12SemanticError> { + self.create_offer_builder_intern(entropy_source, |_, context, _| { + self.blinded_paths_for_phantom_offer(per_node_peers, path_count_limit, context) + .map_err(|_| Bolt12SemanticError::MissingPaths) + }) + .map(|(builder, _)| builder) + } + fn create_refund_builder_intern<ES: EntropySource, PF, I>( &self, entropy_source: ES, make_paths: PF, amount_msats: u64, absolute_expiry: Duration, payment_id: PaymentId, @@ -653,7 +693,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { let nonce = Nonce::from_entropy_source(entropy); let context = - MessageContext::Offers(OffersContext::OutboundPaymentForRefund { payment_id, nonce }); + MessageContext::Offers(OffersContext::OutboundPaymentForRefund { payment_id }); // Create the base builder with common properties let mut builder = RefundBuilder::deriving_signing_pubkey( @@ -795,13 +835,15 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { pub fn create_static_invoice_builder<'a, R: Router>( &self, router: &R, offer: &'a Offer, offer_nonce: Nonce, payment_secret: PaymentSecret, relative_expiry_secs: u32, usable_channels: Vec<ChannelDetails>, - peers: Vec<MessageForwardNode>, + peers: Vec<MessageForwardNode>, payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, ) -> Result<StaticInvoiceBuilder<'a>, Bolt12SemanticError> { let expanded_key = &self.inbound_payment_key; let secp_ctx = &self.secp_ctx; - let payment_context = - PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { offer_nonce }); + let payment_context = PaymentContext::AsyncBolt12Offer(AsyncBolt12OfferContext { + offer_nonce, + payment_metadata, + }); let amount_msat = offer.amount().and_then(|amount| match amount { Amount::Bitcoin { amount_msats } => Some(amount_msats), @@ -863,6 +905,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { pub fn create_invoice_builder_from_refund<'a, ES: EntropySource, R: Router, F>( &'a self, router: &R, entropy_source: ES, refund: &'a Refund, usable_channels: Vec<ChannelDetails>, get_payment_info: F, + payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, ) -> Result<InvoiceBuilder<'a, DerivedSigningPubkey>, Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, @@ -879,7 +922,8 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { let (payment_hash, payment_secret) = get_payment_info(amount_msats, relative_expiry)?; - let payment_context = PaymentContext::Bolt12Refund(Bolt12RefundContext {}); + let payment_context = + PaymentContext::Bolt12Refund(Bolt12RefundContext { payment_metadata }); let payment_paths = self .create_blinded_payment_paths( router, @@ -891,7 +935,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let builder = refund.respond_using_derived_keys( payment_paths, payment_hash, @@ -899,9 +943,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { entropy, )?; - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let created_at = Duration::from_secs(self.highest_seen_timestamp.load(Ordering::Acquire) as u64); - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let builder = refund.respond_using_derived_keys_no_std( payment_paths, payment_hash, @@ -930,6 +974,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { pub fn create_invoice_builder_from_invoice_request_with_keys<'a, R: Router, F>( &self, router: &R, invoice_request: &'a VerifiedInvoiceRequest<DerivedSigningPubkey>, usable_channels: Vec<ChannelDetails>, get_payment_info: F, + payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, ) -> Result<(InvoiceBuilder<'a, DerivedSigningPubkey>, MessageContext), Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, @@ -944,6 +989,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { let context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: invoice_request.offer_id, invoice_request: invoice_request.fields(), + payment_metadata, }); let payment_paths = self @@ -957,9 +1003,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let builder = invoice_request.respond_using_derived_keys(payment_paths, payment_hash); - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let builder = invoice_request.respond_using_derived_keys_no_std( payment_paths, payment_hash, @@ -989,6 +1035,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { pub fn create_invoice_builder_from_invoice_request_without_keys<'a, R: Router, F>( &self, router: &R, invoice_request: &'a VerifiedInvoiceRequest<ExplicitSigningPubkey>, usable_channels: Vec<ChannelDetails>, get_payment_info: F, + payment_metadata: Option<BTreeMap<u64, Vec<u8>>>, ) -> Result<(InvoiceBuilder<'a, ExplicitSigningPubkey>, MessageContext), Bolt12SemanticError> where F: Fn(u64, u32) -> Result<(PaymentHash, PaymentSecret), Bolt12SemanticError>, @@ -1003,6 +1050,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { let context = PaymentContext::Bolt12Offer(Bolt12OfferContext { offer_id: invoice_request.offer_id, invoice_request: invoice_request.fields(), + payment_metadata, }); let payment_paths = self @@ -1016,9 +1064,9 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { ) .map_err(|_| Bolt12SemanticError::MissingPaths)?; - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let builder = invoice_request.respond_with(payment_paths, payment_hash); - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let builder = invoice_request.respond_with_no_std( payment_paths, payment_hash, @@ -1041,13 +1089,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { /// over those blinded paths, which can be verified against the intended outbound payment, /// ensuring the invoice corresponds to a payment we actually want to make. /// - /// # Nonce - /// The nonce is used to create a unique [`MessageContext`] for the reply paths. - /// These will be used to verify the corresponding [`Bolt12Invoice`] when it is received. - /// - /// Note: The provided [`Nonce`] MUST be the same as the [`Nonce`] used for creating the - /// [`InvoiceRequest`] to ensure correct verification of the corresponding [`Bolt12Invoice`]. - /// /// See [`OffersMessageFlow::create_invoice_request_builder`] for more details. /// /// # Peers @@ -1059,11 +1100,10 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { /// [`InvoiceError`]: crate::offers::invoice_error::InvoiceError /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages pub fn enqueue_invoice_request( - &self, invoice_request: InvoiceRequest, payment_id: PaymentId, nonce: Nonce, + &self, invoice_request: InvoiceRequest, payment_id: PaymentId, peers: Vec<MessageForwardNode>, ) -> Result<(), Bolt12SemanticError> { - let context = - MessageContext::Offers(OffersContext::OutboundPaymentForOffer { payment_id, nonce }); + let context = MessageContext::Offers(OffersContext::OutboundPaymentForOffer { payment_id }); let reply_paths = self .create_blinded_paths(peers, context) .map_err(|_| Bolt12SemanticError::MissingPaths)?; @@ -1219,7 +1259,7 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { let message = AsyncPaymentsMessage::HeldHtlcAvailable(HeldHtlcAvailable {}); enqueue_onion_message_with_reply_paths( message, - invoice.message_paths(), + invoice.held_htlc_available_paths(), reply_paths, &mut pending_async_payments_messages, ); @@ -1255,41 +1295,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { ) } - /// Enqueues the created [`DNSSECQuery`] to be sent to the counterparty. - /// - /// # Peers - /// - /// The user must provide a list of [`MessageForwardNode`] that will be used to generate - /// valid reply paths for the counterparty to send back the corresponding response for - /// the [`DNSSECQuery`] message. - /// - /// [`supports_onion_messages`]: crate::types::features::Features::supports_onion_messages - #[cfg(feature = "dnssec")] - pub fn enqueue_dns_onion_message( - &self, message: DNSSECQuery, context: DNSResolverContext, dns_resolvers: Vec<Destination>, - peers: Vec<MessageForwardNode>, - ) -> Result<(), Bolt12SemanticError> { - let reply_paths = self - .create_blinded_paths(peers, MessageContext::DNSResolver(context)) - .map_err(|_| Bolt12SemanticError::MissingPaths)?; - - let message_params = dns_resolvers - .iter() - .flat_map(|destination| reply_paths.iter().map(move |path| (path, destination))) - .take(OFFERS_MESSAGE_REQUEST_LIMIT); - for (reply_path, destination) in message_params { - self.pending_dns_onion_messages.lock().unwrap().push(( - DNSResolverMessage::DNSSECQuery(message.clone()), - MessageSendInstructions::WithSpecifiedReplyPath { - destination: destination.clone(), - reply_path: reply_path.clone(), - }, - )); - } - - Ok(()) - } - /// Gets the enqueued [`OffersMessage`] with their corresponding [`MessageSendInstructions`]. pub fn release_pending_offers_messages(&self) -> Vec<(OffersMessage, MessageSendInstructions)> { core::mem::take(&mut self.pending_offers_messages.lock().unwrap()) @@ -1302,14 +1307,6 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { core::mem::take(&mut self.pending_async_payments_messages.lock().unwrap()) } - /// Gets the enqueued [`DNSResolverMessage`] with their corresponding [`MessageSendInstructions`]. - #[cfg(feature = "dnssec")] - pub fn release_pending_dns_messages( - &self, - ) -> Vec<(DNSResolverMessage, MessageSendInstructions)> { - core::mem::take(&mut self.pending_dns_onion_messages.lock().unwrap()) - } - /// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. Will only /// return an offer if [`Self::set_paths_to_static_invoice_server`] was called and we succeeded in /// interactively building a [`StaticInvoice`] with the static invoice server. @@ -1653,11 +1650,15 @@ impl<MR: MessageRouter, L: Logger> OffersMessageFlow<MR, L> { offer_relative_expiry, usable_channels, peers.clone(), + None, ) .and_then(|builder| builder.build_and_sign(secp_ctx)) .map_err(|_| ())?; - let context = MessageContext::Offers(OffersContext::InvoiceRequest { nonce: offer_nonce }); + let context = MessageContext::Offers(OffersContext::InvoiceRequest { + nonce: offer_nonce, + payment_metadata: None, + }); let forward_invoice_request_path = self .create_blinded_paths(peers, context) .and_then(|paths| paths.into_iter().next().ok_or(()))?; diff --git a/lightning/src/offers/invoice.rs b/lightning/src/offers/invoice.rs index 8d83225f117..e48967d2830 100644 --- a/lightning/src/offers/invoice.rs +++ b/lightning/src/offers/invoice.rs @@ -131,9 +131,9 @@ use crate::offers::invoice_request::{ IV_BYTES as INVOICE_REQUEST_IV_BYTES, }; use crate::offers::merkle::{ - self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvStream, + self, SignError, SignFn, SignatureTlvStream, SignatureTlvStreamRef, TaggedHash, TlvRecord, + TlvStream, }; -use crate::offers::nonce::Nonce; use crate::offers::offer::{ Amount, ExperimentalOfferTlvStream, ExperimentalOfferTlvStreamRef, OfferId, OfferTlvStream, OfferTlvStreamRef, Quantity, EXPERIMENTAL_OFFER_TYPES, OFFER_TYPES, @@ -984,6 +984,11 @@ impl Bolt12Invoice { self.signature } + /// The raw serialized bytes of the invoice. + pub(super) fn invoice_bytes(&self) -> &[u8] { + &self.bytes + } + /// Hash that was used for signing the invoice. pub fn signable_hash(&self) -> [u8; 32] { self.tagged_hash.as_digest().as_ref().clone() @@ -1008,28 +1013,50 @@ impl Bolt12Invoice { (&invoice_request.inner.payer.0, INVOICE_REQUEST_IV_BYTES) }, InvoiceContents::ForRefund { refund, .. } => { - (&refund.payer.0, REFUND_IV_BYTES_WITH_METADATA) + let iv_bytes = if refund.paths().is_empty() { + REFUND_IV_BYTES_WITH_METADATA + } else { + REFUND_IV_BYTES_WITHOUT_METADATA + }; + (&refund.payer.0, iv_bytes) }, }; self.contents.verify(&self.bytes, metadata, key, iv_bytes, secp_ctx) } - /// Verifies that the invoice was for a request or refund created using the given key by - /// checking a payment id and nonce included with the [`BlindedMessagePath`] for which the invoice was - /// sent through. - pub fn verify_using_payer_data<T: secp256k1::Signing>( - &self, payment_id: PaymentId, nonce: Nonce, key: &ExpandedKey, secp_ctx: &Secp256k1<T>, - ) -> Result<PaymentId, ()> { - let metadata = Metadata::payer_data(payment_id, nonce, key); + /// Re-derives the payer's signing keypair for payer proof creation. + /// + /// For an invoice requested with [`Offer::request_invoice`], this performs the same key + /// derivation that occurs when the originating offer was created with + /// [`OfferBuilder::deriving_signing_pubkey`], allowing the payer to recover their signing + /// keypair. Likewise, for the refund flow, this performs the same key derivation used by + /// [`RefundBuilder::deriving_signing_pubkey`]. + /// + /// The keypair is derived from the invoice's own payer metadata (which embeds the payer + /// [`Nonce`]), so no externally-held nonce or payment id is required. In the common + /// proof-of-payment flow, callers can use [`PaidBolt12Invoice::prove_payer_derived`]. + /// + /// [`Offer::request_invoice`]: crate::offers::offer::Offer::request_invoice + /// [`OfferBuilder::deriving_signing_pubkey`]: crate::offers::offer::OfferBuilder::deriving_signing_pubkey + /// [`RefundBuilder::deriving_signing_pubkey`]: crate::offers::refund::RefundBuilder::deriving_signing_pubkey + /// [`PaidBolt12Invoice::prove_payer_derived`]: crate::offers::payer_proof::PaidBolt12Invoice::prove_payer_derived + /// [`Nonce`]: crate::offers::nonce::Nonce + pub fn derive_payer_signing_keys<T: secp256k1::Signing>( + &self, key: &ExpandedKey, secp_ctx: &Secp256k1<T>, + ) -> Result<Keypair, ()> { + // Mirror `verify_using_metadata`'s IV selection so the derived HMAC matches the one + // committed to in the payer metadata. let iv_bytes = match &self.contents { InvoiceContents::ForOffer { .. } => INVOICE_REQUEST_IV_BYTES, - InvoiceContents::ForRefund { .. } => REFUND_IV_BYTES_WITHOUT_METADATA, - }; - self.contents.verify(&self.bytes, &metadata, key, iv_bytes, secp_ctx).and_then( - |extracted_payment_id| { - (payment_id == extracted_payment_id).then(|| payment_id).ok_or(()) + InvoiceContents::ForRefund { refund, .. } => { + if refund.paths().is_empty() { + REFUND_IV_BYTES_WITH_METADATA + } else { + REFUND_IV_BYTES_WITHOUT_METADATA + } }, - ) + }; + self.contents.derive_payer_signing_keys(&self.bytes, key, iv_bytes, secp_ctx) } pub(crate) fn as_tlv_stream(&self) -> FullInvoiceTlvStreamRef<'_> { @@ -1317,20 +1344,8 @@ impl InvoiceContents { &self, bytes: &[u8], metadata: &Metadata, key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], secp_ctx: &Secp256k1<T>, ) -> Result<PaymentId, ()> { - const EXPERIMENTAL_TYPES: core::ops::Range<u64> = - EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; - - let offer_records = TlvStream::new(bytes).range(OFFER_TYPES); - let invreq_records = TlvStream::new(bytes).range(INVOICE_REQUEST_TYPES).filter(|record| { - match record.r#type { - PAYER_METADATA_TYPE => false, // Should be outside range - INVOICE_REQUEST_PAYER_ID_TYPE => !metadata.derives_payer_keys(), - _ => true, - } - }); - let experimental_records = TlvStream::new(bytes).range(EXPERIMENTAL_TYPES); - let tlv_stream = offer_records.chain(invreq_records).chain(experimental_records); - + let exclude_payer_id = metadata.derives_payer_keys(); + let tlv_stream = Self::payer_tlv_stream(bytes, exclude_payer_id); let signing_pubkey = self.payer_signing_pubkey(); signer::verify_payer_metadata( metadata.as_ref(), @@ -1342,6 +1357,38 @@ impl InvoiceContents { ) } + fn derive_payer_signing_keys<T: secp256k1::Signing>( + &self, bytes: &[u8], key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], secp_ctx: &Secp256k1<T>, + ) -> Result<Keypair, ()> { + let metadata = self.payer_metadata(); + let tlv_stream = Self::payer_tlv_stream(bytes, true); + let signing_pubkey = self.payer_signing_pubkey(); + signer::derive_payer_keys(metadata, key, iv_bytes, signing_pubkey, tlv_stream, secp_ctx) + } + + /// Builds the TLV stream used for payer metadata verification and key derivation. + /// + /// When `exclude_payer_id` is true, the payer signing pubkey (type 88) is excluded + /// from the stream, which is needed when deriving payer keys. + fn payer_tlv_stream( + bytes: &[u8], exclude_payer_id: bool, + ) -> impl core::iter::Iterator<Item = TlvRecord<'_>> { + const EXPERIMENTAL_TYPES: core::ops::Range<u64> = + EXPERIMENTAL_OFFER_TYPES.start..EXPERIMENTAL_INVOICE_REQUEST_TYPES.end; + + let offer_records = TlvStream::new(bytes).range(OFFER_TYPES); + let invreq_records = + TlvStream::new(bytes).range(INVOICE_REQUEST_TYPES).filter(move |record| { + match record.r#type { + PAYER_METADATA_TYPE => false, + INVOICE_REQUEST_PAYER_ID_TYPE => !exclude_payer_id, + _ => true, + } + }); + let experimental_records = TlvStream::new(bytes).range(EXPERIMENTAL_TYPES); + offer_records.chain(invreq_records).chain(experimental_records) + } + fn as_tlv_stream(&self) -> PartialInvoiceTlvStreamRef<'_> { let (payer, offer, invoice_request, experimental_offer, experimental_invoice_request) = match self { @@ -1428,7 +1475,7 @@ impl InvoiceFields { fallbacks: self.fallbacks.as_ref(), features, node_id: Some(&self.signing_pubkey), - message_paths: None, + held_htlc_available_paths: None, }, ExperimentalInvoiceTlvStreamRef { #[cfg(test)] @@ -1500,22 +1547,37 @@ impl TryFrom<Vec<u8>> for Bolt12Invoice { /// Valid type range for invoice TLV records. pub(super) const INVOICE_TYPES: core::ops::Range<u64> = 160..240; +/// TLV record type for the invoice creation timestamp. +pub(super) const INVOICE_CREATED_AT_TYPE: u64 = 164; + +/// TLV record type for [`Bolt12Invoice::payment_hash`]. +pub(super) const INVOICE_PAYMENT_HASH_TYPE: u64 = 168; + +/// TLV record type for [`Bolt12Invoice::amount_msats`]. +pub(super) const INVOICE_AMOUNT_TYPE: u64 = 170; + +/// TLV record type for [`Bolt12Invoice::invoice_features`]. +pub(super) const INVOICE_FEATURES_TYPE: u64 = 174; + +/// TLV record type for [`Bolt12Invoice::signing_pubkey`]. +pub(super) const INVOICE_NODE_ID_TYPE: u64 = 176; + tlv_stream!(InvoiceTlvStream, InvoiceTlvStreamRef<'a>, INVOICE_TYPES, { (160, paths: (Vec<BlindedPath>, WithoutLength, Iterable<'a, BlindedPathIter<'a>, BlindedPath>)), (162, blindedpay: (Vec<BlindedPayInfo>, WithoutLength, Iterable<'a, BlindedPayInfoIter<'a>, BlindedPayInfo>)), - (164, created_at: (u64, HighZeroBytesDroppedBigSize)), + (INVOICE_CREATED_AT_TYPE, created_at: (u64, HighZeroBytesDroppedBigSize)), (166, relative_expiry: (u32, HighZeroBytesDroppedBigSize)), - (168, payment_hash: PaymentHash), - (170, amount: (u64, HighZeroBytesDroppedBigSize)), + (INVOICE_PAYMENT_HASH_TYPE, payment_hash: PaymentHash), + (INVOICE_AMOUNT_TYPE, amount: (u64, HighZeroBytesDroppedBigSize)), (172, fallbacks: (Vec<FallbackAddress>, WithoutLength)), - (174, features: (Bolt12InvoiceFeatures, WithoutLength)), - (176, node_id: PublicKey), + (INVOICE_FEATURES_TYPE, features: (Bolt12InvoiceFeatures, WithoutLength)), + (INVOICE_NODE_ID_TYPE, node_id: PublicKey), // Only present in `StaticInvoice`s. - (236, message_paths: (Vec<BlindedMessagePath>, WithoutLength)), + (236, held_htlc_available_paths: (Vec<BlindedMessagePath>, WithoutLength)), }); /// Valid type range for experimental invoice TLV records. -pub(super) const EXPERIMENTAL_INVOICE_TYPES: core::ops::RangeFrom<u64> = 3_000_000_000..; +pub(super) const EXPERIMENTAL_INVOICE_TYPES: core::ops::Range<u64> = 3_000_000_000..4_000_000_000; #[cfg(not(test))] tlv_stream!( @@ -1700,7 +1762,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents { fallbacks, features, node_id, - message_paths, + held_htlc_available_paths, }, experimental_offer_tlv_stream, experimental_invoice_request_tlv_stream, @@ -1710,7 +1772,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents { }, ) = tlv_stream; - if message_paths.is_some() { + if held_htlc_available_paths.is_some() { return Err(Bolt12SemanticError::UnexpectedPaths); } @@ -1892,6 +1954,8 @@ mod tests { let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); + let mut payer_metadata = encrypted_payment_id.to_vec(); + payer_metadata.extend_from_slice(nonce.as_slice()); let payment_paths = payment_paths(); let payment_hash = payment_hash(); @@ -1913,7 +1977,7 @@ mod tests { unsigned_invoice.write(&mut buffer).unwrap(); assert_eq!(unsigned_invoice.bytes, buffer.as_slice()); - assert_eq!(unsigned_invoice.payer_metadata(), &encrypted_payment_id); + assert_eq!(unsigned_invoice.payer_metadata(), payer_metadata.as_slice()); assert_eq!( unsigned_invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)]) @@ -1957,7 +2021,7 @@ mod tests { invoice.write(&mut buffer).unwrap(); assert_eq!(invoice.bytes, buffer.as_slice()); - assert_eq!(invoice.payer_metadata(), &encrypted_payment_id); + assert_eq!(invoice.payer_metadata(), payer_metadata.as_slice()); assert_eq!( invoice.offer_chains(), Some(vec![ChainHash::using_genesis_block(Network::Bitcoin)]) @@ -1975,10 +2039,7 @@ mod tests { assert_eq!(invoice.amount_msats(), 1000); assert_eq!(invoice.invoice_request_features(), &InvoiceRequestFeatures::empty()); assert_eq!(invoice.quantity(), None); - assert_eq!( - invoice.verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx), - Ok(payment_id), - ); + assert_eq!(invoice.verify_using_metadata(&expanded_key, &secp_ctx), Ok(payment_id)); assert_eq!(invoice.payer_note(), None); assert_eq!(invoice.payment_paths(), payment_paths.as_slice()); assert_eq!(invoice.created_at(), now); @@ -2001,7 +2062,7 @@ mod tests { assert_eq!( invoice.as_tlv_stream(), ( - PayerTlvStreamRef { metadata: Some(&encrypted_payment_id.to_vec()) }, + PayerTlvStreamRef { metadata: Some(&payer_metadata) }, OfferTlvStreamRef { chains: None, metadata: None, @@ -2037,7 +2098,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), - message_paths: None, + held_htlc_available_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ExperimentalOfferTlvStreamRef { experimental_foo: None }, @@ -2140,7 +2201,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&recipient_pubkey()), - message_paths: None, + held_htlc_available_paths: None, }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ExperimentalOfferTlvStreamRef { experimental_foo: None }, @@ -3558,7 +3619,7 @@ mod tests { } #[test] - fn fails_parsing_invoice_with_message_paths() { + fn fails_parsing_invoice_with_held_htlc_available_paths() { let expanded_key = ExpandedKey::new([42; 32]); let entropy = FixedEntropy {}; let nonce = Nonce::from_entropy_source(&entropy); @@ -3590,8 +3651,8 @@ mod tests { ); let mut tlv_stream = invoice.as_tlv_stream(); - let message_paths = vec![blinded_path]; - tlv_stream.3.message_paths = Some(&message_paths); + let held_htlc_available_paths = vec![blinded_path]; + tlv_stream.3.held_htlc_available_paths = Some(&held_htlc_available_paths); match Bolt12Invoice::try_from(tlv_stream.to_bytes()) { Ok(_) => panic!("expected error"), diff --git a/lightning/src/offers/invoice_macros.rs b/lightning/src/offers/invoice_macros.rs index 1ac6e40b896..0f21024d7bc 100644 --- a/lightning/src/offers/invoice_macros.rs +++ b/lightning/src/offers/invoice_macros.rs @@ -80,6 +80,15 @@ macro_rules! invoice_builder_methods_common { ( $invoice_fields.features.set_basic_mpp_optional(); $return_value } + + #[doc = concat!("Sets [`", stringify!($invoice_type), "::invoice_features`]")] + #[doc = "to indicate MPP must not be used."] + /// + /// This only controls what the invoice advertises. It does not enforce single-HTLC receipt. + pub fn disallow_mpp($($self_mut)* $self: $self_type) -> $return_type { + $invoice_fields.features.clear_basic_mpp(); + $return_value + } } } #[cfg(test)] diff --git a/lightning/src/offers/invoice_request.rs b/lightning/src/offers/invoice_request.rs index 4311d194dca..07bd15160b7 100644 --- a/lightning/src/offers/invoice_request.rs +++ b/lightning/src/offers/invoice_request.rs @@ -1588,6 +1588,8 @@ mod tests { let secp_ctx = Secp256k1::new(); let payment_id = PaymentId([1; 32]); let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); + let mut payer_metadata = encrypted_payment_id.to_vec(); + payer_metadata.extend_from_slice(nonce.as_slice()); let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) @@ -1602,7 +1604,7 @@ mod tests { invoice_request.write(&mut buffer).unwrap(); assert_eq!(invoice_request.bytes, buffer.as_slice()); - assert_eq!(invoice_request.payer_metadata(), &encrypted_payment_id); + assert_eq!(invoice_request.payer_metadata(), payer_metadata.as_slice()); assert_eq!( invoice_request.chains(), vec![ChainHash::using_genesis_block(Network::Bitcoin)] @@ -1634,7 +1636,7 @@ mod tests { assert_eq!( invoice_request.as_tlv_stream(), ( - PayerTlvStreamRef { metadata: Some(&encrypted_payment_id.to_vec()) }, + PayerTlvStreamRef { metadata: Some(&payer_metadata) }, OfferTlvStreamRef { chains: None, metadata: None, @@ -1735,10 +1737,10 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_ok()); + match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } // Fails verification with altered fields let ( @@ -1774,9 +1776,7 @@ mod tests { .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered payer id let ( @@ -1812,9 +1812,7 @@ mod tests { .unwrap(); let invoice = Bolt12Invoice::try_from(encoded_invoice).unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); } #[test] @@ -2040,6 +2038,12 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::MissingAmount), } + // An offer with amount_msats(0) must be rejected by the builder per BOLT 12. + match OfferBuilder::new(recipient_pubkey()).amount_msats(0).build() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), + } + match OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) @@ -2215,6 +2219,19 @@ mod tests { Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidQuantity), } + match OfferBuilder::new(recipient_pubkey()) + .amount_msats(1000) + .supported_quantity(Quantity::Bounded(ten)) + .build() + .unwrap() + .request_invoice(&expanded_key, nonce, &secp_ctx, payment_id) + .unwrap() + .quantity(0) + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidQuantity), + } + let invoice_request = OfferBuilder::new(recipient_pubkey()) .amount_msats(1000) .supported_quantity(Quantity::Unbounded) diff --git a/lightning/src/offers/merkle.rs b/lightning/src/offers/merkle.rs index 1a38fe5441f..9953f3a3b46 100644 --- a/lightning/src/offers/merkle.rs +++ b/lightning/src/offers/merkle.rs @@ -49,13 +49,10 @@ impl TaggedHash { /// Creates a tagged hash with the given parameters. /// /// Panics if `tlv_stream` is not a well-formed TLV stream containing at least one TLV record. - pub(super) fn from_tlv_stream<'a, I: core::iter::Iterator<Item = TlvRecord<'a>>>( + pub(super) fn from_tlv_stream<'a, I: core::iter::Iterator<Item = TlvRecord<'a>> + 'a>( tag: &'static str, tlv_stream: I, ) -> Self { - let tag_hash = sha256::Hash::hash(tag.as_bytes()); - let merkle_root = root_hash(tlv_stream); - let digest = Message::from_digest(tagged_hash(tag_hash, merkle_root).to_byte_array()); - Self { tag, merkle_root, digest } + Self::from_merkle_root(tag, root_hash(tlv_stream)) } /// Returns the digest to sign. @@ -73,6 +70,13 @@ impl TaggedHash { self.merkle_root } + /// Creates a tagged hash from a pre-computed merkle root. + pub(super) fn from_merkle_root(tag: &'static str, merkle_root: sha256::Hash) -> Self { + let tag_hash = sha256::Hash::hash(tag.as_bytes()); + let digest = Message::from_digest(tagged_hash(tag_hash, merkle_root).to_byte_array()); + Self { tag, merkle_root, digest } + } + pub(super) fn to_bytes(&self) -> [u8; 32] { *self.digest.as_ref() } @@ -146,9 +150,23 @@ pub fn verify_signature( secp_ctx.verify_schnorr(signature, digest, &pubkey) } -/// Computes a merkle root hash for the given data, which must be a well-formed TLV stream -/// containing at least one TLV record. -fn root_hash<'a, I: core::iter::Iterator<Item = TlvRecord<'a>>>(tlv_stream: I) -> sha256::Hash { +/// Per-TLV merkle hashes shared by [`root_hash`] and the selective-disclosure code. +/// Keeping this in one place ensures the signed invoice root and the payer-proof +/// reconstruction hash identical inputs the exact same way. +pub(super) struct TlvHashData { + pub(super) tlv_type: u64, + pub(super) nonce_hash: sha256::Hash, + pub(super) per_tlv_hash: sha256::Hash, +} + +/// Computes the per-TLV branch hashes for every non-signature record in `tlv_stream`. Returns the +/// iterator plus the shared `LnBranch` tag engine used to combine hashes into the root. +pub(super) fn merkle_tlv_data<'a, I>( + tlv_stream: I, +) -> (impl Iterator<Item = TlvHashData> + 'a, sha256::HashEngine) +where + I: core::iter::Iterator<Item = TlvRecord<'a>> + 'a, +{ let mut tlv_stream = tlv_stream.peekable(); let nonce_tag = tagged_hash_engine(sha256::Hash::from_engine({ let first_tlv_record = tlv_stream.peek().unwrap(); @@ -159,12 +177,29 @@ fn root_hash<'a, I: core::iter::Iterator<Item = TlvRecord<'a>>>(tlv_stream: I) - })); let leaf_tag = tagged_hash_engine(sha256::Hash::hash("LnLeaf".as_bytes())); let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes())); + let iter_branch_tag = branch_tag.clone(); - let mut leaves = Vec::new(); - for record in tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)) { - leaves.push(tagged_hash_from_engine(leaf_tag.clone(), &record.record_bytes)); - leaves.push(tagged_hash_from_engine(nonce_tag.clone(), &record.type_bytes)); - } + let tlv_data = + tlv_stream.filter(|record| !SIGNATURE_TYPES.contains(&record.r#type)).map(move |record| { + let leaf_hash = tagged_hash_from_engine(leaf_tag.clone(), record.record_bytes); + let nonce_hash = tagged_hash_from_engine(nonce_tag.clone(), record.type_bytes); + let per_tlv_hash = + tagged_branch_hash_from_engine(iter_branch_tag.clone(), leaf_hash, nonce_hash); + + TlvHashData { tlv_type: record.r#type, nonce_hash, per_tlv_hash } + }); + + (tlv_data, branch_tag) +} + +/// Computes a merkle root hash for the given data, which must be a well-formed TLV stream +/// containing at least one TLV record. +fn root_hash<'a, I: core::iter::Iterator<Item = TlvRecord<'a>> + 'a>( + tlv_stream: I, +) -> sha256::Hash { + let (tlv_data, branch_tag) = merkle_tlv_data(tlv_stream); + let mut leaves: Vec<sha256::Hash> = tlv_data.map(|data| data.per_tlv_hash).collect(); + assert!(!leaves.is_empty(), "TLV stream must contain at least one non-signature record"); // Calculate the merkle root hash in place. let num_leaves = leaves.len(); @@ -190,19 +225,21 @@ fn tagged_hash<T: AsRef<[u8]>>(tag: sha256::Hash, msg: T) -> sha256::Hash { tagged_hash_from_engine(engine, msg) } -fn tagged_hash_engine(tag: sha256::Hash) -> sha256::HashEngine { +pub(super) fn tagged_hash_engine(tag: sha256::Hash) -> sha256::HashEngine { let mut engine = sha256::Hash::engine(); engine.input(tag.as_ref()); engine.input(tag.as_ref()); engine } -fn tagged_hash_from_engine<T: AsRef<[u8]>>(mut engine: sha256::HashEngine, msg: T) -> sha256::Hash { +pub(super) fn tagged_hash_from_engine<T: AsRef<[u8]>>( + mut engine: sha256::HashEngine, msg: T, +) -> sha256::Hash { engine.input(msg.as_ref()); sha256::Hash::from_engine(engine) } -fn tagged_branch_hash_from_engine( +pub(super) fn tagged_branch_hash_from_engine( mut engine: sha256::HashEngine, leaf1: sha256::Hash, leaf2: sha256::Hash, ) -> sha256::Hash { if leaf1 < leaf2 { @@ -243,9 +280,23 @@ pub(super) struct TlvRecord<'a> { type_bytes: &'a [u8], // The entire TLV record. pub(super) record_bytes: &'a [u8], + // The value portion of the TLV record (after type and length). + pub(super) value_bytes: &'a [u8], pub(super) end: usize, } +impl<'a> TlvRecord<'a> { + /// Read a value from this TLV record's value bytes using [`Readable`]. + pub(super) fn read_value<T: Readable>(&self) -> Result<T, crate::ln::msgs::DecodeError> { + let mut value_bytes = self.value_bytes; + let value = Readable::read(&mut value_bytes)?; + if !value_bytes.is_empty() { + return Err(crate::ln::msgs::DecodeError::InvalidValue); + } + Ok(value) + } +} + impl<'a> Iterator for TlvStream<'a> { type Item = TlvRecord<'a>; @@ -261,12 +312,12 @@ impl<'a> Iterator for TlvStream<'a> { let offset = self.data.position(); let end = offset + length; - let _value = &self.data.get_ref()[offset as usize..end as usize]; let record_bytes = &self.data.get_ref()[start as usize..end as usize]; + let value_bytes = &self.data.get_ref()[offset as usize..end as usize]; self.data.set_position(end); - Some(TlvRecord { r#type, type_bytes, record_bytes, end: end as usize }) + Some(TlvRecord { r#type, type_bytes, record_bytes, value_bytes, end: end as usize }) } else { None } @@ -497,4 +548,23 @@ mod tests { self.fmt_bech32_str(f) } } + + #[test] + fn test_tlv_record_read_value_rejects_trailing_bytes() { + use bitcoin::secp256k1::PublicKey; + + use crate::offers::test_utils::payer_pubkey; + use crate::util::ser::{BigSize, Writeable}; + + let pubkey = payer_pubkey(); + let mut tlv_bytes = Vec::new(); + BigSize(88).write(&mut tlv_bytes).unwrap(); + BigSize(35).write(&mut tlv_bytes).unwrap(); + pubkey.write(&mut tlv_bytes).unwrap(); + tlv_bytes.extend_from_slice(&[0x00, 0x01]); + + let record = TlvStream::new(&tlv_bytes).next().unwrap(); + let result: Result<PublicKey, _> = record.read_value(); + assert!(matches!(result, Err(crate::ln::msgs::DecodeError::InvalidValue))); + } } diff --git a/lightning/src/offers/mod.rs b/lightning/src/offers/mod.rs index 5b5cf6cdc78..c80c9e07e8d 100644 --- a/lightning/src/offers/mod.rs +++ b/lightning/src/offers/mod.rs @@ -25,7 +25,9 @@ pub mod merkle; pub mod nonce; pub mod parse; mod payer; +pub mod payer_proof; pub mod refund; +pub mod selective_disclosure; pub(crate) mod signer; pub mod static_invoice; #[cfg(test)] diff --git a/lightning/src/offers/nonce.rs b/lightning/src/offers/nonce.rs index 8c99a464abc..4eee35bc306 100644 --- a/lightning/src/offers/nonce.rs +++ b/lightning/src/offers/nonce.rs @@ -25,7 +25,7 @@ use crate::prelude::*; /// [`Offer::metadata`]: crate::offers::offer::Offer::metadata /// [`Offer::issuer_signing_pubkey`]: crate::offers::offer::Offer::issuer_signing_pubkey /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Nonce(pub(crate) [u8; Self::LENGTH]); impl Nonce { diff --git a/lightning/src/offers/offer.rs b/lightning/src/offers/offer.rs index 5592c50a264..8e6b36f7311 100644 --- a/lightning/src/offers/offer.rs +++ b/lightning/src/offers/offer.rs @@ -402,7 +402,7 @@ macro_rules! offer_builder_methods { ( pub fn build($($self_mut)* $self: $self_type) -> Result<Offer, Bolt12SemanticError> { match $self.offer.amount { Some(Amount::Bitcoin { amount_msats }) => { - if amount_msats > MAX_VALUE_MSAT { + if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT { return Err(Bolt12SemanticError::InvalidAmount); } }, @@ -975,7 +975,7 @@ impl OfferContents { fn is_valid_quantity(&self, quantity: u64) -> bool { match self.supported_quantity { - Quantity::Bounded(n) => quantity <= n.get(), + Quantity::Bounded(n) => quantity > 0 && quantity <= n.get(), Quantity::Unbounded => quantity > 0, Quantity::One => quantity == 1, } @@ -1211,6 +1211,12 @@ pub(super) const OFFER_TYPES: core::ops::Range<u64> = 1..80; /// TLV record type for [`Offer::metadata`]. const OFFER_METADATA_TYPE: u64 = 4; +/// TLV record type for [`Offer::description`]. +pub(super) const OFFER_DESCRIPTION_TYPE: u64 = 10; + +/// TLV record type for [`Offer::issuer`]. +pub(super) const OFFER_ISSUER_TYPE: u64 = 18; + /// TLV record type for [`Offer::issuer_signing_pubkey`]. const OFFER_ISSUER_ID_TYPE: u64 = 22; @@ -1219,11 +1225,11 @@ tlv_stream!(OfferTlvStream, OfferTlvStreamRef<'a>, OFFER_TYPES, { (OFFER_METADATA_TYPE, metadata: (Vec<u8>, WithoutLength)), (6, currency: [u8; 3]), (8, amount: (u64, HighZeroBytesDroppedBigSize)), - (10, description: (String, WithoutLength)), + (OFFER_DESCRIPTION_TYPE, description: (String, WithoutLength)), (12, features: (OfferFeatures, WithoutLength)), (14, absolute_expiry: (u64, HighZeroBytesDroppedBigSize)), (16, paths: (Vec<BlindedMessagePath>, WithoutLength)), - (18, issuer: (String, WithoutLength)), + (OFFER_ISSUER_TYPE, issuer: (String, WithoutLength)), (20, quantity_max: (u64, HighZeroBytesDroppedBigSize)), (OFFER_ISSUER_ID_TYPE, issuer_id: PublicKey), }); @@ -1306,11 +1312,12 @@ impl TryFrom<FullOfferTlvStream> for OfferContents { let amount = match (currency, amount) { (None, None) => None, - (None, Some(amount_msats)) if amount_msats > MAX_VALUE_MSAT => { + (None, Some(amount_msats)) if amount_msats == 0 || amount_msats > MAX_VALUE_MSAT => { return Err(Bolt12SemanticError::InvalidAmount); }, (None, Some(amount_msats)) => Some(Amount::Bitcoin { amount_msats }), (Some(_), None) => return Err(Bolt12SemanticError::MissingAmount), + (Some(_), Some(0)) => return Err(Bolt12SemanticError::InvalidAmount), (Some(currency_bytes), Some(amount)) => { let iso4217_code = CurrencyCode::new(currency_bytes) .map_err(|_| Bolt12SemanticError::InvalidCurrencyCode)?; @@ -1702,6 +1709,12 @@ mod tests { Ok(_) => panic!("expected error"), Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), } + + // An amount of 0 must be rejected per BOLT 12. + match OfferBuilder::new(pubkey(42)).amount_msats(0).build() { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!(e, Bolt12SemanticError::InvalidAmount), + } } #[test] @@ -1974,6 +1987,59 @@ mod tests { Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidCurrencyCode) ), } + + // An offer with amount=0 must be rejected per BOLT 12. + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.0.amount = Some(0); + tlv_stream.0.currency = None; + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + // An offer with amount=0 and a currency must also be rejected. + let mut tlv_stream = offer.as_tlv_stream(); + tlv_stream.0.amount = Some(0); + tlv_stream.0.currency = Some(b"USD"); + + let mut encoded_offer = Vec::new(); + tlv_stream.write(&mut encoded_offer).unwrap(); + + match Offer::try_from(encoded_offer) { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + // BOLT 12 test vectors: verify rejection of offers with amount=0 from their + // bech32 encoding (see bolt12/offers-test.json). + match "lno1pqqq5qqkyyp4he0fg7pqje62jmnq78cr0ashv4q06qql58tyd9rhp3t2wuyugtq".parse::<Offer>() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } + + match "lno1qcp4256ypqqq5qqkyyp4he0fg7pqje62jmnq78cr0ashv4q06qql58tyd9rhp3t2wuyugtq" + .parse::<Offer>() + { + Ok(_) => panic!("expected error"), + Err(e) => assert_eq!( + e, + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::InvalidAmount) + ), + } } #[test] diff --git a/lightning/src/offers/payer_proof.rs b/lightning/src/offers/payer_proof.rs new file mode 100644 index 00000000000..866e43a3754 --- /dev/null +++ b/lightning/src/offers/payer_proof.rs @@ -0,0 +1,2570 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Payer proofs for BOLT 12 invoices. +//! +//! A [`PayerProof`] cryptographically proves that a BOLT 12 invoice was paid by demonstrating: +//! - Possession of the payment preimage (proving the payment occurred) +//! - A valid invoice signature over a merkle root (proving the invoice is authentic) +//! - The payer's signature (proving who authorized the payment) +//! +//! This implements the payer proof extension to BOLT 12 as specified in +//! <https://github.com/lightning/bolts/pull/1295>. + +use alloc::collections::BTreeSet; + +use crate::io; +use crate::ln::channelmanager::PaymentId; +use crate::ln::inbound_payment::ExpandedKey; +use crate::ln::msgs::DecodeError; +use crate::offers::invoice::{ + Bolt12Invoice, DerivedSigningPubkey, ExperimentalInvoiceTlvStream, ExplicitSigningPubkey, + InvoiceTlvStream, SigningPubkeyStrategy, EXPERIMENTAL_INVOICE_TYPES, INVOICE_AMOUNT_TYPE, + INVOICE_CREATED_AT_TYPE, INVOICE_FEATURES_TYPE, INVOICE_NODE_ID_TYPE, + INVOICE_PAYMENT_HASH_TYPE, SIGNATURE_TAG, +}; +use crate::offers::invoice_request::{ + ExperimentalInvoiceRequestTlvStream, InvoiceRequestTlvStream, INVOICE_REQUEST_PAYER_ID_TYPE, +}; +use crate::offers::merkle::{self, SignError, TaggedHash, TlvRecord, TlvStream, SIGNATURE_TYPES}; +use crate::offers::offer::{ + ExperimentalOfferTlvStream, OfferTlvStream, EXPERIMENTAL_OFFER_TYPES, OFFER_DESCRIPTION_TYPE, + OFFER_ISSUER_TYPE, +}; +use crate::offers::parse::{Bech32Encode, Bolt12ParseError, Bolt12SemanticError, ParsedMessage}; +use crate::offers::payer::PAYER_METADATA_TYPE; +use crate::offers::selective_disclosure::{self, SelectiveDisclosure, SelectiveDisclosureError}; +use crate::offers::static_invoice::StaticInvoice; +use crate::types::payment::{PaymentHash, PaymentPreimage}; +use crate::util::ser::{ + BigSize, CursorReadable, HighZeroBytesDroppedBigSize, WithoutLength, Writeable, Writer, +}; +use lightning_types::string::PrintableString; + +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1; +use bitcoin::secp256k1::schnorr::Signature; +use bitcoin::secp256k1::{PublicKey, Secp256k1}; + +use core::convert::TryFrom; +use core::time::Duration; + +#[allow(unused_imports)] +use crate::prelude::*; + +/// The BOLT 12 invoice that was paid. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PaidBolt12Invoice { + /// A standard BOLT 12 invoice, allowing proof of payment. + /// + /// The payer signing key needed to build a payer proof is re-derived from the invoice's own + /// payer metadata, so no separate [`Nonce`] needs to be stored alongside it. + /// + /// [`Nonce`]: crate::offers::nonce::Nonce + Bolt12Invoice(Bolt12Invoice), + /// A static invoice used in async payments, where proof of payment is not possible. + StaticInvoice(StaticInvoice), +} + +// The length-prefixed, variant-tagged wire layout (variant `u8`, then `BigSize` length, then the +// invoice) matches how the paid invoice has always been serialized in its containers. +impl_ser_tlv_based_enum!(PaidBolt12Invoice, + {0, Bolt12Invoice} => (), + {2, StaticInvoice} => (), +); + +/// A paid BOLT 12 invoice. +/// +/// For standard [`Bolt12Invoice`] payments, use [`Self::prove_payer`] or +/// [`Self::prove_payer_derived`] to build a [`PayerProof`] that selectively discloses +/// invoice fields to a third-party verifier. +/// +/// For async payments (i.e., [`StaticInvoice`]), payer proofs are not supported and those +/// methods will return [`PayerProofError::IncompatibleInvoice`]. +/// +/// Surfaced in [`Event::PaymentSent::bolt12_invoice`]. +/// +/// [`Event::PaymentSent::bolt12_invoice`]: crate::events::Event::PaymentSent::bolt12_invoice +impl PaidBolt12Invoice { + /// Returns the [`Bolt12Invoice`] if the payment was for a standard BOLT 12 invoice. + pub fn bolt12_invoice(&self) -> Option<&Bolt12Invoice> { + match self { + PaidBolt12Invoice::Bolt12Invoice(invoice) => Some(invoice), + _ => None, + } + } + + /// Returns the [`StaticInvoice`] if the payment was for an async payment. + pub fn static_invoice(&self) -> Option<&StaticInvoice> { + match self { + PaidBolt12Invoice::StaticInvoice(invoice) => Some(invoice), + _ => None, + } + } + + /// Creates a [`PayerProofBuilder`] for this paid invoice. + pub fn prove_payer( + &self, payment_preimage: PaymentPreimage, + ) -> Result<PayerProofBuilder<ExplicitSigningPubkey>, PayerProofError> { + let invoice = self.bolt12_invoice().ok_or(PayerProofError::IncompatibleInvoice)?; + PayerProofBuilder::new(invoice, payment_preimage) + } + + /// Creates a [`PayerProofBuilder`] with a pre-derived signing keypair. + /// + /// The payer signing key is re-derived from the invoice's own payer metadata, failing early + /// if derivation fails. The supplied `payment_id` (e.g. from [`Event::PaymentSent`]) is + /// checked against the payment id recovered from that metadata to guard against a mismatched + /// invoice or key. + /// + /// [`Event::PaymentSent`]: crate::events::Event::PaymentSent + pub fn prove_payer_derived<T: secp256k1::Signing>( + &self, payment_preimage: PaymentPreimage, expanded_key: &ExpandedKey, + payment_id: PaymentId, secp_ctx: &Secp256k1<T>, + ) -> Result<PayerProofBuilder<DerivedSigningPubkey>, PayerProofError> { + let invoice = self.bolt12_invoice().ok_or(PayerProofError::IncompatibleInvoice)?; + let recovered = invoice + .verify_using_metadata(expanded_key, secp_ctx) + .map_err(|_| PayerProofError::KeyDerivationFailed)?; + if recovered != payment_id { + return Err(PayerProofError::KeyDerivationFailed); + } + PayerProofBuilder::new_derived(invoice, payment_preimage, expanded_key, secp_ctx) + } +} + +const PAYER_PROOF_ISSUER_SIGNATURE_TYPE: u64 = 240; +const PAYER_PROOF_PROOF_SIGNATURE_TYPE: u64 = 241; +const PAYER_PROOF_PREIMAGE_TYPE: u64 = 1001; +const PAYER_PROOF_OMITTED_TLVS_TYPE: u64 = 1002; +const PAYER_PROOF_MISSING_HASHES_TYPE: u64 = 1003; +const PAYER_PROOF_LEAF_HASHES_TYPE: u64 = 1004; +const PAYER_PROOF_PROOF_NOTE_TYPE: u64 = 1005; + +/// Range covering the data-bearing payer-proof TLVs. +pub(super) const PAYER_PROOF_DATA_TYPES: core::ops::Range<u64> = 1001..1_000_000_000; + +/// Human-readable prefix for payer proofs in bech32 encoding. +pub const PAYER_PROOF_HRP: &str = "lnp"; + +/// Tag for `proof_signature` computation per BOLT 12 signature calculation. +/// Format: "lightning" || messagename || fieldname +const PROOF_SIGNATURE_TAG: &str = concat!("lightning", "payer_proof", "proof_signature"); + +/// Error when building or verifying a payer proof. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PayerProofError { + /// The invoice is not a [`Bolt12Invoice`] (e.g., it is a [`StaticInvoice`]). + /// + /// [`StaticInvoice`]: crate::offers::static_invoice::StaticInvoice + IncompatibleInvoice, + /// The preimage doesn't match the invoice's payment hash. + PreimageMismatch, + /// Error during merkle tree operations. + MerkleError(SelectiveDisclosureError), + /// The invoice signature is invalid. + InvalidInvoiceSignature, + /// Failed to re-derive the payer signing key from the invoice's payer metadata. + KeyDerivationFailed, + /// The given TLV type cannot be included in a payer proof. Carries the offending + /// type number. Reasons include `PAYER_METADATA_TYPE`, TLVs in `SIGNATURE_TYPES`, + /// or TLVs in `PAYER_PROOF_DATA_TYPES`. + DisallowedTlvType(u64), + + /// Error decoding the payer proof. + DecodeError(DecodeError), +} + +impl From<SelectiveDisclosureError> for PayerProofError { + fn from(e: SelectiveDisclosureError) -> Self { + PayerProofError::MerkleError(e) + } +} + +impl From<DecodeError> for PayerProofError { + fn from(e: DecodeError) -> Self { + PayerProofError::DecodeError(e) + } +} + +/// A cryptographic proof that a BOLT 12 invoice was paid. +/// +/// Contains the payment preimage, selective disclosure of invoice fields, +/// the invoice signature, and a payer signature proving who paid. +#[derive(Clone, Debug)] +pub struct PayerProof { + bytes: Vec<u8>, + contents: PayerProofContents, + proof_signature: Signature, + merkle_root: sha256::Hash, +} + +/// The contents of a [`PayerProof`] -- everything shared between a signed +/// [`PayerProof`] and its [`UnsignedPayerProof`] sibling, with the exception +/// of the `proof_signature` which is only available after signing. +#[derive(Clone, Debug)] +struct PayerProofContents { + payer_signing_pubkey: PublicKey, + payment_hash: PaymentHash, + issuer_signing_pubkey: PublicKey, + preimage: PaymentPreimage, + invoice_signature: Signature, + proof_note: Option<String>, + disclosed_fields: DisclosedFields, +} + +#[derive(Clone, Debug, Default)] +struct DisclosedFields { + offer_description: Option<String>, + offer_issuer: Option<String>, + invoice_amount_msats: Option<u64>, + invoice_created_at: Option<Duration>, +} + +/// Builds a [`PayerProof`] from a paid invoice and its preimage. +/// +/// By default, only the required fields are included ([`payer_signing_pubkey`], +/// [`payment_hash`], [`issuer_signing_pubkey`]). Additional fields can be included for +/// selective disclosure using the `include_*` methods. +/// +/// [`payer_signing_pubkey`]: PayerProof::payer_signing_pubkey +/// [`payment_hash`]: PayerProof::payment_hash +/// [`issuer_signing_pubkey`]: PayerProof::issuer_signing_pubkey +pub struct PayerProofBuilder<S: SigningPubkeyStrategy> { + /// The paid invoice's TLV bytes, kept so the selective disclosure can be recomputed at build + /// time once the caller has finished choosing which types to include. Owned (rather than a + /// borrow of the `Bolt12Invoice`) so the builder is `'static`, which is much friendlier for + /// language bindings. + invoice_bytes: Vec<u8>, + /// The proof contents, pre-populated with everything known up front. `disclosed_fields` starts + /// empty and is filled in by [`Self::build_unsigned`] from `included_types`. + contents: PayerProofContents, + included_types: BTreeSet<u64>, + signing_strategy: S, +} + +/// The default set of TLV types always included in a payer proof: payer_id, +/// payment_hash, issuer signing pubkey, and invoice features when present. +fn default_included_types(invoice: &Bolt12Invoice) -> BTreeSet<u64> { + let mut types = BTreeSet::new(); + types.insert(INVOICE_REQUEST_PAYER_ID_TYPE); + types.insert(INVOICE_PAYMENT_HASH_TYPE); + types.insert(INVOICE_NODE_ID_TYPE); + if TlvStream::new(invoice.invoice_bytes()).any(|r| r.r#type == INVOICE_FEATURES_TYPE) { + types.insert(INVOICE_FEATURES_TYPE); + } + types +} + +/// Builds the [`PayerProofContents`] known at builder-construction time. The `disclosed_fields` +/// are left empty here and populated from the chosen `included_types` in +/// [`PayerProofBuilder::build_unsigned`]. +fn pending_contents(invoice: &Bolt12Invoice, preimage: PaymentPreimage) -> PayerProofContents { + PayerProofContents { + payer_signing_pubkey: invoice.payer_signing_pubkey(), + payment_hash: invoice.payment_hash(), + issuer_signing_pubkey: invoice.signing_pubkey(), + preimage, + invoice_signature: invoice.signature(), + proof_note: None, + disclosed_fields: DisclosedFields::default(), + } +} + +impl PayerProofBuilder<ExplicitSigningPubkey> { + /// Create a new builder from an invoice and its payment preimage. + /// + /// Returns an error if the preimage doesn't match the invoice's payment hash. + pub(super) fn new( + invoice: &Bolt12Invoice, preimage: PaymentPreimage, + ) -> Result<Self, PayerProofError> { + let computed_hash: PaymentHash = preimage.into(); + if computed_hash != invoice.payment_hash() { + return Err(PayerProofError::PreimageMismatch); + } + + Ok(Self { + invoice_bytes: invoice.invoice_bytes().to_vec(), + contents: pending_contents(invoice, preimage), + included_types: default_included_types(invoice), + signing_strategy: ExplicitSigningPubkey {}, + }) + } + + /// Builds an [`UnsignedPayerProof`] that can be signed with [`UnsignedPayerProof::sign`]. + pub fn build(self) -> Result<UnsignedPayerProof, PayerProofError> { + self.build_unsigned() + } +} + +impl PayerProofBuilder<DerivedSigningPubkey> { + /// Create a new builder with a pre-derived signing keypair. + /// + /// Derives the payer signing key using the same derivation scheme as invoice requests + /// created with `deriving_signing_pubkey`. Fails early if key derivation fails. + fn new_derived<T: secp256k1::Signing>( + invoice: &Bolt12Invoice, preimage: PaymentPreimage, expanded_key: &ExpandedKey, + secp_ctx: &Secp256k1<T>, + ) -> Result<Self, PayerProofError> { + let computed_hash = sha256::Hash::hash(&preimage.0); + if computed_hash.as_byte_array() != &invoice.payment_hash().0 { + return Err(PayerProofError::PreimageMismatch); + } + + let keys = invoice + .derive_payer_signing_keys(expanded_key, secp_ctx) + .map_err(|_| PayerProofError::KeyDerivationFailed)?; + + Ok(Self { + invoice_bytes: invoice.invoice_bytes().to_vec(), + contents: pending_contents(invoice, preimage), + included_types: default_included_types(invoice), + signing_strategy: DerivedSigningPubkey(keys), + }) + } + + /// Builds and signs a [`PayerProof`] using the keypair derived at construction time. + pub fn build_and_sign(self) -> Result<PayerProof, PayerProofError> { + let secp_ctx = Secp256k1::signing_only(); + let keys = self.signing_strategy.0; + let unsigned = self.build_unsigned()?; + // Signing with a derived keypair and an infallible closure cannot fail: + // the signing function never errors and verification succeeds because we + // derived the matching pubkey. + let proof = unsigned + .sign(|proof: &UnsignedPayerProof| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(proof.as_ref().as_digest(), &keys)) + }) + .expect("signing with derived keys and infallible closure cannot fail"); + Ok(proof) + } +} + +impl<S: SigningPubkeyStrategy> PayerProofBuilder<S> { + /// Include a specific TLV type in the proof. + /// + /// Returns an error if the type is not allowed: `PAYER_METADATA_TYPE`, TLVs in + /// `SIGNATURE_TYPES`, or TLVs in `PAYER_PROOF_DATA_TYPES`. + pub fn include_type(mut self, tlv_type: u64) -> Result<Self, PayerProofError> { + if tlv_type == PAYER_METADATA_TYPE + || SIGNATURE_TYPES.contains(&tlv_type) + || PAYER_PROOF_DATA_TYPES.contains(&tlv_type) + { + return Err(PayerProofError::DisallowedTlvType(tlv_type)); + } + self.included_types.insert(tlv_type); + Ok(self) + } + + /// Include the offer description in the proof. + pub fn include_offer_description(mut self) -> Self { + self.included_types.insert(OFFER_DESCRIPTION_TYPE); + self + } + + /// Include the offer issuer in the proof. + pub fn include_offer_issuer(mut self) -> Self { + self.included_types.insert(OFFER_ISSUER_TYPE); + self + } + + /// Include the invoice amount in the proof. + pub fn include_invoice_amount(mut self) -> Self { + self.included_types.insert(INVOICE_AMOUNT_TYPE); + self + } + + /// Include the invoice creation timestamp in the proof. + pub fn include_invoice_created_at(mut self) -> Self { + self.included_types.insert(INVOICE_CREATED_AT_TYPE); + self + } + + /// Attach a `proof_note` to this proof. The note is scoped to the proof and + /// is committed to by the `proof_signature` alongside the invoice's merkle + /// root. It is independent of any [`InvoiceRequest::payer_note`] set during + /// the payment flow. + /// + /// [`InvoiceRequest::payer_note`]: crate::offers::invoice_request::InvoiceRequest::payer_note + pub fn with_proof_note(mut self, note: String) -> Self { + self.contents.proof_note = Some(note); + self + } + + fn build_unsigned(mut self) -> Result<UnsignedPayerProof, PayerProofError> { + let disclosed_fields = + DisclosedFields::from_records(TlvStream::new(&self.invoice_bytes).filter(|r| { + self.included_types.contains(&r.r#type) && !SIGNATURE_TYPES.contains(&r.r#type) + }))?; + + let disclosure = selective_disclosure::compute_selective_disclosure( + TlvStream::new(&self.invoice_bytes), + &self.included_types, + ); + + self.contents.disclosed_fields = disclosed_fields; + + Ok(UnsignedPayerProof::new( + &self.invoice_bytes, + &self.included_types, + self.contents, + disclosure, + )) + } +} + +/// Computes the [`TaggedHash`] for the `proof_signature` over the merkle root +/// of the payer-proof TLV stream. +fn proof_signature_hash(bytes: &[u8]) -> TaggedHash { + TaggedHash::from_valid_tlv_stream_bytes(PROOF_SIGNATURE_TAG, bytes) +} + +/// An unsigned [`PayerProof`] ready for signing. +/// +/// The serialised proof is stored as two byte buffers split at the +/// `proof_signature` TLV insertion point. [`Self::sign`] writes the freshly +/// computed `proof_signature` TLV between them to produce the final +/// [`PayerProof`] bytes, so no second serialisation pass is needed. The +/// [`TaggedHash`] is computed up front over the same concatenated stream. +pub struct UnsignedPayerProof { + /// Bytes of the included invoice records up to and including the + /// `invoice_signature` TLV (`PAYER_PROOF_ISSUER_SIGNATURE_TYPE`). + bytes_before_proof_signature: Vec<u8>, + /// Bytes of the payer-proof data TLVs followed by any disclosed + /// experimental invoice TLVs. Together with the bytes above, these form + /// the merkle-root input the `proof_signature` is computed over. + bytes_after_proof_signature: Vec<u8>, + contents: PayerProofContents, + /// Merkle root of the underlying invoice, surfaced on the resulting + /// [`PayerProof`]. + merkle_root: sha256::Hash, + tagged_hash: TaggedHash, +} + +impl UnsignedPayerProof { + /// Build an `UnsignedPayerProof` from the underlying invoice bytes, the + /// included TLV types, the proof contents (everything except + /// `proof_signature`), and the precomputed selective-disclosure data. + /// + /// This performs the byte-level serialization split at the + /// `proof_signature` TLV insertion point and computes the tagged hash, + /// so callers never see a partially-initialised struct. + fn new( + invoice_bytes: &[u8], included_types: &BTreeSet<u64>, contents: PayerProofContents, + disclosure: SelectiveDisclosure, + ) -> Self { + // Pre-`proof_signature` bytes hold the included invoice records below the signature range + // plus the `invoice_signature` TLV; post-`proof_signature` bytes hold the payer-proof data + // TLVs (preimage, omitted markers, missing/leaf hashes, note) plus any disclosed + // experimental invoice records. The pre-signature buffer is sized to hold its own records + // (a subset of `invoice_bytes`); the post-signature buffer starts at a fixed allowance for + // the data TLVs. Once both halves are built, the pre-signature buffer is grown to also hold + // the bytes `sign()` appends to it (see below). + const PROOF_DATA_TLVS_ALLOCATION_SIZE: usize = 256; + let mut bytes_before_proof_signature = Vec::with_capacity(invoice_bytes.len()); + let mut bytes_after_proof_signature = Vec::with_capacity(PROOF_DATA_TLVS_ALLOCATION_SIZE); + + // Emit included invoice records below the signature range, then the + // `invoice_signature` TLV. The `proof_signature` TLV is inserted at + // sign time between the buffer above and the buffer assembled below. + for record in TlvStream::new(invoice_bytes) + .range(0..PAYER_PROOF_ISSUER_SIGNATURE_TYPE) + .filter(|r| included_types.contains(&r.r#type)) + { + bytes_before_proof_signature.extend_from_slice(record.record_bytes); + } + let invoice_signature_tlv = PayerProofSignatureTlvStreamRef { + invoice_signature: Some(&contents.invoice_signature), + proof_signature: None, + }; + invoice_signature_tlv + .write(&mut bytes_before_proof_signature) + .expect("Vec write should not fail"); + + // Post-signature half: payer-proof data TLVs, then disclosed + // experimental invoice records. + let proof_omitted_markers = (!disclosure.omitted_markers.is_empty()) + .then(|| disclosure.omitted_markers.iter().copied().map(BigSize).collect::<Vec<_>>()); + let data = PayerProofDataTlvStreamRef { + proof_preimage: Some(&contents.preimage), + proof_omitted_markers: proof_omitted_markers.as_ref(), + proof_missing_hashes: (!disclosure.missing_hashes.is_empty()) + .then_some(&disclosure.missing_hashes), + proof_leaf_hashes: (!disclosure.nonce_hashes.is_empty()) + .then_some(&disclosure.nonce_hashes), + proof_note: contents.proof_note.as_ref(), + }; + data.write(&mut bytes_after_proof_signature).expect("Vec write should not fail"); + for record in TlvStream::new(invoice_bytes) + .range(EXPERIMENTAL_OFFER_TYPES.start..) + .filter(|r| included_types.contains(&r.r#type)) + { + bytes_after_proof_signature.extend_from_slice(record.record_bytes); + } + + // `sign()` reuses `bytes_before_proof_signature` as the final proof buffer: it appends the + // `proof_signature` TLV and then all of `bytes_after_proof_signature`. Reserve that exact + // size now so signing never has to resize the buffer. The `proof_signature` TLV is a + // fixed-size record: a `BigSize` type and length prefix around a 64-byte Schnorr signature. + const SIGNATURE_LEN: usize = 64; + let proof_signature_tlv_len = BigSize(PAYER_PROOF_PROOF_SIGNATURE_TYPE).serialized_length() + + BigSize(SIGNATURE_LEN as u64).serialized_length() + + SIGNATURE_LEN; + bytes_before_proof_signature + .reserve(proof_signature_tlv_len + bytes_after_proof_signature.len()); + + // The tagged hash for `proof_signature` is the merkle root over the + // full proof TLV stream excluding the `proof_signature` TLV itself. + // Iterate the two halves in sequence so no third buffer is allocated. + let tlv_stream = TlvStream::new(&bytes_before_proof_signature) + .chain(TlvStream::new(&bytes_after_proof_signature)); + let tagged_hash = TaggedHash::from_tlv_stream(PROOF_SIGNATURE_TAG, tlv_stream); + + Self { + bytes_before_proof_signature, + bytes_after_proof_signature, + contents, + merkle_root: disclosure.merkle_root, + tagged_hash, + } + } + + /// Signs the [`UnsignedPayerProof`] using the given function. + pub fn sign<F: SignPayerProofFn>(self, sign: F) -> Result<PayerProof, SignError> { + let pubkey = self.contents.payer_signing_pubkey; + let proof_signature = merkle::sign_message(sign, &self, pubkey)?; + + // Assemble the final proof bytes by inserting the proof_signature TLV + // between the pre- and post-signature halves we serialised at build + // time. + let mut bytes = self.bytes_before_proof_signature; + let proof_signature_tlv = PayerProofSignatureTlvStreamRef { + invoice_signature: None, + proof_signature: Some(&proof_signature), + }; + proof_signature_tlv.write(&mut bytes).expect("Vec write should not fail"); + bytes.extend_from_slice(&self.bytes_after_proof_signature); + + Ok(PayerProof { + bytes, + contents: self.contents, + proof_signature, + merkle_root: self.merkle_root, + }) + } +} + +impl AsRef<TaggedHash> for UnsignedPayerProof { + fn as_ref(&self) -> &TaggedHash { + &self.tagged_hash + } +} + +/// A function for signing an [`UnsignedPayerProof`]. +pub trait SignPayerProofFn { + /// Signs a [`TaggedHash`] computed over the payer-proof TLV stream, excluding + /// the `proof_signature` TLV being produced. + fn sign_payer_proof(&self, message: &UnsignedPayerProof) -> Result<Signature, ()>; +} + +impl<F> SignPayerProofFn for F +where + F: Fn(&UnsignedPayerProof) -> Result<Signature, ()>, +{ + fn sign_payer_proof(&self, message: &UnsignedPayerProof) -> Result<Signature, ()> { + self(message) + } +} + +impl<F> merkle::SignFn<UnsignedPayerProof> for F +where + F: SignPayerProofFn, +{ + fn sign(&self, message: &UnsignedPayerProof) -> Result<Signature, ()> { + self.sign_payer_proof(message) + } +} + +// The proof's signature TLVs sit in the BOLT 12 `SIGNATURE_TYPES` range and are +// excluded from the standard merkle-root computation. +tlv_stream!( + PayerProofSignatureTlvStream, PayerProofSignatureTlvStreamRef<'a>, SIGNATURE_TYPES, { + (PAYER_PROOF_ISSUER_SIGNATURE_TYPE, invoice_signature: Signature), + (PAYER_PROOF_PROOF_SIGNATURE_TYPE, proof_signature: Signature), + } +); + +// The data-bearing TLVs sit in `PAYER_PROOF_DATA_TYPES`, outside the signature +// range, so the standard merkle root for `proof_signature` includes them as +// leaves. +tlv_stream!( + PayerProofDataTlvStream, PayerProofDataTlvStreamRef<'a>, PAYER_PROOF_DATA_TYPES, { + (PAYER_PROOF_PREIMAGE_TYPE, proof_preimage: PaymentPreimage), + (PAYER_PROOF_OMITTED_TLVS_TYPE, proof_omitted_markers: (Vec<BigSize>, WithoutLength)), + (PAYER_PROOF_MISSING_HASHES_TYPE, proof_missing_hashes: (Vec<sha256::Hash>, WithoutLength)), + (PAYER_PROOF_LEAF_HASHES_TYPE, proof_leaf_hashes: (Vec<sha256::Hash>, WithoutLength)), + (PAYER_PROOF_PROOF_NOTE_TYPE, proof_note: (String, WithoutLength)), + } +); + +// Ordered to match canonical TLV ordering: offer, invoice_request, invoice, +// signature, proof data, experimental_offer, experimental_invoice_request, +// experimental_invoice. +type FullPayerProofTlvStream = ( + OfferTlvStream, + InvoiceRequestTlvStream, + InvoiceTlvStream, + PayerProofSignatureTlvStream, + PayerProofDataTlvStream, + ExperimentalOfferTlvStream, + ExperimentalInvoiceRequestTlvStream, + ExperimentalInvoiceTlvStream, +); + +impl CursorReadable for FullPayerProofTlvStream { + fn read<R: AsRef<[u8]>>(r: &mut io::Cursor<R>) -> Result<Self, DecodeError> { + let offer = CursorReadable::read(r)?; + let invoice_request = CursorReadable::read(r)?; + let invoice = CursorReadable::read(r)?; + let payer_proof_signatures = CursorReadable::read(r)?; + let payer_proof_data = CursorReadable::read(r)?; + let experimental_offer = CursorReadable::read(r)?; + let experimental_invoice_request = CursorReadable::read(r)?; + let experimental_invoice = CursorReadable::read(r)?; + + Ok(( + offer, + invoice_request, + invoice, + payer_proof_signatures, + payer_proof_data, + experimental_offer, + experimental_invoice_request, + experimental_invoice, + )) + } +} + +impl PayerProof { + /// The payment preimage proving the invoice was paid. + pub fn payment_preimage(&self) -> PaymentPreimage { + self.contents.preimage + } + + /// The payer's public key (who paid). + pub fn payer_signing_pubkey(&self) -> PublicKey { + self.contents.payer_signing_pubkey + } + + /// The issuer's signing public key (the key that signed the invoice). + pub fn issuer_signing_pubkey(&self) -> PublicKey { + self.contents.issuer_signing_pubkey + } + + /// The payment hash. + pub fn payment_hash(&self) -> PaymentHash { + self.contents.payment_hash + } + + /// The invoice signature over the merkle root. + pub fn invoice_signature(&self) -> Signature { + self.contents.invoice_signature + } + + /// The payer's schnorr signature proving who authorized the payment. + pub fn proof_signature(&self) -> Signature { + self.proof_signature + } + + /// The disclosed offer description, if included in the proof. + pub fn offer_description(&self) -> Option<PrintableString<'_>> { + self.contents.disclosed_fields.offer_description.as_deref().map(PrintableString) + } + + /// The disclosed offer issuer, if included in the proof. + pub fn offer_issuer(&self) -> Option<PrintableString<'_>> { + self.contents.disclosed_fields.offer_issuer.as_deref().map(PrintableString) + } + + /// The disclosed invoice amount, if included in the proof. + pub fn invoice_amount_msats(&self) -> Option<u64> { + self.contents.disclosed_fields.invoice_amount_msats + } + + /// The disclosed invoice creation time, if included in the proof. + pub fn invoice_created_at(&self) -> Option<Duration> { + self.contents.disclosed_fields.invoice_created_at + } + + /// A note the payer attached to this proof, if any. + /// + /// This is distinct from [`InvoiceRequest::payer_note`]: the invoice-request note is + /// sent to the payee at payment time, while this note is scoped to the proof and is + /// committed to by the [`proof_signature`] alongside the invoice's merkle root. + /// + /// [`InvoiceRequest::payer_note`]: crate::offers::invoice_request::InvoiceRequest::payer_note + /// [`proof_signature`]: Self::proof_signature + pub fn proof_note(&self) -> Option<PrintableString<'_>> { + self.contents.proof_note.as_deref().map(PrintableString) + } + + /// The merkle root of the original invoice. + pub fn merkle_root(&self) -> sha256::Hash { + self.merkle_root + } + + /// The raw bytes of the payer proof. + pub fn bytes(&self) -> &[u8] { + &self.bytes + } +} + +impl Bech32Encode for PayerProof { + const BECH32_HRP: &'static str = PAYER_PROOF_HRP; +} + +impl Writeable for PayerProof { + fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { + WithoutLength(&self.bytes).write(writer) + } +} + +impl AsRef<[u8]> for PayerProof { + fn as_ref(&self) -> &[u8] { + &self.bytes + } +} + +impl DisclosedFields { + fn update(&mut self, record: &TlvRecord<'_>) -> Result<(), DecodeError> { + match record.r#type { + OFFER_DESCRIPTION_TYPE => { + self.offer_description = Some( + String::from_utf8(record.value_bytes.to_vec()) + .map_err(|_| DecodeError::InvalidValue)?, + ); + }, + OFFER_ISSUER_TYPE => { + self.offer_issuer = Some( + String::from_utf8(record.value_bytes.to_vec()) + .map_err(|_| DecodeError::InvalidValue)?, + ); + }, + INVOICE_CREATED_AT_TYPE => { + self.invoice_created_at = Some(Duration::from_secs( + record.read_value::<HighZeroBytesDroppedBigSize<u64>>()?.0, + )); + }, + INVOICE_AMOUNT_TYPE => { + self.invoice_amount_msats = + Some(record.read_value::<HighZeroBytesDroppedBigSize<u64>>()?.0); + }, + _ => {}, + } + + Ok(()) + } + + fn from_records<'a>( + records: impl core::iter::Iterator<Item = TlvRecord<'a>>, + ) -> Result<Self, DecodeError> { + let mut disclosed_fields = DisclosedFields::default(); + for record in records { + disclosed_fields.update(&record)?; + } + Ok(disclosed_fields) + } +} + +struct ParsedPayerProofFields { + contents: PayerProofContents, + proof_signature: Signature, + omitted_markers: Vec<u64>, + missing_hashes: Vec<sha256::Hash>, + leaf_hashes: Vec<sha256::Hash>, +} + +impl TryFrom<FullPayerProofTlvStream> for ParsedPayerProofFields { + type Error = Bolt12ParseError; + + fn try_from(tlv_stream: FullPayerProofTlvStream) -> Result<Self, Self::Error> { + let ( + OfferTlvStream { description, issuer, .. }, + // `payer_id` is the TLV-stream field name (tied to the spec TLV). Rebind to + // `payer_signing_pubkey` to match `PayerProofContents` naming. + InvoiceRequestTlvStream { payer_id: payer_signing_pubkey, .. }, + InvoiceTlvStream { created_at, payment_hash, amount, node_id, .. }, + PayerProofSignatureTlvStream { invoice_signature, proof_signature }, + PayerProofDataTlvStream { + proof_preimage, + proof_omitted_markers, + proof_missing_hashes, + proof_leaf_hashes, + proof_note, + }, + _experimental_offer, + _experimental_invoice_request, + _experimental_invoice, + ) = tlv_stream; + + let payer_signing_pubkey = payer_signing_pubkey.ok_or( + Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPayerSigningPubkey), + )?; + let payment_hash = payment_hash + .ok_or(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaymentHash))?; + let issuer_signing_pubkey = node_id + .ok_or(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey))?; + let invoice_signature = invoice_signature + .ok_or(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature))?; + let preimage = proof_preimage.ok_or(Bolt12ParseError::Decode(DecodeError::InvalidValue))?; + let proof_signature = proof_signature + .ok_or(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature))?; + // Per BOLT 12 PR 1295, both `proof_missing_hashes` and `proof_leaf_hashes` + // TLVs MUST be present. `proof_omitted_markers` MAY be omitted when empty. + let missing_hashes = + proof_missing_hashes.ok_or(Bolt12ParseError::Decode(DecodeError::InvalidValue))?; + let leaf_hashes = + proof_leaf_hashes.ok_or(Bolt12ParseError::Decode(DecodeError::InvalidValue))?; + + Ok(Self { + contents: PayerProofContents { + payer_signing_pubkey, + payment_hash, + issuer_signing_pubkey, + preimage, + invoice_signature, + proof_note, + disclosed_fields: DisclosedFields { + offer_description: description, + offer_issuer: issuer, + invoice_amount_msats: amount, + invoice_created_at: created_at.map(Duration::from_secs), + }, + }, + proof_signature, + omitted_markers: proof_omitted_markers + .unwrap_or_default() + .into_iter() + .map(|marker| marker.0) + .collect(), + missing_hashes, + leaf_hashes, + }) + } +} + +fn tlv_stream_iter<'a>(bytes: &'a [u8]) -> impl core::iter::Iterator<Item = TlvRecord<'a>> { + // Strip both `SIGNATURE_TYPES` and `PAYER_PROOF_DATA_TYPES` so the + // remaining records reconstruct the invoice merkle root. + TlvStream::new(bytes).filter(|record| { + !SIGNATURE_TYPES.contains(&record.r#type) + && !PAYER_PROOF_DATA_TYPES.contains(&record.r#type) + }) +} + +impl TryFrom<Vec<u8>> for PayerProof { + type Error = Bolt12ParseError; + + fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> { + let parsed_proof = ParsedMessage::<FullPayerProofTlvStream>::try_from(bytes)?; + let ParsedMessage { bytes, tlv_stream } = parsed_proof; + let ParsedPayerProofFields { + contents, + proof_signature, + omitted_markers, + missing_hashes, + leaf_hashes, + } = ParsedPayerProofFields::try_from(tlv_stream)?; + let included_records: Vec<_> = tlv_stream_iter(&bytes).collect(); + let included_types = included_records.iter().map(|record| record.r#type).collect(); + + validate_omitted_markers_for_parsing(&omitted_markers, &included_types) + .map_err(Bolt12ParseError::Decode)?; + + if leaf_hashes.len() != included_records.len() { + return Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)); + } + + let merkle_root = selective_disclosure::reconstruct_merkle_root( + &included_records, + &leaf_hashes, + &omitted_markers, + &missing_hashes, + ) + .map_err(|_| Bolt12ParseError::Decode(DecodeError::InvalidValue))?; + + // Verify preimage matches payment hash. + let computed = sha256::Hash::hash(&contents.preimage.0); + if computed.as_byte_array() != &contents.payment_hash.0 { + return Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)); + } + + // Verify the invoice signature against the issuer signing pubkey. + let tagged_hash = TaggedHash::from_merkle_root(SIGNATURE_TAG, merkle_root); + merkle::verify_signature( + &contents.invoice_signature, + &tagged_hash, + contents.issuer_signing_pubkey, + ) + .map_err(|_| Bolt12ParseError::Decode(DecodeError::InvalidValue))?; + + // Verify the payer signature against the merkle root of the proof + // itself, computed over every payer-proof TLV except the + // `proof_signature` TLV being verified. See module docs. + let proof_tagged_hash = proof_signature_hash(&bytes); + merkle::verify_signature( + &proof_signature, + &proof_tagged_hash, + contents.payer_signing_pubkey, + ) + .map_err(|_| Bolt12ParseError::Decode(DecodeError::InvalidValue))?; + + Ok(PayerProof { bytes, contents, proof_signature, merkle_root }) + } +} + +/// Validate omitted markers during parsing. +/// +/// Per spec: +/// - MUST NOT contain 0 +/// - MUST be in one of the two valid ranges: `1..=239` or +/// `1_000_000_000..=3_999_999_999`. Anything in the signature range +/// (`240..=1000`), the payer-proof data range (`1001..=999_999_999`), or +/// above the experimental invoice range (`>= 4_000_000_000`) is rejected. +/// - MUST be in strict ascending order +/// - MUST NOT contain the number of an included TLV field +/// - Markers MUST be minimized: each marker is the marker number following the +/// previous marker (or the previous included type X) — one greater, except a +/// value that would land in the signature/payer-proof gap jumps to the +/// experimental range. This naturally allows a trailing run of omitted TLVs +/// after the final included type. +fn validate_omitted_markers_for_parsing( + omitted_markers: &[u64], included_types: &BTreeSet<u64>, +) -> Result<(), DecodeError> { + // Payer-proof range restriction: each marker MUST be inside one of the two valid ranges + // (`1..=239` or `1_000_000_000..=3_999_999_999`), i.e. outside the signature and + // payer-proof-data ranges and below the end of the experimental range. + for &marker in omitted_markers { + if SIGNATURE_TYPES.contains(&marker) + || PAYER_PROOF_DATA_TYPES.contains(&marker) + || marker >= EXPERIMENTAL_INVOICE_TYPES.end + { + return Err(DecodeError::InvalidValue); + } + } + + // Ordering, non-zero, not-an-included-type, and minimization are enforced by the merkle layer, + // the single source of truth for marker validity (see `selective_disclosure::validate_omitted_markers`). + selective_disclosure::validate_omitted_markers(omitted_markers, included_types) + .map_err(|_| DecodeError::InvalidValue) +} + +impl core::str::FromStr for PayerProof { + type Err = Bolt12ParseError; + + fn from_str(s: &str) -> Result<Self, <Self as core::str::FromStr>::Err> { + Self::from_bech32_str(s) + } +} + +impl core::fmt::Display for PayerProof { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + self.fmt_bech32_str(f) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ln::channelmanager::PaymentId; + use crate::ln::inbound_payment::ExpandedKey; + use crate::offers::nonce::Nonce; + #[cfg(not(c_bindings))] + use crate::offers::refund::RefundBuilder; + #[cfg(c_bindings)] + use crate::offers::refund::RefundMaybeWithDerivedMetadataBuilder as RefundBuilder; + use crate::offers::selective_disclosure::compute_selective_disclosure; + use crate::offers::test_utils::*; + use crate::util::ser::Readable; + use bitcoin::hashes::Hash; + use bitcoin::secp256k1::{Keypair, Secp256k1, SecretKey}; + use core::time::Duration; + + const EXPERIMENTAL_TEST_TLV_TYPE: u64 = 1_000_000_001; + + fn write_tlv_record<T: Writeable>(bytes: &mut Vec<u8>, tlv_type: u64, value: &T) { + let mut value_bytes = Vec::new(); + value.write(&mut value_bytes).expect("Vec write should not fail"); + + BigSize(tlv_type).write(bytes).expect("Vec write should not fail"); + BigSize(value_bytes.len() as u64).write(bytes).expect("Vec write should not fail"); + bytes.extend_from_slice(&value_bytes); + } + + fn write_tlv_record_bytes(bytes: &mut Vec<u8>, tlv_type: u64, value_bytes: &[u8]) { + BigSize(tlv_type).write(bytes).expect("Vec write should not fail"); + BigSize(value_bytes.len() as u64).write(bytes).expect("Vec write should not fail"); + bytes.extend_from_slice(value_bytes); + } + + /// Builds a proof whose underlying invoice carries a synthetic experimental TLV + /// at type [`EXPERIMENTAL_TEST_TLV_TYPE`] (`1_000_000_001`). Constructed by + /// hand because the public `RefundBuilder` API doesn't expose a way to write + /// arbitrary experimental TLV types — the existing `experimental_foo` family + /// of methods uses fixed type numbers that wouldn't exercise the same code + /// path. + fn build_round_trip_proof_with_included_experimental_tlv() -> PayerProof { + let secp_ctx = Secp256k1::new(); + + let payer_secret = SecretKey::from_slice(&[42; 32]).unwrap(); + let payer_keys = Keypair::from_secret_key(&secp_ctx, &payer_secret); + let payer_signing_pubkey = payer_keys.public_key(); + + let issuer_secret = SecretKey::from_slice(&[43; 32]).unwrap(); + let issuer_keys = Keypair::from_secret_key(&secp_ctx, &issuer_secret); + let issuer_signing_pubkey = issuer_keys.public_key(); + + let preimage = PaymentPreimage([44; 32]); + let payment_hash = PaymentHash(sha256::Hash::hash(&preimage.0).to_byte_array()); + + let mut invoice_bytes = Vec::new(); + write_tlv_record_bytes(&mut invoice_bytes, PAYER_METADATA_TYPE, &[45; 32]); + write_tlv_record(&mut invoice_bytes, INVOICE_REQUEST_PAYER_ID_TYPE, &payer_signing_pubkey); + write_tlv_record(&mut invoice_bytes, INVOICE_PAYMENT_HASH_TYPE, &payment_hash); + write_tlv_record(&mut invoice_bytes, INVOICE_NODE_ID_TYPE, &issuer_signing_pubkey); + write_tlv_record_bytes( + &mut invoice_bytes, + EXPERIMENTAL_TEST_TLV_TYPE, + b"experimental-payer-proof-field", + ); + + let invoice_message = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice_bytes); + let invoice_signature = + secp_ctx.sign_schnorr_no_aux_rand(invoice_message.as_digest(), &issuer_keys); + + let included_types: BTreeSet<u64> = [ + INVOICE_REQUEST_PAYER_ID_TYPE, + INVOICE_PAYMENT_HASH_TYPE, + INVOICE_NODE_ID_TYPE, + EXPERIMENTAL_TEST_TLV_TYPE, + ] + .into_iter() + .collect(); + let disclosed_fields = DisclosedFields::from_records( + TlvStream::new(&invoice_bytes).filter(|r| included_types.contains(&r.r#type)), + ) + .unwrap(); + let disclosure = + compute_selective_disclosure(TlvStream::new(&invoice_bytes), &included_types); + + let contents = PayerProofContents { + payer_signing_pubkey, + payment_hash, + issuer_signing_pubkey, + preimage, + invoice_signature, + proof_note: None, + disclosed_fields, + }; + let unsigned = + UnsignedPayerProof::new(&invoice_bytes, &included_types, contents, disclosure); + + unsigned + .sign(|proof: &UnsignedPayerProof| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(proof.as_ref().as_digest(), &payer_keys)) + }) + .unwrap() + } + + /// Builds a proof with two consecutive *trailing* omitted experimental TLVs at + /// types `1_000_000_001` and `1_000_000_003`. The exact layout is load-bearing + /// for the `omitted_markers == [177, 178]` assertion on the parsed proof, and + /// the public `RefundBuilder` API doesn't expose a way to produce that exact + /// pair of trailing experimental types — so this helper writes the invoice + /// bytes by hand. + fn build_round_trip_proof_with_multiple_trailing_omitted_tlvs() -> PayerProof { + let secp_ctx = Secp256k1::new(); + + let payer_secret = SecretKey::from_slice(&[52; 32]).unwrap(); + let payer_keys = Keypair::from_secret_key(&secp_ctx, &payer_secret); + let payer_signing_pubkey = payer_keys.public_key(); + + let issuer_secret = SecretKey::from_slice(&[53; 32]).unwrap(); + let issuer_keys = Keypair::from_secret_key(&secp_ctx, &issuer_secret); + let issuer_signing_pubkey = issuer_keys.public_key(); + + let preimage = PaymentPreimage([54; 32]); + let payment_hash = PaymentHash(sha256::Hash::hash(&preimage.0).to_byte_array()); + + let mut invoice_bytes = Vec::new(); + write_tlv_record_bytes(&mut invoice_bytes, PAYER_METADATA_TYPE, &[55; 32]); + write_tlv_record(&mut invoice_bytes, INVOICE_REQUEST_PAYER_ID_TYPE, &payer_signing_pubkey); + write_tlv_record(&mut invoice_bytes, INVOICE_PAYMENT_HASH_TYPE, &payment_hash); + write_tlv_record(&mut invoice_bytes, INVOICE_NODE_ID_TYPE, &issuer_signing_pubkey); + write_tlv_record_bytes(&mut invoice_bytes, 1_000_000_001, b"first-omitted-experimental"); + write_tlv_record_bytes(&mut invoice_bytes, 1_000_000_003, b"second-omitted-experimental"); + + let invoice_message = + TaggedHash::from_valid_tlv_stream_bytes(SIGNATURE_TAG, &invoice_bytes); + let invoice_signature = + secp_ctx.sign_schnorr_no_aux_rand(invoice_message.as_digest(), &issuer_keys); + + let included_types: BTreeSet<u64> = + [INVOICE_REQUEST_PAYER_ID_TYPE, INVOICE_PAYMENT_HASH_TYPE, INVOICE_NODE_ID_TYPE] + .into_iter() + .collect(); + let disclosed_fields = DisclosedFields::from_records( + TlvStream::new(&invoice_bytes).filter(|r| included_types.contains(&r.r#type)), + ) + .unwrap(); + let disclosure = + compute_selective_disclosure(TlvStream::new(&invoice_bytes), &included_types); + assert_eq!(disclosure.omitted_markers, vec![177, 178]); + + let contents = PayerProofContents { + payer_signing_pubkey, + payment_hash, + issuer_signing_pubkey, + preimage, + invoice_signature, + proof_note: None, + disclosed_fields, + }; + let unsigned = + UnsignedPayerProof::new(&invoice_bytes, &included_types, contents, disclosure); + + unsigned + .sign(|proof: &UnsignedPayerProof| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(proof.as_ref().as_digest(), &payer_keys)) + }) + .unwrap() + } + + fn build_round_trip_proof_with_disclosed_fields() -> PayerProof { + let preimage = PaymentPreimage([64; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 42_000) + .unwrap() + .description("coffee beans".into()) + .issuer("LDK Roastery".into()) + .build() + .unwrap() + .respond_with_no_std( + payment_paths(), + payment_hash, + recipient_pubkey(), + Duration::from_secs(1_700_000_000), + ) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let paid_invoice = PaidBolt12Invoice::Bolt12Invoice(invoice); + paid_invoice + .prove_payer(preimage) + .unwrap() + .include_offer_description() + .include_offer_issuer() + .include_invoice_amount() + .include_invoice_created_at() + .build() + .unwrap() + .sign(|proof: &UnsignedPayerProof| payer_sign(proof)) + .unwrap() + } + + /// Returns a fresh builder over a dummy paid invoice, for exercising the `include_type` API. + fn payer_proof_builder() -> PayerProofBuilder<ExplicitSigningPubkey> { + let preimage = PaymentPreimage([64; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 42_000) + .unwrap() + .build() + .unwrap() + .respond_with_no_std( + payment_paths(), + payment_hash, + recipient_pubkey(), + Duration::from_secs(1_700_000_000), + ) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + PaidBolt12Invoice::Bolt12Invoice(invoice).prove_payer(preimage).unwrap() + } + + #[test] + fn test_selective_disclosure_computation() { + // Test that the merkle selective disclosure works correctly + // Simple TLV stream with types 1, 2 + let tlv_bytes = vec![ + 0x01, 0x03, 0xe8, 0x03, 0xe8, // type 1, length 3, value + 0x02, 0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x03, // type 2 + ]; + + let mut included = BTreeSet::new(); + included.insert(1); + + let disclosure = compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + assert_eq!(disclosure.nonce_hashes.len(), 1); // One included TLV + assert!(!disclosure.missing_hashes.is_empty()); // Should have missing hashes for omitted + } + + /// Test the omitted_markers marker algorithm with two included runs (10 and 40). + /// + /// TLVs: 0 (omitted), 10 (included), 20 (omitted), 30 (omitted), + /// 40 (included), 50 (omitted), 60 (omitted) + /// + /// Expected markers: [11, 12, 41, 42] + /// + /// The algorithm: + /// - TLV 0 is always omitted and implicit (not in markers) + /// - For omitted TLV after included: marker = prev_included_type + 1 + /// - For consecutive omitted TLVs: marker = prev_marker + 1 + #[test] + fn test_omitted_markers_two_included_runs() { + // Build a synthetic TLV stream + // TLV format: type (BigSize) || length (BigSize) || value + let mut tlv_bytes = Vec::new(); + + // TLV 0: type=0, len=4, value=dummy + tlv_bytes.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); + // TLV 10: type=10, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); + // TLV 20: type=20, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); + // TLV 30: type=30, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x1e, 0x02, 0x00, 0x00]); + // TLV 40: type=40, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x28, 0x02, 0x00, 0x00]); + // TLV 50: type=50, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x32, 0x02, 0x00, 0x00]); + // TLV 60: type=60, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x3c, 0x02, 0x00, 0x00]); + + // Include types 10 and 40 + let mut included = BTreeSet::new(); + included.insert(10); + included.insert(40); + + let disclosure = compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + + assert_eq!(disclosure.omitted_markers, vec![11, 12, 41, 42]); + + // nonce_hashes should have 2 entries (one for each included TLV) + assert_eq!(disclosure.nonce_hashes.len(), 2); + } + + /// Test the omitted_markers + missing_hashes algorithms against the BOLT 12 + /// PR 1295 spec example (post commit `d6dbb9d8`). + /// + /// TLVs: 0, 10, 20, 30 (all omitted), 40 (included), 50, 60 (both omitted) + /// + /// Per spec lines 1131-1146 of `12-offer-encoding.md`: + /// - `omitted_tlvs` array = `[1, 2, 3, 41, 42]` (markers 1..3 cover the + /// leading omitted run after implicit TLV0; 41,42 cover the trailing run) + /// - `missing_hashes` is in post-order DFS order: + /// 1. leaf hash for TLV 50 + /// 2. leaf hash for TLV 60 + /// 3. the entire `(0,10) | (20,30)` left subtree (asterisk node) + #[test] + fn test_omitted_markers_spec_example() { + // TLV format: type (BigSize) || length (BigSize) || value + let mut tlv_bytes = Vec::new(); + + // TLV 0: type=0, len=4, value=dummy + tlv_bytes.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); + // TLV 10: type=10, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); + // TLV 20: type=20, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); + // TLV 30: type=30, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x1e, 0x02, 0x00, 0x00]); + // TLV 40: type=40, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x28, 0x02, 0x00, 0x00]); + // TLV 50: type=50, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x32, 0x02, 0x00, 0x00]); + // TLV 60: type=60, len=2, value=dummy + tlv_bytes.extend_from_slice(&[0x3c, 0x02, 0x00, 0x00]); + + // Include only TLV 40 (matching the spec example) + let mut included = BTreeSet::new(); + included.insert(40); + + let disclosure = compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + + // Per spec example: omitted_markers = [1, 2, 3, 41, 42] + assert_eq!(disclosure.omitted_markers, vec![1, 2, 3, 41, 42]); + + // One leaf_hash for the single included TLV (40) + assert_eq!(disclosure.nonce_hashes.len(), 1); + + // Post-order DFS missing_hashes: [TLV50_leaf, TLV60_leaf, left_subtree] + assert_eq!(disclosure.missing_hashes.len(), 3); + } + + /// Test that the marker algorithm handles edge cases correctly. + #[test] + fn test_omitted_markers_edge_cases() { + // Test with only one included TLV at the start + let mut tlv_bytes = Vec::new(); + tlv_bytes.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); // TLV 0 + tlv_bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); // TLV 10 + tlv_bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); // TLV 20 + tlv_bytes.extend_from_slice(&[0x1e, 0x02, 0x00, 0x00]); // TLV 30 + + let mut included = BTreeSet::new(); + included.insert(10); + + let disclosure = compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + + // After included type 10, omitted types 20 and 30 get markers 11 and 12 + assert_eq!(disclosure.omitted_markers, vec![11, 12]); + } + + /// Test that all included TLVs produce no omitted markers (except implicit TLV0). + #[test] + fn test_omitted_markers_all_included() { + let mut tlv_bytes = Vec::new(); + tlv_bytes.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); // TLV 0 (always omitted) + tlv_bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); // TLV 10 + tlv_bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); // TLV 20 + + let mut included = BTreeSet::new(); + included.insert(10); + included.insert(20); + + let disclosure = compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + + // Only TLV 0 is omitted (implicit), so no markers needed + assert!(disclosure.omitted_markers.is_empty()); + } + + /// Test validation of omitted_markers - must not contain 0. + #[test] + fn test_validate_omitted_markers_rejects_zero() { + let omitted = vec![0, 11, 12]; + let included: BTreeSet<u64> = [10, 30].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_err()); + } + + /// Test validation of omitted_markers - must not contain signature types. + #[test] + fn test_validate_omitted_markers_rejects_signature_types() { + // included=[10], markers=[1, 2, 250] — 250 is a signature type + let omitted = vec![1, 2, 250]; + let included: BTreeSet<u64> = [10].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_err()); + } + + /// Test validation of omitted_markers - must not contain payer-proof data + /// range types (1001..=999_999_999) per BOLT 12 PR 1295. + #[test] + fn test_validate_omitted_markers_rejects_data_range_types() { + let included: BTreeSet<u64> = [10].iter().copied().collect(); + + // 1001 is the low end of the data range + assert!(validate_omitted_markers_for_parsing(&[1, 2, 1001], &included).is_err()); + // somewhere in the middle of the data range + assert!(validate_omitted_markers_for_parsing(&[1, 2, 500_000_000], &included).is_err()); + // 999_999_999 is the high end of the data range + assert!(validate_omitted_markers_for_parsing(&[1, 2, 999_999_999], &included).is_err()); + } + + /// Test validation of omitted_markers - must not contain values above the + /// experimental invoice range (>= 4_000_000_000) per BOLT 12 PR 1295. + #[test] + fn test_validate_omitted_markers_rejects_above_experimental_range() { + let included: BTreeSet<u64> = [10].iter().copied().collect(); + + // 4_000_000_000 is the lowest invalid value + assert!(validate_omitted_markers_for_parsing(&[1, 2, 4_000_000_000], &included).is_err()); + // far above + assert!(validate_omitted_markers_for_parsing(&[1, 2, u64::MAX], &included).is_err()); + } + + /// Test validation of omitted_markers - must be strictly ascending. + #[test] + fn test_validate_omitted_markers_rejects_non_ascending() { + // markers=[1, 11, 9]: 1 ok, 11 ok (after included 10), but 9 <= 11 fails ascending + let omitted = vec![1, 11, 9]; + let included: BTreeSet<u64> = [10, 30].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_err()); + } + + /// Test validation of omitted_markers - must not contain included types. + #[test] + fn test_validate_omitted_markers_rejects_included_types() { + // included=[10, 30], markers=[1, 10] — 10 is in included set + let omitted = vec![1, 10]; + let included: BTreeSet<u64> = [10, 30].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(matches!(result, Err(DecodeError::InvalidValue))); + } + + /// Test that a minimized trailing run is accepted. + #[test] + fn test_validate_omitted_markers_accepts_trailing_run() { + // included=[10, 20], markers=[1, 21, 22] — both 21 and 22 > max included (20) + let omitted = vec![1, 21, 22]; + let included: BTreeSet<u64> = [10, 20].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_ok()); + } + + /// Test that valid minimized omitted_markers pass validation. + #[test] + fn test_validate_omitted_markers_accepts_valid() { + // Realistic payer proof: included types include required fields (88, 168, 176) + // so max_included=176 and markers are well below it. + // Layout: 0(omit), 10(incl), 20(omit), 30(omit), 40(incl), 50(omit), 88(incl), + // 168(incl), 176(incl) + // markers=[11, 12, 41, 89] + let omitted = vec![11, 12, 41, 89]; + let included: BTreeSet<u64> = [10, 40, 88, 168, 176].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_ok()); + } + + /// Reproduces the producer/consumer gap-jump mismatch. + /// + /// `compute_omitted_markers` emits `[1, ..., 239, 1_000_000_000]` for 240 + /// consecutive omitted TLVs (see the merkle.rs test + /// `compute_omitted_markers_jumps_to_high_range_after_239`): the marker after + /// 239 jumps over the signature/payer-proof gap into the experimental range. + /// `validate_omitted_markers_for_parsing` must accept that jump as a valid + /// minimized sequence, otherwise a proof the producer can legitimately build + /// is rejected on parse. + #[test] + fn test_validate_omitted_markers_accepts_gap_jump() { + let mut omitted: Vec<u64> = (1..=239).collect(); + omitted.push(1_000_000_000); + let included: BTreeSet<u64> = BTreeSet::new(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_ok(), "gap-jumped markers must be accepted, got {:?}", result); + } + + /// An included TLV of type 239 followed by an omitted TLV: the producer emits + /// marker `next_marker(239)` = `1_000_000_000`. The reader's + /// jump-after-included-type path must accept it. + #[test] + fn test_validate_omitted_markers_accepts_gap_jump_after_included() { + let omitted = vec![1_000_000_000]; + let included: BTreeSet<u64> = [239].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!( + result.is_ok(), + "gap-jump after included type 239 must be accepted, got {:?}", + result + ); + } + + /// Test that non-minimized markers are rejected. + #[test] + fn test_validate_omitted_markers_rejects_non_minimized() { + // included=[10, 40], markers=[11, 15, 41, 42] + // marker 15 should be 12 (continuation of run after 11) + let omitted = vec![11, 15, 41, 42]; + let included: BTreeSet<u64> = [10, 40].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_err()); + } + + /// Test that non-minimized first marker in a run is rejected. + #[test] + fn test_validate_omitted_markers_rejects_non_minimized_run_start() { + // included=[10, 40], markers=[11, 12, 45, 46] + // marker 45 should be 41 (first omitted after included 40) + let omitted = vec![11, 12, 45, 46]; + let included: BTreeSet<u64> = [10, 40].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_err()); + } + + /// Test minimized markers with omitted TLVs before any included type. + #[test] + fn test_validate_omitted_markers_accepts_leading_run() { + // included=[40], markers=[1, 2, 41] + // Two omitted before any included type, one after 40 + let omitted = vec![1, 2, 41]; + let included: BTreeSet<u64> = [40].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_ok()); + } + + /// Test minimized markers with consecutive included types (no markers between them). + #[test] + fn test_validate_omitted_markers_accepts_consecutive_included() { + // included=[10, 20, 40], markers=[1, 41] + // One omitted before 10, no omitted between 10-20 or 20-40, one after 40 + let omitted = vec![1, 41]; + let included: BTreeSet<u64> = [10, 20, 40].iter().copied().collect(); + + let result = validate_omitted_markers_for_parsing(&omitted, &included); + assert!(result.is_ok()); + } + + /// Test that invreq_metadata (type 0) cannot be explicitly included via include_type. + #[test] + fn test_invreq_metadata_not_allowed() { + assert_eq!(PAYER_METADATA_TYPE, 0); + } + + /// Test that out-of-order TLVs are rejected during parsing. + #[test] + fn test_parsing_rejects_out_of_order_tlvs() { + use core::convert::TryFrom; + + // Create a malformed TLV stream with out-of-order types (20 before 10) + // TLV format: type (BigSize) || length (BigSize) || value + let mut bytes = Vec::new(); + // TLV type 20, length 2, value + bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); + // TLV type 10, length 2, value (OUT OF ORDER!) + bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); + + let result = PayerProof::try_from(bytes); + assert!(result.is_err()); + } + + /// Test that duplicate TLVs are rejected during parsing. + #[test] + fn test_parsing_rejects_duplicate_tlvs() { + use core::convert::TryFrom; + + // Create a malformed TLV stream with duplicate type 10 + let mut bytes = Vec::new(); + // TLV type 10, length 2, value + bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); + // TLV type 10 again (DUPLICATE!) + bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); + + let result = PayerProof::try_from(bytes); + assert!(result.is_err()); + } + + /// Test that an invalid `proof_missing_hashes` length (not a multiple of 32) + /// is rejected. + #[test] + fn test_parsing_rejects_invalid_hash_length() { + use core::convert::TryFrom; + + // `proof_missing_hashes` decodes as a `WithoutLength` `Vec<sha256::Hash>`, + // so a value length that is not a multiple of 32 cannot decode to whole + // hashes. + let mut bytes = Vec::new(); + BigSize(PAYER_PROOF_MISSING_HASHES_TYPE).write(&mut bytes).unwrap(); + BigSize(33).write(&mut bytes).unwrap(); // 33 is not a multiple of 32 + bytes.extend_from_slice(&[0x00; 33]); + + let result = PayerProof::try_from(bytes); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::ShortRead))), + "expected Decode(ShortRead), got {:?}", + result, + ); + } + + /// Test that an invalid `proof_leaf_hashes` length (not a multiple of 32) is + /// rejected. + #[test] + fn test_parsing_rejects_invalid_leaf_hashes_length() { + use core::convert::TryFrom; + + // `proof_leaf_hashes` decodes as a `WithoutLength` `Vec<sha256::Hash>`, + // so a value length that is not a multiple of 32 cannot decode to whole + // hashes. + let mut bytes = Vec::new(); + BigSize(PAYER_PROOF_LEAF_HASHES_TYPE).write(&mut bytes).unwrap(); + BigSize(31).write(&mut bytes).unwrap(); // 31 is not a multiple of 32 + bytes.extend_from_slice(&[0x00; 31]); + + let result = PayerProof::try_from(bytes); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::ShortRead))), + "expected Decode(ShortRead), got {:?}", + result, + ); + } + + /// `include_type` must reject `payer_metadata` (0), the signature/payer-proof ranges, and the + /// gap before the experimental ranges, while accepting types below the signature range and the + /// experimental ranges. + #[test] + fn test_include_type_rejects_signature_types() { + let gap_top = EXPERIMENTAL_OFFER_TYPES.start - 1; + for ty in [0, 240, 250, 1000, 1001, gap_top] { + assert!(matches!( + payer_proof_builder().include_type(ty), + Err(PayerProofError::DisallowedTlvType(t)) if t == ty, + )); + } + for ty in [239, EXPERIMENTAL_OFFER_TYPES.start, u64::MAX] { + assert!(payer_proof_builder().include_type(ty).is_ok()); + } + } + + #[test] + fn test_round_trip_accepts_included_experimental_tlv() { + let proof = build_round_trip_proof_with_included_experimental_tlv(); + let result = PayerProof::try_from(proof.bytes().to_vec()); + assert!( + result.is_ok(), + "Included experimental TLVs should survive payer proof parsing: {:?}", + result + ); + } + + #[test] + fn test_round_trip_accepts_multiple_trailing_omitted_tlvs() { + let proof = build_round_trip_proof_with_multiple_trailing_omitted_tlvs(); + let result = PayerProof::try_from(proof.bytes().to_vec()); + assert!( + result.is_ok(), + "Multiple trailing omitted TLVs should survive payer proof parsing: {:?}", + result + ); + } + + /// Confirms that type 0 (`payer_metadata`) is rejected when parsing a payer proof — + /// matching the same behavior as `FullOfferTlvStream`. + /// + /// `FullPayerProofTlvStream` has no sub-stream that covers type 0 (the lowest sub-stream + /// is `OfferTlvStream`, range `1..80`). Each `CursorReadable` impl reads the type BigSize, + /// finds it out of range, rewinds the type bytes, and breaks — without consuming the + /// length or value. The cursor is therefore left before the type-0 TLV, and the + /// all-bytes-consumed check in `ParsedMessage::try_from` rejects the input with + /// `DecodeError::InvalidValue` before any semantic validation runs. + #[test] + fn test_parsing_rejects_payer_metadata() { + let proof = build_round_trip_proof_with_multiple_trailing_omitted_tlvs(); + let mut bytes = Vec::new(); + write_tlv_record_bytes(&mut bytes, PAYER_METADATA_TYPE, &[1; 32]); + bytes.extend_from_slice(proof.bytes()); + + let result = PayerProof::try_from(bytes); + assert!(matches!(result, Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)))); + } + + #[test] + fn test_round_trip_rejects_unknown_odd_data_range_tlv() { + // Unknown odd TLVs in the `PAYER_PROOF_DATA_TYPES` range are merkle + // leaves; inserting one after signing shifts the merkle root and the + // `proof_signature` no longer verifies. + let unknown_odd_data_range_type = PAYER_PROOF_PROOF_NOTE_TYPE + 2; + assert_eq!(unknown_odd_data_range_type % 2, 1); + assert!(PAYER_PROOF_DATA_TYPES.contains(&unknown_odd_data_range_type)); + + let proof = build_round_trip_proof_with_multiple_trailing_omitted_tlvs(); + let mut bytes = proof.bytes().to_vec(); + write_tlv_record_bytes(&mut bytes, unknown_odd_data_range_type, b"ignored"); + + assert!(matches!( + PayerProof::try_from(bytes), + Err(Bolt12ParseError::Decode(DecodeError::InvalidValue)) + )); + } + + #[test] + fn test_parsed_proof_exposes_disclosed_fields() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let parsed = PayerProof::try_from(proof.bytes().to_vec()).unwrap(); + + assert_eq!(parsed.offer_description().map(|s| s.0), Some("coffee beans")); + assert_eq!(parsed.offer_issuer().map(|s| s.0), Some("LDK Roastery")); + assert_eq!(parsed.invoice_amount_msats(), Some(42_000)); + assert_eq!(parsed.invoice_created_at(), Some(Duration::from_secs(1_700_000_000))); + } + + /// Test that unknown even TLV types in every payer-proof BOLT 12 sub-stream + /// namespace are rejected by the `tlv_stream!`-based parser, and that types + /// in the unused gap ranges between sub-streams are rejected by + /// `ParsedMessage`'s all-bytes-consumed check. + /// + /// Per BOLT convention, even types are mandatory-to-understand. For payer + /// proofs this is stricter than the general invoice rule because including + /// an unknown even TLV in a proof implies the verifier must check something + /// about it, and it cannot. See the upstream discussion: + /// <https://github.com/lightningdevkit/rust-lightning/pull/4297#discussion_r3107812262>. + #[test] + fn test_parsing_rejects_unknown_even_tlvs_in_every_range() { + use core::convert::TryFrom; + + /// Parse a payer-proof byte stream that contains only a single TLV with + /// the given type and a 4-byte dummy value, and assert it is rejected + /// with the expected error variant. + fn assert_rejected(tlv_type: u64, expected: DecodeError, label: &str) { + let mut bytes = Vec::new(); + BigSize(tlv_type).write(&mut bytes).expect("Vec write should not fail"); + BigSize(4).write(&mut bytes).expect("Vec write should not fail"); + bytes.extend_from_slice(b"test"); + + match PayerProof::try_from(bytes) { + Err(Bolt12ParseError::Decode(ref err)) if err == &expected => {}, + other => panic!( + "{} (type {}): expected {:?}, got {:?}", + label, tlv_type, expected, other, + ), + } + } + + // Sub-stream ranges: rejected by `tlv_stream!`'s unknown-even fallback. + assert_rejected(50, DecodeError::UnknownRequiredFeature, "offer range"); + assert_rejected(100, DecodeError::UnknownRequiredFeature, "invoice_request range"); + assert_rejected(200, DecodeError::UnknownRequiredFeature, "invoice range"); + // 240 and 241 are the known signature TLVs; 254 is unknown. + assert_rejected(254, DecodeError::UnknownRequiredFeature, "payer-proof/signature range"); + // 1001..=1005 are the known data TLVs; 1006 is unknown. + assert_rejected(1006, DecodeError::UnknownRequiredFeature, "payer-proof data range (low)"); + assert_rejected( + 1_000_000, + DecodeError::UnknownRequiredFeature, + "payer-proof data range (mid)", + ); + assert_rejected( + 1_500_000_000, + DecodeError::UnknownRequiredFeature, + "experimental offer range", + ); + assert_rejected( + 2_500_000_000, + DecodeError::UnknownRequiredFeature, + "experimental invoice_request range", + ); + assert_rejected( + 3_500_000_000, + DecodeError::UnknownRequiredFeature, + "experimental invoice range", + ); + + // Type 0 is rejected separately by the `payer_metadata` check + // (see `test_parsing_rejects_payer_metadata`). + } + + /// Test that malformed TLV framing is rejected without panicking. + /// + /// TlvStream::new() panics on malformed BigSize values or out-of-bounds + /// lengths. The parser must validate framing before constructing TlvStream. + #[test] + fn test_parsing_rejects_malformed_tlv_framing() { + use core::convert::TryFrom; + + // Truncated BigSize type (0xFD prefix requires 2 more bytes) + let result = PayerProof::try_from(vec![0xFD, 0x01]); + assert!(result.is_err(), "Truncated BigSize type should be rejected"); + + // Valid type but truncated length + let result = PayerProof::try_from(vec![0x0a]); + assert!(result.is_err(), "Missing length should be rejected"); + + // Length exceeds remaining bytes + let result = PayerProof::try_from(vec![0x0a, 0x04, 0x00, 0x00]); + assert!(result.is_err(), "Length exceeding data should be rejected"); + + // Empty input should not panic + let result = PayerProof::try_from(vec![]); + assert!(result.is_err(), "Empty input should be rejected"); + + // Completely invalid bytes + let result = PayerProof::try_from(vec![0xFF, 0xFF]); + assert!(result.is_err(), "Invalid bytes should be rejected"); + } + + /// Test that duplicate type-0 TLVs are rejected. + /// + /// Previously the ordering check used `u64` initialized to 0, which + /// skipped the check for the first TLV if its type was 0, allowing + /// duplicate type-0 records. + #[test] + fn test_parsing_rejects_duplicate_type_zero() { + use core::convert::TryFrom; + + // Two TLV records both with type 0 + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[0x00, 0x02, 0x00, 0x00]); // type 0, len 2 + bytes.extend_from_slice(&[0x00, 0x02, 0x00, 0x00]); // type 0 again (DUPLICATE!) + + let result = PayerProof::try_from(bytes); + assert!(result.is_err(), "Duplicate type-0 TLVs should be rejected"); + } + + /// Test that a `proof_signature` TLV with a value shorter than 64 bytes is + /// rejected. + #[test] + fn test_parsing_rejects_short_proof_signature() { + use core::convert::TryFrom; + + // `proof_signature` decodes as a 64-byte schnorr `Signature`; a 32-byte + // value is too short. + let mut bytes = Vec::new(); + BigSize(PAYER_PROOF_PROOF_SIGNATURE_TYPE).write(&mut bytes).unwrap(); + BigSize(32).write(&mut bytes).unwrap(); // too short for a 64-byte signature + bytes.extend_from_slice(&[0x00; 32]); + + let result = PayerProof::try_from(bytes); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::ShortRead))), + "expected Decode(ShortRead), got {:?}", + result, + ); + } + + /// Helper: serialize a payer_proof's bytes minus any TLV record matching `drop_type`. + fn proof_bytes_without_tlv(proof: &PayerProof, drop_type: u64) -> Vec<u8> { + let mut out = Vec::new(); + for record in TlvStream::new(proof.bytes()) { + if record.r#type != drop_type { + out.extend_from_slice(record.record_bytes); + } + } + out + } + + /// Helper: copy a payer_proof's bytes, applying `mutator` to the value of any + /// TLV record matching `target_type`. The TLV's length stays the same; only + /// the value bytes are mutated in place. + fn proof_bytes_with_mutated_tlv_value<F: FnMut(&mut [u8])>( + proof: &PayerProof, target_type: u64, mut mutator: F, + ) -> Vec<u8> { + let mut out = Vec::with_capacity(proof.bytes().len()); + for record in TlvStream::new(proof.bytes()) { + if record.r#type == target_type { + let prefix_len = record.record_bytes.len() - record.value_bytes.len(); + out.extend_from_slice(&record.record_bytes[..prefix_len]); + let mut value = record.value_bytes.to_vec(); + mutator(&mut value); + out.extend_from_slice(&value); + } else { + out.extend_from_slice(record.record_bytes); + } + } + out + } + + /// Helper: drop the first 32-byte sha256 hash from any TLV record matching + /// `target_type`, re-encoding the BigSize length. Useful for crafting a + /// shorter `proof_leaf_hashes` / `proof_missing_hashes` to test count checks. + fn proof_bytes_with_first_hash_dropped(proof: &PayerProof, target_type: u64) -> Vec<u8> { + let mut out = Vec::with_capacity(proof.bytes().len()); + for record in TlvStream::new(proof.bytes()) { + if record.r#type == target_type { + assert!( + record.value_bytes.len() >= 32, + "target TLV {} value too short to drop a hash", + target_type + ); + BigSize(target_type).write(&mut out).expect("Vec write should not fail"); + let new_len = record.value_bytes.len() - 32; + BigSize(new_len as u64).write(&mut out).expect("Vec write should not fail"); + out.extend_from_slice(&record.value_bytes[32..]); + } else { + out.extend_from_slice(record.record_bytes); + } + } + out + } + + /// Per BOLT 12 PR 1295: SHA256(`proof_preimage`) must equal `invoice_payment_hash`, + /// otherwise the reader MUST reject. Flipping a byte in `proof_preimage` (TLV 1001) + /// must therefore fail parsing. + #[test] + fn test_parsing_rejects_modified_preimage() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let mutated = + proof_bytes_with_mutated_tlv_value(&proof, PAYER_PROOF_PREIMAGE_TYPE, |value| { + value[0] ^= 0x01; + }); + let result = PayerProof::try_from(mutated); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::InvalidValue))), + "modified proof_preimage must be rejected with InvalidValue, got {:?}", + result + ); + } + + /// Flipping a byte inside `proof_leaf_hashes` (TLV 1004) changes the + /// reconstructed invoice merkle root, which makes the issuer's `signature` + /// fail verification. + #[test] + fn test_parsing_rejects_modified_leaf_hash() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let mutated = + proof_bytes_with_mutated_tlv_value(&proof, PAYER_PROOF_LEAF_HASHES_TYPE, |value| { + value[0] ^= 0x01; + }); + let result = PayerProof::try_from(mutated); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::InvalidValue))), + "modified proof_leaf_hashes must be rejected with InvalidValue, got {:?}", + result + ); + } + + /// Flipping a byte inside `proof_missing_hashes` (TLV 1003) changes the + /// reconstructed invoice merkle root, which makes the issuer's `signature` + /// fail verification. + #[test] + fn test_parsing_rejects_modified_missing_hash() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let mutated = + proof_bytes_with_mutated_tlv_value(&proof, PAYER_PROOF_MISSING_HASHES_TYPE, |value| { + value[0] ^= 0x01; + }); + let result = PayerProof::try_from(mutated); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::InvalidValue))), + "modified proof_missing_hashes must be rejected with InvalidValue, got {:?}", + result + ); + } + + /// Per BOLT 12 PR 1295: `proof_leaf_hashes` MUST contain exactly one hash for + /// each non-signature TLV field. Dropping one hash must therefore fail parsing. + #[test] + fn test_parsing_rejects_leaf_hashes_count_mismatch() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_with_first_hash_dropped(&proof, PAYER_PROOF_LEAF_HASHES_TYPE); + let result = PayerProof::try_from(stripped); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::InvalidValue))), + "proof_leaf_hashes count mismatch must be rejected with InvalidValue, got {:?}", + result + ); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if `proof_missing_hashes` + /// (TLV 1003) is missing. + #[test] + fn test_parsing_rejects_missing_proof_missing_hashes() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, PAYER_PROOF_MISSING_HASHES_TYPE); + let result = PayerProof::try_from(stripped); + assert!(result.is_err(), "missing proof_missing_hashes TLV must be rejected"); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if `proof_leaf_hashes` + /// (TLV 1004) is missing. + #[test] + fn test_parsing_rejects_missing_proof_leaf_hashes() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, PAYER_PROOF_LEAF_HASHES_TYPE); + let result = PayerProof::try_from(stripped); + assert!(result.is_err(), "missing proof_leaf_hashes TLV must be rejected"); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if `invreq_payer_id` + /// (TLV 88) is missing. + #[test] + fn test_parsing_rejects_missing_payer_id() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, INVOICE_REQUEST_PAYER_ID_TYPE); + let result = PayerProof::try_from(stripped); + assert!( + matches!( + result, + Err(Bolt12ParseError::InvalidSemantics( + Bolt12SemanticError::MissingPayerSigningPubkey + )) + ), + "missing invreq_payer_id TLV must be rejected with MissingPayerSigningPubkey, got {:?}", + result + ); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if `invoice_payment_hash` + /// (TLV 168) is missing. + #[test] + fn test_parsing_rejects_missing_payment_hash() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, INVOICE_PAYMENT_HASH_TYPE); + let result = PayerProof::try_from(stripped); + assert!( + matches!( + result, + Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingPaymentHash)) + ), + "missing invoice_payment_hash TLV must be rejected with MissingPaymentHash, got {:?}", + result + ); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if `invoice_node_id` + /// (TLV 176) is missing. + #[test] + fn test_parsing_rejects_missing_node_id() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, INVOICE_NODE_ID_TYPE); + let result = PayerProof::try_from(stripped); + assert!( + matches!( + result, + Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSigningPubkey)) + ), + "missing invoice_node_id TLV must be rejected with MissingSigningPubkey, got {:?}", + result + ); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if the issuer + /// `signature` (TLV 240) is missing. + #[test] + fn test_parsing_rejects_missing_invoice_signature() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, PAYER_PROOF_ISSUER_SIGNATURE_TYPE); + let result = PayerProof::try_from(stripped); + assert!( + matches!( + result, + Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)) + ), + "missing invoice signature TLV must be rejected with MissingSignature, got {:?}", + result + ); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if `proof_signature` + /// (TLV 241) is missing. + #[test] + fn test_parsing_rejects_missing_proof_signature() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, PAYER_PROOF_PROOF_SIGNATURE_TYPE); + let result = PayerProof::try_from(stripped); + assert!( + matches!( + result, + Err(Bolt12ParseError::InvalidSemantics(Bolt12SemanticError::MissingSignature)) + ), + "missing proof_signature TLV must be rejected with MissingSignature, got {:?}", + result + ); + } + + /// Per BOLT 12 PR 1295: the reader MUST reject a payer_proof if `proof_preimage` + /// (TLV 1001) is missing. + #[test] + fn test_parsing_rejects_missing_proof_preimage() { + let proof = build_round_trip_proof_with_disclosed_fields(); + let stripped = proof_bytes_without_tlv(&proof, PAYER_PROOF_PREIMAGE_TYPE); + let result = PayerProof::try_from(stripped); + assert!( + matches!(result, Err(Bolt12ParseError::Decode(DecodeError::InvalidValue))), + "missing proof_preimage TLV must be rejected with InvalidValue, got {:?}", + result + ); + } + + #[test] + fn test_round_trip_with_trailing_experimental_tlvs() { + use core::convert::TryFrom; + + let preimage = PaymentPreimage([1; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000) + .unwrap() + .experimental_foo(42) + .experimental_bar(43) + .build() + .unwrap() + .respond_with_no_std(payment_paths(), payment_hash, recipient_pubkey(), now()) + .unwrap() + .experimental_baz(44) + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let secp_ctx = Secp256k1::signing_only(); + let payer_keys = payer_keys(); + let paid_invoice = PaidBolt12Invoice::Bolt12Invoice(invoice); + let payer_proof = paid_invoice + .prove_payer(preimage) + .unwrap() + .build() + .unwrap() + .sign(|proof: &UnsignedPayerProof| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(proof.as_ref().as_digest(), &payer_keys)) + }) + .unwrap(); + let parsed = PayerProof::try_from(payer_proof.bytes().to_vec()).unwrap(); + + assert_eq!(parsed.bytes(), payer_proof.bytes()); + assert_eq!(parsed.payment_preimage(), preimage); + assert_eq!(parsed.payment_hash(), payment_hash); + } + + #[test] + fn test_build_with_derived_signing_keys_for_refund_invoice() { + use core::convert::TryFrom; + + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + let preimage = PaymentPreimage([2; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + + let invoice = RefundBuilder::deriving_signing_pubkey( + payer_pubkey(), + &expanded_key, + nonce, + &secp_ctx, + 1000, + payment_id, + ) + .unwrap() + .path(blinded_path()) + .experimental_foo(42) + .experimental_bar(43) + .build() + .unwrap() + .respond_with_no_std(payment_paths(), payment_hash, recipient_pubkey(), now()) + .unwrap() + .experimental_baz(44) + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let paid_invoice = PaidBolt12Invoice::Bolt12Invoice(invoice); + let payer_proof = paid_invoice + .prove_payer_derived(preimage, &expanded_key, payment_id, &secp_ctx) + .unwrap() + .with_proof_note("refund".into()) + .build_and_sign() + .unwrap(); + let parsed = PayerProof::try_from(payer_proof.bytes().to_vec()).unwrap(); + + assert_eq!(parsed.payment_preimage(), preimage); + assert_eq!(parsed.payment_hash(), payment_hash); + assert_eq!(parsed.proof_note().map(|note| note.to_string()), Some("refund".to_string())); + } + + /// `PaidBolt12Invoice` round-trips through its `Writeable`/`Readable` implementations. This is + /// the contract the containers (`HTLCSource`, `PendingOutboundPayment`, `Event::PaymentSent`) + /// rely on when they serialize the paid invoice. + #[test] + fn test_bolt12_invoice_type_round_trips() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + let preimage = PaymentPreimage([2; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + + let invoice = RefundBuilder::deriving_signing_pubkey( + payer_pubkey(), + &expanded_key, + nonce, + &secp_ctx, + 1000, + payment_id, + ) + .unwrap() + .path(blinded_path()) + .build() + .unwrap() + .respond_with_no_std(payment_paths(), payment_hash, recipient_pubkey(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let original = PaidBolt12Invoice::Bolt12Invoice(invoice); + let bytes = original.encode(); + let read = <PaidBolt12Invoice as Readable>::read(&mut io::Cursor::new(&bytes)).unwrap(); + assert_eq!(read, original); + } + + /// Per BOLT 12 PR 1295: building a payer proof with a preimage whose SHA256 + /// doesn't match the invoice's `payment_hash` must fail at construction time + /// with `PreimageMismatch`. + #[test] + fn test_prove_payer_rejects_wrong_preimage() { + let preimage = PaymentPreimage([1; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + let invoice = RefundBuilder::new(vec![1; 32], payer_pubkey(), 1000) + .unwrap() + .build() + .unwrap() + .respond_with_no_std(payment_paths(), payment_hash, recipient_pubkey(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let wrong_preimage = PaymentPreimage([0xDE; 32]); + let paid_invoice = PaidBolt12Invoice::Bolt12Invoice(invoice); + assert!(matches!( + paid_invoice.prove_payer(wrong_preimage), + Err(PayerProofError::PreimageMismatch) + )); + } + + /// Per BOLT 12 PR 1295: deriving the payer signing key with the wrong + /// `payment_id` must fail at construction time with `KeyDerivationFailed`. + #[test] + fn test_prove_payer_derived_rejects_wrong_payment_id() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + let preimage = PaymentPreimage([2; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + + let invoice = RefundBuilder::deriving_signing_pubkey( + payer_pubkey(), + &expanded_key, + nonce, + &secp_ctx, + 1000, + payment_id, + ) + .unwrap() + .path(blinded_path()) + .build() + .unwrap() + .respond_with_no_std(payment_paths(), payment_hash, recipient_pubkey(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + + let paid_invoice = PaidBolt12Invoice::Bolt12Invoice(invoice); + + let wrong_payment_id = PaymentId([0xFF; 32]); + let result = + paid_invoice.prove_payer_derived(preimage, &expanded_key, wrong_payment_id, &secp_ctx); + assert!(matches!(result, Err(PayerProofError::KeyDerivationFailed))); + } + + /// The builder owns its data instead of borrowing the `Bolt12Invoice`, which is what makes it + /// friendly to language bindings. This test builds the builder in an inner scope where the paid + /// invoice is then dropped, and finishes the proof afterwards -- it only compiles because the + /// builder no longer holds a reference to the invoice. + #[test] + fn payer_proof_builder_outlives_invoice() { + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + let payment_id = PaymentId([1; 32]); + let preimage = PaymentPreimage([2; 32]); + let payment_hash = PaymentHash(*sha256::Hash::hash(&preimage.0).as_byte_array()); + + let builder = { + let invoice = RefundBuilder::deriving_signing_pubkey( + payer_pubkey(), + &expanded_key, + nonce, + &secp_ctx, + 1000, + payment_id, + ) + .unwrap() + .path(blinded_path()) + .build() + .unwrap() + .respond_with_no_std(payment_paths(), payment_hash, recipient_pubkey(), now()) + .unwrap() + .build() + .unwrap() + .sign(recipient_sign) + .unwrap(); + let paid_invoice = PaidBolt12Invoice::Bolt12Invoice(invoice); + paid_invoice + .prove_payer_derived(preimage, &expanded_key, payment_id, &secp_ctx) + .unwrap() + // `paid_invoice` (and the `Bolt12Invoice` it owns) is dropped here. + }; + + let proof = builder.include_offer_description().build_and_sign().unwrap(); + assert_eq!(proof.payment_hash(), payment_hash); + assert_eq!(proof.payment_preimage(), preimage); + } + + // BOLT 12 payer proof test vectors (from bolt12/payer-proof-test.json). + // Each vector carries its own invoice; all share the payer secret and preimage. + const PAYER_SECRET_HEX: &str = + "4242424242424242424242424242424242424242424242424242424242424242"; + const PREIMAGE_HEX: &str = "0101010101010101010101010101010101010101010101010101010101010101"; + + struct PayerProofVector { + name: &'static str, + invoice_hex: &'static str, + included_types: &'static [u64], + note: Option<&'static str>, + leaf_hashes_hex: &'static str, + omitted_tlvs: &'static [u64], + missing_hashes_hex: &'static str, + /// The merkle root of the invoice the proof is derived from. + merkle_root_hex: &'static str, + bech32: &'static str, + /// `true` when LDK's encoder reproduces `bech32` byte-for-byte. The + /// `empty_proof_omitted_tlvs_explicit` vector serializes an empty + /// `proof_omitted_tlvs` TLV, which LDK omits per the spec's "MAY omit" + /// rule, so for that vector only the parse path is exercised. + byte_exact: bool, + } + + const PAYER_PROOF_VECTORS: &[PayerProofVector] = &[ + PayerProofVector { + name: "full_disclosure", + invoice_hex: "0010000000000000000000000000000000001621024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382520203e858210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1ca076027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145001000000000000000000000000000000000a21c00000001000000020003000000000000000400000000000000050000a40467527988a82072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793aa0203e8ae0d08000000000000000000000000b021024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382f040fbb932e6a9d5b4d88ca0ddc9cf9f8cc880ef41e3ec9574da89f624db898ab3e9d3ed6caa8744633b855167da009119d9834ae71f7b06f02732dc4c1debab0577feb2d05e010142", + included_types: &[22, 82, 88, 160, 162, 164, 168, 170, 174, 176, 3000000001], + note: None, + leaf_hashes_hex: "8c9057ed88f3c5a6b6441dcac3b5e4cefb3615904d7362b86e78427fb695f4618dc54a97453dee6f207fa5216a30f1567442712ca98852bc789b73885029283cf2deaf5f30be3ced89fc7c24d422819bf06af0e48a31423bbd0e2634f3c3de67f54f80c94a87383f2a8ef7c3e461c62b67a51da5bccf6cd96a7dbab29bea51fa7849b8b856e1d2a63d9ce7dc1a78e05cbb2def1f5d7709c48e8707e0a59fe51e19e7e4eee6bf56c6c589fe50035490c1a7c91b753cb8007c4b52838a6772f997f0191c35000247554b8d0a196898a794bf3de89982571178d931affb654f0c1adc0b8de03f1a0b0531bff146982d7d613ef6e1ef8d3bdd9590971fc18d835ffbc14cfffaa314261bcbb2ed4ca24d5717bb608d8a6cc9910790bc1d49af7858ab7e92b77b9e3843650f6cd7ee94b6753ea9df3533710b04dee686ad376515a5cbabaab91b367e30fea7026daf9f2590bb7e9cc31db8221f4013c67289e38f22c8", + omitted_tlvs: &[], + missing_hashes_hex: "0b510ba4c6884d603159ced2f0ca21e772424b59e52a2191bbfbcf07377805a1", + merkle_root_hex: "cb9e0c81bb39fc244f9f523c748ab4de0e09f1a5fef74359c2e1f7cc7cdc7447", + bech32: "lnp1zcssyj7z5vfx29flqlnsuzatppeyu6u9ugtl3ntz3n4k996zg7a5jvuz2gpq86zcyypjgef743p5fzqq9nqxh0ah7y87rzv3ud0eleps9kl2d5348hq2k89qwcp87v0tc4rzc87uuxmn0m8l2tfh6aw75s7wz8r56fd299ckt74zqpcr9s9he72nyjs86pfe3vjqzaxups47g3xedv2e4fk877c7v6rgpxgszqhd4w73ddqusdcmjthj7pxprpd57qakmn2jh2dh3kwhezwg7gs3g5qpqqqqqqqqqqqqqqqqqqqqqqqqqq9zrsqqqqqpqqqqqqsqqvqqqqqqqqqqqpqqqqqqqqqqqqzsqq9yq3n4y7vg4qs89ntwss3vgplmd5ycdy83zv9hmmt7ctmltcwnp0va2g0sz5mr0ya2qgp73tsdpqqqqqqqqqqqqqqqqqqqpvppqf9u9gcjv52n7pl8pc96kzrjfe4ctcshlrxk9r8tv2t5y3amfyec9uzqlwun9e4f6k6d3r9qmhyul8uvezqw7s0raj2hfk5f7cjdhzv2k05a8mtv42r5gcems4gk0ksqjyvanq62uu0hkphsyuedcnqaaw4s2al3gpykzve5p8698d233l9uvc5ndl95dekpmwxev0zyke74valsll8r43wyy2far0qjnzcdvueq5aewyzsxcp5alfc8nhujq7m82dthxhwhl5p7jgqpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq87s86eqpdgshfxx3pxkqv2eemf0pj3puaeyyj6eu54zrydml08swdmcqksl6qlvl5qkprys2lkc3u7956myg8w2cw67fnhmxc2eqntnv2uxu7zz07mftarp3hz549698hhx7grl55sk5v832e6yyufv4xy990rcndecs5pf9q709h40tuctu08d3878cfx5y2qehur27rjg5v2z8w7suf3570pauel4f7qvjj588qlj4rhhc0jxr33tv7j3mfdueakdj6nah2efh6j3lfuynw9c2msa9f3annnacxncupwtkt00rawhwzwy36rs0c99nlj3ux08unhwd06kcmzcnljsqd2fpsd8eydh209cqp7yk55r3fnh97vh7qv3cdgqqfr42judpgvk3x98jjlnm6yesft3z7xexxhlke20psddczuduql35zc9xxllz35c947kz0hku8hc6w7ajkgfw87p3kp4l77pfnll4gc5ycduhvhdfj3y64chhdsgmznvexgs0y9ur4y677zc4dlf9dmmncuyxeg0dnt7a99kw5l2nhe4xdcskpx7u6r26dm9zkjuh2a2hydnvl3sl6nsymd0nujepwm7nnp3mwpzraqp83nj383c7gkgl6edqhspq9pq", + byte_exact: true, + }, + PayerProofVector { + name: "minimal_disclosure", + invoice_hex: "0010000000000000000000000000000000001621024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382520203e858210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1ca076027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145001000000000000000000000000000000000a21c00000001000000020003000000000000000400000000000000050000a40467527988a82072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793aa0203e8b021024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382f0400a33224568b6aae6ed252012bd7fe1072c03ebdca7fa44f95b03f1cd09be28b0a83c9c32105978cd80da068979662c80fa00ff250ccdc4d18b709ffd1c7ae319", + included_types: &[88, 168, 176], + note: None, + leaf_hashes_hex: "f2deaf5f30be3ced89fc7c24d422819bf06af0e48a31423bbd0e2634f3c3de67f0191c35000247554b8d0a196898a794bf3de89982571178d931affb654f0c1a7e92b77b9e3843650f6cd7ee94b6753ea9df3533710b04dee686ad376515a5cb", + omitted_tlvs: &[1, 2, 89, 90, 91, 169], + missing_hashes_hex: "bf8cb2b1d6fa9bcdcab501b59f82c65c506b7f43514737f7197f1fcfeaebad41b9406f4ce526a6a0d4e0b3a63ed89a832e31cb9939dfe1a7b5dd7232d32c02abcd9c44b53b31700c9ed0e3330ce425f7f18fac2fc1d566a34468439274f0e3169f9830f2c3070cfbad13fde30ee36cd7143591164ed12040a9cd595c96840ac9998ab7fa9c743fb9dbdb0d8d46fbe3ad333400bd07f328dcdb6008790bc9d2db", + merkle_root_hex: "0501ea6d4ad9fe7fce7edd5e3795987bd409d66c5709c2a17f9c0dfb839e3d8e", + bech32: "lnp1tqssxfr986kyx3ygqqkvq6alklcslcvfj834l8lyxqkmafkjx57up2cu4qs89ntwss3vgplmd5ycdy83zv9hmmt7ctmltcwnp0va2g0sz5mr0yasyypyhs4rzfj320c8uu8qh2cgwf8xhp0zzluv6c5vad3fwsj8hdyn8qhsgq9rxgj9dzm24ehdy5sp90tluyrjcqltmjnl538etvplrngfhc5tp2punsepqktcekqd5p5f09nzeq86qrlj2rxdcngckuyll5w84cce79qva0p96s9zmynmt672hpqq74p0hdag733w3hvq9wcnupgtn0ef8d690svmg6j8vaq0jlyadmq5ru35xnzaf7398gwjawyfd6adn9z4en7s86fqqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyql6ql2qcqsyk26tw5l6qlt5zlcev436mafhnw2k5qmt8uzcew9q6mlgdg5wdlhr9l3lnl2awk5rw2qdaxw2f4x5r2wpvax8mvf4qewx89ejwwluxnmthtjxtfjcq4tekwyfdfmx9cqe8ksuveseep97lccltp0c82kdg6ydppeya8suvtflxps7tpswr8m45flmccwudkdw9p4jytya5fqgz5u6k2uj6zq4jve32ml48r587uahkcd34r0hcadxv6qp0g87v5dekmqppushjwjm07s8mrq7t027heshc7wmz0u0sjdgg5pn0cx4u8y3gc5ywaapcnrfu7rmenlqxgux5qqy364fwxs5xtgnznef0eaazvcy4c30rvnrtlmv48scxn7j2mhh83cgdjs7mxha62tvaf7480n2vm3pvzdae5x45mk29d9ev", + byte_exact: true, + }, + PayerProofVector { + name: "with_note", + invoice_hex: "0010000000000000000000000000000000001621024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382520203e858210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1ca076027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145001000000000000000000000000000000000a21c00000001000000020003000000000000000400000000000000050000a40467527988a82072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793aa0203e8b021024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382f0400a33224568b6aae6ed252012bd7fe1072c03ebdca7fa44f95b03f1cd09be28b0a83c9c32105978cd80da068979662c80fa00ff250ccdc4d18b709ffd1c7ae319", + included_types: &[88, 168, 176], + note: Some("test note"), + leaf_hashes_hex: "f2deaf5f30be3ced89fc7c24d422819bf06af0e48a31423bbd0e2634f3c3de67f0191c35000247554b8d0a196898a794bf3de89982571178d931affb654f0c1a7e92b77b9e3843650f6cd7ee94b6753ea9df3533710b04dee686ad376515a5cb", + omitted_tlvs: &[1, 2, 89, 90, 91, 169], + missing_hashes_hex: "bf8cb2b1d6fa9bcdcab501b59f82c65c506b7f43514737f7197f1fcfeaebad41b9406f4ce526a6a0d4e0b3a63ed89a832e31cb9939dfe1a7b5dd7232d32c02abcd9c44b53b31700c9ed0e3330ce425f7f18fac2fc1d566a34468439274f0e3169f9830f2c3070cfbad13fde30ee36cd7143591164ed12040a9cd595c96840ac9998ab7fa9c743fb9dbdb0d8d46fbe3ad333400bd07f328dcdb6008790bc9d2db", + merkle_root_hex: "0501ea6d4ad9fe7fce7edd5e3795987bd409d66c5709c2a17f9c0dfb839e3d8e", + bech32: "lnp1tqssxfr986kyx3ygqqkvq6alklcslcvfj834l8lyxqkmafkjx57up2cu4qs89ntwss3vgplmd5ycdy83zv9hmmt7ctmltcwnp0va2g0sz5mr0yasyypyhs4rzfj320c8uu8qh2cgwf8xhp0zzluv6c5vad3fwsj8hdyn8qhsgq9rxgj9dzm24ehdy5sp90tluyrjcqltmjnl538etvplrngfhc5tp2punsepqktcekqd5p5f09nzeq86qrlj2rxdcngckuyll5w84cce79qz53lesac2aq2pr8tg9fa3na7wnczs5wa5nkds5qcugmvuk4arqawacga8gtmdxw7yaj8pw7pjwj2tafmd9mjkgcj7nxlmjhxzpxnhyt7s86fqqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyql6ql2qcqsyk26tw5l6qlt5zlcev436mafhnw2k5qmt8uzcew9q6mlgdg5wdlhr9l3lnl2awk5rw2qdaxw2f4x5r2wpvax8mvf4qewx89ejwwluxnmthtjxtfjcq4tekwyfdfmx9cqe8ksuveseep97lccltp0c82kdg6ydppeya8suvtflxps7tpswr8m45flmccwudkdw9p4jytya5fqgz5u6k2uj6zq4jve32ml48r587uahkcd34r0hcadxv6qp0g87v5dekmqppushjwjm07s8mrq7t027heshc7wmz0u0sjdgg5pn0cx4u8y3gc5ywaapcnrfu7rmenlqxgux5qqy364fwxs5xtgnznef0eaazvcy4c30rvnrtlmv48scxn7j2mhh83cgdjs7mxha62tvaf7480n2vm3pvzdae5x45mk29d9e07s8mgfw3jhxapqdehhgeg", + byte_exact: true, + }, + PayerProofVector { + name: "left_subtree_omitted", + invoice_hex: "0010000000000000000000000000000000001621024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382520203e858210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1ca076027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145001000000000000000000000000000000000a21c00000001000000020003000000000000000400000000000000050000a40467527988a82072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793aa0203e8b021024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382f0400a33224568b6aae6ed252012bd7fe1072c03ebdca7fa44f95b03f1cd09be28b0a83c9c32105978cd80da068979662c80fa00ff250ccdc4d18b709ffd1c7ae319", + included_types: &[88, 168, 170, 176], + note: None, + leaf_hashes_hex: "f2deaf5f30be3ced89fc7c24d422819bf06af0e48a31423bbd0e2634f3c3de67f0191c35000247554b8d0a196898a794bf3de89982571178d931affb654f0c1adc0b8de03f1a0b0531bff146982d7d613ef6e1ef8d3bdd9590971fc18d835ffb7e92b77b9e3843650f6cd7ee94b6753ea9df3533710b04dee686ad376515a5cb", + omitted_tlvs: &[1, 2, 89, 90, 91], + missing_hashes_hex: "bf8cb2b1d6fa9bcdcab501b59f82c65c506b7f43514737f7197f1fcfeaebad41b9406f4ce526a6a0d4e0b3a63ed89a832e31cb9939dfe1a7b5dd7232d32c02abcd9c44b53b31700c9ed0e3330ce425f7f18fac2fc1d566a34468439274f0e3169f9830f2c3070cfbad13fde30ee36cd7143591164ed12040a9cd595c96840ac9", + merkle_root_hex: "0501ea6d4ad9fe7fce7edd5e3795987bd409d66c5709c2a17f9c0dfb839e3d8e", + bech32: "lnp1tqssxfr986kyx3ygqqkvq6alklcslcvfj834l8lyxqkmafkjx57up2cu4qs89ntwss3vgplmd5ycdy83zv9hmmt7ctmltcwnp0va2g0sz5mr0ya2qgp73vppqf9u9gcjv52n7pl8pc96kzrjfe4ctcshlrxk9r8tv2t5y3amfyec9uzqpgejy3tgk64wdmf9yqft6llpqukq867u5layf72mq0cu6zd79zc2s0yuxgg9j7xdsrdqdztevckgp7sqlujsenwy6x9hp8lar3awxx03grks8mzc6kkxwefefp4md6xk7wymvd6mv6fllhes4yu3jkgdw3868ylegkjauwa404ju0asaauwwl292qwrjtv2m7ra8jl0apr9fw5u2l5p7jgqpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq87s86s9qyp9jkjml5p7hq9l3jetr4h6n0xu4dgpkk0c93ju2p4h7s63gumlwxtlrl8746adgxu5qm6vu5n2dgx5uze6v0kcn2pjuvwtnyualcd8khwhyvkn9sp2hnvugj6nkvtspj0dpcenpnjztal337kzlsw4v635g6zrjf60pcckn7vrpukrqux0htgnlh3sacmv6u2rtygkfmgjqs9fe4v4e95yptyl6qlvsredat6lxzlremvfl37zf4pzsxdlq6hsuj9rzs3mh58zvd8nc00x0uqers6sqqj8249c6zsedzv2099l8h5fnqjhz9udjvd0ldj57rq6ms9cmcplrg9s2vdl79rfsttavyl0dc0035aam9vsju0urrvrtlahay4h0w0rssm9pakd0m55ke6na2wlx5ehzzcymmngdtfhv526tjc", + byte_exact: true, + }, + PayerProofVector { + name: "empty_proof_omitted_tlvs_explicit", + invoice_hex: "0010000000000000000000000000000000001621024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382520203e858210324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1ca076027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e6686809910102edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145001000000000000000000000000000000000a21c00000001000000020003000000000000000400000000000000050000a40467527988a82072cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793aa0203e8b021024bc2a31265153f07e70e0bab08724e6b85e217f8cd628ceb62974247bb493382f0400a33224568b6aae6ed252012bd7fe1072c03ebdca7fa44f95b03f1cd09be28b0a83c9c32105978cd80da068979662c80fa00ff250ccdc4d18b709ffd1c7ae319", + included_types: &[22, 82, 88, 160, 162, 164, 168, 170, 176], + note: None, + leaf_hashes_hex: "8c9057ed88f3c5a6b6441dcac3b5e4cefb3615904d7362b86e78427fb695f4618dc54a97453dee6f207fa5216a30f1567442712ca98852bc789b73885029283cf2deaf5f30be3ced89fc7c24d422819bf06af0e48a31423bbd0e2634f3c3de67f54f80c94a87383f2a8ef7c3e461c62b67a51da5bccf6cd96a7dbab29bea51fa7849b8b856e1d2a63d9ce7dc1a78e05cbb2def1f5d7709c48e8707e0a59fe51e19e7e4eee6bf56c6c589fe50035490c1a7c91b753cb8007c4b52838a6772f997f0191c35000247554b8d0a196898a794bf3de89982571178d931affb654f0c1adc0b8de03f1a0b0531bff146982d7d613ef6e1ef8d3bdd9590971fc18d835ffb7e92b77b9e3843650f6cd7ee94b6753ea9df3533710b04dee686ad376515a5cb", + omitted_tlvs: &[], + missing_hashes_hex: "0b510ba4c6884d603159ced2f0ca21e772424b59e52a2191bbfbcf07377805a1", + merkle_root_hex: "0501ea6d4ad9fe7fce7edd5e3795987bd409d66c5709c2a17f9c0dfb839e3d8e", + bech32: "lnp1zcssyj7z5vfx29flqlnsuzatppeyu6u9ugtl3ntz3n4k996zg7a5jvuz2gpq86zcyypjgef743p5fzqq9nqxh0ah7y87rzv3ud0eleps9kl2d5348hq2k89qwcp87v0tc4rzc87uuxmn0m8l2tfh6aw75s7wz8r56fd299ckt74zqpcr9s9he72nyjs86pfe3vjqzaxups47g3xedv2e4fk877c7v6rgpxgszqhd4w73ddqusdcmjthj7pxprpd57qakmn2jh2dh3kwhezwg7gs3g5qpqqqqqqqqqqqqqqqqqqqqqqqqqq9zrsqqqqqpqqqqqqsqqvqqqqqqqqqqqpqqqqqqqqqqqqzsqq9yq3n4y7vg4qs89ntwss3vgplmd5ycdy83zv9hmmt7ctmltcwnp0va2g0sz5mr0ya2qgp73vppqf9u9gcjv52n7pl8pc96kzrjfe4ctcshlrxk9r8tv2t5y3amfyec9uzqpgejy3tgk64wdmf9yqft6llpqukq867u5layf72mq0cu6zd79zc2s0yuxgg9j7xdsrdqdztevckgp7sqlujsenwy6x9hp8lar3awxx03gr0luckkg3kpste9q0ncpl8qnlzu7vgw7999faja6803yspek75f0u55q3c2cruc2luzdv3j9zwq438xjf72vlvq29nlkzkax5hc3tw3l5p7jgqpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpqyqszqgpq87s86sql5p7kgqt2y96f35gf4srzkww6tcv5g08wfpykk099gserwlmeurnw7q9587s8m8aqysgeyzhaky083dxkezpmjkrkhjva7ekzkgy6umzhph8ssnlk62lgcvdc49fw3faaehjqla9y94rpu2kw3p8zt9f3pftc7ymwwy9q2fg8nedat6lxzlremvfl37zf4pzsxdlq6hsuj9rzs3mh58zvd8nc00x0a20sry54pec8u4gaa7ru3suv2m855w6t0x0dnvk5ld6k2d755060pym3wzku8f2v0vuulwp578qtjajmmclt4msn3ywsur7pfvlu50pnelyamnt74kxckylu5qr2jgvrf7frd6newqq03949qu2vae0n9lsrywr2qqzga25hrg2r95f3fu5hu773xvz2ugh3kf34lak2ncvrtwqhr0q8udqkpf3hlc5dxpd04snaahpa7xnhhv4jzt3lsvdsd0lkl5jkaaeuwzrv58ke4lwjjm8204fmu6nxugtqn0wdp4dxaj3tfwt", + byte_exact: false, + }, + ]; + + fn hex_decode(s: &str) -> Vec<u8> { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect() + } + + fn hex_encode(b: &[u8]) -> String { + b.iter().map(|x| format!("{:02x}", x)).collect() + } + + /// Split a concatenated hex string into 32-byte hash hex strings. + fn split_hashes_hex(hex: &str) -> Vec<String> { + (0..hex.len()).step_by(64).map(|i| hex[i..i + 64].to_string()).collect() + } + + /// Build a focused failure report for two bech32 strings that are expected + /// to be byte-identical except in the `payer_signature` region. + /// + /// Returns `None` when the strings match exactly. Otherwise returns a + /// `String` summarizing the divergence: how many leading/trailing bytes + /// match, the byte range of the differing region, and a short snippet + /// from each side. This avoids dumping ~1700-char bech32 strings into + /// the panic message. + fn report_bech32_mismatch(label: &str, got: &str, want: &str) -> Option<String> { + if got == want { + return None; + } + + let first_diff = got.bytes().zip(want.bytes()).position(|(a, b)| a != b); + let Some(first) = first_diff else { + return Some(format!( + "{}: bech32 length differs (got {} chars, want {} chars), \ + but the common prefix matches", + label, + got.len(), + want.len(), + )); + }; + + // Walk from the end to find where the strings reconverge. + let trailing_match = + got.bytes().rev().zip(want.bytes().rev()).position(|(a, b)| a != b).unwrap_or(0); + let got_diff_end = got.len() - trailing_match; + let want_diff_end = want.len() - trailing_match; + let snippet = 40usize; + let got_snippet = &got[first..got_diff_end.min(first + snippet)]; + let want_snippet = &want[first..want_diff_end.min(first + snippet)]; + let got_truncated = got_diff_end > first + snippet; + let want_truncated = want_diff_end > first + snippet; + + Some(format!( + "{label}: bech32 differs in chars [{first}..{got_diff_end}] (got len {got_len}) \ + and [{first}..{want_diff_end}] (want len {want_len}). \ + First {first} chars match; last {trailing_match} chars match.\n \ + got : \"{got_snippet}{got_ellipsis}\"\n \ + want : \"{want_snippet}{want_ellipsis}\"", + label = label, + first = first, + got_diff_end = got_diff_end, + want_diff_end = want_diff_end, + got_len = got.len(), + want_len = want.len(), + trailing_match = trailing_match, + got_snippet = got_snippet, + got_ellipsis = if got_truncated { "…" } else { "" }, + want_snippet = want_snippet, + want_ellipsis = if want_truncated { "…" } else { "" }, + )) + } + + #[test] + fn check_against_spec_vectors() { + let secp_ctx = Secp256k1::new(); + let payer_keys = Keypair::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&hex_decode(PAYER_SECRET_HEX)).unwrap(), + ); + + let preimage = PaymentPreimage(hex_decode(PREIMAGE_HEX).try_into().unwrap()); + + for vector in PAYER_PROOF_VECTORS { + let invoice = Bolt12Invoice::try_from(hex_decode(vector.invoice_hex)) + .unwrap_or_else(|e| panic!("{}: failed to parse invoice: {:?}", vector.name, e)); + + let mut builder = PayerProofBuilder::new(&invoice, preimage) + .unwrap_or_else(|e| panic!("{}: builder failed: {:?}", vector.name, e)); + for &typ in vector.included_types { + if typ != INVOICE_REQUEST_PAYER_ID_TYPE + && typ != INVOICE_PAYMENT_HASH_TYPE + && typ != INVOICE_NODE_ID_TYPE + { + builder = builder.include_type(typ).unwrap_or_else(|e| { + panic!("{}: include_type({}) failed: {:?}", vector.name, typ, e) + }); + } + } + + if let Some(note) = vector.note { + builder = builder.with_proof_note(note.to_owned()); + } + + // The selective-disclosure data is derived from the invoice's merkle + // tree and is independent of how the proof's optional TLVs are + // encoded, so every spec vector must match here. Recompute it + // independently of the builder so we can compare leaf hashes, + // omitted markers, missing hashes, and merkle root against the + // spec vector before signing the proof. + let invoice_bytes_for_check = invoice.invoice_bytes(); + let included_types_for_check: BTreeSet<u64> = + vector.included_types.iter().copied().collect(); + let disclosure = compute_selective_disclosure( + TlvStream::new(invoice_bytes_for_check), + &included_types_for_check, + ); + + let got_leaves: Vec<String> = + disclosure.nonce_hashes.iter().map(|h| hex_encode(h.as_ref())).collect(); + assert_eq!( + got_leaves, + split_hashes_hex(vector.leaf_hashes_hex), + "{}: leaf_hashes mismatch", + vector.name + ); + + assert_eq!( + disclosure.omitted_markers, vector.omitted_tlvs, + "{}: omitted_tlvs mismatch", + vector.name + ); + + let got_missing: Vec<String> = + disclosure.missing_hashes.iter().map(|h| hex_encode(h.as_ref())).collect(); + assert_eq!( + got_missing, + split_hashes_hex(vector.missing_hashes_hex), + "{}: missing_hashes mismatch", + vector.name + ); + + let got_root = hex_encode(disclosure.merkle_root.as_ref()); + assert_eq!(got_root, vector.merkle_root_hex, "{}: merkle_root mismatch", vector.name); + + let unsigned = builder + .build_unsigned() + .unwrap_or_else(|e| panic!("{}: build failed: {:?}", vector.name, e)); + + let proof = unsigned + .sign(|proof: &UnsignedPayerProof| { + Ok(secp_ctx.sign_schnorr_no_aux_rand(proof.as_ref().as_digest(), &payer_keys)) + }) + .unwrap_or_else(|e| panic!("{}: sign failed: {:?}", vector.name, e)); + + // Every spec vector must be readable, including the one that encodes + // an explicit empty `proof_omitted_tlvs` TLV. + vector + .bech32 + .parse::<PayerProof>() + .unwrap_or_else(|e| panic!("{}: spec proof failed to parse: {:?}", vector.name, e)); + + if vector.byte_exact { + // LDK's encoder must also reproduce the spec proof byte-for-byte. + if let Some(report) = + report_bech32_mismatch(vector.name, &proof.to_string(), vector.bech32) + { + panic!("{}", report); + } + } + } + } +} diff --git a/lightning/src/offers/refund.rs b/lightning/src/offers/refund.rs index c0fd9dfdd3e..85ea3b61435 100644 --- a/lightning/src/offers/refund.rs +++ b/lightning/src/offers/refund.rs @@ -210,15 +210,12 @@ macro_rules! refund_builder_methods { ( /// /// Also, sets the metadata when [`RefundBuilder::build`] is called such that it can be used by /// [`Bolt12Invoice::verify_using_metadata`] to determine if the invoice was produced for the - /// refund given an [`ExpandedKey`]. However, if [`RefundBuilder::path`] is called, then the - /// metadata must be included in each [`BlindedMessagePath`] instead. In this case, use - /// [`Bolt12Invoice::verify_using_payer_data`]. + /// refund given an [`ExpandedKey`]. /// /// The `payment_id` is encrypted in the metadata and should be unique. This ensures that only /// one invoice will be paid for the refund and that payments can be uniquely identified. /// /// [`Bolt12Invoice::verify_using_metadata`]: crate::offers::invoice::Bolt12Invoice::verify_using_metadata - /// [`Bolt12Invoice::verify_using_payer_data`]: crate::offers::invoice::Bolt12Invoice::verify_using_payer_data /// [`ExpandedKey`]: crate::ln::inbound_payment::ExpandedKey pub fn deriving_signing_pubkey( node_id: PublicKey, expanded_key: &ExpandedKey, nonce: Nonce, @@ -329,6 +326,8 @@ macro_rules! refund_builder_methods { ( if $self.refund.payer.0.has_derivation_material() { let mut metadata = core::mem::take(&mut $self.refund.payer.0); + // Don't derive keys if no blinded paths were given since this means the payer id must + // be a public node id. let iv_bytes = if $self.refund.paths.is_none() { metadata = metadata.without_keys(); IV_BYTES_WITH_METADATA @@ -1167,9 +1166,6 @@ mod tests { Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), Err(()) => panic!("verification failed"), } - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); let mut tlv_stream = refund.as_tlv_stream(); tlv_stream.2.amount = Some(2000); @@ -1248,10 +1244,10 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_ok()); + match invoice.verify_using_metadata(&expanded_key, &secp_ctx) { + Ok(payment_id) => assert_eq!(payment_id, PaymentId([1; 32])), + Err(()) => panic!("verification failed"), + } // Fails verification with altered fields let mut tlv_stream = refund.as_tlv_stream(); @@ -1268,9 +1264,7 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); // Fails verification with altered payer_id let mut tlv_stream = refund.as_tlv_stream(); @@ -1288,9 +1282,7 @@ mod tests { .unwrap() .sign(recipient_sign) .unwrap(); - assert!(invoice - .verify_using_payer_data(payment_id, nonce, &expanded_key, &secp_ctx) - .is_err()); + assert!(invoice.verify_using_metadata(&expanded_key, &secp_ctx).is_err()); } #[test] diff --git a/lightning/src/offers/selective_disclosure.rs b/lightning/src/offers/selective_disclosure.rs new file mode 100644 index 00000000000..35975cf74b5 --- /dev/null +++ b/lightning/src/offers/selective_disclosure.rs @@ -0,0 +1,763 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Selective disclosure support for BOLT 12 payer proofs. + +use alloc::collections::BTreeSet; + +use bitcoin::hashes::{sha256, Hash}; + +use crate::offers::invoice::INVOICE_TYPES; +use crate::offers::merkle::{ + merkle_tlv_data, tagged_branch_hash_from_engine, tagged_hash_engine, tagged_hash_from_engine, + TlvHashData, TlvRecord, +}; +use crate::offers::offer::EXPERIMENTAL_OFFER_TYPES; +use crate::offers::payer::PAYER_METADATA_TYPE; + +#[allow(unused_imports)] +use crate::prelude::*; + +/// Error during selective disclosure operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SelectiveDisclosureError { + /// The omitted markers are not in strict ascending order. + InvalidOmittedMarkersOrder, + /// The omitted markers contain an invalid marker (0 or signature type). + InvalidOmittedMarker, + /// The nonce_hashes count doesn't match included TLVs. + LeafHashCountMismatch, + /// Insufficient missing_hashes to reconstruct the tree. + InsufficientMissingHashes, +} + +/// Data needed to reconstruct a merkle root with selective disclosure. +/// +/// This is used in payer proofs to allow verification of an invoice signature +/// without revealing all invoice fields. +#[derive(Clone, Debug, PartialEq)] +pub(super) struct SelectiveDisclosure { + /// Nonce hashes for included TLVs (in TLV type order). + pub(super) nonce_hashes: Vec<sha256::Hash>, + /// Marker numbers for omitted TLVs (excluding implicit TLV0). + pub(super) omitted_markers: Vec<u64>, + /// Minimal merkle hashes for omitted subtrees. + pub(super) missing_hashes: Vec<sha256::Hash>, + /// The complete merkle root. + pub(super) merkle_root: sha256::Hash, +} + +/// Compute selective disclosure data from a TLV stream. +/// +/// This builds the full merkle tree and extracts the data needed for a payer proof: +/// - `nonce_hashes`: nonce hashes for included TLVs +/// - `omitted_markers`: marker numbers for omitted TLVs +/// - `missing_hashes`: minimal merkle hashes for omitted subtrees +/// +/// # Arguments +/// * `records` - Iterator of [`TlvRecord`]s from the invoice +/// * `included_types` - Set of TLV types to include in the disclosure +pub(super) fn compute_selective_disclosure<'a>( + records: impl Iterator<Item = TlvRecord<'a>> + 'a, included_types: &'a BTreeSet<u64>, +) -> SelectiveDisclosure { + debug_assert!(!included_types.contains(&PAYER_METADATA_TYPE)); + let (tlv_data, branch_tag) = merkle_tlv_data(records); + let tlv_data: Vec<TlvHashData> = tlv_data.collect(); + assert!(!tlv_data.is_empty(), "TLV stream must contain at least one non-signature record"); + + let omitted_markers: Vec<u64> = + compute_omitted_markers(tlv_data.iter(), included_types).collect(); + let nonce_hashes = tlv_data + .iter() + .filter(|data| included_types.contains(&data.tlv_type)) + .map(|data| data.nonce_hash) + .collect(); + let (merkle_root, missing_hashes) = + build_tree_with_disclosure(&tlv_data, included_types, &branch_tag); + + SelectiveDisclosure { nonce_hashes, omitted_markers, missing_hashes, merkle_root } +} + +/// Returns the marker number that follows `prev` (an included TLV type or a +/// previous marker) per BOLT 12 PR 1295. +/// +/// A marker is one greater than the previous value, except that a value landing +/// in the gap between the invoice TLV range and the experimental range (the +/// signature/payer-proof range) jumps to the start of the experimental range. +/// The producer and the readers all go through this so their marker sequences +/// stay in agreement. +pub(super) fn next_marker(prev: u64) -> u64 { + let next = prev.saturating_add(1); + if (INVOICE_TYPES.end..EXPERIMENTAL_OFFER_TYPES.start).contains(&next) { + EXPERIMENTAL_OFFER_TYPES.start + } else { + next + } +} + +/// Compute omitted markers per BOLT 12 payer proof spec. +/// +/// Each omitted TLV gets the marker number following the previous included TLV +/// type or the previous marker (see [`next_marker`]). TLV type 0 is implicitly +/// omitted (never assigned a marker). +fn compute_omitted_markers<'a>( + tlv_data: impl Iterator<Item = &'a TlvHashData> + 'a, included_types: &'a BTreeSet<u64>, +) -> impl Iterator<Item = u64> + 'a { + tlv_data + // TLV 0 participates in the merkle tree but is implicitly omitted per BOLT 1295 and never + // produces an omitted marker. The scan below starts at `PAYER_METADATA_TYPE` for that reason. + .filter(|data| data.tlv_type != PAYER_METADATA_TYPE) + .scan(PAYER_METADATA_TYPE, |prev_value, data| { + if included_types.contains(&data.tlv_type) { + *prev_value = data.tlv_type; + Some(None) + } else { + let marker = next_marker(*prev_value); + *prev_value = marker; + Some(Some(marker)) + } + }) + .flatten() +} + +/// Build merkle tree recursively (DFS, left-to-right) and collect missing_hashes. +/// +/// Per the spec, missing_hashes are in depth-first left-to-right order. +/// +/// Note: a level-by-level approach (as used by `root_hash()`) cannot produce +/// DFS-ordered missing_hashes because it processes all subtrees at each depth +/// simultaneously rather than completing each subtree before the next. +fn build_tree_with_disclosure( + tlv_data: &[TlvHashData], included_types: &BTreeSet<u64>, branch_tag: &sha256::HashEngine, +) -> (sha256::Hash, Vec<sha256::Hash>) { + let mut missing_hashes = Vec::new(); + let (root, _) = build_tree_dfs(tlv_data, included_types, branch_tag, &mut missing_hashes); + (root, missing_hashes) +} + +fn build_tree_dfs( + tlv_data: &[TlvHashData], included_types: &BTreeSet<u64>, branch_tag: &sha256::HashEngine, + missing_hashes: &mut Vec<sha256::Hash>, +) -> (sha256::Hash, bool) { + if tlv_data.len() == 1 { + return (tlv_data[0].per_tlv_hash, included_types.contains(&tlv_data[0].tlv_type)); + } + + let mid = tlv_data.len().next_power_of_two() / 2; + let (left_data, right_data) = tlv_data.split_at(mid); + let (left_hash, left_incl) = + build_tree_dfs(left_data, included_types, branch_tag, missing_hashes); + let (right_hash, right_incl) = + build_tree_dfs(right_data, included_types, branch_tag, missing_hashes); + + if left_incl && !right_incl { + missing_hashes.push(right_hash); + } else if !left_incl && right_incl { + missing_hashes.push(left_hash); + } + + let combined = tagged_branch_hash_from_engine(branch_tag.clone(), left_hash, right_hash); + (combined, left_incl || right_incl) +} + +/// Decodes the per-position inclusion map (`true` = included, `false` = omitted) from included +/// TLV types and omitted markers, with the implicit omitted TLV0 at the front. +fn decode_positions( + included_types: impl ExactSizeIterator<Item = u64>, omitted_markers: &[u64], +) -> Vec<bool> { + let mut positions = Vec::with_capacity(1 + included_types.len() + omitted_markers.len()); + positions.push(false); // TLV0 is always omitted. + + let mut included = included_types.peekable(); + let mut markers = omitted_markers.iter().copied().peekable(); + let mut prev_marker = PAYER_METADATA_TYPE; + + loop { + match (included.peek().copied(), markers.peek().copied()) { + (None, None) => break, + // No more markers: every remaining position is included. + (Some(_), None) => { + included.next(); + positions.push(true); + }, + // No more included types: every remaining position is omitted. + (None, Some(marker)) => { + markers.next(); + prev_marker = marker; + positions.push(false); + }, + // Continuation of the current run -> omitted position. + (Some(_), Some(marker)) if marker == next_marker(prev_marker) => { + markers.next(); + prev_marker = marker; + positions.push(false); + }, + // Jump -> an included TLV sits here; the marker is reprocessed next iteration. + (Some(inc_type), Some(_)) => { + included.next(); + prev_marker = inc_type; + positions.push(true); + }, + } + } + + positions +} + +/// Reconstruct merkle root from selective disclosure data. +/// +/// `missing_hashes` must be in DFS (left-to-right recursive traversal) order, +/// matching the order produced by [`build_tree_with_disclosure`]. +pub(super) fn reconstruct_merkle_root( + included_records: &[TlvRecord<'_>], nonce_hashes: &[sha256::Hash], omitted_markers: &[u64], + missing_hashes: &[sha256::Hash], +) -> Result<sha256::Hash, SelectiveDisclosureError> { + debug_assert!({ + let included_types: BTreeSet<u64> = included_records.iter().map(|r| r.r#type).collect(); + validate_omitted_markers(omitted_markers, &included_types).is_ok() + }); + + if included_records.len() != nonce_hashes.len() { + return Err(SelectiveDisclosureError::LeafHashCountMismatch); + } + + let leaf_tag = tagged_hash_engine(sha256::Hash::hash("LnLeaf".as_bytes())); + let branch_tag = tagged_hash_engine(sha256::Hash::hash("LnBranch".as_bytes())); + + // Build per-position hash array: Some(hash) for included positions, None for omitted (including + // the implicit TLV0 at position 0). `decode_positions` is the shared source of truth + // for the run/jump structure, so this consumer cannot drift from the encoder or the test. + let positions = decode_positions(included_records.iter().map(|r| r.r#type), omitted_markers); + let mut hashes: Vec<Option<sha256::Hash>> = Vec::with_capacity(positions.len()); + + let mut inc_idx = 0; + for included in positions { + if included { + let record = &included_records[inc_idx]; + let leaf_hash = tagged_hash_from_engine(leaf_tag.clone(), record.record_bytes); + let nonce_hash = nonce_hashes[inc_idx]; + hashes.push(Some(tagged_branch_hash_from_engine( + branch_tag.clone(), + leaf_hash, + nonce_hash, + ))); + inc_idx += 1; + } else { + hashes.push(None); + } + } + + let mut missing_idx: usize = 0; + let root = reconstruct_merkle_root_dfs(&hashes, &branch_tag, missing_hashes, &mut missing_idx)?; + + if missing_idx != missing_hashes.len() { + return Err(SelectiveDisclosureError::InsufficientMissingHashes); + } + + root.ok_or(SelectiveDisclosureError::InsufficientMissingHashes) +} + +fn reconstruct_merkle_root_dfs( + hashes: &[Option<sha256::Hash>], branch_tag: &sha256::HashEngine, + missing_hashes: &[sha256::Hash], missing_idx: &mut usize, +) -> Result<Option<sha256::Hash>, SelectiveDisclosureError> { + if hashes.len() == 1 { + return Ok(hashes[0]); + } + + let mid = hashes.len().next_power_of_two() / 2; + let (left_hashes, right_hashes) = hashes.split_at(mid); + let left = reconstruct_merkle_root_dfs(left_hashes, branch_tag, missing_hashes, missing_idx)?; + let right = reconstruct_merkle_root_dfs(right_hashes, branch_tag, missing_hashes, missing_idx)?; + + match (left, right) { + (None, None) => Ok(None), + (Some(l), None) => { + if *missing_idx >= missing_hashes.len() { + return Err(SelectiveDisclosureError::InsufficientMissingHashes); + } + let r = missing_hashes[*missing_idx]; + *missing_idx += 1; + Ok(Some(tagged_branch_hash_from_engine(branch_tag.clone(), l, r))) + }, + (None, Some(r)) => { + if *missing_idx >= missing_hashes.len() { + return Err(SelectiveDisclosureError::InsufficientMissingHashes); + } + let l = missing_hashes[*missing_idx]; + *missing_idx += 1; + Ok(Some(tagged_branch_hash_from_engine(branch_tag.clone(), l, r))) + }, + (Some(l), Some(r)) => Ok(Some(tagged_branch_hash_from_engine(branch_tag.clone(), l, r))), + } +} + +/// Validates that `markers` is a minimized omitted-marker sequence per BOLT 12 PR 1295, relative +/// to `included_types`. Each marker MUST be strictly ascending, non-zero, MUST NOT be an included +/// TLV type, and MUST be minimized: it equals the marker following the previous marker (continuing +/// a run) or the previous included type (starting a new run), per [`next_marker`]. The +/// signature-gap jump is handled by [`next_marker`], so signature-range markers are rejected +/// implicitly. This is the single source of truth for marker minimality; callers layer any +/// additional range restrictions on top (e.g. the payer-proof valid ranges). +pub(super) fn validate_omitted_markers( + markers: &[u64], included_types: &BTreeSet<u64>, +) -> Result<(), SelectiveDisclosureError> { + let mut inc_iter = included_types.iter().copied().peekable(); + // After the implicit payer metadata marker, the first minimized marker is the next marker. + let mut expected_next: u64 = next_marker(PAYER_METADATA_TYPE); + let mut prev = PAYER_METADATA_TYPE; + + for &marker in markers { + if marker == PAYER_METADATA_TYPE { + return Err(SelectiveDisclosureError::InvalidOmittedMarker); + } + if marker <= prev { + return Err(SelectiveDisclosureError::InvalidOmittedMarkersOrder); + } + if included_types.contains(&marker) { + return Err(SelectiveDisclosureError::InvalidOmittedMarker); + } + + // Minimization: `marker` continues the current run (`expected_next`), or an included type + // X sits between the previous position and `marker` with `next_marker(X) == marker`. + if marker != expected_next { + let mut found = false; + for inc_type in inc_iter.by_ref() { + if next_marker(inc_type) == marker { + found = true; + break; + } + if inc_type >= marker { + return Err(SelectiveDisclosureError::InvalidOmittedMarker); + } + } + if !found { + return Err(SelectiveDisclosureError::InvalidOmittedMarker); + } + } + + expected_next = next_marker(marker); + prev = marker; + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::compute_omitted_markers; + use crate::offers::merkle::{TlvHashData, TlvRecord, TlvStream}; + use alloc::collections::BTreeSet; + use bitcoin::hashes::{sha256, Hash}; + + /// Reconstruct the position inclusion map (`true` = included, `false` = omitted) from included + /// types and omitted markers, using the same [`super::decode_positions`] logic + /// `reconstruct_merkle_root` uses to place hashes. + fn reconstruct_positions(included_types: &[u64], omitted_markers: &[u64]) -> Vec<bool> { + super::decode_positions(included_types.iter().copied(), omitted_markers) + } + + /// Builds a synthetic TLV stream with one record per type in `types`, each carrying a fixed + /// 2-byte value. Types must be < 253 so each encodes as a single BigSize byte. Only the types + /// and their order matter for selective-disclosure marker/position logic. + fn synthetic_tlv_stream(types: &[u64]) -> Vec<u8> { + let mut bytes = Vec::new(); + for &tlv_type in types { + assert!(tlv_type < 253, "helper only supports single-byte BigSize types"); + bytes.extend_from_slice(&[tlv_type as u8, 0x02, 0x00, 0x00]); + } + bytes + } + + /// Computes the disclosure for `included` over `tlv_bytes`, checks the omitted markers and the + /// reconstructed positions, then reconstructs the merkle root and asserts it matches the + /// full-tree root. Unlike feeding hand-written markers to `reconstruct_positions`, this proves + /// the producer (`compute_selective_disclosure`) and consumer agree on the same stream. + fn assert_disclosure_round_trip( + tlv_bytes: &[u8], included: &[u64], expected_markers: &[u64], expected_positions: &[bool], + ) { + let included_types: BTreeSet<u64> = included.iter().copied().collect(); + + let disclosure = + super::compute_selective_disclosure(TlvStream::new(tlv_bytes), &included_types); + assert_eq!(disclosure.omitted_markers.as_slice(), expected_markers); + assert_eq!( + reconstruct_positions(included, &disclosure.omitted_markers).as_slice(), + expected_positions, + ); + + let included_records: Vec<TlvRecord<'_>> = + TlvStream::new(tlv_bytes).filter(|r| included_types.contains(&r.r#type)).collect(); + let reconstructed = super::reconstruct_merkle_root( + &included_records, + &disclosure.nonce_hashes, + &disclosure.omitted_markers, + &disclosure.missing_hashes, + ) + .unwrap(); + assert_eq!(reconstructed, disclosure.merkle_root); + } + + /// BOLT 12 payer proof spec example. + /// TLVs: 0(omit), 10(incl), 20(omit), 30(omit), 40(incl), 50(omit), 60(omit) + #[test] + fn test_reconstruct_positions_spec_example() { + assert_disclosure_round_trip( + &synthetic_tlv_stream(&[0, 10, 20, 30, 40, 50, 60]), + &[10, 40], + &[11, 12, 41, 42], + &[false, true, false, false, true, false, false], + ); + } + + /// Omitted TLVs before the first included one. + /// TLVs: 0(omit), 5(omit), 10(incl), 20(omit) + #[test] + fn test_reconstruct_positions_omitted_before_included() { + assert_disclosure_round_trip( + &synthetic_tlv_stream(&[0, 5, 10, 20]), + &[10], + &[1, 11], + &[false, false, true, false], + ); + } + + /// Only included TLVs (just the implicit TLV0 is omitted). + /// TLVs: 0(omit), 10(incl), 20(incl) + #[test] + fn test_reconstruct_positions_no_omitted() { + assert_disclosure_round_trip( + &synthetic_tlv_stream(&[0, 10, 20]), + &[10, 20], + &[], + &[false, true, true], + ); + } + + /// Only omitted TLVs (nothing included). This is not a real proof shape -- a proof must + /// disclose the required fields -- so there is no disclosed leaf to anchor reconstruction. + /// The producer still emits markers/positions, but reconstructing the root must fail. + /// TLVs: 0(omit), 5(omit), 10(omit) + #[test] + fn test_reconstruct_positions_no_included() { + let tlv_bytes = synthetic_tlv_stream(&[0, 5, 10]); + let included_types = BTreeSet::new(); + let disclosure = + super::compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included_types); + assert_eq!(disclosure.omitted_markers, vec![1, 2]); + assert_eq!( + reconstruct_positions(&[], &disclosure.omitted_markers), + vec![false, false, false], + ); + + assert_eq!( + super::reconstruct_merkle_root( + &[], + &disclosure.nonce_hashes, + &disclosure.omitted_markers, + &disclosure.missing_hashes, + ), + Err(super::SelectiveDisclosureError::InsufficientMissingHashes), + ); + } + + #[test] + fn test_validate_omitted_markers_edge_cases() { + let included_types = |types: &[u64]| -> BTreeSet<u64> { types.iter().copied().collect() }; + + assert!(super::validate_omitted_markers(&[1, 2, 3, 41, 42], &included_types(&[40])).is_ok()); + assert!(super::validate_omitted_markers(&[11, 12], &included_types(&[10])).is_ok()); + assert!(super::validate_omitted_markers(&[], &included_types(&[10, 20])).is_ok()); + assert!(super::validate_omitted_markers(&[1_000_000_000], &included_types(&[239])).is_ok()); + + assert_eq!( + super::validate_omitted_markers(&[0], &included_types(&[])), + Err(super::SelectiveDisclosureError::InvalidOmittedMarker) + ); + assert_eq!( + super::validate_omitted_markers(&[11, 11], &included_types(&[10])), + Err(super::SelectiveDisclosureError::InvalidOmittedMarkersOrder) + ); + assert_eq!( + super::validate_omitted_markers(&[10], &included_types(&[10])), + Err(super::SelectiveDisclosureError::InvalidOmittedMarker) + ); + assert_eq!( + super::validate_omitted_markers(&[11, 15, 41], &included_types(&[10, 40])), + Err(super::SelectiveDisclosureError::InvalidOmittedMarker) + ); + assert_eq!( + super::validate_omitted_markers(&[11, 12, 45], &included_types(&[10, 40])), + Err(super::SelectiveDisclosureError::InvalidOmittedMarker) + ); + } + + #[test] + fn compute_selective_disclosure_skips_signature_tlv_records() { + let bytes_without_signature = vec![ + 0x00, 0x01, 0x00, // payer_metadata + 0x0a, 0x01, 0x01, // type 10 + 0x14, 0x01, 0x02, // type 20 + ]; + let bytes_with_signature = vec![ + 0x00, 0x01, 0x00, // payer_metadata + 0x0a, 0x01, 0x01, // type 10 + 0xf0, 0x00, // signature type 240, ignored by merkle calculation + 0x14, 0x01, 0x02, // type 20 + ]; + let included = [10, 20].into_iter().collect::<BTreeSet<_>>(); + + assert_eq!( + super::compute_selective_disclosure(TlvStream::new(&bytes_with_signature), &included), + super::compute_selective_disclosure( + TlvStream::new(&bytes_without_signature), + &included + ) + ); + } + + /// Test round-trip: compute selective disclosure then reconstruct merkle root. + #[test] + fn test_selective_disclosure_round_trip() { + // Build TLV stream matching spec example structure + // TLVs: 0, 10, 20, 30, 40, 50, 60 + let mut tlv_bytes = Vec::new(); + tlv_bytes.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); // TLV 0 + tlv_bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); // TLV 10 + tlv_bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); // TLV 20 + tlv_bytes.extend_from_slice(&[0x1e, 0x02, 0x00, 0x00]); // TLV 30 + tlv_bytes.extend_from_slice(&[0x28, 0x02, 0x00, 0x00]); // TLV 40 + tlv_bytes.extend_from_slice(&[0x32, 0x02, 0x00, 0x00]); // TLV 50 + tlv_bytes.extend_from_slice(&[0x3c, 0x02, 0x00, 0x00]); // TLV 60 + + // Include types 10 and 40 + let mut included = BTreeSet::new(); + included.insert(10); + included.insert(40); + + // Compute selective disclosure + let disclosure = super::compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + + // Verify markers match spec example + assert_eq!(disclosure.omitted_markers, vec![11, 12, 41, 42]); + + // Verify nonce_hashes count matches included TLVs + assert_eq!(disclosure.nonce_hashes.len(), 2); + + // Collect included records for reconstruction + let included_records: Vec<TlvRecord<'_>> = + TlvStream::new(&tlv_bytes).filter(|r| included.contains(&r.r#type)).collect(); + + // Reconstruct merkle root + let reconstructed = super::reconstruct_merkle_root( + &included_records, + &disclosure.nonce_hashes, + &disclosure.omitted_markers, + &disclosure.missing_hashes, + ) + .unwrap(); + + // Must match original + assert_eq!(reconstructed, disclosure.merkle_root); + } + + /// Test that the synthetic 7-node example still requires four missing hashes. + /// + /// For the synthetic tree with TLVs [0(o), 10(I), 20(o), 30(o), 40(I), 50(o), 60(o)]: + /// - hash(0) covers type 0 + /// - hash(B(20,30)) covers types 20-30 + /// - hash(50) covers type 50 + /// - hash(60) covers type 60 + /// + /// This still needs 4 missing hashes. The DFS-ordering fix changes the order + /// they are emitted and consumed in, but not the count for this tree shape. + #[test] + fn test_missing_hashes_for_synthetic_tree() { + // Build TLV stream: 0, 10, 20, 30, 40, 50, 60 + let mut tlv_bytes = Vec::new(); + tlv_bytes.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); // TLV 0 + tlv_bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); // TLV 10 + tlv_bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); // TLV 20 + tlv_bytes.extend_from_slice(&[0x1e, 0x02, 0x00, 0x00]); // TLV 30 + tlv_bytes.extend_from_slice(&[0x28, 0x02, 0x00, 0x00]); // TLV 40 + tlv_bytes.extend_from_slice(&[0x32, 0x02, 0x00, 0x00]); // TLV 50 + tlv_bytes.extend_from_slice(&[0x3c, 0x02, 0x00, 0x00]); // TLV 60 + + // Include types 10 and 40 (same as spec example) + let mut included = BTreeSet::new(); + included.insert(10); + included.insert(40); + + let disclosure = super::compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + + // We should still have 4 missing hashes for omitted types: + // - type 0 (single leaf) + // - types 20+30 (combined branch) + // - type 50 (single leaf) + // - type 60 (single leaf) + assert_eq!( + disclosure.missing_hashes.len(), + 4, + "Expected 4 missing hashes for omitted types [0, 20+30, 50, 60]" + ); + + // Verify the round-trip still works with the correct ordering + let included_records: Vec<TlvRecord<'_>> = + TlvStream::new(&tlv_bytes).filter(|r| included.contains(&r.r#type)).collect(); + + let reconstructed = super::reconstruct_merkle_root( + &included_records, + &disclosure.nonce_hashes, + &disclosure.omitted_markers, + &disclosure.missing_hashes, + ) + .unwrap(); + + assert_eq!(reconstructed, disclosure.merkle_root); + } + + /// Test that reconstruction fails with wrong number of missing_hashes. + #[test] + fn test_reconstruction_fails_with_wrong_missing_hashes() { + let mut tlv_bytes = Vec::new(); + tlv_bytes.extend_from_slice(&[0x00, 0x04, 0x00, 0x00, 0x00, 0x00]); // TLV 0 + tlv_bytes.extend_from_slice(&[0x0a, 0x02, 0x00, 0x00]); // TLV 10 + tlv_bytes.extend_from_slice(&[0x14, 0x02, 0x00, 0x00]); // TLV 20 + + let mut included = BTreeSet::new(); + included.insert(10); + + let disclosure = super::compute_selective_disclosure(TlvStream::new(&tlv_bytes), &included); + + let included_records: Vec<TlvRecord<'_>> = + TlvStream::new(&tlv_bytes).filter(|r| included.contains(&r.r#type)).collect(); + + // Try with empty missing_hashes (should fail) + let result = super::reconstruct_merkle_root( + &included_records, + &disclosure.nonce_hashes, + &disclosure.omitted_markers, + &[], // Wrong! + ); + + assert!(result.is_err()); + } + + /// Verify that [`compute_omitted_markers`] jumps from the top of the low + /// marker range (239) to the start of the high range (1_000_000_000) per + /// BOLT 12 PR 1295, rather than entering the signature type range. Real + /// BOLT 12 invoices have far fewer than 239 non-signature TLVs, so this + /// case is unreachable in practice. + #[test] + fn compute_omitted_markers_jumps_to_high_range_after_239() { + // 240 consecutive omitted TLVs at types 1..=240. The first 239 markers + // climb 1..=239; the 240th would be 240 (in the signature range), so it + // jumps to 1_000_000_000 instead. + let dummy_hash = sha256::Hash::all_zeros(); + let included = BTreeSet::new(); + let tlv_data: Vec<TlvHashData> = (1u64..=240) + .map(|tlv_type| TlvHashData { + tlv_type, + nonce_hash: dummy_hash, + per_tlv_hash: dummy_hash, + }) + .collect(); + + let markers: Vec<u64> = compute_omitted_markers(tlv_data.iter(), &included).collect(); + + let mut expected: Vec<u64> = (1..=239).collect(); + expected.push(1_000_000_000); + assert_eq!(markers, expected); + } + + /// An *included* TLV at the top of the low range (type 239) followed by an + /// omitted TLV: the marker must skip the signature/payer-proof gap and jump + /// to the start of the experimental range, not land on 240. + #[test] + fn compute_omitted_markers_jumps_after_included_at_top_of_low_range() { + let dummy_hash = sha256::Hash::all_zeros(); + let included = [239u64].into_iter().collect::<BTreeSet<_>>(); + let tlv_data = [ + TlvHashData { tlv_type: 239, nonce_hash: dummy_hash, per_tlv_hash: dummy_hash }, + TlvHashData { + tlv_type: 1_500_000_000, + nonce_hash: dummy_hash, + per_tlv_hash: dummy_hash, + }, + ]; + let markers: Vec<u64> = compute_omitted_markers(tlv_data.iter(), &included).collect(); + assert_eq!(markers, vec![1_000_000_000]); + } + + /// After a jump into the experimental range, subsequent omitted markers + /// continue sequentially within that range. + #[test] + fn compute_omitted_markers_continue_in_experimental_range_after_jump() { + let dummy_hash = sha256::Hash::all_zeros(); + let included = [239u64].into_iter().collect::<BTreeSet<_>>(); + let tlv_data = [ + TlvHashData { tlv_type: 239, nonce_hash: dummy_hash, per_tlv_hash: dummy_hash }, + TlvHashData { + tlv_type: 3_000_000_000, + nonce_hash: dummy_hash, + per_tlv_hash: dummy_hash, + }, + TlvHashData { + tlv_type: 3_000_000_001, + nonce_hash: dummy_hash, + per_tlv_hash: dummy_hash, + }, + ]; + let markers: Vec<u64> = compute_omitted_markers(tlv_data.iter(), &included).collect(); + assert_eq!(markers, vec![1_000_000_000, 1_000_000_001]); + } + + /// [`next_marker`] increments by one within a range but jumps over the + /// signature/payer-proof gap, so producer and readers stay in agreement. + #[test] + fn next_marker_jumps_the_gap() { + assert_eq!(super::next_marker(super::PAYER_METADATA_TYPE), 1); + assert_eq!(super::next_marker(5), 6); + assert_eq!(super::next_marker(238), 239); + // 240 would land in the signature range, so it jumps to the experimental range. + assert_eq!(super::next_marker(239), 1_000_000_000); + assert_eq!(super::next_marker(1_000_000_000), 1_000_000_001); + } + + #[test] + fn validate_omitted_markers_direct() { + use alloc::collections::BTreeSet; + let none: BTreeSet<u64> = BTreeSet::new(); + + // A minimized leading run with nothing included is accepted. + assert!(super::validate_omitted_markers(&[1, 2, 3], &none).is_ok()); + // The empty sequence is accepted. + assert!(super::validate_omitted_markers(&[], &none).is_ok()); + // Zero is rejected (it is the implicit TLV0 marker). + assert!(super::validate_omitted_markers(&[0], &none).is_err()); + // A non-ascending sequence is rejected. + assert!(super::validate_omitted_markers(&[2, 1], &none).is_err()); + // A gap with no intervening included type to justify it is non-minimized -> rejected. + assert!(super::validate_omitted_markers(&[1, 3], &none).is_err()); + + // The same `[1, 3]` is accepted when included type 2 sits between them, because + // next_marker(2) == 3 justifies the jump. + let inc2: BTreeSet<u64> = [2u64].into_iter().collect(); + assert!(super::validate_omitted_markers(&[1, 3], &inc2).is_ok()); + // A marker equal to an included type is rejected. + assert!(super::validate_omitted_markers(&[2], &inc2).is_err()); + + // The signature-gap jump is accepted when justified by an included type at the top of the + // low range: included 239, omitted marker 1_000_000_000 (next_marker(239)). + let inc239: BTreeSet<u64> = [239u64].into_iter().collect(); + assert!(super::validate_omitted_markers(&[1_000_000_000], &inc239).is_ok()); + // ...but not without that justification. + assert!(super::validate_omitted_markers(&[1_000_000_000], &none).is_err()); + } +} diff --git a/lightning/src/offers/signer.rs b/lightning/src/offers/signer.rs index e51a120b6d7..5f5f12a03f7 100644 --- a/lightning/src/offers/signer.rs +++ b/lightning/src/offers/signer.rs @@ -63,11 +63,6 @@ pub(super) enum Metadata { /// This variant should only be used at verification time, never when building. RecipientData(Nonce), - /// Metadata for deriving keys included as payer data in a blinded path. - /// - /// This variant should only be used at verification time, never when building. - PayerData([u8; PaymentId::LENGTH + Nonce::LENGTH]), - /// Metadata to be derived from message contents and given material. /// /// This variant should only be used at building time. @@ -80,16 +75,6 @@ pub(super) enum Metadata { } impl Metadata { - pub fn payer_data(payment_id: PaymentId, nonce: Nonce, expanded_key: &ExpandedKey) -> Self { - let encrypted_payment_id = expanded_key.crypt_for_offer(payment_id.0, nonce); - - let mut bytes = [0u8; PaymentId::LENGTH + Nonce::LENGTH]; - bytes[..PaymentId::LENGTH].copy_from_slice(encrypted_payment_id.as_slice()); - bytes[PaymentId::LENGTH..].copy_from_slice(nonce.as_slice()); - - Metadata::PayerData(bytes) - } - pub fn as_bytes(&self) -> Option<&Vec<u8>> { match self { Metadata::Bytes(bytes) => Some(bytes), @@ -107,10 +92,6 @@ impl Metadata { debug_assert!(false); false }, - Metadata::PayerData(_) => { - debug_assert!(false); - false - }, Metadata::Derived(_) => true, Metadata::DerivedSigningPubkey(_) => true, } @@ -125,7 +106,6 @@ impl Metadata { // Nonce::LENGTH had been set explicitly. Metadata::Bytes(bytes) => bytes.len() == PaymentId::LENGTH + Nonce::LENGTH, Metadata::RecipientData(_) => false, - Metadata::PayerData(_) => true, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -140,7 +120,6 @@ impl Metadata { // been set explicitly. Metadata::Bytes(bytes) => bytes.len() == Nonce::LENGTH, Metadata::RecipientData(_) => true, - Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => true, } @@ -158,10 +137,6 @@ impl Metadata { debug_assert!(false); self }, - Metadata::PayerData(_) => { - debug_assert!(false); - self - }, Metadata::Derived(_) => self, Metadata::DerivedSigningPubkey(material) => Metadata::Derived(material), } @@ -176,10 +151,6 @@ impl Metadata { debug_assert!(false); (self, None) }, - Metadata::PayerData(_) => { - debug_assert!(false); - (self, None) - }, Metadata::Derived(metadata_material) => { (Metadata::Bytes(metadata_material.derive_metadata(iv_bytes, tlv_stream)), None) }, @@ -204,7 +175,6 @@ impl AsRef<[u8]> for Metadata { match self { Metadata::Bytes(bytes) => &bytes, Metadata::RecipientData(nonce) => &nonce.0, - Metadata::PayerData(bytes) => bytes.as_slice(), Metadata::Derived(_) => { debug_assert!(false); &[] @@ -222,7 +192,6 @@ impl fmt::Debug for Metadata { match self { Metadata::Bytes(bytes) => bytes.fmt(f), Metadata::RecipientData(Nonce(bytes)) => bytes.fmt(f), - Metadata::PayerData(bytes) => bytes.fmt(f), Metadata::Derived(_) => f.write_str("Derived"), Metadata::DerivedSigningPubkey(_) => f.write_str("DerivedSigningPubkey"), } @@ -241,7 +210,6 @@ impl PartialEq for Metadata { } }, Metadata::RecipientData(_) => false, - Metadata::PayerData(_) => false, Metadata::Derived(_) => false, Metadata::DerivedSigningPubkey(_) => false, } @@ -290,7 +258,8 @@ impl MetadataMaterial { self.hmac.input(DERIVED_METADATA_AND_KEYS_HMAC_INPUT); self.maybe_include_encrypted_payment_id(); - let bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or_default(); + let mut bytes = self.encrypted_payment_id.map(|id| id.to_vec()).unwrap_or_default(); + bytes.extend_from_slice(self.nonce.as_slice()); let hmac = Hmac::from_engine(self.hmac); let privkey = SecretKey::from_slice(hmac.as_byte_array()).unwrap(); @@ -321,6 +290,33 @@ pub(super) fn derive_keys(nonce: Nonce, expanded_key: &ExpandedKey) -> Keypair { Keypair::from_secret_key(&secp_ctx, &privkey) } +/// Re-derives the payer signing keypair from the on-wire payer `metadata`. +/// +/// Performs the same derivation as keys created by [`Metadata::derive_from`] when using +/// [`Metadata::DerivedSigningPubkey`] with a [`MetadataMaterial`] built from a `payment_id`. +/// The `metadata` is the payer metadata as it appears on the wire (the encrypted payment id +/// followed by the [`Nonce`]); the nonce no longer needs to be supplied separately. +/// +/// The `tlv_stream` must contain the records matching what was used during the original +/// key derivation. +pub(super) fn derive_payer_keys<'a, T: secp256k1::Signing>( + metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>, + secp_ctx: &Secp256k1<T>, +) -> Result<Keypair, ()> { + match verify_payer_metadata_inner( + metadata, + expanded_key, + iv_bytes, + signing_pubkey, + tlv_stream, + secp_ctx, + )? { + Some(keys) => Ok(keys), + None => Err(()), + } +} + /// Verifies data given in a TLV stream was used to produce the given metadata, consisting of: /// - a 256-bit [`PaymentId`], /// - a 128-bit [`Nonce`], and possibly @@ -335,6 +331,34 @@ pub(super) fn verify_payer_metadata<'a, T: secp256k1::Signing>( signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>, secp_ctx: &Secp256k1<T>, ) -> Result<PaymentId, ()> { + verify_payer_metadata_inner( + metadata, + expanded_key, + iv_bytes, + signing_pubkey, + tlv_stream, + secp_ctx, + )?; + + let mut encrypted_payment_id = [0u8; PaymentId::LENGTH]; + encrypted_payment_id.copy_from_slice(&metadata[..PaymentId::LENGTH]); + let nonce = Nonce::try_from(&metadata[PaymentId::LENGTH..][..Nonce::LENGTH]).unwrap(); + let payment_id = expanded_key.crypt_for_offer(encrypted_payment_id, nonce); + + Ok(PaymentId(payment_id)) +} + +/// Shared core of [`verify_payer_metadata`] and [`derive_payer_keys`]. +/// +/// Builds the payer HMAC from the given metadata and TLV stream, then verifies it against the +/// `signing_pubkey`. The `metadata` must be at least `PaymentId::LENGTH` bytes, with the first +/// `PaymentId::LENGTH` bytes being the encrypted payment ID and the remainder being the nonce +/// (and possibly an HMAC). +fn verify_payer_metadata_inner<'a, T: secp256k1::Signing>( + metadata: &[u8], expanded_key: &ExpandedKey, iv_bytes: &[u8; IV_LEN], + signing_pubkey: PublicKey, tlv_stream: impl core::iter::Iterator<Item = TlvRecord<'a>>, + secp_ctx: &Secp256k1<T>, +) -> Result<Option<Keypair>, ()> { if metadata.len() < PaymentId::LENGTH { return Err(()); } @@ -352,12 +376,7 @@ pub(super) fn verify_payer_metadata<'a, T: secp256k1::Signing>( Hmac::from_engine(hmac), signing_pubkey, secp_ctx, - )?; - - let nonce = Nonce::try_from(&metadata[PaymentId::LENGTH..][..Nonce::LENGTH]).unwrap(); - let payment_id = expanded_key.crypt_for_offer(encrypted_payment_id, nonce); - - Ok(PaymentId(payment_id)) + ) } /// Verifies data given in a TLV stream was used to produce the given metadata, consisting of: diff --git a/lightning/src/offers/static_invoice.rs b/lightning/src/offers/static_invoice.rs index 77f486a6a06..860835912a1 100644 --- a/lightning/src/offers/static_invoice.rs +++ b/lightning/src/offers/static_invoice.rs @@ -99,7 +99,7 @@ struct InvoiceContents { fallbacks: Option<Vec<FallbackAddress>>, features: Bolt12InvoiceFeatures, signing_pubkey: PublicKey, - message_paths: Vec<BlindedMessagePath>, + held_htlc_available_paths: Vec<BlindedMessagePath>, #[cfg(test)] experimental_baz: Option<u64>, } @@ -122,14 +122,17 @@ impl<'a> StaticInvoiceBuilder<'a> { /// overridden by [`StaticInvoiceBuilder::relative_expiry`]. pub fn for_offer_using_derived_keys<T: secp256k1::Signing>( offer: &'a Offer, payment_paths: Vec<BlindedPaymentPath>, - message_paths: Vec<BlindedMessagePath>, created_at: Duration, expanded_key: &ExpandedKey, - nonce: Nonce, secp_ctx: &Secp256k1<T>, + held_htlc_available_paths: Vec<BlindedMessagePath>, created_at: Duration, + expanded_key: &ExpandedKey, nonce: Nonce, secp_ctx: &Secp256k1<T>, ) -> Result<Self, Bolt12SemanticError> { if offer.chains().len() > 1 { return Err(Bolt12SemanticError::UnexpectedChain); } - if payment_paths.is_empty() || message_paths.is_empty() || offer.paths().is_empty() { + if payment_paths.is_empty() + || held_htlc_available_paths.is_empty() + || offer.paths().is_empty() + { return Err(Bolt12SemanticError::MissingPaths); } @@ -147,8 +150,13 @@ impl<'a> StaticInvoiceBuilder<'a> { return Err(Bolt12SemanticError::InvalidSigningPubkey); } - let invoice = - InvoiceContents::new(offer, payment_paths, message_paths, created_at, signing_pubkey); + let invoice = InvoiceContents::new( + offer, + payment_paths, + held_htlc_available_paths, + created_at, + signing_pubkey, + ); Ok(Self { offer_bytes: &offer.bytes, invoice, keys }) } @@ -264,8 +272,8 @@ macro_rules! invoice_accessors { ($self: ident, $contents: expr) => { /// Paths to the recipient for indicating that a held HTLC is available to claim when they next /// come online. - pub fn message_paths(&$self) -> &[BlindedMessagePath] { - $contents.message_paths() + pub fn held_htlc_available_paths(&$self) -> &[BlindedMessagePath] { + $contents.held_htlc_available_paths() } /// The quantity of items supported, from [`Offer::supported_quantity`]. @@ -400,7 +408,7 @@ impl StaticInvoice { /// Whether the [`Offer`] that this invoice is based on is expired. #[cfg(feature = "std")] pub fn is_offer_expired(&self) -> bool { - self.contents.is_expired() + self.contents.is_offer_expired() } /// Whether the [`Offer`] that this invoice is based on is expired, given the current time as @@ -438,12 +446,13 @@ impl InvoiceContents { fn new( offer: &Offer, payment_paths: Vec<BlindedPaymentPath>, - message_paths: Vec<BlindedMessagePath>, created_at: Duration, signing_pubkey: PublicKey, + held_htlc_available_paths: Vec<BlindedMessagePath>, created_at: Duration, + signing_pubkey: PublicKey, ) -> Self { Self { offer: offer.contents.clone(), payment_paths, - message_paths, + held_htlc_available_paths, created_at, relative_expiry: None, fallbacks: None, @@ -465,7 +474,7 @@ impl InvoiceContents { let invoice = InvoiceTlvStreamRef { paths: Some(Iterable(self.payment_paths.iter().map(|path| path.inner_blinded_path()))), - message_paths: Some(self.message_paths.as_ref()), + held_htlc_available_paths: Some(self.held_htlc_available_paths.as_ref()), blindedpay: Some(Iterable(self.payment_paths.iter().map(|path| &path.payinfo))), created_at: Some(self.created_at.as_secs()), relative_expiry: self.relative_expiry.map(|duration| duration.as_secs() as u32), @@ -519,8 +528,8 @@ impl InvoiceContents { self.offer.paths() } - fn message_paths(&self) -> &[BlindedMessagePath] { - &self.message_paths[..] + fn held_htlc_available_paths(&self) -> &[BlindedMessagePath] { + &self.held_htlc_available_paths[..] } fn supported_quantity(&self) -> Quantity { @@ -670,7 +679,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents { fallbacks, features, node_id, - message_paths, + held_htlc_available_paths, payment_hash, amount, }, @@ -689,7 +698,8 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents { } let payment_paths = construct_payment_paths(blindedpay, paths)?; - let message_paths = message_paths.ok_or(Bolt12SemanticError::MissingPaths)?; + let held_htlc_available_paths = + held_htlc_available_paths.ok_or(Bolt12SemanticError::MissingPaths)?; let created_at = match created_at { None => return Err(Bolt12SemanticError::MissingCreationTime), @@ -713,7 +723,7 @@ impl TryFrom<PartialInvoiceTlvStream> for InvoiceContents { Ok(InvoiceContents { offer: OfferContents::try_from((offer_tlv_stream, experimental_offer_tlv_stream))?, payment_paths, - message_paths, + held_htlc_available_paths, created_at, relative_expiry, fallbacks, @@ -875,7 +885,7 @@ mod tests { assert_eq!(invoice.offer_features(), &OfferFeatures::empty()); assert_eq!(invoice.absolute_expiry(), None); assert_eq!(invoice.offer_message_paths(), &[blinded_path()]); - assert_eq!(invoice.message_paths(), &[blinded_path()]); + assert_eq!(invoice.held_htlc_available_paths(), &[blinded_path()]); assert_eq!(invoice.issuer(), None); assert_eq!(invoice.supported_quantity(), Quantity::One); assert_ne!(invoice.signing_pubkey(), recipient_pubkey()); @@ -921,7 +931,7 @@ mod tests { fallbacks: None, features: None, node_id: Some(&signing_pubkey), - message_paths: Some(&paths), + held_htlc_available_paths: Some(&paths), }, SignatureTlvStreamRef { signature: Some(&invoice.signature()) }, ExperimentalOfferTlvStreamRef { experimental_foo: None }, @@ -993,6 +1003,43 @@ mod tests { } } + #[cfg(feature = "std")] + #[test] + fn is_offer_expired_does_not_check_invoice_expiry() { + // Regression test: `StaticInvoice::is_offer_expired` must reflect the offer's expiry, + // not the invoice's own expiry. Build an invoice whose offer has no absolute expiry + // (so the offer never expires) but whose own `created_at + relative_expiry` lies in + // the past (so the invoice itself is expired). + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + let invoice = StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + Duration::from_secs(0), + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .relative_expiry(1) + .build_and_sign(&secp_ctx) + .unwrap(); + + assert!(invoice.is_expired()); + assert!(!invoice.is_offer_expired()); + } + #[test] fn builds_invoice_from_offer_using_derived_key() { let node_id = recipient_pubkey(); @@ -1318,10 +1365,10 @@ mod tests { }, } - // Error if message paths are missing. - let missing_message_paths_invoice = invoice(); - let mut tlv_stream = missing_message_paths_invoice.as_tlv_stream(); - tlv_stream.1.message_paths = None; + // Error if held_htlc_available_paths are missing. + let missing_held_htlc_available_paths_invoice = invoice(); + let mut tlv_stream = missing_held_htlc_available_paths_invoice.as_tlv_stream(); + tlv_stream.1.held_htlc_available_paths = None; match StaticInvoice::try_from(tlv_stream_to_bytes(&tlv_stream)) { Ok(_) => panic!("expected error"), Err(e) => { diff --git a/lightning/src/onion_message/async_payments.rs b/lightning/src/onion_message/async_payments.rs index 41108cdccd7..96914518b6b 100644 --- a/lightning/src/onion_message/async_payments.rs +++ b/lightning/src/onion_message/async_payments.rs @@ -279,25 +279,25 @@ impl OnionMessageContents for ReleaseHeldHtlc { } } -impl_writeable_tlv_based!(OfferPathsRequest, { +impl_ser_tlv_based!(OfferPathsRequest, { (0, invoice_slot, required), }); -impl_writeable_tlv_based!(OfferPaths, { +impl_ser_tlv_based!(OfferPaths, { (0, paths, required_vec), (2, paths_absolute_expiry, option), }); -impl_writeable_tlv_based!(ServeStaticInvoice, { +impl_ser_tlv_based!(ServeStaticInvoice, { (0, invoice, required), (2, forward_invoice_request_path, required), }); -impl_writeable_tlv_based!(StaticInvoicePersisted, {}); +impl_ser_tlv_based!(StaticInvoicePersisted, {}); -impl_writeable_tlv_based!(HeldHtlcAvailable, {}); +impl_ser_tlv_based!(HeldHtlcAvailable, {}); -impl_writeable_tlv_based!(ReleaseHeldHtlc, {}); +impl_ser_tlv_based!(ReleaseHeldHtlc, {}); impl AsyncPaymentsMessage { /// Returns whether `tlv_type` corresponds to a TLV record for async payment messages. diff --git a/lightning/src/onion_message/dns_resolution.rs b/lightning/src/onion_message/dns_resolution.rs index e857a359c78..7ab210af7f2 100644 --- a/lightning/src/onion_message/dns_resolution.rs +++ b/lightning/src/onion_message/dns_resolution.rs @@ -40,12 +40,16 @@ use core::fmt; use core::ops::Deref; use crate::blinded_path::message::DNSResolverContext; +#[cfg(feature = "dnssec")] +use crate::blinded_path::message::MessageContext; use crate::io; #[cfg(feature = "dnssec")] use crate::ln::channelmanager::PaymentId; use crate::ln::msgs::DecodeError; #[cfg(feature = "dnssec")] use crate::offers::offer::Offer; +#[cfg(feature = "dnssec")] +use crate::onion_message::messenger::Destination; use crate::onion_message::messenger::{MessageSendInstructions, Responder, ResponseInstruction}; use crate::onion_message::packet::OnionMessageContents; use crate::prelude::*; @@ -77,6 +81,19 @@ pub trait DNSResolverMessageHandler { /// [`OnionMessenger`]: crate::onion_message::messenger::OnionMessenger fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext); + /// Handle a [`DNSSECError`] message (in response to a [`DNSSECQuery`] we presumably sent), + /// indicating that the resolver was unable to resolve the requested name. + /// + /// The provided [`DNSResolverContext`] was authenticated by the [`OnionMessenger`] as coming from + /// a blinded path that we created. + /// + /// Receiving this lets us avoid waiting for a [`DNSSECProof`] which will never come, failing the + /// pending operation early instead (at least if the name is + /// [definitely unresolvable](DNSSECError::definitely_unresolvable)). + /// + /// [`OnionMessenger`]: crate::onion_message::messenger::OnionMessenger + fn handle_dnssec_error(&self, message: DNSSECError, context: DNSResolverContext); + /// Gets the node feature flags which this handler itself supports. Useful for setting the /// `dns_resolver` flag if this handler supports returning [`DNSSECProof`] messages in response /// to [`DNSSECQuery`] messages. @@ -99,6 +116,9 @@ impl<T: DNSResolverMessageHandler + ?Sized, D: Deref<Target = T>> DNSResolverMes fn handle_dnssec_proof(&self, message: DNSSECProof, context: DNSResolverContext) { self.deref().handle_dnssec_proof(message, context) } + fn handle_dnssec_error(&self, message: DNSSECError, context: DNSResolverContext) { + self.deref().handle_dnssec_error(message, context) + } fn provided_node_features(&self) -> NodeFeatures { self.deref().provided_node_features() } @@ -115,10 +135,14 @@ pub enum DNSResolverMessage { DNSSECQuery(DNSSECQuery), /// A response containing a DNSSEC proof DNSSECProof(DNSSECProof), + /// An error in response to a [`DNSSECQuery`], indicating that the requested name could not be + /// resolved into a [`DNSSECProof`]. + DNSSECError(DNSSECError), } const DNSSEC_QUERY_TYPE: u64 = 65536; const DNSSEC_PROOF_TYPE: u64 = 65538; +const DNSSEC_ERROR_TYPE: u64 = 65550; #[derive(Clone, Debug, Hash, PartialEq, Eq)] /// A message which is sent to a DNSSEC prover requesting a DNSSEC proof for the given name. @@ -136,11 +160,30 @@ pub struct DNSSECProof { pub proof: Vec<u8>, } +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +/// A message which is sent in response to a [`DNSSECQuery`] when the resolver was unable to build a +/// [`DNSSECProof`] for the requested name. +/// +/// This lets the recipient stop waiting for a [`DNSSECProof`] which will not be forthcoming. +pub struct DNSSECError { + /// The name which the [`DNSSECQuery`] was for and which we were unable to resolve. + pub name: Name, + /// Whether the name is known to be permanently unresolvable, as opposed to having failed due to + /// some transient error. + /// + /// This is set if the requested name does not exist (i.e. the resolver received an NXDOMAIN + /// response) or if the name is not in a DNSSEC-signed zone, in which case retrying or querying a + /// different resolver will not help. It is not set for transient failures (e.g. a timeout + /// communicating with an upstream DNS server), where a retry or a different resolver may yet + /// succeed. + pub definitely_unresolvable: bool, +} + impl DNSResolverMessage { /// Returns whether `tlv_type` corresponds to a TLV record for DNS Resolvers. pub fn is_known_type(tlv_type: u64) -> bool { match tlv_type { - DNSSEC_QUERY_TYPE | DNSSEC_PROOF_TYPE => true, + DNSSEC_QUERY_TYPE | DNSSEC_PROOF_TYPE | DNSSEC_ERROR_TYPE => true, _ => false, } } @@ -158,6 +201,11 @@ impl Writeable for DNSResolverMessage { w.write_all(&name.as_str().as_bytes())?; proof.write(w) }, + Self::DNSSECError(DNSSECError { name, definitely_unresolvable }) => { + (name.as_str().len() as u8).write(w)?; + w.write_all(&name.as_str().as_bytes())?; + definitely_unresolvable.write(w) + }, } } } @@ -176,6 +224,12 @@ impl ReadableArgs<u64> for DNSResolverMessage { let proof = Readable::read(r)?; Ok(DNSResolverMessage::DNSSECProof(DNSSECProof { name, proof })) }, + DNSSEC_ERROR_TYPE => { + let s = Hostname::read(r)?; + let name = s.try_into().map_err(|_| DecodeError::InvalidValue)?; + let definitely_unresolvable = Readable::read(r)?; + Ok(DNSResolverMessage::DNSSECError(DNSSECError { name, definitely_unresolvable })) + }, _ => Err(DecodeError::InvalidValue), } } @@ -187,6 +241,7 @@ impl OnionMessageContents for DNSResolverMessage { match self { DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query".to_string(), DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof".to_string(), + DNSResolverMessage::DNSSECError(_) => "DNSSEC Error".to_string(), } } #[cfg(not(c_bindings))] @@ -194,12 +249,14 @@ impl OnionMessageContents for DNSResolverMessage { match self { DNSResolverMessage::DNSSECQuery(_) => "DNS(SEC) Query", DNSResolverMessage::DNSSECProof(_) => "DNSSEC Proof", + DNSResolverMessage::DNSSECError(_) => "DNSSEC Error", } } fn tlv_type(&self) -> u64 { match self { DNSResolverMessage::DNSSECQuery(_) => DNSSEC_QUERY_TYPE, DNSResolverMessage::DNSSECProof(_) => DNSSEC_PROOF_TYPE, + DNSResolverMessage::DNSSECError(_) => DNSSEC_ERROR_TYPE, } } } @@ -319,7 +376,7 @@ impl fmt::Display for HumanReadableName { #[cfg(feature = "dnssec")] struct PendingResolution { start_height: u32, - context: DNSResolverContext, + pending_query_contexts: Vec<DNSResolverContext>, name: HumanReadableName, payment_id: PaymentId, } @@ -409,26 +466,45 @@ impl OMNameResolver { /// Begins the process of resolving a BIP 353 Human Readable Name. /// - /// Returns a [`DNSSECQuery`] onion message and a [`DNSResolverContext`] which should be sent - /// to a resolver (with the context used to generate the blinded response path) on success. - pub fn resolve_name<ES: EntropySource + ?Sized>( - &self, payment_id: PaymentId, name: HumanReadableName, entropy_source: &ES, - ) -> Result<(DNSSECQuery, DNSResolverContext), ()> { + /// Sets up the state to handle query responses and returns a list of [`DNSSECQuery`] onion + /// messages and the [`MessageSendInstructions`] over which each should be sent - one entry per + /// provided `destination`. + pub fn initiate_resolution<ES: EntropySource + ?Sized>( + &self, payment_id: PaymentId, name: HumanReadableName, destinations: Vec<Destination>, + entropy_source: &ES, + ) -> Result<Vec<(DNSResolverMessage, MessageSendInstructions)>, ()> { + if destinations.is_empty() { + return Err(()); + } + let dns_name = Name::try_from(format!("{}.user._bitcoin-payment.{}.", name.user(), name.domain())); debug_assert!( dns_name.is_ok(), "The HumanReadableName constructor shouldn't allow names which are too long" ); - let mut context = DNSResolverContext { nonce: [0; 16] }; - context.nonce.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]); if let Ok(dns_name) = dns_name { let start_height = self.latest_block_height.load(Ordering::Acquire) as u32; + let query = DNSResolverMessage::DNSSECQuery(DNSSECQuery(dns_name.clone())); + let mut pending_query_contexts = Vec::with_capacity(destinations.len()); + let messages = destinations + .into_iter() + .map(|destination| { + let mut context = DNSResolverContext { nonce: [0; 16] }; + context.nonce.copy_from_slice(&entropy_source.get_secure_random_bytes()[..16]); + pending_query_contexts.push(context.clone()); + let instructions = MessageSendInstructions::WithReplyPath { + destination, + context: MessageContext::DNSResolver(context), + }; + (query.clone(), instructions) + }) + .collect(); let mut pending_resolves = self.pending_resolves.lock().unwrap(); - let context_ret = context.clone(); - let resolution = PendingResolution { start_height, context, name, payment_id }; - pending_resolves.entry(dns_name.clone()).or_insert_with(Vec::new).push(resolution); - Ok((DNSSECQuery(dns_name), context_ret)) + let resolution = + PendingResolution { start_height, pending_query_contexts, name, payment_id }; + pending_resolves.entry(dns_name).or_insert_with(Vec::new).push(resolution); + Ok(messages) } else { Err(()) } @@ -444,10 +520,15 @@ impl OMNameResolver { /// different [`HumanReadableName`]s. /// /// If an [`Offer`] is found, it, as well as the [`PaymentId`] and original `name` passed to - /// [`Self::resolve_name`] are returned. + /// [`Self::initiate_resolution`] are returned. + /// + /// If the proof is invalid and there are no remaining queries for this name, or if the proof is + /// valid and does not contain a valid BIP 353 entry or BOLT 12 [`Offer`], `Err` will be + /// returned with the set of [`HumanReadableName`] and [`PaymentId`] requests which should be + /// considered failed. pub fn handle_dnssec_proof_for_offer( &self, msg: DNSSECProof, context: DNSResolverContext, - ) -> Option<(Vec<(HumanReadableName, PaymentId)>, Offer)> { + ) -> Result<(Vec<(HumanReadableName, PaymentId)>, Offer), Vec<(HumanReadableName, PaymentId)>> { let (completed_requests, uri) = self.handle_dnssec_proof_for_uri(msg, context)?; if let Some((_onchain, params)) = uri.split_once('?') { for param in params.split('&') { @@ -458,33 +539,38 @@ impl OMNameResolver { }; if k.eq_ignore_ascii_case("lno") { if let Ok(offer) = Offer::from_str(v) { - return Some((completed_requests, offer)); + return Ok((completed_requests, offer)); } - return None; + return Err(completed_requests); } } } - None + Err(completed_requests) } /// Handles a [`DNSSECProof`] message, attempting to verify it and match it against any pending /// queries. /// /// If verification succeeds, all matching [`PaymentId`] and [`HumanReadableName`]s passed to - /// [`Self::resolve_name`], as well as the resolved bitcoin: URI are returned. + /// [`Self::initiate_resolution`], as well as the resolved bitcoin: URI are returned. /// /// Note that a single proof for a wildcard DNS entry may complete several requests for /// different [`HumanReadableName`]s. /// + /// If the proof is invalid and there are no remaining queries for this name, or if the proof is + /// valid and does not contain a valid BIP 353 entry, `Err` will be returned with the set of + /// [`HumanReadableName`] and [`PaymentId`] requests which should be considered failed. + /// /// This method is useful for those who handle bitcoin: URIs already, handling more than just /// BOLT12 [`Offer`]s. pub fn handle_dnssec_proof_for_uri( &self, msg: DNSSECProof, context: DNSResolverContext, - ) -> Option<(Vec<(HumanReadableName, PaymentId)>, String)> { + ) -> Result<(Vec<(HumanReadableName, PaymentId)>, String), Vec<(HumanReadableName, PaymentId)>> + { let DNSSECProof { name: answer_name, proof } = msg; let mut pending_resolves = self.pending_resolves.lock().unwrap(); - if let hash_map::Entry::Occupied(entry) = pending_resolves.entry(answer_name) { - if !entry.get().iter().any(|query| query.context == context) { + if let hash_map::Entry::Occupied(mut entry) = pending_resolves.entry(answer_name) { + if !entry.get().iter().any(|query| query.pending_query_contexts.contains(&context)) { // If we don't have any pending queries with the context included in the blinded // path (implying someone sent us this response not using the blinded path we gave // when making the query), return immediately to avoid the extra time for the proof @@ -493,64 +579,146 @@ impl OMNameResolver { // If there was at least one query with the same context, we go ahead and complete // all queries for the same name, as there's no point in waiting for another proof // for the same name. - return None; + return Err(Vec::new()); } - let parsed_rrs = parse_rr_stream(&proof); - let validated_rrs = - parsed_rrs.as_ref().and_then(|rrs| verify_rr_stream(rrs).map_err(|_| &())); - if let Ok(validated_rrs) = validated_rrs { - #[allow(unused_assignments, unused_mut)] - let mut time = self.latest_block_time.load(Ordering::Acquire) as u64; - #[cfg(feature = "std")] - { - use std::time::{SystemTime, UNIX_EPOCH}; - let now = SystemTime::now().duration_since(UNIX_EPOCH); - time = now.expect("Time must be > 1970").as_secs(); + let mut valid_resolution = false; + let res = parse_rr_stream(&proof).and_then(|rrs| { + if let Some(verified_rrs) = self.verify_dnssec_proof_for_rrs(entry.key(), &rrs) { + valid_resolution = true; + self.map_rrs_to_uri(verified_rrs).ok_or(()) + } else { + Err(()) } - if time != 0 { - // Block times may be up to two hours in the future and some time into the past - // (we assume no more than two hours, though the actual limits are rather - // complicated). - // Thus, we have to let the proof times be rather fuzzy. - let max_time_offset = if cfg!(feature = "std") { 0 } else { 60 * 2 }; - if validated_rrs.valid_from > time + max_time_offset { - return None; - } - if validated_rrs.expires < time - max_time_offset { - return None; + }); + if valid_resolution { + let requests = + entry.remove_entry().1.into_iter().map(|r| (r.name, r.payment_id)).collect(); + match res { + Ok(txt) => Ok((requests, txt)), + Err(()) => Err(requests), + } + } else { + // Note that because each context has a unique random nonce, at most one resolution + // can run out of pending queries here. Still, the API returns a Vec as we may join + // multiple queries for the same name in the future. + let mut failed_resolutions = Vec::new(); + entry.get_mut().retain_mut(|query| { + query.pending_query_contexts.retain(|c| *c != context); + if query.pending_query_contexts.is_empty() { + failed_resolutions.push((query.name, query.payment_id)); + false + } else { + true } + }); + + if entry.get().is_empty() { + entry.remove_entry(); + } + Err(failed_resolutions) + } + } else { + Err(Vec::new()) + } + } + + fn verify_dnssec_proof_for_rrs<'a>( + &self, resolved_name: &Name, rrs: &'a [RR], + ) -> Option<Vec<&'a RR>> { + let validated_rrs = verify_rr_stream(rrs); + if let Ok(validated_rrs) = validated_rrs { + #[allow(unused_assignments, unused_mut)] + let mut time = self.latest_block_time.load(Ordering::Acquire) as u64; + #[cfg(all(feature = "std", not(fuzzing)))] + { + use std::time::{SystemTime, UNIX_EPOCH}; + let now = SystemTime::now().duration_since(UNIX_EPOCH); + time = now.expect("Time must be > 1970").as_secs(); + } + if time != 0 { + // Block times may be up to two hours in the future and some time into the past + // (we assume no more than two hours, though the actual limits are rather + // complicated). + // Thus, we have to let the proof times be rather fuzzy. + let max_time_offset = + if cfg!(all(feature = "std", not(fuzzing))) { 0 } else { 60 * 60 * 2 }; + if validated_rrs.valid_from > time + max_time_offset { + return None; } - let resolved_rrs = validated_rrs.resolve_name(&entry.key()); - if resolved_rrs.is_empty() { + if validated_rrs.expires < time - max_time_offset { return None; } + } + let resolved_rrs = validated_rrs.resolve_name(resolved_name); + if resolved_rrs.is_empty() { + return None; + } + + Some(resolved_rrs) + } else { + None + } + } - let (_, requests) = entry.remove_entry(); - - const URI_PREFIX: &str = "bitcoin:"; - let mut candidate_records = resolved_rrs - .iter() - .filter_map( - |rr| if let RR::Txt(txt) = rr { Some(txt.data.as_vec()) } else { None }, - ) - .filter_map(|data| String::from_utf8(data).ok()) - .filter(|data_string| data_string.len() > URI_PREFIX.len()) - .filter(|data_string| { - data_string[..URI_PREFIX.len()].eq_ignore_ascii_case(URI_PREFIX) - }); - // Check that there is exactly one TXT record that begins with - // bitcoin: as required by BIP 353 (and is valid UTF-8). - match (candidate_records.next(), candidate_records.next()) { - (Some(txt), None) => { - let completed_requests = - requests.into_iter().map(|r| (r.name, r.payment_id)).collect(); - return Some((completed_requests, txt)); - }, - _ => {}, + fn map_rrs_to_uri(&self, resolved_rrs: Vec<&RR>) -> Option<String> { + const URI_PREFIX: &str = "bitcoin:"; + let mut candidate_records = resolved_rrs + .iter() + .filter_map(|rr| if let RR::Txt(txt) = rr { Some(txt.data.as_vec()) } else { None }) + .filter_map(|data| String::from_utf8(data).ok()) + .filter(|data_string| data_string.len() > URI_PREFIX.len()) + .filter(|data_string| { + let pfx = &data_string.as_bytes()[..URI_PREFIX.len()]; + pfx.eq_ignore_ascii_case(URI_PREFIX.as_bytes()) + }); + // Check that there is exactly one TXT record that begins with + // bitcoin: as required by BIP 353 (and is valid UTF-8). + match (candidate_records.next(), candidate_records.next()) { + (Some(txt), None) => Some(txt), + _ => None, + } + } + + /// Handles a [`DNSSECError`] message, indicating that one of the resolvers we sent a + /// [`DNSSECQuery`] to was unable to provide a [`DNSSECProof`] for the requested name. + /// + /// A resolution will be considered failed once we have received a [`DNSSECError`] for all the + /// queries we made for it, as a [`DNSSECProof`] may still arrive from one of the other + /// resolvers we queried. When a resolution does fail, its [`HumanReadableName`] and + /// [`PaymentId`] (as passed to [`Self::initiate_resolution`]) are included in the returned + /// list. + /// + /// As with [`Self::handle_dnssec_proof_for_uri`], the [`DNSResolverContext`] is checked against + /// the contexts of any pending resolutions for the name to ensure the error was received over a + /// blinded path we created when making the relevant [`DNSSECQuery`]. + pub fn handle_dnssec_error( + &self, msg: DNSSECError, context: DNSResolverContext, + ) -> Vec<(HumanReadableName, PaymentId)> { + let DNSSECError { name, .. } = msg; + let mut failed_resolutions = Vec::new(); + let mut pending_resolves = self.pending_resolves.lock().unwrap(); + if let hash_map::Entry::Occupied(mut entry) = pending_resolves.entry(name) { + entry.get_mut().retain_mut(|resolution| { + // Drop the context matching the blinded path this error was received over, if + // any. If no contexts match (including because a previous error already removed + // this context), the error does not pertain to this resolution and it is left + // untouched. + // Note that because each context has a unique random nonce, at most one resolution + // can run out of pending queries here. Still, the API returns a Vec as we may join + // multiple queries for the same name in the future. + resolution.pending_query_contexts.retain(|c| *c != context); + if resolution.pending_query_contexts.is_empty() { + failed_resolutions.push((resolution.name, resolution.payment_id)); + false + } else { + true } + }); + if entry.get().is_empty() { + entry.remove(); } } - None + failed_resolutions } } @@ -558,6 +726,37 @@ impl OMNameResolver { mod tests { use super::*; + #[cfg(feature = "dnssec")] + use crate::util::test_utils::pubkey; + + #[cfg(feature = "dnssec")] + fn dest(b: u8) -> Destination { + Destination::Node(pubkey(b)) + } + + /// Extracts the DNS [`Name`] and the per-query [`DNSResolverContext`]s from the messages + /// returned by [`OMNameResolver::initiate_resolution`]. + #[cfg(feature = "dnssec")] + fn dns_name_and_contexts( + messages: &[(DNSResolverMessage, MessageSendInstructions)], + ) -> (Name, Vec<DNSResolverContext>) { + let name = match &messages[0] { + (DNSResolverMessage::DNSSECQuery(DNSSECQuery(name)), _) => name.clone(), + _ => panic!("Unexpected initiate_resolution output"), + }; + let contexts = messages + .iter() + .map(|(_, instructions)| match instructions { + MessageSendInstructions::WithReplyPath { + context: MessageContext::DNSResolver(context), + .. + } => context.clone(), + _ => panic!("Unexpected initiate_resolution output"), + }) + .collect(); + (name, contexts) + } + #[test] fn test_hrn_display_format() { let user = "user"; @@ -596,6 +795,21 @@ mod tests { assert!(HumanReadableName::new("user", &huge_domain).is_err()); } + #[test] + fn test_dnssec_error_roundtrip() { + let name = Name::try_from("test.user._bitcoin-payment.example.com.".to_owned()).unwrap(); + for definitely_unresolvable in [false, true] { + let msg = DNSResolverMessage::DNSSECError(DNSSECError { + name: name.clone(), + definitely_unresolvable, + }); + let mut buf = Vec::new(); + msg.write(&mut buf).unwrap(); + let read = DNSResolverMessage::read(&mut &buf[..], DNSSEC_ERROR_TYPE).unwrap(); + assert_eq!(msg, read); + } + } + #[test] #[cfg(feature = "dnssec")] fn test_expiry() { @@ -604,22 +818,30 @@ mod tests { let name = HumanReadableName::new("user", "example.com").unwrap(); // Queue up a resolution - resolver.resolve_name(PaymentId([0; 32]), name.clone(), &keys).unwrap(); + resolver + .initiate_resolution(PaymentId([0; 32]), name.clone(), vec![dest(42)], &keys) + .unwrap(); assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); // and check that it expires after two blocks resolver.new_best_block(44, 42); assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 0); // Queue up another resolution - resolver.resolve_name(PaymentId([1; 32]), name.clone(), &keys).unwrap(); + resolver + .initiate_resolution(PaymentId([1; 32]), name.clone(), vec![dest(42)], &keys) + .unwrap(); assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); // it won't expire after one block resolver.new_best_block(45, 42); assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); assert_eq!(resolver.pending_resolves.lock().unwrap().iter().next().unwrap().1.len(), 1); // and queue up a second and third resolution of the same name - resolver.resolve_name(PaymentId([2; 32]), name.clone(), &keys).unwrap(); - resolver.resolve_name(PaymentId([3; 32]), name.clone(), &keys).unwrap(); + resolver + .initiate_resolution(PaymentId([2; 32]), name.clone(), vec![dest(42)], &keys) + .unwrap(); + resolver + .initiate_resolution(PaymentId([3; 32]), name.clone(), vec![dest(42)], &keys) + .unwrap(); assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); assert_eq!(resolver.pending_resolves.lock().unwrap().iter().next().unwrap().1.len(), 3); // after another block the first will expire, but the second and third won't @@ -634,4 +856,85 @@ mod tests { resolver.new_best_block(47, 42); assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 0); } + + #[test] + #[cfg(feature = "dnssec")] + fn test_dnssec_error() { + let keys = crate::sign::KeysManager::new(&[33; 32], 0, 0, true); + let resolver = OMNameResolver::new(42, 42); + let name = HumanReadableName::new("user", "example.com").unwrap(); + + // Resolve a name, sending the query to two resolvers. Each query gets its own unique + // context in its reply path. + let messages = resolver + .initiate_resolution(PaymentId([0; 32]), name.clone(), vec![dest(1), dest(2)], &keys) + .unwrap(); + assert_eq!(messages.len(), 2); + let (dns_name, contexts) = dns_name_and_contexts(&messages); + assert_ne!(contexts[0], contexts[1]); + assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); + + // An error whose context doesn't match any pending query is ignored entirely, even if it + // claims the name is unresolvable. + let mut wrong_context = contexts[0].clone(); + wrong_context.nonce[0] ^= 0x01; + let wrong = DNSSECError { name: dns_name.clone(), definitely_unresolvable: true }; + assert!(resolver.handle_dnssec_error(wrong, wrong_context).is_empty()); + assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); + + // An error over the first query's reply path fails that query but not the resolution + // itself - even though `definitely_unresolvable` is set - as a proof may yet arrive from + // the other query. + let err = DNSSECError { name: dns_name.clone(), definitely_unresolvable: true }; + assert!(resolver.handle_dnssec_error(err, contexts[0].clone()).is_empty()); + assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); + + // A duplicate error over the same reply path is ignored - the first query's context was + // already dropped, so a single misbehaving resolver cannot fail the whole resolution. + let dup = DNSSECError { name: dns_name.clone(), definitely_unresolvable: true }; + assert!(resolver.handle_dnssec_error(dup, contexts[0].clone()).is_empty()); + assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 1); + + // An error over the second query's reply path fails the last outstanding query, so the + // resolution now fails - even though this error only indicates a transient failure. + let err = DNSSECError { name: dns_name, definitely_unresolvable: false }; + let failed = resolver.handle_dnssec_error(err, contexts[1].clone()); + assert_eq!(failed, vec![(name, PaymentId([0; 32]))]); + assert_eq!(resolver.pending_resolves.lock().unwrap().len(), 0); + } + + #[test] + #[cfg(feature = "dnssec")] + fn test_dnssec_error_only_fails_matching_resolution() { + // An error only counts against the resolution whose blinded path (context) it was received + // over; other resolutions for the same name (queued with a different `PaymentId`, and thus a + // different context) are left untouched. + let keys = crate::sign::KeysManager::new(&[33; 32], 0, 0, true); + let resolver = OMNameResolver::new(42, 42); + let name = HumanReadableName::new("user", "example.com").unwrap(); + + let messages = resolver + .initiate_resolution(PaymentId([0; 32]), name.clone(), vec![dest(1)], &keys) + .unwrap(); + let (dns_name, contexts_a) = dns_name_and_contexts(&messages); + resolver + .initiate_resolution(PaymentId([1; 32]), name.clone(), vec![dest(2)], &keys) + .unwrap(); + { + let pending_resolves = resolver.pending_resolves.lock().unwrap(); + let pending_queries_for_name = &pending_resolves.iter().next().unwrap().1; + assert_eq!(pending_queries_for_name.len(), 2); + } + + // A single error over payment 0's reply path fails only its (single-query) resolution. + let err = DNSSECError { name: dns_name, definitely_unresolvable: false }; + let failed = resolver.handle_dnssec_error(err, contexts_a[0].clone()); + assert_eq!(failed, vec![(name, PaymentId([0; 32]))]); + + // Payment 1's resolution is still pending. + let pending_resolves = resolver.pending_resolves.lock().unwrap(); + let pending_queries_for_name = &pending_resolves.iter().next().unwrap().1; + assert_eq!(pending_queries_for_name.len(), 1); + assert_eq!(pending_queries_for_name[0].payment_id, PaymentId([1; 32])); + } } diff --git a/lightning/src/onion_message/functional_tests.rs b/lightning/src/onion_message/functional_tests.rs index 75e2aaf3c5f..94536e068b4 100644 --- a/lightning/src/onion_message/functional_tests.rs +++ b/lightning/src/onion_message/functional_tests.rs @@ -14,7 +14,7 @@ use super::async_payments::{ ServeStaticInvoice, StaticInvoicePersisted, }; use super::dns_resolution::{ - DNSResolverMessage, DNSResolverMessageHandler, DNSSECProof, DNSSECQuery, + DNSResolverMessage, DNSResolverMessageHandler, DNSSECError, DNSSECProof, DNSSECQuery, }; use super::messenger::{ CustomOnionMessageHandler, DefaultMessageRouter, Destination, MessageSendInstructions, @@ -24,10 +24,10 @@ use super::offers::{OffersMessage, OffersMessageHandler}; use super::packet::{OnionMessageContents, Packet}; use crate::blinded_path::message::{ AsyncPaymentsContext, BlindedMessagePath, DNSResolverContext, MessageContext, - MessageForwardNode, OffersContext, MESSAGE_PADDING_ROUND_OFF, + MessageForwardNode, NextMessageHop, OffersContext, MESSAGE_PADDING_ROUND_OFF, }; use crate::blinded_path::utils::is_padded; -use crate::blinded_path::EmptyNodeIdLookUp; +use crate::blinded_path::NodeIdLookUp; use crate::events::{Event, EventsProvider}; use crate::ln::msgs::{self, BaseMessageHandler, DecodeError, OnionMessageHandler}; use crate::routing::gossip::{NetworkGraph, P2PGossipSync}; @@ -60,17 +60,40 @@ struct MessengerNode { Arc<TestKeysInterface>, Arc<TestNodeSigner>, Arc<TestLogger>, - Arc<EmptyNodeIdLookUp>, + Arc<TestNodeIdLookUp>, Arc<MessageRouter>, Arc<TestOffersMessageHandler>, Arc<TestAsyncPaymentsMessageHandler>, Arc<TestDNSResolverMessageHandler>, Arc<TestCustomMessageHandler>, >, + node_id_lookup: Arc<TestNodeIdLookUp>, custom_message_handler: Arc<TestCustomMessageHandler>, gossip_sync: Arc<P2PGossipSync<Arc<NetGraph>, Arc<TestChainSource>, Arc<TestLogger>>>, } +/// A [`NodeIdLookUp`] that resolves SCIDs to node ids from an insertable map (empty by default, +/// so it behaves like an empty lookup unless a mapping is added). +struct TestNodeIdLookUp { + scid_to_node_id: Mutex<HashMap<u64, PublicKey>>, +} + +impl TestNodeIdLookUp { + fn new() -> Self { + Self { scid_to_node_id: Mutex::new(new_hash_map()) } + } + + fn add_mapping(&self, scid: u64, node_id: PublicKey) { + self.scid_to_node_id.lock().unwrap().insert(scid, node_id); + } +} + +impl NodeIdLookUp for TestNodeIdLookUp { + fn next_node_id(&self, short_channel_id: u64) -> Option<PublicKey> { + self.scid_to_node_id.lock().unwrap().get(&short_channel_id).copied() + } +} + impl Drop for MessengerNode { fn drop(&mut self) { if std::thread::panicking() { @@ -132,6 +155,7 @@ impl DNSResolverMessageHandler for TestDNSResolverMessageHandler { None } fn handle_dnssec_proof(&self, _message: DNSSECProof, _context: DNSResolverContext) {} + fn handle_dnssec_error(&self, _message: DNSSECError, _context: DNSResolverContext) {} } #[derive(Clone, Debug, PartialEq)] @@ -275,10 +299,15 @@ fn create_nodes(num_messengers: u8) -> Vec<MessengerNode> { struct MessengerCfg { secret_override: Option<SecretKey>, intercept_offline_peer_oms: bool, + intercept_unknown_scid_oms: bool, } impl MessengerCfg { fn new() -> Self { - Self { secret_override: None, intercept_offline_peer_oms: false } + Self { + secret_override: None, + intercept_offline_peer_oms: false, + intercept_unknown_scid_oms: false, + } } fn with_node_secret(mut self, secret: SecretKey) -> Self { self.secret_override = Some(secret); @@ -288,6 +317,10 @@ impl MessengerCfg { self.intercept_offline_peer_oms = true; self } + fn with_unknown_scid_interception(mut self) -> Self { + self.intercept_unknown_scid_oms = true; + self + } } fn create_nodes_using_cfgs(cfgs: Vec<MessengerCfg>) -> Vec<MessengerNode> { @@ -304,31 +337,32 @@ fn create_nodes_using_cfgs(cfgs: Vec<MessengerCfg>) -> Vec<MessengerNode> { let entropy_source = Arc::new(TestKeysInterface::new(&seed, Network::Testnet)); let node_signer = Arc::new(TestNodeSigner::new(secret_key)); - let node_id_lookup = Arc::new(EmptyNodeIdLookUp {}); + let node_id_lookup = Arc::new(TestNodeIdLookUp::new()); let message_router = DefaultMessageRouter::new(Arc::clone(&network_graph), Arc::clone(&entropy_source)); let offers_message_handler = Arc::new(TestOffersMessageHandler {}); let async_payments_message_handler = Arc::new(TestAsyncPaymentsMessageHandler {}); let dns_resolver_message_handler = Arc::new(TestDNSResolverMessageHandler {}); let custom_message_handler = Arc::new(TestCustomMessageHandler::new()); - let messenger = if cfg.intercept_offline_peer_oms { + let messenger = if cfg.intercept_offline_peer_oms || cfg.intercept_unknown_scid_oms { OnionMessenger::new_with_offline_peer_interception( Arc::clone(&entropy_source), Arc::clone(&node_signer), logger, - node_id_lookup, + Arc::clone(&node_id_lookup), Arc::new(message_router), offers_message_handler, async_payments_message_handler, dns_resolver_message_handler, Arc::clone(&custom_message_handler), + cfg.intercept_unknown_scid_oms, ) } else { OnionMessenger::new( Arc::clone(&entropy_source), Arc::clone(&node_signer), logger, - node_id_lookup, + Arc::clone(&node_id_lookup), Arc::new(message_router), offers_message_handler, async_payments_message_handler, @@ -341,6 +375,7 @@ fn create_nodes_using_cfgs(cfgs: Vec<MessengerCfg>) -> Vec<MessengerNode> { node_id: node_signer.get_node_id(Recipient::Node).unwrap(), entropy_source, messenger, + node_id_lookup, custom_message_handler, gossip_sync: Arc::clone(&gossip_sync), }); @@ -1144,9 +1179,14 @@ fn intercept_offline_peer_oms() { let mut events = release_events(&nodes[1]); assert_eq!(events.len(), 1); let onion_message = match events.remove(0) { - Event::OnionMessageIntercepted { peer_node_id, message } => { - assert_eq!(peer_node_id, final_node_vec[0].node_id); - message + Event::OnionMessageIntercepted { prev_hop, next_hop, message } => { + assert_eq!(prev_hop, Some(nodes[0].node_id)); + if let NextMessageHop::NodeId(peer_node_id) = next_hop { + assert_eq!(peer_node_id, final_node_vec[0].node_id); + message + } else { + panic!(); + } }, _ => panic!(), }; @@ -1173,6 +1213,130 @@ fn intercept_offline_peer_oms() { pass_along_path(&vec![nodes.remove(1), final_node_vec.remove(0)]); } +#[test] +fn intercept_unknown_scid_oms() { + // Ensure that if OnionMessenger is initialized with + // new_with_offline_peer_interception and `intercept_for_unknown_scids` set, we will + // intercept OMs that use an unknown SCID as the next hop, generate the right events, and + // forward OMs when they are re-injected by the user. + let node_cfgs = vec![ + MessengerCfg::new(), + MessengerCfg::new().with_unknown_scid_interception(), + MessengerCfg::new(), + ]; + let mut nodes = create_nodes_using_cfgs(node_cfgs); + + let peer_conn_evs = release_events(&nodes[1]); + assert_eq!(peer_conn_evs.len(), 2); + for (i, ev) in peer_conn_evs.iter().enumerate() { + match ev { + Event::OnionMessagePeerConnected { peer_node_id } => { + let node_idx = if i == 0 { 0 } else { 2 }; + assert_eq!(peer_node_id, &nodes[node_idx].node_id); + }, + _ => panic!(), + } + } + + // Use a SCID-based intermediate hop to trigger the unknown SCID interception path. Since no + // mapping was added to the `TestNodeIdLookUp`, the SCID cannot be resolved, so the + // OnionMessenger will generate an `OnionMessageIntercepted` event with a `ShortChannelId` + // next hop. + let scid = 42; + let message = TestCustomMessage::Pong; + let intermediate_nodes = + [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: Some(scid) }]; + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + nodes[2].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[2].entropy_source, + &Secp256k1::new(), + ); + let destination = Destination::BlindedPath(blinded_path); + let instructions = MessageSendInstructions::WithoutReplyPath { destination }; + + nodes[0].messenger.send_onion_message(message, instructions).unwrap(); + let mut final_node_vec = nodes.split_off(2); + pass_along_path(&nodes); + + // We expect an `OnionMessageIntercepted` event with a `ShortChannelId` next hop since the + // SCID is not resolvable (no mapping was added to the `TestNodeIdLookUp`). + let mut events = release_events(&nodes[1]); + assert_eq!(events.len(), 1); + let onion_message = match events.remove(0) { + Event::OnionMessageIntercepted { prev_hop, next_hop, message } => { + assert_eq!(prev_hop, Some(nodes[0].node_id)); + if let NextMessageHop::ShortChannelId(intercepted_scid) = next_hop { + assert_eq!(intercepted_scid, scid); + message + } else { + panic!("Expected ShortChannelId next hop, got NodeId"); + } + }, + _ => panic!(), + }; + + // The user resolves the SCID externally and forwards the intercepted message to the + // correct peer. + nodes[1].messenger.forward_onion_message(onion_message, &final_node_vec[0].node_id).unwrap(); + final_node_vec[0].custom_message_handler.expect_message(TestCustomMessage::Pong); + pass_along_path(&vec![nodes.remove(1), final_node_vec.remove(0)]); +} + +#[test] +fn intercept_resolved_scid_offline_peer_oms() { + // Ensure that when a forwarded OM's next hop is a SCID that resolves to a known but offline + // peer, the offline-peer interception path reports the resolved node id rather than the SCID, + // even though `intercept_for_unknown_scids` is disabled. + let node_cfgs = vec![ + MessengerCfg::new(), + MessengerCfg::new().with_offline_peer_interception(), + MessengerCfg::new(), + ]; + let mut nodes = create_nodes_using_cfgs(node_cfgs); + + // Clear the initial `OnionMessagePeerConnected` events. + let _ = release_events(&nodes[1]); + + // Resolve the SCID to nodes[2] and disconnect it so it appears as a known-but-offline peer. + let scid = 42; + nodes[1].node_id_lookup.add_mapping(scid, nodes[2].node_id); + disconnect_peers(&nodes[1], &nodes[2]); + + let message = TestCustomMessage::Pong; + let intermediate_nodes = + [MessageForwardNode { node_id: nodes[1].node_id, short_channel_id: Some(scid) }]; + let blinded_path = BlindedMessagePath::new( + &intermediate_nodes, + nodes[2].node_id, + nodes[2].messenger.node_signer.get_receive_auth_key(), + MessageContext::Custom(Vec::new()), + false, + &*nodes[2].entropy_source, + &Secp256k1::new(), + ); + let destination = Destination::BlindedPath(blinded_path); + let instructions = MessageSendInstructions::WithoutReplyPath { destination }; + + nodes[0].messenger.send_onion_message(message, instructions).unwrap(); + let final_node_vec = nodes.split_off(2); + pass_along_path(&nodes); + + // The next hop resolved to a known (but offline) peer, so the event must carry its node id + // rather than the SCID variant (which `intercept_for_unknown_scids` would have produced). + let mut events = release_events(&nodes[1]); + assert_eq!(events.len(), 1); + match events.remove(0) { + Event::OnionMessageIntercepted { next_hop, .. } => { + assert_eq!(next_hop, NextMessageHop::NodeId(final_node_vec[0].node_id)); + }, + _ => panic!(), + } +} + #[test] fn spec_test_vector() { let node_cfgs = [ diff --git a/lightning/src/onion_message/messenger.rs b/lightning/src/onion_message/messenger.rs index e688c020ac6..a434e5739e2 100644 --- a/lightning/src/onion_message/messenger.rs +++ b/lightning/src/onion_message/messenger.rs @@ -273,6 +273,7 @@ pub struct OnionMessenger< dns_resolver_handler: DRH, custom_handler: CMH, intercept_messages_for_offline_peers: bool, + intercept_for_unknown_scids: bool, pending_intercepted_msgs_events: Mutex<Vec<Event>>, pending_peer_connected_events: Mutex<Vec<Event>>, pending_events_processor: AtomicBool, @@ -358,7 +359,7 @@ pub struct Responder { reply_path: BlindedMessagePath, } -impl_writeable_tlv_based!(Responder, { +impl_ser_tlv_based!(Responder, { (0, reply_path, required), }); @@ -469,6 +470,11 @@ pub trait MessageRouter { /// Creates [`BlindedMessagePath`]s to the `recipient` node. The nodes in `peers` are assumed to /// be direct peers with the `recipient`. + /// + /// While payments will fail if most of `context` is modified, modifying + /// [`OffersContext::InvoiceRequest::payment_metadata`] prior to blinded path construction is + /// allowed. + /// fn create_blinded_paths<T: secp256k1::Signing + secp256k1::Verification>( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, context: MessageContext, peers: Vec<MessageForwardNode>, secp_ctx: &Secp256k1<T>, @@ -558,10 +564,6 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger, ES: EntropySource> // Limit the number of blinded paths that are computed. const MAX_PATHS: usize = 3; - // Ensure peers have at least three channels so that it is more difficult to infer the - // recipient's node_id. - const MIN_PEER_CHANNELS: usize = 3; - let network_graph = network_graph.deref().read_only(); let is_recipient_announced = network_graph.nodes().contains_key(&NodeId::from_pubkey(&recipient)); @@ -591,32 +593,6 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger, ES: EntropySource> let compact_paths = !never_compact_path && size_constrained; - let has_one_peer = peers.len() == 1; - let mut peer_info = peers - .map(|peer| MessageForwardNode { - short_channel_id: if compact_paths { peer.short_channel_id } else { None }, - ..peer - }) - // Limit to peers with announced channels unless the recipient is unannounced. - .filter_map(|peer| { - network_graph - .node(&NodeId::from_pubkey(&peer.node_id)) - .filter(|info| { - !is_recipient_announced || info.channels.len() >= MIN_PEER_CHANNELS - }) - .map(|info| (peer, info.is_tor_only(), info.channels.len())) - // Allow messages directly with the only peer when unannounced. - .or_else(|| (!is_recipient_announced && has_one_peer).then(|| (peer, false, 0))) - }) - // Exclude Tor-only nodes when the recipient is announced. - .filter(|(_, is_tor_only, _)| !(*is_tor_only && is_recipient_announced)) - .collect::<Vec<_>>(); - - // Prefer using non-Tor nodes with the most channels as the introduction node. - peer_info.sort_unstable_by(|(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| { - a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse()) - }); - let build_path = |intermediate_hops: &[MessageForwardNode]| { // Calculate the dummy hops given the total hop count target (including the recipient). let dummy_hops_count = path_len_incl_dummys.saturating_sub(intermediate_hops.len() + 1); @@ -633,12 +609,39 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger, ES: EntropySource> ) }; - // Try to create paths from peer info, fall back to direct path if needed - let mut paths = peer_info - .into_iter() - .map(|(peer, _, _)| build_path(&[peer])) - .take(MAX_PATHS) - .collect::<Vec<_>>(); + let has_one_peer = peers.len() == 1; + let mut paths = if !is_recipient_announced { + let mut peer_info = peers + .map(|peer| MessageForwardNode { + short_channel_id: if compact_paths { peer.short_channel_id } else { None }, + ..peer + }) + .filter_map(|peer| { + network_graph + .node(&NodeId::from_pubkey(&peer.node_id)) + .map(|info| (peer, info.is_tor_only(), info.channels.len())) + // Allow messages directly with the only peer + .or_else(|| has_one_peer.then(|| (peer, false, 0))) + }) + .collect::<Vec<_>>(); + + // Prefer using non-Tor nodes with the most channels as the introduction node. + peer_info.sort_unstable_by( + |(_, a_tor_only, a_channels), (_, b_tor_only, b_channels)| { + a_tor_only.cmp(b_tor_only).then(a_channels.cmp(b_channels).reverse()) + }, + ); + + // Try to create paths from peer info, fall back to direct path if needed + peer_info + .into_iter() + .map(|(peer, _, _)| build_path(&[peer])) + .take(MAX_PATHS) + .collect::<Vec<_>>() + } else { + vec![] + }; + if paths.is_empty() { if is_recipient_announced { paths = vec![build_path(&[])]; @@ -1168,12 +1171,13 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand }, } }; - let receiving_context_auth_key = node_signer.get_receive_auth_key(); + let receive_auth_key = node_signer.get_receive_auth_key(); + let expanded_key = &node_signer.get_expanded_key(); let next_hop = onion_utils::decode_next_untagged_hop( onion_decode_ss, &msg.onion_routing_packet.hop_data[..], msg.onion_routing_packet.hmac, - (control_tlvs_ss, &custom_handler, receiving_context_auth_key, &logger), + (control_tlvs_ss, &custom_handler, receive_auth_key, expanded_key, &logger), ); // Constructs the next onion message using packet data and blinding logic. @@ -1219,21 +1223,24 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand message, control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context }), reply_path, - control_tlvs_authenticated, + control_tlvs_from_local_node, + control_tlvs_from_phantom_participant: _, }, None, )) => match (message, context) { (ParsedOnionMessageContents::Offers(msg), Some(MessageContext::Offers(ctx))) => { match ctx { OffersContext::InvoiceRequest { .. } => { - // Note: We introduced the `control_tlvs_authenticated` check in LDK v0.2 + // Note: We introduced the `control_tlvs_from_*` check in LDK v0.2 // to simplify and standardize onion message authentication. // To continue supporting offers created before v0.2, we allow // unauthenticated control TLVs for these messages, as they can be // verified using the legacy method. }, _ => { - if !control_tlvs_authenticated { + // In any other offers context, we only allow message authenticated as + // coming from our local, node, not any other phantom participant. + if !control_tlvs_from_local_node { log_trace!(logger, "Received an unauthenticated offers onion message"); return Err(()); } @@ -1248,14 +1255,14 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand ParsedOnionMessageContents::AsyncPayments(msg), Some(MessageContext::AsyncPayments(ctx)), ) => { - if !control_tlvs_authenticated { + if !control_tlvs_from_local_node { log_trace!(logger, "Received an unauthenticated async payments onion message"); return Err(()); } Ok(PeeledOnion::AsyncPayments(msg, ctx, reply_path)) }, (ParsedOnionMessageContents::Custom(msg), Some(MessageContext::Custom(ctx))) => { - if !control_tlvs_authenticated { + if !control_tlvs_from_local_node { log_trace!(logger, "Received an unauthenticated custom onion message"); return Err(()); } @@ -1268,7 +1275,7 @@ pub fn peel_onion_message<NS: NodeSigner, L: Logger, CMH: CustomOnionMessageHand ParsedOnionMessageContents::DNSResolver(msg), Some(MessageContext::DNSResolver(ctx)), ) => { - if !control_tlvs_authenticated { + if !control_tlvs_from_local_node { log_trace!(logger, "Received an unauthenticated DNS resolver onion message"); return Err(()); } @@ -1387,6 +1394,7 @@ impl< dns_resolver, custom_handler, false, + false, ) } @@ -1394,11 +1402,18 @@ impl< /// intended to be forwarded to offline peers, we will intercept them for /// later forwarding. /// + /// If `intercept_for_unknown_scids` is set, we will additionally intercept onion messages whose + /// next hop is a [`NextMessageHop::ShortChannelId`] that cannot be resolved to a connected + /// peer, generating an [`Event::OnionMessageIntercepted`] with a + /// [`NextMessageHop::ShortChannelId`] next hop. This variant of the event was introduced in + /// LDK 0.3, so users who persist [`Event::OnionMessageIntercepted`] events and may need to + /// downgrade to LDK 0.2 must leave this disabled. + /// /// Interception flow: - /// 1. If an onion message for an offline peer is received, `OnionMessenger` will - /// generate an [`Event::OnionMessageIntercepted`]. Event handlers can - /// then choose to persist this onion message for later forwarding, or drop - /// it. + /// 1. If an onion message for an offline peer or (if `intercept_for_unknown_scids` is set) an + /// unknown SCID is received, `OnionMessenger` will generate an + /// [`Event::OnionMessageIntercepted`]. Event handlers can then choose to persist this + /// onion message for later forwarding, or drop it. /// 2. When the offline peer later comes back online, `OnionMessenger` will /// generate an [`Event::OnionMessagePeerConnected`]. Event handlers will /// then fetch all previously intercepted onion messages for this peer. @@ -1414,6 +1429,7 @@ impl< pub fn new_with_offline_peer_interception( entropy_source: ES, node_signer: NS, logger: L, node_id_lookup: NL, message_router: MR, offers_handler: OMH, async_payments_handler: APH, dns_resolver: DRH, custom_handler: CMH, + intercept_for_unknown_scids: bool, ) -> Self { Self::new_inner( entropy_source, @@ -1426,13 +1442,14 @@ impl< dns_resolver, custom_handler, true, + intercept_for_unknown_scids, ) } fn new_inner( entropy_source: ES, node_signer: NS, logger: L, node_id_lookup: NL, message_router: MR, offers_handler: OMH, async_payments_handler: APH, dns_resolver: DRH, custom_handler: CMH, - intercept_messages_for_offline_peers: bool, + intercept_messages_for_offline_peers: bool, intercept_for_unknown_scids: bool, ) -> Self { let mut secp_ctx = Secp256k1::new(); secp_ctx.seeded_randomize(&entropy_source.get_secure_random_bytes()); @@ -1449,6 +1466,7 @@ impl< dns_resolver_handler: dns_resolver, custom_handler, intercept_messages_for_offline_peers, + intercept_for_unknown_scids, pending_intercepted_msgs_events: Mutex::new(Vec::new()), pending_peer_connected_events: Mutex::new(Vec::new()), pending_events_processor: AtomicBool::new(false), @@ -1538,6 +1556,7 @@ impl< let result = if is_forward { self.enqueue_forwarded_onion_message( + None, NextMessageHop::NodeId(first_node_id), onion_message, log_suffix, @@ -1653,14 +1672,29 @@ impl< } fn enqueue_forwarded_onion_message( - &self, next_hop: NextMessageHop, onion_message: OnionMessage, log_suffix: fmt::Arguments, + &self, prev_hop: Option<PublicKey>, next_hop: NextMessageHop, onion_message: OnionMessage, + log_suffix: fmt::Arguments, ) -> Result<(), SendError> { let next_node_id = match next_hop { NextMessageHop::NodeId(pubkey) => pubkey, NextMessageHop::ShortChannelId(scid) => match self.node_id_lookup.next_node_id(scid) { Some(pubkey) => pubkey, None => { - log_trace!(self.logger, "Dropping forwarded onion messager: unable to resolve next hop using SCID {} {}", scid, log_suffix); + if self.intercept_for_unknown_scids { + log_trace!( + self.logger, + "Generating OnionMessageIntercepted event for SCID {} {}", + scid, + log_suffix + ); + self.enqueue_intercepted_event(Event::OnionMessageIntercepted { + prev_hop, + next_hop, + message: onion_message, + }); + return Ok(()); + } + log_trace!(self.logger, "Dropping forwarded onion message: unable to resolve next hop using SCID {} {}", scid, log_suffix); return Err(SendError::GetNodeIdFailed); }, }, @@ -1703,7 +1737,11 @@ impl< log_suffix ); self.enqueue_intercepted_event(Event::OnionMessageIntercepted { - peer_node_id: next_node_id, + prev_hop, + // Report the resolved node id rather than `next_hop`, which may be a + // `ShortChannelId` that we resolved to a known-but-offline peer. The + // `ShortChannelId` variant is reserved for the unknown-SCID interception path. + next_hop: NextMessageHop::NodeId(next_node_id), message: onion_message, }); Ok(()) @@ -2271,6 +2309,19 @@ impl< }; self.dns_resolver_handler.handle_dnssec_proof(msg, context); }, + DNSResolverMessage::DNSSECError(msg) => { + let context = match context { + Some(ctx) => ctx, + None => { + log_trace!( + logger, + "Ignoring DNSSECError onion message due to missing context" + ); + return; + }, + }; + self.dns_resolver_handler.handle_dnssec_error(msg, context); + }, } }, Ok(PeeledOnion::Custom(message, context, reply_path)) => { @@ -2284,6 +2335,7 @@ impl< }, Ok(PeeledOnion::Forward(next_hop, onion_message)) => { let _ = self.enqueue_forwarded_onion_message( + Some(peer_node_id), next_hop, onion_message, format_args!("when forwarding peeled onion message from {}", peer_node_id), @@ -2336,28 +2388,6 @@ impl< /// [`SimpleArcChannelManager`]: crate::ln::channelmanager::SimpleArcChannelManager /// [`SimpleArcPeerManager`]: crate::ln::peer_handler::SimpleArcPeerManager #[cfg(not(c_bindings))] -#[cfg(feature = "dnssec")] -pub type SimpleArcOnionMessenger<M, T, F, L> = OnionMessenger< - Arc<KeysManager>, - Arc<KeysManager>, - Arc<L>, - Arc<SimpleArcChannelManager<M, T, F, L>>, - Arc<DefaultMessageRouter<Arc<NetworkGraph<Arc<L>>>, Arc<L>, Arc<KeysManager>>>, - Arc<SimpleArcChannelManager<M, T, F, L>>, - Arc<SimpleArcChannelManager<M, T, F, L>>, - Arc<SimpleArcChannelManager<M, T, F, L>>, - IgnoringMessageHandler, ->; - -/// Useful for simplifying the parameters of [`SimpleArcChannelManager`] and -/// [`SimpleArcPeerManager`]. See their docs for more details. -/// -/// This is not exported to bindings users as type aliases aren't supported in most languages. -/// -/// [`SimpleArcChannelManager`]: crate::ln::channelmanager::SimpleArcChannelManager -/// [`SimpleArcPeerManager`]: crate::ln::peer_handler::SimpleArcPeerManager -#[cfg(not(c_bindings))] -#[cfg(not(feature = "dnssec"))] pub type SimpleArcOnionMessenger<M, T, F, L> = OnionMessenger< Arc<KeysManager>, Arc<KeysManager>, @@ -2378,29 +2408,6 @@ pub type SimpleArcOnionMessenger<M, T, F, L> = OnionMessenger< /// [`SimpleRefChannelManager`]: crate::ln::channelmanager::SimpleRefChannelManager /// [`SimpleRefPeerManager`]: crate::ln::peer_handler::SimpleRefPeerManager #[cfg(not(c_bindings))] -#[cfg(feature = "dnssec")] -pub type SimpleRefOnionMessenger<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, M, T, F, L> = - OnionMessenger< - &'a KeysManager, - &'a KeysManager, - &'b L, - &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, - &'i DefaultMessageRouter<&'g NetworkGraph<&'b L>, &'b L, &'a KeysManager>, - &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, - &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, - &'j SimpleRefChannelManager<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, M, T, F, L>, - IgnoringMessageHandler, - >; - -/// Useful for simplifying the parameters of [`SimpleRefChannelManager`] and -/// [`SimpleRefPeerManager`]. See their docs for more details. -/// -/// This is not exported to bindings users as type aliases aren't supported in most languages. -/// -/// [`SimpleRefChannelManager`]: crate::ln::channelmanager::SimpleRefChannelManager -/// [`SimpleRefPeerManager`]: crate::ln::peer_handler::SimpleRefPeerManager -#[cfg(not(c_bindings))] -#[cfg(not(feature = "dnssec"))] pub type SimpleRefOnionMessenger<'a, 'b, 'c, 'd, 'e, 'f, 'g, 'h, 'i, 'j, M, T, F, L> = OnionMessenger< &'a KeysManager, @@ -2504,7 +2511,8 @@ fn packet_payloads_and_keys< control_tlvs, reply_path: reply_path.take(), message, - control_tlvs_authenticated: false, + control_tlvs_from_local_node: false, + control_tlvs_from_phantom_participant: false, }, prev_control_tlvs_ss.unwrap(), )); @@ -2514,7 +2522,8 @@ fn packet_payloads_and_keys< control_tlvs: ReceiveControlTlvs::Unblinded(ReceiveTlvs { context: None }), reply_path: reply_path.take(), message, - control_tlvs_authenticated: false, + control_tlvs_from_local_node: false, + control_tlvs_from_phantom_participant: false, }, prev_control_tlvs_ss.unwrap(), )); diff --git a/lightning/src/onion_message/packet.rs b/lightning/src/onion_message/packet.rs index 2e0ccaf3a3e..cd9a923b070 100644 --- a/lightning/src/onion_message/packet.rs +++ b/lightning/src/onion_message/packet.rs @@ -19,7 +19,8 @@ use super::offers::OffersMessage; use crate::blinded_path::message::{ BlindedMessagePath, DummyTlv, ForwardTlvs, NextMessageHop, ReceiveTlvs, }; -use crate::crypto::streams::{ChaChaDualPolyReadAdapter, ChaChaPolyWriteAdapter}; +use crate::crypto::streams::{ChaChaPolyWriteAdapter, ChaChaTriPolyReadAdapter, TriPolyAADUsed}; +use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::DecodeError; use crate::ln::onion_utils; use crate::sign::ReceiveAuthKey; @@ -121,9 +122,16 @@ pub(super) enum Payload<T: OnionMessageContents> { }, /// This payload is for the final hop. Receive { - /// The [`ReceiveControlTlvs`] were authenticated with the additional key which was + /// The [`ReceiveControlTlvs`] were authenticated with the [`ReceiveAuthKey`] which was /// provided to [`ReadableArgs::read`]. - control_tlvs_authenticated: bool, + control_tlvs_from_local_node: bool, + /// The [`ReceiveControlTlvs`] were authenticated with the + /// [`ExpandedKey::phantom_node_blinded_path_key`] which was provided to + /// [`ReadableArgs::read`]. + /// Note that this is currently never actually read, but exists to signal the type of + /// authentication we can do. + #[allow(dead_code)] + control_tlvs_from_phantom_participant: bool, control_tlvs: ReceiveControlTlvs, reply_path: Option<BlindedMessagePath>, message: T, @@ -233,7 +241,8 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) { control_tlvs: ReceiveControlTlvs::Blinded(encrypted_bytes), reply_path, message, - control_tlvs_authenticated: _, + control_tlvs_from_local_node: _, + control_tlvs_from_phantom_participant: _, } => { _encode_varint_length_prefixed_tlv!(w, { (2, reply_path, option), @@ -253,7 +262,8 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) { control_tlvs: ReceiveControlTlvs::Unblinded(control_tlvs), reply_path, message, - control_tlvs_authenticated: _, + control_tlvs_from_local_node: _, + control_tlvs_from_phantom_participant: _, } => { let write_adapter = ChaChaPolyWriteAdapter::new(self.1, &control_tlvs); _encode_varint_length_prefixed_tlv!(w, { @@ -269,24 +279,27 @@ impl<T: OnionMessageContents> Writeable for (Payload<T>, [u8; 32]) { // Uses the provided secret to simultaneously decode and decrypt the control TLVs and data TLV. impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> - ReadableArgs<(SharedSecret, &H, ReceiveAuthKey, &L)> + ReadableArgs<(SharedSecret, &H, ReceiveAuthKey, &ExpandedKey, &L)> for Payload<ParsedOnionMessageContents<<H as CustomOnionMessageHandler>::CustomMessage>> { fn read<R: Read>( - r: &mut R, args: (SharedSecret, &H, ReceiveAuthKey, &L), + r: &mut R, args: (SharedSecret, &H, ReceiveAuthKey, &ExpandedKey, &L), ) -> Result<Self, DecodeError> { - let (encrypted_tlvs_ss, handler, receive_tlvs_key, logger) = args; + let (encrypted_tlvs_ss, handler, receive_tlvs_key, expanded_key, logger) = args; let v: BigSize = Readable::read(r)?; let mut rd = FixedLengthReader::new(r, v.0); let mut reply_path: Option<BlindedMessagePath> = None; - let mut read_adapter: Option<ChaChaDualPolyReadAdapter<ControlTlvs>> = None; + let mut read_adapter: Option<ChaChaTriPolyReadAdapter<ControlTlvs>> = None; let rho = onion_utils::gen_rho_from_shared_secret(&encrypted_tlvs_ss.secret_bytes()); + let read_adapter_args = + (rho, receive_tlvs_key.0, expanded_key.phantom_node_blinded_path_key); let mut message_type: Option<u64> = None; let mut message = None; + decode_tlv_stream_with_custom_tlv_decode!(&mut rd, { (2, reply_path, option), - (4, read_adapter, (option: LengthReadableArgs, (rho, receive_tlvs_key.0))), + (4, read_adapter, (option: LengthReadableArgs, read_adapter_args)), }, |msg_type, msg_reader| { if msg_type < 64 { return Ok(false) } // Don't allow reading more than one data TLV from an onion message. @@ -322,21 +335,22 @@ impl<H: CustomOnionMessageHandler + ?Sized, L: Logger + ?Sized> match read_adapter { None => return Err(DecodeError::InvalidValue), - Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => { - if used_aad || message_type.is_some() { + Some(ChaChaTriPolyReadAdapter { readable: ControlTlvs::Forward(tlvs), used_aad }) => { + if used_aad != TriPolyAADUsed::None || message_type.is_some() { return Err(DecodeError::InvalidValue); } Ok(Payload::Forward(ForwardControlTlvs::Unblinded(tlvs))) }, - Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Dummy, used_aad }) => { - Ok(Payload::Dummy { control_tlvs_authenticated: used_aad }) + Some(ChaChaTriPolyReadAdapter { readable: ControlTlvs::Dummy, used_aad }) => { + Ok(Payload::Dummy { control_tlvs_authenticated: used_aad != TriPolyAADUsed::None }) }, - Some(ChaChaDualPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => { + Some(ChaChaTriPolyReadAdapter { readable: ControlTlvs::Receive(tlvs), used_aad }) => { Ok(Payload::Receive { control_tlvs: ReceiveControlTlvs::Unblinded(tlvs), reply_path, message: message.ok_or(DecodeError::InvalidValue)?, - control_tlvs_authenticated: used_aad, + control_tlvs_from_local_node: used_aad == TriPolyAADUsed::First, + control_tlvs_from_phantom_participant: used_aad == TriPolyAADUsed::Second, }) }, } diff --git a/lightning/src/routing/gossip.rs b/lightning/src/routing/gossip.rs index 3794c381817..6eb583e57f6 100644 --- a/lightning/src/routing/gossip.rs +++ b/lightning/src/routing/gossip.rs @@ -57,7 +57,7 @@ use core::{cmp, fmt}; pub use lightning_types::routing::RoutingFees; -#[cfg(feature = "std")] +#[cfg(all(feature = "std", not(fuzzing)))] use std::time::{SystemTime, UNIX_EPOCH}; /// We remove stale channel directional info two weeks after the last update, per BOLT 7's @@ -843,7 +843,7 @@ impl<G: Deref<Target = NetworkGraph<L>>, U: UtxoLookup, L: Logger> BaseMessageHa let mut gossip_start_time = 0; #[allow(unused)] let should_sync = self.should_request_full_sync(); - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] { gossip_start_time = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -1350,7 +1350,7 @@ impl EffectiveCapacity { } } -impl_writeable_tlv_based!(RoutingFees, { +impl_ser_tlv_based!(RoutingFees, { (0, base_msat, required), (2, proportional_millionths, required) }); @@ -1765,12 +1765,13 @@ impl<L: Logger> PartialEq for NetworkGraph<L> { /// /// We over-allocate by a bit because ~15% more is better than the double we get if we're slightly /// too low. -const CHAN_COUNT_ESTIMATE: usize = 63_000; +pub const CHAN_COUNT_ESTIMATE: usize = 63_000; + /// In Jan, 2026 there were about 17K nodes /// /// We over-allocate by a bit because 15% more is better than the double we get if we're slightly /// too low. -const NODE_COUNT_ESTIMATE: usize = 20_000; +pub const NODE_COUNT_ESTIMATE: usize = 20_000; impl<L: Logger> NetworkGraph<L> { /// Creates a new, empty, network graph. @@ -2014,9 +2015,9 @@ impl<L: Logger> NetworkGraph<L> { &self, short_channel_id: u64, capacity_sats: Option<u64>, timestamp: u64, features: ChannelFeatures, node_id_1: NodeId, node_id_2: NodeId, ) -> Result<(), LightningError> { - if node_id_1 == node_id_2 { + if node_id_1 >= node_id_2 { return Err(LightningError { - err: "Channel announcement node had a channel with itself".to_owned(), + err: "node_ids in channel_announcements must be sorted".to_owned(), action: ErrorAction::IgnoreError, }); }; @@ -2123,6 +2124,13 @@ impl<L: Logger> NetworkGraph<L> { ) -> Result<(), LightningError> { let channels = self.channels.read().unwrap(); + if msg.node_id_1 >= msg.node_id_2 { + return Err(LightningError { + err: "node_ids in channel_announcements must be sorted".to_owned(), + action: ErrorAction::IgnoreError, + }); + } + if let Some(chan) = channels.get(&msg.short_channel_id) { if chan.capacity_sats.is_some() { // If we'd previously looked up the channel on-chain and checked the script @@ -2195,7 +2203,7 @@ impl<L: Logger> NetworkGraph<L> { #[allow(unused_mut, unused_assignments)] let mut announcement_received_time = 0; - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] { announcement_received_time = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -2235,11 +2243,11 @@ impl<L: Logger> NetworkGraph<L> { /// /// The channel and any node for which this was their last channel are removed from the graph. pub fn channel_failed_permanent(&self, short_channel_id: u64) { - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let current_time_unix = Some( SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs(), ); - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let current_time_unix = None; self.channel_failed_permanent_with_time(short_channel_id, current_time_unix) @@ -2262,11 +2270,11 @@ impl<L: Logger> NetworkGraph<L> { /// Marks a node in the graph as permanently failed, effectively removing it and its channels /// from local storage. pub fn node_failed_permanent(&self, node_id: &PublicKey) { - #[cfg(feature = "std")] + #[cfg(all(feature = "std", not(fuzzing)))] let current_time_unix = Some( SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs(), ); - #[cfg(not(feature = "std"))] + #[cfg(any(not(feature = "std"), fuzzing))] let current_time_unix = None; let node_id = NodeId::from_pubkey(node_id); @@ -2303,7 +2311,6 @@ impl<L: Logger> NetworkGraph<L> { } } - #[cfg(feature = "std")] /// Removes information about channels that we haven't heard any updates about in some time. /// This can be used regularly to prune the network graph of channels that likely no longer /// exist. @@ -2320,6 +2327,7 @@ impl<L: Logger> NetworkGraph<L> { /// /// This method is only available with the `std` feature. See /// [`NetworkGraph::remove_stale_channels_and_tracking_with_time`] for non-`std` use. + #[cfg(all(feature = "std", not(fuzzing)))] pub fn remove_stale_channels_and_tracking(&self) { let time = SystemTime::now().duration_since(UNIX_EPOCH).expect("Time must be > 1970").as_secs(); @@ -2403,10 +2411,10 @@ impl<L: Logger> NetworkGraph<L> { if let Some(time) = time { current_time_unix.saturating_sub(*time) < REMOVED_ENTRIES_TRACKING_AGE_LIMIT_SECS } else { - // NOTE: In the case of non-`std`, we won't have access to the current UNIX time at the time of removal, - // so we'll just set the removal time here to the current UNIX time on the very next invocation - // of this function. - #[cfg(not(feature = "std"))] + // NOTE: In the case of non-`std` or fuzzing, we won't have access to the current UNIX + // time at the time of removal, so we'll just set the removal time here to the current + // UNIX time on the very next invocation of this function. + #[cfg(any(not(feature = "std"), fuzzing))] { let mut tracked_time = Some(current_time_unix); core::mem::swap(time, &mut tracked_time); @@ -2476,7 +2484,7 @@ impl<L: Logger> NetworkGraph<L> { }); } - #[cfg(all(feature = "std", not(test), not(feature = "_test_utils")))] + #[cfg(all(feature = "std", not(test), not(feature = "_test_utils"), not(fuzzing)))] { // Note that many tests rely on being able to set arbitrarily old timestamps, thus we // disable this check during tests! @@ -2831,8 +2839,15 @@ pub(crate) mod tests { pub(crate) fn get_signed_channel_announcement<F: Fn(&mut UnsignedChannelAnnouncement)>( f: F, node_1_key: &SecretKey, node_2_key: &SecretKey, secp_ctx: &Secp256k1<secp256k1::All>, ) -> ChannelAnnouncement { - let node_id_1 = PublicKey::from_secret_key(&secp_ctx, node_1_key); - let node_id_2 = PublicKey::from_secret_key(&secp_ctx, node_2_key); + let mut node_id_1 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, node_1_key)); + let mut node_id_2 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, node_2_key)); + let mut signer_1 = node_1_key; + let mut signer_2 = node_2_key; + if node_id_1 > node_id_2 { + core::mem::swap(&mut node_id_1, &mut node_id_2); + core::mem::swap(&mut signer_1, &mut signer_2); + } + let node_1_btckey = &SecretKey::from_slice(&[40; 32]).unwrap(); let node_2_btckey = &SecretKey::from_slice(&[39; 32]).unwrap(); @@ -2840,8 +2855,8 @@ pub(crate) mod tests { features: channelmanager::provided_channel_features(&UserConfig::default()), chain_hash: ChainHash::using_genesis_block(Network::Testnet), short_channel_id: 0, - node_id_1: NodeId::from_pubkey(&node_id_1), - node_id_2: NodeId::from_pubkey(&node_id_2), + node_id_1, + node_id_2, bitcoin_key_1: NodeId::from_pubkey(&PublicKey::from_secret_key( &secp_ctx, node_1_btckey, @@ -2855,8 +2870,8 @@ pub(crate) mod tests { f(&mut unsigned_announcement); let msghash = hash_to_message!(&Sha256dHash::hash(&unsigned_announcement.encode()[..])[..]); ChannelAnnouncement { - node_signature_1: secp_ctx.sign_ecdsa(&msghash, node_1_key), - node_signature_2: secp_ctx.sign_ecdsa(&msghash, node_2_key), + node_signature_1: secp_ctx.sign_ecdsa(&msghash, signer_1), + node_signature_2: secp_ctx.sign_ecdsa(&msghash, signer_2), bitcoin_signature_1: secp_ctx.sign_ecdsa(&msghash, node_1_btckey), bitcoin_signature_2: secp_ctx.sign_ecdsa(&msghash, node_2_btckey), contents: unsigned_announcement, @@ -3126,7 +3141,7 @@ pub(crate) mod tests { .handle_channel_announcement(Some(node_1_pubkey), &channel_to_itself_announcement) { Ok(_) => panic!(), - Err(e) => assert_eq!(e.err, "Channel announcement node had a channel with itself"), + Err(e) => assert_eq!(e.err, "node_ids in channel_announcements must be sorted"), }; // Test that channel announcements with the wrong chain hash are ignored (network graph is testnet, diff --git a/lightning/src/routing/router.rs b/lightning/src/routing/router.rs index b27dee1a450..936d35ea471 100644 --- a/lightning/src/routing/router.rs +++ b/lightning/src/routing/router.rs @@ -10,6 +10,7 @@ //! The router finds paths within a [`NetworkGraph`] for a payment. use bitcoin::secp256k1::{self, PublicKey, Secp256k1}; +use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; use lightning_invoice::Bolt11Invoice; use crate::blinded_path::payment::{ @@ -17,7 +18,6 @@ use crate::blinded_path::payment::{ PaymentRelay, ReceiveTlvs, }; use crate::blinded_path::{BlindedHop, Direction, IntroductionNode}; -use crate::crypto::chacha20::ChaCha20; use crate::ln::channel_state::ChannelDetails; use crate::ln::channelmanager::{PaymentId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use crate::ln::msgs::{DecodeError, MAX_VALUE_MSAT}; @@ -283,6 +283,12 @@ pub trait Router { /// Creates [`BlindedPaymentPath`]s for payment to the `recipient` node. The channels in `first_hops` /// are assumed to be with the `recipient`'s peers. The payment secret and any constraints are /// given in `tlvs`. The `local_node_receive_key` is required to authenticate the blinded payment paths. + /// + /// While payments will fail if most of `tlvs` is modified, modifying + /// [`ReceiveTlvs::payment_context`]'s [`PaymentContext::payment_metadata`] fields prior to + /// blinded path construction is allowed. + /// + /// [`PaymentContext::payment_metadata`]: crate::blinded_path::payment::PaymentContext::payment_metadata fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs, amount_msats: Option<u64>, @@ -512,6 +518,7 @@ pub struct RouteHop { /// to reach this node. pub channel_features: ChannelFeatures, /// The fee taken on this hop (for paying for the use of the *next* channel in the path). + /// /// If this is the last hop in [`Path::hops`]: /// * if we're sending to a [`BlindedPaymentPath`], this is the fee paid for use of the entire /// blinded path (including any Trampoline hops) @@ -519,8 +526,12 @@ pub struct RouteHop { pub fee_msat: u64, /// The CLTV delta added for this hop. /// If this is the last hop in [`Path::hops`]: - /// * if we're sending to a [`BlindedPaymentPath`], this is the CLTV delta for the entire blinded - /// path (including any Trampoline hops) + /// * if we're sending to a [`BlindedPaymentPath`] *with* trampoline hops, this is the CLTV + /// delta for the entire blinded path including the trampoline hops, and is thus equal to the + /// sum of [`TrampolineHop::cltv_expiry_delta`] for all the [`BlindedTail::trampoline_hops`]. + /// * if we're sending to a [`BlindedPaymentPath`], *without* trampoline hops, this is the CLTV + /// delta for the entire blinded path (including + /// [`BlindedTail::excess_final_cltv_expiry_delta`]). /// * otherwise, this is the CLTV delta expected at the destination pub cltv_expiry_delta: u32, /// Indicates whether this hop is possibly announced in the public network graph. @@ -534,7 +545,7 @@ pub struct RouteHop { pub maybe_announced_channel: bool, } -impl_writeable_tlv_based!(RouteHop, { +impl_ser_tlv_based!(RouteHop, { (0, pubkey, required), (1, maybe_announced_channel, (default_value, true)), (2, node_features, required), @@ -557,12 +568,13 @@ pub struct TrampolineHop { /// the entire blinded path. pub fee_msat: u64, /// The CLTV delta added for this hop. + /// /// If this is the last Trampoline hop within [`BlindedTail`], this is the CLTV delta for the entire - /// blinded path. + /// blinded path (including the [`BlindedTail::excess_final_cltv_expiry_delta`]). pub cltv_expiry_delta: u32, } -impl_writeable_tlv_based!(TrampolineHop, { +impl_ser_tlv_based!(TrampolineHop, { (0, pubkey, required), (2, node_features, required), (4, fee_msat, required), @@ -592,7 +604,7 @@ pub struct BlindedTail { pub final_value_msat: u64, } -impl_writeable_tlv_based!(BlindedTail, { +impl_ser_tlv_based!(BlindedTail, { (0, hops, required_vec), (2, blinding_point, required), (4, excess_final_cltv_expiry_delta, required), @@ -633,7 +645,7 @@ impl Path { } } - /// Gets the final hop's CLTV expiry delta. + /// Gets the final hop's CLTV expiry delta, if there's a final non-blinded hop. #[rustfmt::skip] pub fn final_cltv_expiry_delta(&self) -> Option<u32> { match &self.blinded_tail { @@ -642,12 +654,23 @@ impl Path { } } + /// Gets the total CLTV expiry delta which will be added to the current block height (plus some + /// extra headroom) when sending the HTLC + pub fn total_cltv_expiry_delta(&self) -> u32 { + self.hops.iter().map(|hop| hop.cltv_expiry_delta).sum() + } + /// True if this [`Path`] has at least one Trampoline hop. pub fn has_trampoline_hops(&self) -> bool { self.blinded_tail.as_ref().is_some_and(|bt| !bt.trampoline_hops.is_empty()) } } +impl_ser_tlv_based!(Path,{ + (1, hops, required_vec), + (3, blinded_tail, option), +}); + /// A route directs a payment from the sender (us) to the recipient. If the recipient supports MPP, /// it can take multiple paths. Each path is composed of one or more hops through the network. #[derive(Clone, Debug, Hash, PartialEq, Eq)] @@ -659,9 +682,7 @@ pub struct Route { /// The `route_params` parameter passed to [`find_route`]. /// /// This is used by `ChannelManager` to track information which may be required for retries. - /// - /// Will be `None` for objects serialized with LDK versions prior to 0.0.117. - pub route_params: Option<RouteParameters>, + pub route_params: RouteParameters, } impl Route { @@ -674,8 +695,8 @@ impl Route { /// [`htlc_minimum_msat`]: https://github.com/lightning/bolts/blob/master/07-routing-gossip.md#the-channel_update-message #[rustfmt::skip] pub fn get_total_fees(&self) -> u64 { - let overpaid_value_msat = self.route_params.as_ref() - .map_or(0, |p| self.get_total_amount().saturating_sub(p.final_value_msat)); + let overpaid_value_msat = + self.get_total_amount().saturating_sub(self.route_params.final_value_msat); overpaid_value_msat + self.paths.iter().map(|path| path.fee_msat()).sum::<u64>() } @@ -688,6 +709,108 @@ impl Route { pub fn get_total_amount(&self) -> u64 { self.paths.iter().map(|path| path.final_value_msat()).sum() } + + pub(crate) fn debug_assert_route_meets_params<L: Logger>(&self, logger: L) -> Result<(), ()> { + let route_params = &self.route_params; + // Check that we actually pay less than the max fee we set. + if let Some(max_total_fee) = route_params.max_total_routing_fee_msat { + let total_fee = self.get_total_fees(); + if total_fee > max_total_fee { + let err = format!("Router returned an attempt to pay with a higher fee ({total_fee}msat) than we allowed ({max_total_fee}msat). Your router is critically buggy!"); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + } + + if self.paths.is_empty() { + let err = "Selected route had no paths. Your router is buggy!"; + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + + for path in self.paths.iter() { + if path.hops.is_empty() { + let err = "Unusable path in route (path.hops.len() must be at least 1)"; + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + + let total_cltv_delta = path.total_cltv_expiry_delta(); + if total_cltv_delta > route_params.payment_params.max_total_cltv_expiry_delta { + let err = format!( + "Path had a total CLTV of {total_cltv_delta} which is greater than the maximum we're allowed {}", + route_params.payment_params.max_total_cltv_expiry_delta, + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + + if path.hops.len() > route_params.payment_params.max_path_length.into() { + let err = format!( + "Path had a length of {}, which is greater than the maximum we're allowed ({})", + path.hops.len(), + route_params.payment_params.max_path_length, + ); + #[cfg(any(test, feature = "_test_utils"))] + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + // This is a bug, but there's not a material safety risk to making this + // payment, so we don't bother to error here. + } + + if let Some(tail) = &path.blinded_tail { + let trampoline_cltv_sum: u32 = + tail.trampoline_hops.iter().map(|hop| hop.cltv_expiry_delta).sum(); + let last_hop_cltv_delta = path.hops.last().unwrap().cltv_expiry_delta; + if !tail.trampoline_hops.is_empty() && trampoline_cltv_sum != last_hop_cltv_delta { + let err = format!( + "Path had a total trampoline CLTV of {trampoline_cltv_sum}, which is not equal to the total last-hop CLTV delta of {last_hop_cltv_delta}" + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + } + let last_trampoline_cltv_opt = + tail.trampoline_hops.last().map(|h| h.cltv_expiry_delta); + let last_trampoline_cltv = last_trampoline_cltv_opt.unwrap_or(u32::MAX); + if tail.excess_final_cltv_expiry_delta > last_trampoline_cltv { + let err = format!( + "Last trampoline CLTV of {last_trampoline_cltv} is less than the excess blinded path cltv of {}", + tail.excess_final_cltv_expiry_delta + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + } + if tail.excess_final_cltv_expiry_delta > last_hop_cltv_delta { + let err = format!( + "Last path hop CLTV of {last_hop_cltv_delta} is less than the excess blinded path cltv of {}", + tail.excess_final_cltv_expiry_delta + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + } + } + } + + // Test that we don't contain any "extra" MPP parts - while we're allowed to overshoot + // the `final_value_msat` specified in the `route_params`, we aren't allowed to have + // any MPP parts which aren't needed to meet `route_params.final_value_msat`. + let min_mpp_part = self.paths.iter().map(|h| h.final_value_msat()).min().unwrap_or(0); + if self.get_total_amount() - min_mpp_part >= route_params.final_value_msat { + let err = format!( + "Router returned an attempt to include more MPP parts than needed. The smallest MPP part ({min_mpp_part}msat) was not needed for a payment of {}msat. Your router is critically buggy!", + route_params.final_value_msat + ); + debug_assert!(false, "{}", err); + log_error!(logger, "{}", err); + return Err(()); + } + + Ok(()) + } } impl fmt::Display for Route { @@ -721,12 +844,10 @@ impl Writeable for Route { } else if !blinded_tails.is_empty() { blinded_tails.push(None); } } write_tlv_fields!(writer, { - // For compatibility with LDK versions prior to 0.0.117, we take the individual - // RouteParameters' fields and reconstruct them on read. - (1, self.route_params.as_ref().map(|p| &p.payment_params), option), + (1, self.route_params.payment_params, required), (2, blinded_tails, optional_vec), - (3, self.route_params.as_ref().map(|p| p.final_value_msat), option), - (5, self.route_params.as_ref().and_then(|p| p.max_total_routing_fee_msat), option), + (3, self.route_params.final_value_msat, required), + (5, self.route_params.max_total_routing_fee_msat, option), }); Ok(()) } @@ -752,9 +873,9 @@ impl Readable for Route { paths.push(Path { hops, blinded_tail: None }); } _init_and_read_len_prefixed_tlv_fields!(reader, { - (1, payment_params, (option: ReadableArgs, min_final_cltv_expiry_delta)), + (1, payment_params, (required: ReadableArgs, min_final_cltv_expiry_delta)), (2, blinded_tails, optional_vec), - (3, final_value_msat, option), + (3, final_value_msat, required), (5, max_total_routing_fee_msat, option) }); let blinded_tails = blinded_tails.unwrap_or(Vec::new()); @@ -765,12 +886,10 @@ impl Readable for Route { } } - // If we previously wrote the corresponding fields, reconstruct RouteParameters. - let route_params = match (payment_params, final_value_msat) { - (Some(payment_params), Some(final_value_msat)) => { - Some(RouteParameters { payment_params, final_value_msat, max_total_routing_fee_msat }) - } - _ => None, + let route_params = RouteParameters { + payment_params: payment_params.0.unwrap(), + final_value_msat: final_value_msat.0.unwrap(), + max_total_routing_fee_msat, }; Ok(Route { paths, route_params }) @@ -1049,7 +1168,7 @@ impl PaymentParameters { /// [`PaymentParameters::expiry_time`]. pub fn from_bolt11_invoice(invoice: &Bolt11Invoice) -> Self { let mut payment_params = Self::from_node_id( - invoice.recover_payee_pub_key(), + invoice.get_payee_pub_key(), invoice.min_final_cltv_expiry_delta() as u32, ) .with_route_hints(invoice.route_hints()) @@ -1245,7 +1364,7 @@ pub struct RouteParametersConfig { pub max_channel_saturation_power_of_half: u8, } -impl_writeable_tlv_based!(RouteParametersConfig, { +impl_ser_tlv_based!(RouteParametersConfig, { (1, max_total_routing_fee_msat, option), (3, max_total_cltv_expiry_delta, required), (5, max_path_count, required), @@ -1450,7 +1569,7 @@ impl Readable for RouteHint { } } -impl_writeable_tlv_based!(RouteHintHop, { +impl_ser_tlv_based!(RouteHintHop, { (0, src_node_id, required), (1, htlc_minimum_msat, option), (2, short_channel_id, required), @@ -2299,10 +2418,12 @@ impl<'a> PaymentPath<'a> { /// contribution this path can make to the final value of the payment. /// May be slightly lower than the actual max due to rounding errors when aggregating fees /// along the path. + /// Returns an error with the index of a later hop to discard if the following hops' aggregate + /// fees overflow. #[rustfmt::skip] fn max_final_value_msat( &self, used_liquidities: &HashMap<CandidateHopId, u64>, channel_saturation_pow_half: u8 - ) -> (usize, u64) { + ) -> Result<(usize, u64), usize> { let mut max_path_contribution = (0, u64::MAX); for (idx, (hop, _)) in self.hops.iter().enumerate() { let hop_effective_capacity_msat = hop.candidate.effective_capacity(); @@ -2318,7 +2439,8 @@ impl<'a> PaymentPath<'a> { // Aggregate the fees of the hops that come after this one, and use those fees to compute the // maximum amount that this hop can contribute to the final value received by the payee. let (next_hops_aggregated_base, next_hops_aggregated_prop) = - crate::blinded_path::payment::compute_aggregated_base_prop_fee(next_hops_feerates_iter).unwrap(); + crate::blinded_path::payment::compute_aggregated_base_prop_fee(next_hops_feerates_iter) + .map_err(|_| idx + 1)?; // floor(((hop_max_msat - agg_base) * 1_000_000) / (1_000_000 + agg_prop)) let hop_max_final_value_contribution = (hop_max_msat as u128) @@ -2335,14 +2457,26 @@ impl<'a> PaymentPath<'a> { } else { debug_assert!(false); } } - max_path_contribution + Ok(max_path_contribution) + } +} + +fn mark_candidate_liquidity_exhausted( + used_liquidities: &mut HashMap<CandidateHopId, u64>, candidate: &CandidateRouteHop, +) { + let exhausted = u64::max_value(); + if let Some(scid) = candidate.short_channel_id() { + *used_liquidities.entry(CandidateHopId::Clear((scid, false))).or_default() = exhausted; + *used_liquidities.entry(CandidateHopId::Clear((scid, true))).or_default() = exhausted; + } else { + *used_liquidities.entry(candidate.id()).or_default() = exhausted; } } #[inline(always)] /// Calculate the fees required to route the given amount over a channel with the given fees. #[rustfmt::skip] -fn compute_fees(amount_msat: u64, channel_fees: RoutingFees) -> Option<u64> { +pub(crate) fn compute_fees(amount_msat: u64, channel_fees: RoutingFees) -> Option<u64> { amount_msat.checked_mul(channel_fees.proportional_millionths as u64) .and_then(|part| (channel_fees.base_msat as u64).checked_add(part / 1_000_000)) } @@ -2491,9 +2625,11 @@ pub fn find_route<L: Logger, GL: Logger, S: ScoreLookUp>( scorer: &S, score_params: &S::ScoreParams, random_seed_bytes: &[u8; 32] ) -> Result<Route, &'static str> { let graph_lock = network_graph.read_only(); - let mut route = get_route(our_node_pubkey, &route_params, &graph_lock, first_hops, logger, + let mut route = get_route(our_node_pubkey, &route_params, &graph_lock, first_hops, &logger, scorer, score_params, random_seed_bytes)?; add_random_cltv_offset(&mut route, &route_params.payment_params, &graph_lock, random_seed_bytes); + route.debug_assert_route_meets_params(&logger) + .map_err(|()| "Generated route doesn't comply with the parameters you specified. This indicates a bug in the router. Please report this bug!")?; Ok(route) } @@ -3513,7 +3649,17 @@ pub(crate) fn get_route<L: Logger, S: ScoreLookUp>( // underpaid htlc_minimum_msat with fees. debug_assert_eq!(payment_path.get_value_msat(), value_contribution_msat); let (lowest_value_contrib_hop, max_path_contribution_msat) = - payment_path.max_final_value_msat(&used_liquidities, channel_saturation_pow_half); + match payment_path.max_final_value_msat(&used_liquidities, channel_saturation_pow_half) { + Ok(contribution) => contribution, + Err(candidate_idx_to_skip) => { + let candidate = &payment_path.hops[candidate_idx_to_skip].0.candidate; + log_trace!(logger, + "Ignoring path because aggregate fees including hop {} overflow.", + LoggedCandidateHop(candidate)); + mark_candidate_liquidity_exhausted(&mut used_liquidities, candidate); + continue 'paths_collection; + } + }; let desired_value_contribution = cmp::min(max_path_contribution_msat, final_value_msat); value_contribution_msat = payment_path.update_value_and_recompute_fees(desired_value_contribution); @@ -3777,7 +3923,7 @@ pub(crate) fn get_route<L: Logger, S: ScoreLookUp>( } } - let route = Route { paths, route_params: Some(route_params.clone()) }; + let route = Route { paths, route_params: route_params.clone() }; // Make sure we would never create a route whose total fees exceed max_total_routing_fee_msat. if let Some(max_total_routing_fee_msat) = route_params.max_total_routing_fee_msat { @@ -3820,11 +3966,11 @@ fn add_random_cltv_offset(route: &mut Route, payment_params: &PaymentParameters, } // Init PRNG with the path-dependant nonce, which is static for private paths. - let mut prng = ChaCha20::new(random_seed_bytes, &path_nonce); + let mut prng = ChaCha20::new(Key::new(*random_seed_bytes), Nonce::new(path_nonce), 0); let mut random_path_bytes = [0u8; ::core::mem::size_of::<usize>()]; // Pick a random path length in [1 .. 3] - prng.process_in_place(&mut random_path_bytes); + prng.apply_keystream(&mut random_path_bytes); let random_walk_length = usize::from_be_bytes(random_path_bytes).wrapping_rem(3).wrapping_add(1); for random_hop in 0..random_walk_length { @@ -3835,7 +3981,7 @@ fn add_random_cltv_offset(route: &mut Route, payment_params: &PaymentParameters, if let Some(cur_node_id) = cur_hop { if let Some(cur_node) = network_nodes.get(&cur_node_id) { // Randomly choose the next unvisited hop. - prng.process_in_place(&mut random_path_bytes); + prng.apply_keystream(&mut random_path_bytes); if let Some(random_channel) = usize::from_be_bytes(random_path_bytes) .checked_rem(cur_node.channels.len()) .and_then(|index| cur_node.channels.get(index)) @@ -3862,8 +4008,8 @@ fn add_random_cltv_offset(route: &mut Route, payment_params: &PaymentParameters, // Limit the offset so we never exceed the max_total_cltv_expiry_delta. To improve plausibility, // we choose the limit to be the largest possible multiple of MEDIAN_HOP_CLTV_EXPIRY_DELTA. - let path_total_cltv_expiry_delta: u32 = path.hops.iter().map(|h| h.cltv_expiry_delta).sum(); - let mut max_path_offset = payment_params.max_total_cltv_expiry_delta - path_total_cltv_expiry_delta; + let mut max_path_offset = + payment_params.max_total_cltv_expiry_delta - path.total_cltv_expiry_delta(); max_path_offset = cmp::max( max_path_offset - (max_path_offset % MEDIAN_HOP_CLTV_EXPIRY_DELTA), max_path_offset % MEDIAN_HOP_CLTV_EXPIRY_DELTA); @@ -3956,7 +4102,6 @@ mod tests { use crate::blinded_path::payment::{BlindedPayInfo, BlindedPaymentPath}; use crate::blinded_path::BlindedHop; use crate::chain::transaction::OutPoint; - use crate::crypto::chacha20::ChaCha20; use crate::ln::chan_utils::make_funding_redeemscript; use crate::ln::channel_state::{ChannelCounterparty, ChannelDetails, ChannelShutdownState}; use crate::ln::channelmanager; @@ -3965,7 +4110,7 @@ mod tests { use crate::routing::gossip::{EffectiveCapacity, NetworkGraph, NodeId, P2PGossipSync}; use crate::routing::router::{ add_random_cltv_offset, build_route_from_hops_internal, default_node_features, get_route, - BlindedPathCandidate, BlindedTail, CandidateRouteHop, InFlightHtlcs, Path, + BlindedPathCandidate, BlindedTail, CandidateRouteHop, InFlightHtlcs, Path, Payee, PaymentParameters, PublicHopCandidate, Route, RouteHint, RouteHintHop, RouteHop, RouteParameters, RoutingFees, ScorerAccountingForInFlightHtlcs, DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA, MAX_PATH_LENGTH_ESTIMATE, @@ -3984,6 +4129,8 @@ mod tests { use crate::util::test_utils as ln_test_utils; use bitcoin::amount::Amount; + use bitcoin::bech32::primitives::decode::CheckedHrpstring; + use bitcoin::bech32::{ByteIterExt, Fe32IterExt}; use bitcoin::constants::ChainHash; use bitcoin::hashes::Hash; use bitcoin::hex::FromHex; @@ -3993,10 +4140,60 @@ mod tests { use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::TxOut; + use chacha20_poly1305::chacha20::ChaCha20; + use chacha20_poly1305::{Key, Nonce}; + use lightning_invoice::{Bolt11Bech32, Bolt11Invoice, Currency, InvoiceBuilder}; use crate::io::Cursor; use crate::prelude::*; use crate::sync::{Arc, Mutex}; + use crate::types::payment::{PaymentHash, PaymentSecret}; + + fn invoice_with_included_payee_pub_key_and_bad_recovery_id() -> (Bolt11Invoice, PublicKey) { + let secp_ctx = Secp256k1::new(); + let private_key = SecretKey::from_slice(&[42; 32]).unwrap(); + let public_key = PublicKey::from_secret_key(&secp_ctx, &private_key); + + let invoice = InvoiceBuilder::new(Currency::Bitcoin) + .description("Test".to_string()) + .amount_milli_satoshis(1000) + .payment_hash(PaymentHash([0; 32])) + .payment_secret(PaymentSecret([21; 32])) + .payee_pub_key(public_key) + .min_final_cltv_expiry_delta(144) + .duration_since_epoch(core::time::Duration::from_secs(1234567)) + .build_signed(|hash| secp_ctx.sign_ecdsa_recoverable(hash, &private_key)) + .unwrap(); + + let invoice_string = invoice.to_string(); + let parsed = CheckedHrpstring::new::<Bolt11Bech32>(&invoice_string).unwrap(); + let hrp = parsed.hrp(); + let mut data: Vec<_> = parsed.fe32_iter::<&mut dyn Iterator<Item = u8>>().collect(); + let signature_start = data.len() - 104; + let mut signature_bytes: Vec<u8> = + data[signature_start..].iter().copied().fes_to_bytes().collect(); + signature_bytes[64] = 2; + let signature_data: Vec<_> = signature_bytes.into_iter().bytes_to_fes().collect(); + data.splice(signature_start.., signature_data); + + let bad_invoice_string = data + .into_iter() + .with_checksum::<bitcoin::bech32::Bech32>(&hrp) + .chars() + .collect::<String>(); + (bad_invoice_string.parse().unwrap(), public_key) + } + + #[test] + fn payment_params_from_bolt11_invoice_uses_included_payee_pub_key() { + let (invoice, public_key) = invoice_with_included_payee_pub_key_and_bad_recovery_id(); + let payment_params = PaymentParameters::from_bolt11_invoice(&invoice); + + match payment_params.payee { + Payee::Clear { node_id, .. } => assert_eq!(node_id, public_key), + Payee::Blinded { .. } => panic!("BOLT11 invoice should create a clear payee"), + } + } #[rustfmt::skip] fn get_channel_details(short_channel_id: Option<u64>, node_id: PublicKey, @@ -4026,6 +4223,7 @@ mod tests { outbound_capacity_msat, next_outbound_htlc_limit_msat: outbound_capacity_msat, next_outbound_htlc_minimum_msat: 0, + next_splice_out_maximum_sat: outbound_capacity_msat / 1000, inbound_capacity_msat: 42, unspendable_punishment_reserve: None, confirmations_required: None, @@ -4040,6 +4238,8 @@ mod tests { channel_shutdown_state: Some(ChannelShutdownState::NotShuttingDown), pending_inbound_htlcs: Vec::new(), pending_outbound_htlcs: Vec::new(), + current_dust_exposure_msat: None, + splice_details: None, } } @@ -7370,7 +7570,7 @@ mod tests { short_channel_id: 0, fee_msat: 225, cltv_expiry_delta: 0, maybe_announced_channel: true, }, ], blinded_tail: None }], - route_params: None, + route_params: RouteParameters::from_payment_params_and_value(PaymentParameters::from_node_id(ln_test_utils::pubkey(42), 0), 225), }; assert_eq!(route.get_total_fees(), 250); @@ -7403,7 +7603,7 @@ mod tests { short_channel_id: 0, fee_msat: 150, cltv_expiry_delta: 0, maybe_announced_channel: true, }, ], blinded_tail: None }], - route_params: None, + route_params: RouteParameters::from_payment_params_and_value(PaymentParameters::from_node_id(ln_test_utils::pubkey(42), 0), 300), }; assert_eq!(route.get_total_fees(), 200); @@ -7415,7 +7615,13 @@ mod tests { // In an earlier version of `Route::get_total_fees` and `Route::get_total_amount`, they // would both panic if the route was completely empty. We test to ensure they return 0 // here, even though its somewhat nonsensical as a route. - let route = Route { paths: Vec::new(), route_params: None }; + let route = Route { + paths: Vec::new(), + route_params: RouteParameters::from_payment_params_and_value( + PaymentParameters::from_node_id(ln_test_utils::pubkey(42), 0), + 0, + ), + }; assert_eq!(route.get_total_fees(), 0); assert_eq!(route.get_total_amount(), 0); @@ -7584,10 +7790,10 @@ mod tests { for p in route.paths { // 1. Select random observation point - let mut prng = ChaCha20::new(&random_seed_bytes, &[0u8; 12]); + let mut prng = ChaCha20::new(Key::new(random_seed_bytes), Nonce::new([0; 12]),0); let mut random_bytes = [0u8; ::core::mem::size_of::<usize>()]; - prng.process_in_place(&mut random_bytes); + prng.apply_keystream(&mut random_bytes); let random_path_index = usize::from_be_bytes(random_bytes).wrapping_rem(p.hops.len()); let observation_point = NodeId::from_pubkey(&p.hops.get(random_path_index).unwrap().pubkey); @@ -8010,7 +8216,7 @@ mod tests { cltv_expiry_delta: 0, maybe_announced_channel: true, }], blinded_tail: None }], - route_params: None, + route_params: RouteParameters::from_payment_params_and_value(PaymentParameters::from_node_id(ln_test_utils::pubkey(42), 0), 200), }; let encoded_route = route.encode(); let decoded_route: Route = Readable::read(&mut Cursor::new(&encoded_route[..])).unwrap(); @@ -8206,7 +8412,7 @@ mod tests { excess_final_cltv_expiry_delta: 0, final_value_msat: 200, }), - }], route_params: None}; + }], route_params: RouteParameters::from_payment_params_and_value(PaymentParameters::from_node_id(ln_test_utils::pubkey(42), 0), 200)}; let payment_params = PaymentParameters::from_node_id(ln_test_utils::pubkey(47), 18); let (_, network_graph, _, _, _) = build_line_graph(); @@ -9054,7 +9260,7 @@ mod tests { assert_eq!(route.paths.len(), 1); assert_eq!(route.get_total_amount(), amt_msat); assert_eq!(route.paths[0].hops.len(), 2); - assert_eq!(route.paths[0].hops[0].short_channel_id, 1); + assert_eq!(route.paths[0].hops[0].short_channel_id, 44); assert_eq!(route.paths[0].hops[1].short_channel_id, 45); assert_eq!(route.get_total_fees(), 123); } @@ -9205,6 +9411,67 @@ mod tests { assert_eq!(route.paths[0].hops[0].short_channel_id, 44); } + #[test] + fn aggregated_prop_fee_overflow_fails_route() { + // If the fee cap is disabled, we may consider invoice hints with very large + // proportional fees. Aggregating those fees can overflow, in which case we should fail + // routing cleanly rather than panic. + let secp_ctx = Secp256k1::new(); + let logger = Arc::new(ln_test_utils::TestLogger::new()); + let network_graph = Arc::new(NetworkGraph::new(Network::Testnet, Arc::clone(&logger))); + let scorer = ln_test_utils::TestScorer::new(); + let random_seed_bytes = [42; 32]; + let config = UserConfig::default(); + + let (_, our_node_id, _, nodes) = get_nodes(&secp_ctx); + let route_hint = RouteHint(vec![ + RouteHintHop { + src_node_id: nodes[0], + short_channel_id: 100, + fees: RoutingFees { base_msat: 0, proportional_millionths: u32::MAX }, + cltv_expiry_delta: 10, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }, + RouteHintHop { + src_node_id: nodes[1], + short_channel_id: 101, + fees: RoutingFees { base_msat: 0, proportional_millionths: u32::MAX }, + cltv_expiry_delta: 10, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }, + ]); + + let payment_params = PaymentParameters::from_node_id(nodes[2], 42) + .with_route_hints(vec![route_hint]) + .unwrap() + .with_bolt11_features(channelmanager::provided_bolt11_invoice_features(&config)) + .unwrap(); + let first_hops = [get_channel_details( + Some(1), + nodes[0], + channelmanager::provided_init_features(&config), + 100_000_000, + )]; + let route_params = RouteParameters { + payment_params, + final_value_msat: 1, + max_total_routing_fee_msat: None, + }; + let route = get_route( + &our_node_id, + &route_params, + &network_graph.read_only(), + Some(&first_hops.iter().collect::<Vec<_>>()), + Arc::clone(&logger), + &scorer, + &Default::default(), + &random_seed_bytes, + ); + assert!(route.is_err()); + } + #[test] fn prefers_paths_by_cost_amt_ratio() { // Previously, we preferred paths during MPP selection based on their absolute cost, rather @@ -9525,6 +9792,7 @@ pub(crate) mod bench_utils { outbound_capacity_msat: 10_000_000_000, next_outbound_htlc_minimum_msat: 0, next_outbound_htlc_limit_msat: 10_000_000_000, + next_splice_out_maximum_sat: 10_000_000, inbound_capacity_msat: 0, unspendable_punishment_reserve: None, confirmations_required: None, @@ -9541,6 +9809,8 @@ pub(crate) mod bench_utils { channel_shutdown_state: Some(ChannelShutdownState::NotShuttingDown), pending_inbound_htlcs: Vec::new(), pending_outbound_htlcs: Vec::new(), + current_dust_exposure_msat: None, + splice_details: None, } } diff --git a/lightning/src/routing/scoring.rs b/lightning/src/routing/scoring.rs index 47621e37380..53031ef5bc0 100644 --- a/lightning/src/routing/scoring.rs +++ b/lightning/src/routing/scoring.rs @@ -587,7 +587,7 @@ pub struct ProbabilisticScoringFeeParameters { /// (implying scaling all estimated probabilities down by a factor of ~79%) resulted in the /// most accurate total success probabilities. /// - /// Default value: 1,024 msat (i.e. we're willing to pay 1 sat to avoid each additional hop). + /// Default value: 5,120 msat (i.e. we're willing to pay 5.12 sats to avoid each additional hop). /// /// [`historical_liquidity_penalty_multiplier_msat`]: Self::historical_liquidity_penalty_multiplier_msat pub base_penalty_msat: u64, @@ -606,8 +606,8 @@ pub struct ProbabilisticScoringFeeParameters { /// probabilities down by a factor of ~79%) resulted in the most accurate total success /// probabilities. /// - /// Default value: 131,072 msat (i.e. we're willing to pay 0.125bps to avoid each additional - /// hop). + /// Default value: 655,360 msat (i.e. we're willing to pay roughly 6.1 basis points to avoid + /// each additional hop). /// /// [`base_penalty_msat`]: Self::base_penalty_msat /// [`historical_liquidity_penalty_amount_multiplier_msat`]: Self::historical_liquidity_penalty_amount_multiplier_msat @@ -673,8 +673,8 @@ pub struct ProbabilisticScoringFeeParameters { /// track which of several buckets those bounds fall into, exponentially decaying the /// probability of each bucket as new samples are added. /// - /// Default value: 10,000 msat (i.e. willing to pay 1 sat to avoid an 80% probability channel, - /// or 6 sats to avoid a 25% probability channel). + /// Default value: 50,000 msat (i.e. willing to pay 5 sats to avoid an 80% probability channel, + /// or 30 sats to avoid a 25% probability channel). /// /// [`liquidity_penalty_multiplier_msat`]: Self::liquidity_penalty_multiplier_msat pub historical_liquidity_penalty_multiplier_msat: u64, @@ -695,8 +695,8 @@ pub struct ProbabilisticScoringFeeParameters { /// channel, we track which of several buckets those bounds fall into, exponentially decaying /// the probability of each bucket as new samples are added. /// - /// Default value: 1,250 msat (i.e. willing to pay about 0.125 bps per hop to avoid 78% - /// probability channels, or 0.5bps to avoid a 38% probability + /// Default value: 6,250 msat (i.e. willing to pay about 6.4 bps per hop to avoid 78% + /// probability channels, or 25bps to avoid a 38% probability /// channel). /// /// [`liquidity_penalty_amount_multiplier_msat`]: Self::liquidity_penalty_amount_multiplier_msat @@ -715,7 +715,7 @@ pub struct ProbabilisticScoringFeeParameters { /// as this makes balance discovery attacks harder to execute, thereby creating an incentive /// to restrict `htlc_maximum_msat` and improve privacy. /// - /// Default value: 250 msat + /// Default value: 1,250 msat pub anti_probing_penalty_msat: u64, /// This penalty is applied when the total amount flowing over a channel exceeds our current @@ -787,15 +787,15 @@ pub struct ProbabilisticScoringFeeParameters { impl Default for ProbabilisticScoringFeeParameters { fn default() -> Self { Self { - base_penalty_msat: 1024, - base_penalty_amount_multiplier_msat: 131_072, + base_penalty_msat: 5_120, + base_penalty_amount_multiplier_msat: 655_360, liquidity_penalty_multiplier_msat: 0, liquidity_penalty_amount_multiplier_msat: 0, manual_node_penalties: new_hash_map(), - anti_probing_penalty_msat: 250, + anti_probing_penalty_msat: 1_250, considered_impossible_penalty_msat: 1_0000_0000_000, - historical_liquidity_penalty_multiplier_msat: 10_000, - historical_liquidity_penalty_amount_multiplier_msat: 1_250, + historical_liquidity_penalty_multiplier_msat: 50_000, + historical_liquidity_penalty_amount_multiplier_msat: 6_250, linear_success_probability: false, probing_diversity_penalty_msat: 0, } @@ -1110,6 +1110,9 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> ProbabilisticScorer<G, L> { /// with `scid` towards the given `target` node, based on the historical estimated liquidity /// bounds. /// + /// Note that probabilities for paths which are highly unlikely to succeed, but not impossible + /// are capped to a lower-bound of [`PROB_LOWER_BOUND`]. + /// /// Returns `None` if: /// - the given channel is not in the network graph, the provided `target` is not a party to /// the channel, or we don't have forwarding parameters for either direction in the channel. @@ -1119,10 +1122,9 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> ProbabilisticScorer<G, L> { /// These are the same bounds as returned by /// [`Self::historical_estimated_channel_liquidity_probabilities`] (but not those returned by /// [`Self::estimated_channel_liquidity_range`]). - #[rustfmt::skip] pub fn historical_estimated_payment_success_probability( - &self, scid: u64, target: &NodeId, amount_msat: u64, params: &ProbabilisticScoringFeeParameters, - allow_fallback_estimation: bool, + &self, scid: u64, target: &NodeId, amount_msat: u64, + params: &ProbabilisticScoringFeeParameters, allow_fallback_estimation: bool, ) -> Option<f64> { let graph = self.network_graph.read_only(); @@ -1130,63 +1132,99 @@ impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> ProbabilisticScorer<G, L> { if let Some((directed_info, source)) = chan.as_directed_to(target) { if let Some(liq) = self.channel_liquidities.get(&scid) { let capacity_msat = directed_info.effective_capacity().as_msat(); + if amount_msat >= capacity_msat { + return Some(PROB_LOWER_BOUND); + } let dir_liq = liq.as_directed(source, target, capacity_msat); - let res = dir_liq.liquidity_history.calculate_success_probability_times_billion( - ¶ms, amount_msat, capacity_msat - ).map(|p| p as f64 / (1024 * 1024 * 1024) as f64); - if res.is_some() { - return res; + let res = dir_liq + .liquidity_history + .calculate_success_probability_times_billion( + ¶ms, + amount_msat, + capacity_msat, + ) + .map(|p| p as f64 / (1024 * 1024 * 1024) as f64); + if let Some(prob) = res { + if prob < PROB_LOWER_BOUND { + return Some(PROB_LOWER_BOUND); + } else { + return Some(prob); + } } } if allow_fallback_estimation { let amt = amount_msat; - return Some( - self.calc_live_prob(scid, source, target, directed_info, amt, params, true) - ); + return Some(self.calc_live_prob( + scid, + source, + target, + directed_info, + amt, + params, + true, + )); } } } None } - #[rustfmt::skip] fn calc_live_prob( &self, scid: u64, source: &NodeId, target: &NodeId, directed_info: DirectedChannelInfo, - amt: u64, params: &ProbabilisticScoringFeeParameters, - min_zero_penalty: bool, + amt: u64, params: &ProbabilisticScoringFeeParameters, min_zero_penalty: bool, ) -> f64 { let capacity_msat = directed_info.effective_capacity().as_msat(); let dummy_liq = ChannelLiquidity::new(Duration::ZERO); - let liq = self.channel_liquidities.get(&scid) - .unwrap_or(&dummy_liq) - .as_directed(&source, &target, capacity_msat); + let liq = self.channel_liquidities.get(&scid).unwrap_or(&dummy_liq).as_directed( + &source, + &target, + capacity_msat, + ); let min_liq = liq.min_liquidity_msat(); let max_liq = liq.max_liquidity_msat(); - if amt <= liq.min_liquidity_msat() { + if amt <= min_liq { return 1.0; - } else if amt > liq.max_liquidity_msat() { + } else if amt > capacity_msat { return 0.0; + } else if amt >= max_liq { + return PROB_LOWER_BOUND; } let (num, den) = success_probability(amt, min_liq, max_liq, capacity_msat, ¶ms, min_zero_penalty); - num as f64 / den as f64 + let res = num as f64 / den as f64; + if res < PROB_LOWER_BOUND { + PROB_LOWER_BOUND + } else { + res + } } /// Query the probability of payment success sending the given `amount_msat` over the channel /// with `scid` towards the given `target` node, based on the live estimated liquidity bounds. /// + /// Note that probabilities for paths which are highly unlikely to succeed, but not impossible + /// are capped to a lower-bound of [`PROB_LOWER_BOUND`]. + /// /// This will return `Some` for any channel which is present in the [`NetworkGraph`], including /// if we have no bound information beside the channel's capacity. - #[rustfmt::skip] pub fn live_estimated_payment_success_probability( - &self, scid: u64, target: &NodeId, amount_msat: u64, params: &ProbabilisticScoringFeeParameters, + &self, scid: u64, target: &NodeId, amount_msat: u64, + params: &ProbabilisticScoringFeeParameters, ) -> Option<f64> { let graph = self.network_graph.read_only(); if let Some(chan) = graph.channels().get(&scid) { if let Some((directed_info, source)) = chan.as_directed_to(target) { - return Some(self.calc_live_prob(scid, source, target, directed_info, amount_msat, params, false)); + return Some(self.calc_live_prob( + scid, + source, + target, + directed_info, + amount_msat, + params, + false, + )); } } None @@ -1291,8 +1329,18 @@ impl ChannelLiquidity { /// Bounds `-log10` to avoid excessive liquidity penalties for payments with low success /// probabilities. +/// +/// The log10 equivalent of [`PROB_LOWER_BOUND`]. const NEGATIVE_LOG10_UPPER_BOUND: u64 = 2; +/// The minimum probability we will use when scoring a channel where we believe success may be +/// possible, even if its unlikely. +/// +/// Allowing the probability to go arbitrarily low results in penalties which grow unnecessarily +/// huge for small changes in probability (as penalties are based on the `log10` of the +/// probability). +pub const PROB_LOWER_BOUND: f64 = 0.01; + /// The rough cutoff at which our precision falls off and we should stop bothering to try to log a /// ratio, as X in 1/X. const PRECISION_LOWER_BOUND_DENOMINATOR: u64 = log_approx::LOWER_BITS_BOUND; @@ -1322,17 +1370,18 @@ fn three_f64_pow_9(a: f64, b: f64, c: f64) -> (f64, f64, f64) { const MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64: u64 = 78; #[inline(always)] -#[rustfmt::skip] fn linear_success_probability( total_inflight_amount_msat: u64, min_liquidity_msat: u64, max_liquidity_msat: u64, min_zero_implies_no_successes: bool, ) -> (u64, u64) { - let (numerator, mut denominator) = - (max_liquidity_msat - total_inflight_amount_msat, - (max_liquidity_msat - min_liquidity_msat).saturating_add(1)); - - if min_zero_implies_no_successes && min_liquidity_msat == 0 && - denominator < u64::max_value() / MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64 + let (numerator, mut denominator) = ( + max_liquidity_msat - total_inflight_amount_msat, + (max_liquidity_msat - min_liquidity_msat).saturating_add(1), + ); + + if min_zero_implies_no_successes + && min_liquidity_msat == 0 + && denominator < u64::max_value() / MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64 { denominator = denominator * MIN_ZERO_IMPLIES_NO_SUCCESSES_PENALTY_ON_64 / 64 } @@ -1464,8 +1513,7 @@ impl< // liquidity penalty at all (as the success probability is 100%). } else if total_inflight_amount_msat >= max_liquidity_msat { // Equivalent to hitting the else clause below with the amount equal to the effective - // capacity and without any certainty on the liquidity upper bound, plus the - // impossibility penalty. + // capacity and without any certainty on the liquidity upper bound. let negative_log10_times_2048 = NEGATIVE_LOG10_UPPER_BOUND * 2048; res = Self::combined_penalty_msat(amount_msat, negative_log10_times_2048, score_params.liquidity_penalty_multiplier_msat, @@ -1489,12 +1537,11 @@ impl< } } - if total_inflight_amount_msat >= max_liquidity_msat { + if total_inflight_amount_msat > max_liquidity_msat { res = res.saturating_add(score_params.considered_impossible_penalty_msat); } if total_inflight_amount_msat >= available_capacity { - // We're trying to send more than the capacity, use a max penalty. res = res.saturating_add(Self::combined_penalty_msat(amount_msat, NEGATIVE_LOG10_UPPER_BOUND * 2048, score_params.historical_liquidity_penalty_multiplier_msat, @@ -1871,6 +1918,21 @@ impl<G: Deref<Target = NetworkGraph<L>> + Clone, L: Logger + Clone> CombinedScor } } +impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> CombinedScorer<G, L> { + /// Returns a reference to the merged [`ProbabilisticScorer`] used for routing decisions, + /// which combines locally acquired data with any externally supplied scores. + pub fn scorer(&self) -> &ProbabilisticScorer<G, L> { + &self.scorer + } + + /// Returns a reference to the [`ProbabilisticScorer`] tracking only locally acquired data + /// (i.e. excluding any externally supplied scores merged via [`Self::merge`] or + /// [`Self::set_scores`]). + pub fn local_only_scorer(&self) -> &ProbabilisticScorer<G, L> { + &self.local_only_scorer + } +} + impl<G: Deref<Target = NetworkGraph<L>>, L: Logger> ScoreLookUp for CombinedScorer<G, L> { type ScoreParams = ProbabilisticScoringFeeParameters; @@ -2116,8 +2178,8 @@ mod bucketed_history { } } - impl_writeable_tlv_based!(HistoricalBucketRangeTracker, { (0, buckets, required) }); - impl_writeable_tlv_based!(LegacyHistoricalBucketRangeTracker, { (0, buckets, required) }); + impl_ser_tlv_based!(HistoricalBucketRangeTracker, { (0, buckets, required) }); + impl_ser_tlv_based!(LegacyHistoricalBucketRangeTracker, { (0, buckets, required) }); #[derive(Clone, Copy)] #[repr(C)] // Force the fields in memory to be in the order we specify. @@ -2690,20 +2752,28 @@ mod tests { let node_1_secret = &SecretKey::from_slice(&[39; 32]).unwrap(); let node_2_secret = &SecretKey::from_slice(&[40; 32]).unwrap(); let secp_ctx = Secp256k1::new(); + let mut node_id_1 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_1_key)); + let mut node_id_2 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_2_key)); + let mut node_signer_1 = &node_1_key; + let mut node_signer_2 = &node_2_key; + if node_id_1 > node_id_2 { + core::mem::swap(&mut node_id_1, &mut node_id_2); + core::mem::swap(&mut node_signer_1, &mut node_signer_2); + } let unsigned_announcement = UnsignedChannelAnnouncement { features: channelmanager::provided_channel_features(&UserConfig::default()), chain_hash: genesis_hash, short_channel_id, - node_id_1: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_1_key)), - node_id_2: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_2_key)), + node_id_1, + node_id_2, bitcoin_key_1: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_1_secret)), bitcoin_key_2: NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_2_secret)), excess_data: Vec::new(), }; let msghash = hash_to_message!(&Sha256dHash::hash(&unsigned_announcement.encode()[..])[..]); let signed_announcement = ChannelAnnouncement { - node_signature_1: secp_ctx.sign_ecdsa(&msghash, &node_1_key), - node_signature_2: secp_ctx.sign_ecdsa(&msghash, &node_2_key), + node_signature_1: secp_ctx.sign_ecdsa(&msghash, node_signer_1), + node_signature_2: secp_ctx.sign_ecdsa(&msghash, node_signer_2), bitcoin_signature_1: secp_ctx.sign_ecdsa(&msghash, &node_1_secret), bitcoin_signature_2: secp_ctx.sign_ecdsa(&msghash, &node_2_secret), contents: unsigned_announcement, @@ -2717,10 +2787,23 @@ mod tests { fn update_channel( network_graph: &mut NetworkGraph<&TestLogger>, short_channel_id: u64, node_key: SecretKey, - channel_flags: u8, htlc_maximum_msat: u64, timestamp: u32, + mut channel_flags: u8, htlc_maximum_msat: u64, timestamp: u32, ) { let genesis_hash = ChainHash::using_genesis_block(Network::Testnet); let secp_ctx = Secp256k1::new(); + let node_id = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, &node_key)); + // `add_channel` may have swapped the node order to satisfy the spec's sorted node_ids + // requirement, so override `channel_flags` bit 0 to match the actual node position. + { + let read_only = network_graph.read_only(); + if let Some(channel) = read_only.channel(short_channel_id) { + if channel.node_one == node_id { + channel_flags &= !1; + } else { + channel_flags |= 1; + } + } + } let unsigned_update = UnsignedChannelUpdate { chain_hash: genesis_hash, short_channel_id, @@ -3178,6 +3261,8 @@ mod tests { let usage = ChannelUsage { amount_msat: 250, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300); let usage = ChannelUsage { amount_msat: 500, ..usage }; + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2000); + let usage = ChannelUsage { amount_msat: 501, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value()); let usage = ChannelUsage { amount_msat: 750, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value()); @@ -3395,22 +3480,22 @@ mod tests { assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0); let usage = ChannelUsage { amount_msat: 1, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0); - let usage = ChannelUsage { amount_msat: 1_023, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2_000); let usage = ChannelUsage { amount_msat: 1_024, ..usage }; + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2_000); + let usage = ChannelUsage { amount_msat: 1_025, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value()); // Fully decay liquidity upper bound. scorer.time_passed(Duration::from_secs(10 * 9)); let usage = ChannelUsage { amount_msat: 0, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0); - let usage = ChannelUsage { amount_msat: 1_024, ..usage }; + let usage = ChannelUsage { amount_msat: 1_025, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value()); scorer.time_passed(Duration::from_secs(10 * 10)); let usage = ChannelUsage { amount_msat: 0, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 0); - let usage = ChannelUsage { amount_msat: 1_024, ..usage }; + let usage = ChannelUsage { amount_msat: 1_025, ..usage }; assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value()); } @@ -3484,7 +3569,7 @@ mod tests { let mut scorer = ProbabilisticScorer::new(decay_params, &network_graph, &logger); let source = source_node_id(); let usage = ChannelUsage { - amount_msat: 500, + amount_msat: 501, inflight_htlc_msat: 0, effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_000, htlc_maximum_msat: 1_000 }, }; @@ -3499,10 +3584,10 @@ mod tests { assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value()); scorer.time_passed(Duration::from_secs(10)); - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 473); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 477); scorer.payment_path_failed(&payment_path_for_amount(250), 43, Duration::from_secs(10)); - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 304); let mut serialized_scorer = Vec::new(); scorer.write(&mut serialized_scorer).unwrap(); @@ -3510,7 +3595,7 @@ mod tests { let mut serialized_scorer = io::Cursor::new(&serialized_scorer); let deserialized_scorer = <ProbabilisticScorer<_, _>>::read(&mut serialized_scorer, (decay_params, &network_graph, &logger)).unwrap(); - assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, ¶ms), 300); + assert_eq!(deserialized_scorer.channel_penalty_msat(&candidate, usage, ¶ms), 304); } #[rustfmt::skip] @@ -3541,7 +3626,13 @@ mod tests { info, short_channel_id: 42, }); - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), u64::max_value()); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 2000); + + let over_usage = ChannelUsage { + amount_msat: 501, + ..usage + }; + assert_eq!(scorer.channel_penalty_msat(&candidate, over_usage, ¶ms), u64::max_value()); if decay_before_reload { scorer.time_passed(Duration::from_secs(10)); @@ -3594,47 +3685,47 @@ mod tests { info, short_channel_id: 42, }); - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 42_252); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 211_262); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 36_005); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 180_032); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 2_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 32_851); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 164_259); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 3_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 30_832); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 154_165); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 4_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 29_886); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 149_434); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 5_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 28_939); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 144_702); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 6_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 28_435); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 142_178); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_450_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_993); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 139_969); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 7_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_993); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 139_969); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 8_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_488); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 137_446); let usage = ChannelUsage { effective_capacity: EffectiveCapacity::Total { capacity_msat: 9_950_000_000, htlc_maximum_msat: 1_000 }, ..usage }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 27_047); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 135_238); } #[test] @@ -3868,7 +3959,7 @@ mod tests { assert!(scorer.historical_estimated_payment_success_probability(42, &target, 1, ¶ms, false) .unwrap() > 0.35); assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, 500, ¶ms, false), - Some(0.0)); + Some(super::PROB_LOWER_BOUND)); // Even after we tell the scorer we definitely have enough available liquidity, it will // still remember that there was some failure in the past, and assign a non-0 penalty. @@ -3969,8 +4060,11 @@ mod tests { let logger = TestLogger::new(); let network_graph = network_graph(&logger); let source = source_node_id(); + let anti_probing_penalty_msat = + ProbabilisticScoringFeeParameters::default().anti_probing_penalty_msat; + assert_eq!(anti_probing_penalty_msat, 1_250); let params = ProbabilisticScoringFeeParameters { - anti_probing_penalty_msat: 500, + anti_probing_penalty_msat, ..ProbabilisticScoringFeeParameters::zero_penalty() }; let scorer = ProbabilisticScorer::new(ProbabilisticScoringDecayParameters::default(), &network_graph, &logger); @@ -3996,7 +4090,7 @@ mod tests { inflight_htlc_msat: 0, effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 1_024_000 }, }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 500); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 1_250); // Check we receive anti-probing penalty for htlc_maximum_msat == channel_capacity/2. let usage = ChannelUsage { @@ -4004,7 +4098,7 @@ mod tests { inflight_htlc_msat: 0, effective_capacity: EffectiveCapacity::Total { capacity_msat: 1_024_000, htlc_maximum_msat: 512_000 }, }; - assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 500); + assert_eq!(scorer.channel_penalty_msat(&candidate, usage, ¶ms), 1_250); // Check we receive no anti-probing penalty for htlc_maximum_msat == channel_capacity/2 - 1. let usage = ChannelUsage { @@ -4121,9 +4215,9 @@ mod tests { assert_eq!(scorer.historical_estimated_channel_liquidity_probabilities(42, &target), Some(([32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]))); - // The success probability estimate itself should be zero. + // The success probability estimate itself should be PROB_LOWER_BOUND. assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms, false), - Some(0.0)); + Some(super::PROB_LOWER_BOUND)); // Now test again with the amount in the bottom bucket. amount_msat /= 2; @@ -4140,7 +4234,7 @@ mod tests { Some(([63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [32, 31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]))); assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, ¶ms, false), - Some(0.0)); + Some(super::PROB_LOWER_BOUND)); } #[test] diff --git a/lightning/src/routing/test_utils.rs b/lightning/src/routing/test_utils.rs index a433fa30c5b..daaf65367c0 100644 --- a/lightning/src/routing/test_utils.rs +++ b/lightning/src/routing/test_utils.rs @@ -36,8 +36,14 @@ pub(crate) fn channel_announcement( node_1_privkey: &SecretKey, node_2_privkey: &SecretKey, features: ChannelFeatures, short_channel_id: u64, secp_ctx: &Secp256k1<All>, ) -> ChannelAnnouncement { - let node_id_1 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, node_1_privkey)); - let node_id_2 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, node_2_privkey)); + let mut node_id_1 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, node_1_privkey)); + let mut node_id_2 = NodeId::from_pubkey(&PublicKey::from_secret_key(&secp_ctx, node_2_privkey)); + let mut signer_1 = node_1_privkey; + let mut signer_2 = node_2_privkey; + if node_id_1 > node_id_2 { + core::mem::swap(&mut node_id_1, &mut node_id_2); + core::mem::swap(&mut signer_1, &mut signer_2); + } let unsigned_announcement = UnsignedChannelAnnouncement { features, @@ -52,10 +58,10 @@ pub(crate) fn channel_announcement( let msghash = hash_to_message!(&Sha256dHash::hash(&unsigned_announcement.encode()[..])[..]); ChannelAnnouncement { - node_signature_1: secp_ctx.sign_ecdsa(&msghash, node_1_privkey), - node_signature_2: secp_ctx.sign_ecdsa(&msghash, node_2_privkey), - bitcoin_signature_1: secp_ctx.sign_ecdsa(&msghash, node_1_privkey), - bitcoin_signature_2: secp_ctx.sign_ecdsa(&msghash, node_2_privkey), + node_signature_1: secp_ctx.sign_ecdsa(&msghash, signer_1), + node_signature_2: secp_ctx.sign_ecdsa(&msghash, signer_2), + bitcoin_signature_1: secp_ctx.sign_ecdsa(&msghash, signer_1), + bitcoin_signature_2: secp_ctx.sign_ecdsa(&msghash, signer_2), contents: unsigned_announcement.clone(), } } @@ -119,9 +125,25 @@ pub(crate) fn add_or_update_node( pub(crate) fn update_channel( gossip_sync: &P2PGossipSync<Arc<NetworkGraph<Arc<test_utils::TestLogger>>>, Arc<test_utils::TestChainSource>, Arc<test_utils::TestLogger>>, - secp_ctx: &Secp256k1<All>, node_privkey: &SecretKey, update: UnsignedChannelUpdate + secp_ctx: &Secp256k1<All>, node_privkey: &SecretKey, mut update: UnsignedChannelUpdate ) { let node_pubkey = PublicKey::from_secret_key(&secp_ctx, node_privkey); + let node_id = NodeId::from_pubkey(&node_pubkey); + + // `channel_announcement` may have swapped the node order to satisfy the spec's sorted node_ids + // requirement, so override `channel_flags` bit 0 to match the actual node position recorded in + // the network graph. + { + let network_graph = gossip_sync.network_graph().read_only(); + if let Some(channel) = network_graph.channel(update.short_channel_id) { + if channel.node_one == node_id { + update.channel_flags &= !1; + } else { + update.channel_flags |= 1; + } + } + } + let msghash = hash_to_message!(&Sha256dHash::hash(&update.encode()[..])[..]); let valid_channel_update = ChannelUpdate { signature: secp_ctx.sign_ecdsa(&msghash, node_privkey), diff --git a/lightning/src/routing/utxo.rs b/lightning/src/routing/utxo.rs index 466b9416f41..10270364075 100644 --- a/lightning/src/routing/utxo.rs +++ b/lightning/src/routing/utxo.rs @@ -293,11 +293,34 @@ impl PendingChecks { Ok(()) } + fn pending_channel_announcement_matches( + msg: &msgs::UnsignedChannelAnnouncement, full_msg: Option<&msgs::ChannelAnnouncement>, + pending_state: &UtxoMessages, + ) -> bool { + match &pending_state.channel_announce { + Some(ChannelAnnouncement::Full(pending_msg)) => Some(pending_msg) == full_msg, + Some(ChannelAnnouncement::Unsigned(pending_msg)) => pending_msg == msg, + None => { + // This can be reached if `resolve_single_future` has already consumed + // `channel_announce` via `.take()` while the `Arc<Mutex<UtxoMessages>>` is still + // alive (e.g. held on the stack of `check_resolved_futures`). In that case, + // `complete` should also have been taken. Treat it as non-matching and let the + // new request fly. + debug_assert!( + pending_state.complete.is_none(), + "channel_announce is None but complete is still pending" + ); + false + }, + } + } + fn check_replace_previous_entry( msg: &msgs::UnsignedChannelAnnouncement, full_msg: Option<&msgs::ChannelAnnouncement>, - replacement: Option<Weak<Mutex<UtxoMessages>>>, + replacement: Option<(&Arc<Mutex<UtxoMessages>>, &UtxoMessages)>, pending_channels: &mut HashMap<u64, Weak<Mutex<UtxoMessages>>>, ) -> Result<(), msgs::LightningError> { + let replacement_state = replacement.map(|(state, _)| state); match pending_channels.entry(msg.short_channel_id) { hash_map::Entry::Occupied(mut e) => { // There's already a pending lookup for the given SCID. Check if the messages @@ -305,24 +328,32 @@ impl PendingChecks { // lookup if we haven't gotten that far yet). match Weak::upgrade(&e.get()) { Some(pending_msgs) => { - // This may be called with the mutex held on a different UtxoMessages - // struct, however in that case we have a global lockorder of new messages - // -> old messages, which makes this safe. - let pending_matches = match &pending_msgs - .unsafe_well_ordered_double_lock_self() - .channel_announce - { - Some(ChannelAnnouncement::Full(pending_msg)) => { - Some(pending_msg) == full_msg + let pending_matches = match replacement { + Some((replacement, replacement_messages)) + if Arc::ptr_eq(&pending_msgs, replacement) => + { + // The pending entry points to the state whose mutex the caller + // already holds. Compare through the held guard instead of locking + // it again. + Self::pending_channel_announcement_matches( + msg, + full_msg, + replacement_messages, + ) }, - Some(ChannelAnnouncement::Unsigned(pending_msg)) => pending_msg == msg, - None => { - // This shouldn't actually be reachable. We set the - // `channel_announce` field under the same lock as setting the - // channel map entry. Still, we can just treat it as - // non-matching and let the new request fly. - debug_assert!(false); - false + _ => { + // This may be called with the mutex held on a different + // UtxoMessages struct, however in that case we have a global + // lockorder of new messages -> old messages, which makes this safe. + let pending_state = + pending_msgs.unsafe_well_ordered_double_lock_self(); + let matches = Self::pending_channel_announcement_matches( + msg, + full_msg, + &pending_state, + ); + drop(pending_state); + matches }, }; if pending_matches { @@ -336,16 +367,16 @@ impl PendingChecks { // Note that in the replace case whether to replace is somewhat // arbitrary - both results will be handled, we're just updating the // value that will be compared to future lookups with the same SCID. - if let Some(item) = replacement { - *e.get_mut() = item; + if let Some(item) = replacement_state { + *e.get_mut() = Arc::downgrade(item); } } }, None => { // The earlier lookup already resolved. We can't be sure its the same // so just remove/replace it and move on. - if let Some(item) = replacement { - *e.get_mut() = item; + if let Some(item) = replacement_state { + *e.get_mut() = Arc::downgrade(item); } else { e.remove(); } @@ -353,8 +384,8 @@ impl PendingChecks { } }, hash_map::Entry::Vacant(v) => { - if let Some(item) = replacement { - v.insert(item); + if let Some(item) = replacement_state { + v.insert(Arc::downgrade(item)); } }, } @@ -438,7 +469,7 @@ impl PendingChecks { Self::check_replace_previous_entry( msg, full_msg, - Some(Arc::downgrade(&future.state)), + Some((&future.state, &async_messages)), &mut pending_checks.channels, )?; async_messages.channel_announce = Some(if let Some(msg) = full_msg { @@ -1024,6 +1055,56 @@ mod tests { assert!(!is_test_feature_set); } + #[test] + fn test_no_deadlock_same_future_different_announcement() { + // A user's UtxoLookup may return the same UtxoFuture for repeated lookups for a + // given SCID. A different channel_announcement with that SCID should replace the + // pending message without re-locking the already-held future state. + let (valid_announcement, chain_source, network_graph, good_script, ..) = get_test_objects(); + let scid = valid_announcement.contents.short_channel_id; + + let notifier = Arc::new(Notifier::new()); + let future = UtxoFuture::new(Arc::clone(¬ifier)); + *chain_source.utxo_ret.lock().unwrap() = UtxoResult::Async(future.clone()); + + assert_eq!( + network_graph + .update_channel_from_announcement(&valid_announcement, &Some(&chain_source)) + .unwrap_err() + .err, + "Channel being checked async" + ); + assert_eq!(chain_source.get_utxo_call_count.load(Ordering::Relaxed), 1); + + let secp_ctx = Secp256k1::new(); + let replacement_pk_1 = &SecretKey::from_slice(&[99; 32]).unwrap(); + let replacement_pk_2 = &SecretKey::from_slice(&[98; 32]).unwrap(); + let replacement_announcement = get_signed_channel_announcement( + |msg| msg.features.set_unknown_feature_optional(), + replacement_pk_1, + replacement_pk_2, + &secp_ctx, + ); + assert_eq!( + network_graph + .update_channel_from_announcement(&replacement_announcement, &Some(&chain_source)) + .unwrap_err() + .err, + "Channel being checked async" + ); + assert_eq!(chain_source.get_utxo_call_count.load(Ordering::Relaxed), 2); + + future + .resolve(Ok(TxOut { value: Amount::from_sat(1_000_000), script_pubkey: good_script })); + assert!(notifier.notify_pending()); + network_graph.pending_checks.check_resolved_futures(&network_graph); + #[rustfmt::skip] + let is_replacement_feature_set = + network_graph.read_only().channels().get(&scid).unwrap().announcement_message + .as_ref().unwrap().contents.features.supports_unknown_test_feature(); + assert!(is_replacement_feature_set); + } + #[test] fn test_checks_backpressure() { // Test that too_many_checks_pending returns true when there are many checks pending, and diff --git a/lightning/src/sign/ecdsa.rs b/lightning/src/sign/ecdsa.rs index e13285722af..c0bd3759caa 100644 --- a/lightning/src/sign/ecdsa.rs +++ b/lightning/src/sign/ecdsa.rs @@ -254,8 +254,14 @@ pub trait EcdsaChannelSigner: ChannelSigner { /// /// `input_index`: The index of the input within the new funding transaction `tx`, /// spending the previous funding transaction's output + /// + /// An `Err` can be returned to signal that the signer is unavailable/cannot produce a valid + /// signature and should be retried later. Once the signer is ready to provide a signature after + /// previously returning an `Err`, [`ChannelManager::signer_unblocked`] must be called. + /// + /// [`ChannelManager::signer_unblocked`]: crate::ln::channelmanager::ChannelManager::signer_unblocked fn sign_splice_shared_input( &self, channel_parameters: &ChannelTransactionParameters, tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Signature; + ) -> Result<Signature, ()>; } diff --git a/lightning/src/sign/mod.rs b/lightning/src/sign/mod.rs index 84bfbb902ea..f2907ae12a8 100644 --- a/lightning/src/sign/mod.rs +++ b/lightning/src/sign/mod.rs @@ -38,7 +38,7 @@ use bitcoin::{secp256k1, Psbt, Sequence, Txid, WPubkeyHash, Witness}; use lightning_invoice::RawBolt11Invoice; use crate::chain::transaction::OutPoint; -use crate::crypto::utils::{hkdf_extract_expand_twice, sign, sign_with_aux_rand}; +use crate::crypto::utils::{apply_chacha20, hkdf_extract_expand_twice, sign, sign_with_aux_rand}; use crate::ln::chan_utils; use crate::ln::chan_utils::{ get_countersigner_payment_script, get_revokeable_redeemscript, make_funding_redeemscript, @@ -51,36 +51,25 @@ use crate::ln::channel_keys::{ RevocationBasepoint, RevocationKey, }; use crate::ln::inbound_payment::ExpandedKey; -#[cfg(taproot)] -use crate::ln::msgs::PartialSignatureWithNonce; use crate::ln::msgs::{UnsignedChannelAnnouncement, UnsignedGossipMessage}; use crate::ln::script::ShutdownScript; use crate::offers::invoice::UnsignedBolt12Invoice; use crate::types::features::ChannelTypeFeatures; use crate::types::payment::PaymentPreimage; -use crate::util::async_poll::MaybeSend; +use crate::util::native_async::MaybeSend; use crate::util::ser::{ReadableArgs, Writeable}; use crate::util::transaction_utils; -use crate::crypto::chacha20::ChaCha20; use crate::prelude::*; use crate::sign::ecdsa::EcdsaChannelSigner; -#[cfg(taproot)] -use crate::sign::taproot::TaprootChannelSigner; use crate::util::atomic_counter::AtomicCounter; use core::convert::TryInto; use core::future::Future; use core::ops::Deref; use core::sync::atomic::{AtomicUsize, Ordering}; -#[cfg(taproot)] -use musig2::types::{PartialSignature, PublicNonce}; - -pub(crate) mod type_resolver; pub mod ecdsa; -#[cfg(taproot)] -pub mod taproot; pub mod tx_builder; pub(crate) const COMPRESSED_PUBLIC_KEY_SIZE: usize = bitcoin::secp256k1::constants::PUBLIC_KEY_SIZE; @@ -120,17 +109,24 @@ pub struct DelayedPaymentOutputDescriptor { impl DelayedPaymentOutputDescriptor { /// The maximum length a well-formed witness spending one of these should have. /// + /// This depends on the descriptor's [`to_self_delay`], whose `OP_CSV` push in the revocable + /// redeemscript varies in length. + /// /// Note: If you have the `grind_signatures` feature enabled, this will be at least 1 byte /// shorter. - pub const MAX_WITNESS_LENGTH: u64 = (1 /* witness items */ - + 1 /* sig push */ - + MAX_STANDARD_SIGNATURE_SIZE - + 1 /* empty vec push */ - + 1 /* redeemscript push */ - + chan_utils::REVOKEABLE_REDEEMSCRIPT_MAX_LENGTH) as u64; + /// + /// [`to_self_delay`]: Self::to_self_delay + pub fn max_witness_length(&self) -> u64 { + (1 /* witness items */ + + 1 /* sig push */ + + MAX_STANDARD_SIGNATURE_SIZE + + 1 /* empty vec push */ + + 1 /* redeemscript push */ + + chan_utils::revokeable_redeemscript_len(self.to_self_delay)) as u64 + } } -impl_writeable_tlv_based!(DelayedPaymentOutputDescriptor, { +impl_ser_tlv_based!(DelayedPaymentOutputDescriptor, { (0, outpoint, required), (2, per_commitment_point, required), (4, to_self_delay, required), @@ -228,7 +224,7 @@ impl StaticPaymentOutputDescriptor { chan_params.is_some_and(|p| p.channel_type_features.supports_anchors_zero_fee_htlc_tx()) } } -impl_writeable_tlv_based!(StaticPaymentOutputDescriptor, { +impl_ser_tlv_based!(StaticPaymentOutputDescriptor, { (0, outpoint, required), (2, output, required), (4, channel_keys_id, required), @@ -331,7 +327,7 @@ pub enum SpendableOutputDescriptor { StaticPaymentOutput(StaticPaymentOutputDescriptor), } -impl_writeable_tlv_based_enum_legacy!(SpendableOutputDescriptor, +impl_ser_tlv_based_enum_legacy!(SpendableOutputDescriptor, (0, StaticOutput) => { (0, outpoint, required), (1, channel_keys_id, option), @@ -513,7 +509,7 @@ impl SpendableOutputDescriptor { sequence: Sequence(descriptor.to_self_delay as u32), witness: Witness::new(), }); - witness_weight += DelayedPaymentOutputDescriptor::MAX_WITNESS_LENGTH; + witness_weight += descriptor.max_witness_length(); #[cfg(feature = "grind_signatures")] { // Guarantees a low R signature @@ -593,7 +589,7 @@ pub struct ChannelDerivationParameters { pub transaction_parameters: ChannelTransactionParameters, } -impl_writeable_tlv_based!(ChannelDerivationParameters, { +impl_ser_tlv_based!(ChannelDerivationParameters, { (0, value_satoshis, required), (2, keys_id, required), (4, transaction_parameters, (required: ReadableArgs, Some(value_satoshis.0.unwrap()))), @@ -627,7 +623,7 @@ pub struct HTLCDescriptor { pub counterparty_sig: Signature, } -impl_writeable_tlv_based!(HTLCDescriptor, { +impl_ser_tlv_based!(HTLCDescriptor, { (0, channel_derivation_parameters, required), (1, feerate_per_kw, (default_value, 0)), (2, commitment_txid, required), @@ -757,9 +753,12 @@ pub trait ChannelSigner { /// /// Note that the commitment number starts at `(1 << 48) - 1` and counts backwards. /// - /// This method is *not* asynchronous. This method is expected to always return `Ok` - /// immediately after we reconnect to peers, and returning an `Err` may lead to an immediate - /// `panic`. This method will be made asynchronous in a future release. + /// An `Err` can be returned to signal that the signer is unavailable/cannot produce a new + /// commitment point and should be retried later. Once the signer is ready to provide a new + /// commitment point after previously returning an `Err`, [`ChannelManager::signer_unblocked`] + /// must be called. + /// + /// [`ChannelManager::signer_unblocked`]: crate::ln::channelmanager::ChannelManager::signer_unblocked fn get_per_commitment_point( &self, idx: u64, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<PublicKey, ()>; @@ -1084,18 +1083,7 @@ impl<T: OutputSpender + ?Sized, O: Deref<Target = T>> OutputSpender for O { /// A dynamic [`SignerProvider`] temporarily needed for doc tests. /// /// This is not exported to bindings users as it is not intended for public consumption. -#[cfg(taproot)] #[doc(hidden)] -#[deprecated(note = "Remove once taproot cfg is removed")] -pub type DynSignerProvider = - dyn SignerProvider<EcdsaSigner = InMemorySigner, TaprootSigner = InMemorySigner>; - -/// A dynamic [`SignerProvider`] temporarily needed for doc tests. -/// -/// This is not exported to bindings users as it is not intended for public consumption. -#[cfg(not(taproot))] -#[doc(hidden)] -#[deprecated(note = "Remove once taproot cfg is removed")] pub type DynSignerProvider = dyn SignerProvider<EcdsaSigner = InMemorySigner>; /// A trait that can return signer instances for individual channels. @@ -1107,11 +1095,9 @@ pub type DynSignerProvider = dyn SignerProvider<EcdsaSigner = InMemorySigner>; /// [`ChannelManager`]: crate::ln::channelmanager::ChannelManager /// [`MonitorUpdatingPersister`]: crate::util::persist::MonitorUpdatingPersister pub trait SignerProvider { - /// A type which implements [`EcdsaChannelSigner`] which will be returned by [`Self::derive_channel_signer`]. + /// A type which implements [`EcdsaChannelSigner`] which will be returned by + /// [`Self::derive_channel_signer`]. type EcdsaSigner: EcdsaChannelSigner; - #[cfg(taproot)] - /// A type which implements [`TaprootChannelSigner`] - type TaprootSigner: TaprootChannelSigner; /// Generates a unique `channel_keys_id` that can be used to obtain a [`Self::EcdsaSigner`] through /// [`SignerProvider::derive_channel_signer`]. The `user_channel_id` is provided to allow @@ -1151,8 +1137,6 @@ pub trait SignerProvider { impl<T: SignerProvider + ?Sized, SP: Deref<Target = T>> SignerProvider for SP { type EcdsaSigner = T::EcdsaSigner; - #[cfg(taproot)] - type TaprootSigner = T::TaprootSigner; fn generate_channel_keys_id(&self, inbound: bool, user_channel_id: u128) -> [u8; 32] { self.deref().generate_channel_keys_id(inbound, user_channel_id) @@ -1953,7 +1937,7 @@ impl EcdsaChannelSigner for InMemorySigner { fn sign_splice_shared_input( &self, channel_parameters: &ChannelTransactionParameters, tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Signature { + ) -> Result<Signature, ()> { assert!(channel_parameters.is_populated(), "Channel parameters must be fully populated"); assert_eq!( tx.input[input_index].previous_output, @@ -1979,66 +1963,7 @@ impl EcdsaChannelSigner for InMemorySigner { ) .unwrap()[..]; let msg = hash_to_message!(sighash); - sign(secp_ctx, &msg, &funding_key) - } -} - -#[cfg(taproot)] -#[allow(unused)] -impl TaprootChannelSigner for InMemorySigner { - fn generate_local_nonce_pair( - &self, commitment_number: u64, secp_ctx: &Secp256k1<All>, - ) -> PublicNonce { - todo!() - } - - fn partially_sign_counterparty_commitment( - &self, counterparty_nonce: PublicNonce, commitment_tx: &CommitmentTransaction, - inbound_htlc_preimages: Vec<PaymentPreimage>, - outbound_htlc_preimages: Vec<PaymentPreimage>, secp_ctx: &Secp256k1<All>, - ) -> Result<(PartialSignatureWithNonce, Vec<schnorr::Signature>), ()> { - todo!() - } - - fn finalize_holder_commitment( - &self, commitment_tx: &HolderCommitmentTransaction, - counterparty_partial_signature: PartialSignatureWithNonce, secp_ctx: &Secp256k1<All>, - ) -> Result<PartialSignature, ()> { - todo!() - } - - fn sign_justice_revoked_output( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - secp_ctx: &Secp256k1<All>, - ) -> Result<schnorr::Signature, ()> { - todo!() - } - - fn sign_justice_revoked_htlc( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<All>, - ) -> Result<schnorr::Signature, ()> { - todo!() - } - - fn sign_holder_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, htlc_descriptor: &HTLCDescriptor, - secp_ctx: &Secp256k1<All>, - ) -> Result<schnorr::Signature, ()> { - todo!() - } - - fn sign_counterparty_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, amount: u64, per_commitment_point: &PublicKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<All>, - ) -> Result<schnorr::Signature, ()> { - todo!() - } - - fn partially_sign_closing_transaction( - &self, closing_tx: &ClosingTransaction, secp_ctx: &Secp256k1<All>, - ) -> Result<PartialSignature, ()> { - todo!() + Ok(sign(secp_ctx, &msg, &funding_key)) } } @@ -2548,8 +2473,6 @@ impl OutputSpender for KeysManager { impl SignerProvider for KeysManager { type EcdsaSigner = InMemorySigner; - #[cfg(taproot)] - type TaprootSigner = InMemorySigner; fn generate_channel_keys_id(&self, _inbound: bool, user_channel_id: u128) -> [u8; 32] { let child_idx = self.channel_child_index.fetch_add(1, Ordering::AcqRel); @@ -2697,8 +2620,6 @@ impl OutputSpender for PhantomKeysManager { impl SignerProvider for PhantomKeysManager { type EcdsaSigner = InMemorySigner; - #[cfg(taproot)] - type TaprootSigner = InMemorySigner; fn generate_channel_keys_id(&self, inbound: bool, user_channel_id: u128) -> [u8; 32] { self.inner.generate_channel_keys_id(inbound, user_channel_id) @@ -2791,7 +2712,9 @@ impl EntropySource for RandomBytes { let index = self.index.next(); let mut nonce = [0u8; 16]; nonce[..8].copy_from_slice(&index.to_be_bytes()); - ChaCha20::get_single_block(&self.seed, &nonce) + let mut chacha_bytes = [0; 32]; + apply_chacha20(self.seed, nonce, &mut chacha_bytes); + chacha_bytes } } @@ -2801,6 +2724,71 @@ pub fn dyn_sign() { let _signer: Box<dyn EcdsaChannelSigner>; } +// Regression test: the sweep-weight estimate for a `to_local` (`DelayedPaymentOutput`) output must +// reflect the channel's `to_self_delay`. +// +// The revocable redeemscript encodes `to_self_delay` with an `OP_CSV` push that can vary in size +// from 1 byte (for `to_self_delay <= 16`) up to 4 bytes. `create_spendable_outputs_psbt` used to +// estimate every such output with the maximum 4-byte push, overshooting the real sweep weight by up +// to 3 WU for a small `to_self_delay`. If this occurred along with a short signature, an assertion +// would fail in `KeysManager::spend_spendable_outputs`. +#[test] +fn sweep_weight_estimate_accounts_for_to_self_delay() { + let secp_ctx = Secp256k1::new(); + let per_commitment_point = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[1u8; 32]).unwrap()); + let delayed_payment_key = DelayedPaymentKey(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[3u8; 32]).unwrap(), + )); + let revocation_pubkey = RevocationKey(PublicKey::from_secret_key( + &secp_ctx, + &SecretKey::from_slice(&[2u8; 32]).unwrap(), + )); + let change_script = ScriptBuf::new_p2wpkh(&WPubkeyHash::from_byte_array([7u8; 20])); + + let estimate = |to_self_delay: u16| { + let witness_script = + get_revokeable_redeemscript(&revocation_pubkey, to_self_delay, &delayed_payment_key); + let descriptor = + SpendableOutputDescriptor::DelayedPaymentOutput(DelayedPaymentOutputDescriptor { + outpoint: OutPoint { txid: Txid::from_byte_array([1u8; 32]), index: 0 }, + per_commitment_point, + to_self_delay, + output: TxOut { + value: Amount::from_sat(1_000_000), + script_pubkey: witness_script.to_p2wsh(), + }, + revocation_pubkey, + channel_keys_id: [1u8; 32], + channel_value_satoshis: 1_000_000, + channel_transaction_parameters: None, + }); + SpendableOutputDescriptor::create_spendable_outputs_psbt( + &secp_ctx, + &[&descriptor], + vec![], + change_script.clone(), + 253, + None, + ) + .unwrap() + .1 + }; + + // The estimate should adjust according to the `to_self_delay` push length. + let max_estimate = estimate(65_535); // 4-byte `OP_CSV` push + for (to_self_delay, push_len) in + [(0u16, 1u64), (16, 1), (17, 2), (127, 2), (128, 3), (32_767, 3), (32_768, 4), (65_535, 4)] + { + assert_eq!( + estimate(to_self_delay), + max_estimate - (4 - push_len), + "wrong sweep-weight estimate for to_self_delay={to_self_delay}", + ); + } +} + #[cfg(ldk_bench)] pub mod benches { use crate::sign::{EntropySource, KeysManager}; diff --git a/lightning/src/sign/taproot.rs b/lightning/src/sign/taproot.rs deleted file mode 100644 index 22470f4f8b6..00000000000 --- a/lightning/src/sign/taproot.rs +++ /dev/null @@ -1,155 +0,0 @@ -//! Defines a Taproot-specific signer type. - -use alloc::vec::Vec; -use bitcoin::secp256k1; -use bitcoin::secp256k1::{schnorr::Signature, PublicKey, Secp256k1, SecretKey}; -use bitcoin::transaction::Transaction; - -use musig2::types::{PartialSignature, PublicNonce}; - -use crate::ln::chan_utils::{ - ClosingTransaction, CommitmentTransaction, HTLCOutputInCommitment, HolderCommitmentTransaction, -}; -use crate::ln::msgs::PartialSignatureWithNonce; -use crate::sign::{ChannelSigner, HTLCDescriptor}; -use crate::types::payment::PaymentPreimage; - -/// A Taproot-specific signer type that defines signing-related methods that are either unique to -/// Taproot or have argument or return types that differ from the ones an ECDSA signer would be -/// expected to have. -pub trait TaprootChannelSigner: ChannelSigner { - /// Generate a local nonce pair, which requires committing to ahead of time. - /// The counterparty needs the public nonce generated herein to compute a partial signature. - fn generate_local_nonce_pair( - &self, commitment_number: u64, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> PublicNonce; - - /// Create a signature for a counterparty's commitment transaction and associated HTLC transactions. - /// - /// Note that if signing fails or is rejected, the channel will be force-closed. - /// - /// Policy checks should be implemented in this function, including checking the amount - /// sent to us and checking the HTLCs. - /// - /// The preimages of outbound and inbound HTLCs that were fulfilled since the last commitment - /// are provided. A validating signer should ensure that an outbound HTLC output is removed - /// only when the matching preimage is provided and after the corresponding inbound HTLC has - /// been removed for forwarded payments. - /// - /// Note that all the relevant preimages will be provided, but there may also be additional - /// irrelevant or duplicate preimages. - // - // TODO: Document the things someone using this interface should enforce before signing. - fn partially_sign_counterparty_commitment( - &self, counterparty_nonce: PublicNonce, commitment_tx: &CommitmentTransaction, - inbound_htlc_preimages: Vec<PaymentPreimage>, - outbound_htlc_preimages: Vec<PaymentPreimage>, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Result<(PartialSignatureWithNonce, Vec<Signature>), ()>; - - /// Creates a signature for a holder's commitment transaction. - /// - /// This will be called - /// - with a non-revoked `commitment_tx`. - /// - with the latest `commitment_tx` when we initiate a force-close. - /// - /// This may be called multiple times for the same transaction. - /// - /// An external signer implementation should check that the commitment has not been revoked. - /// - // TODO: Document the things someone using this interface should enforce before signing. - fn finalize_holder_commitment( - &self, commitment_tx: &HolderCommitmentTransaction, - counterparty_partial_signature: PartialSignatureWithNonce, - secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Result<PartialSignature, ()>; - - /// Create a signature for the given input in a transaction spending an HTLC transaction output - /// or a commitment transaction `to_local` output when our counterparty broadcasts an old state. - /// - /// A justice transaction may claim multiple outputs at the same time if timelocks are - /// similar, but only a signature for the input at index `input` should be signed for here. - /// It may be called multiple times for same output(s) if a fee-bump is needed with regards - /// to an upcoming timelock expiration. - /// - /// Amount is value of the output spent by this input, committed to in the BIP 341 signature. - /// - /// `per_commitment_key` is revocation secret which was provided by our counterparty when they - /// revoked the state which they eventually broadcast. It's not a _holder_ secret key and does - /// not allow the spending of any funds by itself (you need our holder `revocation_secret` to do - /// so). - fn sign_justice_revoked_output( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Result<Signature, ()>; - - /// Create a signature for the given input in a transaction spending a commitment transaction - /// HTLC output when our counterparty broadcasts an old state. - /// - /// A justice transaction may claim multiple outputs at the same time if timelocks are - /// similar, but only a signature for the input at index `input` should be signed for here. - /// It may be called multiple times for same output(s) if a fee-bump is needed with regards - /// to an upcoming timelock expiration. - /// - /// `amount` is the value of the output spent by this input, committed to in the BIP 341 - /// signature. - /// - /// `per_commitment_key` is revocation secret which was provided by our counterparty when they - /// revoked the state which they eventually broadcast. It's not a _holder_ secret key and does - /// not allow the spending of any funds by itself (you need our holder revocation_secret to do - /// so). - /// - /// `htlc` holds HTLC elements (hash, timelock), thus changing the format of the witness script - /// (which is committed to in the BIP 341 signatures). - fn sign_justice_revoked_htlc( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Result<Signature, ()>; - - /// Computes the signature for a commitment transaction's HTLC output used as an input within - /// `htlc_tx`, which spends the commitment transaction at index `input`. The signature returned - /// must be be computed using [`TapSighashType::Default`]. - /// - /// Note that this may be called for HTLCs in the penultimate commitment transaction if a - /// [`ChannelMonitor`] [replica](https://github.com/lightningdevkit/rust-lightning/blob/main/GLOSSARY.md#monitor-replicas) - /// broadcasts it before receiving the update for the latest commitment transaction. - /// - /// - /// [`TapSighashType::Default`]: bitcoin::sighash::TapSighashType::Default - /// [`ChannelMonitor`]: crate::chain::channelmonitor::ChannelMonitor - fn sign_holder_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, htlc_descriptor: &HTLCDescriptor, - secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Result<Signature, ()>; - - /// Create a signature for a claiming transaction for a HTLC output on a counterparty's commitment - /// transaction, either offered or received. - /// - /// Such a transaction may claim multiples offered outputs at same time if we know the - /// preimage for each when we create it, but only the input at index `input` should be - /// signed for here. It may be called multiple times for same output(s) if a fee-bump is - /// needed with regards to an upcoming timelock expiration. - /// - /// `witness_script` is either an offered or received script as defined in BOLT3 for HTLC - /// outputs. - /// - /// `amount` is value of the output spent by this input, committed to in the BIP 341 signature. - /// - /// `per_commitment_point` is the dynamic point corresponding to the channel state - /// detected onchain. It has been generated by our counterparty and is used to derive - /// channel state keys, which are then included in the witness script and committed to in the - /// BIP 341 signature. - fn sign_counterparty_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, amount: u64, per_commitment_point: &PublicKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Result<Signature, ()>; - - /// Create a signature for a (proposed) closing transaction. - /// - /// Note that, due to rounding, there may be one "missing" satoshi, and either party may have - /// chosen to forgo their output as dust. - fn partially_sign_closing_transaction( - &self, closing_tx: &ClosingTransaction, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Result<PartialSignature, ()>; - - // TODO: sign channel announcement -} diff --git a/lightning/src/sign/tx_builder.rs b/lightning/src/sign/tx_builder.rs index 27b8b1a9a2b..8f699fc85aa 100644 --- a/lightning/src/sign/tx_builder.rs +++ b/lightning/src/sign/tx_builder.rs @@ -1,5 +1,4 @@ //! Defines the `TxBuilder` trait, and the `SpecTxBuilder` type -#![allow(dead_code)] use core::cmp; @@ -10,7 +9,10 @@ use crate::ln::chan_utils::{ second_stage_tx_fees_sat, ChannelTransactionParameters, CommitmentTransaction, HTLCOutputInCommitment, }; -use crate::ln::channel::{CommitmentStats, ANCHOR_OUTPUT_VALUE_SATOSHI}; +use crate::ln::channel::{ + get_v2_channel_reserve_satoshis, CommitmentStats, ANCHOR_OUTPUT_VALUE_SATOSHI, + FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE, MIN_CHANNEL_VALUE_SATOSHIS, +}; use crate::prelude::*; use crate::types::features::ChannelTypeFeatures; use crate::util::logger::Logger; @@ -34,35 +36,18 @@ impl HTLCAmountDirection { } pub(crate) struct NextCommitmentStats { - pub is_outbound_from_holder: bool, - pub inbound_htlcs_count: usize, - pub inbound_htlcs_value_msat: u64, - pub holder_balance_before_fee_msat: u64, - pub counterparty_balance_before_fee_msat: u64, + pub holder_balance_msat: u64, + pub counterparty_balance_msat: u64, + pub dust_exposure_msat: u64, + #[cfg(any(test, fuzzing))] pub nondust_htlc_count: usize, + #[cfg(any(test, fuzzing))] pub commit_tx_fee_sat: u64, - pub dust_exposure_msat: u64, - pub extra_accepted_htlc_dust_exposure_msat: u64, } -impl NextCommitmentStats { - pub(crate) fn get_holder_counterparty_balances_incl_fee_msat(&self) -> Result<(u64, u64), ()> { - if self.is_outbound_from_holder { - Ok(( - self.holder_balance_before_fee_msat - .checked_sub(self.commit_tx_fee_sat * 1000) - .ok_or(())?, - self.counterparty_balance_before_fee_msat, - )) - } else { - Ok(( - self.holder_balance_before_fee_msat, - self.counterparty_balance_before_fee_msat - .checked_sub(self.commit_tx_fee_sat * 1000) - .ok_or(())?, - )) - } - } +pub(crate) struct ChannelStats { + pub commitment_stats: NextCommitmentStats, + pub available_balances: crate::ln::channel::AvailableBalances, } fn commit_plus_htlc_tx_fees_msat( @@ -113,33 +98,33 @@ fn commit_plus_htlc_tx_fees_msat( (total_fees_msat, extra_accepted_htlc_total_fees_msat) } -fn subtract_addl_outputs( - is_outbound_from_holder: bool, value_to_self_after_htlcs_msat: u64, - value_to_remote_after_htlcs_msat: u64, channel_type: &ChannelTypeFeatures, -) -> Result<(u64, u64), ()> { - let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() { +fn total_anchors_sat(channel_type: &ChannelTypeFeatures) -> u64 { + if channel_type.supports_anchors_zero_fee_htlc_tx() { ANCHOR_OUTPUT_VALUE_SATOSHI * 2 } else { 0 - }; + } +} - // We MUST use checked subs here, as the funder's balance is not guaranteed to be greater - // than or equal to `total_anchors_sat`. - // - // This is because when the remote party sends an `update_fee` message, we build the new - // commitment transaction *before* checking whether the remote party's balance is enough to - // cover the total anchor sum. +fn checked_sub_from_funder( + is_outbound_from_holder: bool, value_to_holder: u64, value_to_counterparty: u64, + value_to_subtract: u64, +) -> Result<(u64, u64), ()> { + if is_outbound_from_holder { + Ok((value_to_holder.checked_sub(value_to_subtract).ok_or(())?, value_to_counterparty)) + } else { + Ok((value_to_holder, value_to_counterparty.checked_sub(value_to_subtract).ok_or(())?)) + } +} +fn saturating_sub_from_funder( + is_outbound_from_holder: bool, value_to_holder: u64, value_to_counterparty: u64, + value_to_subtract: u64, +) -> (u64, u64) { if is_outbound_from_holder { - Ok(( - value_to_self_after_htlcs_msat.checked_sub(total_anchors_sat * 1000).ok_or(())?, - value_to_remote_after_htlcs_msat, - )) + (value_to_holder.saturating_sub(value_to_subtract), value_to_counterparty) } else { - Ok(( - value_to_self_after_htlcs_msat, - value_to_remote_after_htlcs_msat.checked_sub(total_anchors_sat * 1000).ok_or(())?, - )) + (value_to_holder, value_to_counterparty.saturating_sub(value_to_subtract)) } } @@ -153,171 +138,846 @@ fn get_dust_buffer_feerate(feerate_per_kw: u32) -> u32 { cmp::max(feerate_per_kw.saturating_add(2530), feerate_plus_quarter.unwrap_or(u32::MAX)) } -pub(crate) trait TxBuilder { - fn get_next_commitment_stats( - &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, - value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection], - addl_nondust_htlc_count: usize, feerate_per_kw: u32, - dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64, - channel_type: &ChannelTypeFeatures, - ) -> Result<NextCommitmentStats, ()>; - fn commit_tx_fee_sat( - &self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures, - ) -> u64; - fn subtract_non_htlc_outputs( - &self, is_outbound_from_holder: bool, value_to_self_after_htlcs: u64, - value_to_remote_after_htlcs: u64, channel_type: &ChannelTypeFeatures, - ) -> (u64, u64); - fn build_commitment_transaction<L: Logger>( - &self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey, - channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>, - value_to_self_msat: u64, htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32, - broadcaster_dust_limit_satoshis: u64, logger: &L, - ) -> (CommitmentTransaction, CommitmentStats); +#[derive(Clone, Copy, Debug)] +pub(crate) struct ChannelConstraints { + pub holder_dust_limit_satoshis: u64, + pub counterparty_selected_channel_reserve_satoshis: u64, + pub counterparty_dust_limit_satoshis: u64, + pub holder_selected_channel_reserve_satoshis: u64, + pub counterparty_htlc_minimum_msat: u64, + pub counterparty_max_htlc_value_in_flight_msat: u64, + pub counterparty_max_accepted_htlcs: u64, } -pub(crate) struct SpecTxBuilder {} +fn get_dust_exposure_stats( + local: bool, commitment_htlcs: &[HTLCAmountDirection], feerate_per_kw: u32, + dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64, + channel_type: &ChannelTypeFeatures, +) -> (u64, Option<u64>) { + let excess_feerate = + feerate_per_kw.saturating_sub(dust_exposure_limiting_feerate.unwrap_or(feerate_per_kw)); + if channel_type.supports_anchor_zero_fee_commitments() { + debug_assert_eq!(feerate_per_kw, 0); + debug_assert_eq!(excess_feerate, 0); + } -impl TxBuilder for SpecTxBuilder { - fn get_next_commitment_stats( - &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, - value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection], - addl_nondust_htlc_count: usize, feerate_per_kw: u32, - dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64, - channel_type: &ChannelTypeFeatures, - ) -> Result<NextCommitmentStats, ()> { - let excess_feerate = - feerate_per_kw.saturating_sub(dust_exposure_limiting_feerate.unwrap_or(feerate_per_kw)); - if channel_type.supports_anchor_zero_fee_commitments() { - debug_assert_eq!(feerate_per_kw, 0); - debug_assert_eq!(excess_feerate, 0); - debug_assert_eq!(addl_nondust_htlc_count, 0); - } + // Increment the feerate by a buffer to calculate dust exposure + let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw); - // Calculate inbound htlc count - let inbound_htlcs_count = - next_commitment_htlcs.iter().filter(|htlc| !htlc.outbound).count(); - - // Calculate balances after htlcs - let value_to_counterparty_msat = - (channel_value_satoshis * 1000).checked_sub(value_to_holder_msat).ok_or(())?; - let outbound_htlcs_value_msat: u64 = next_commitment_htlcs - .iter() - .filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat)) - .sum(); - let inbound_htlcs_value_msat: u64 = next_commitment_htlcs - .iter() - .filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat)) - .sum(); - let value_to_holder_after_htlcs_msat = - value_to_holder_msat.checked_sub(outbound_htlcs_value_msat).ok_or(())?; - let value_to_counterparty_after_htlcs_msat = - value_to_counterparty_msat.checked_sub(inbound_htlcs_value_msat).ok_or(())?; - - // Subtract the anchors from the channel funder - let (holder_balance_before_fee_msat, counterparty_balance_before_fee_msat) = - subtract_addl_outputs( - is_outbound_from_holder, - value_to_holder_after_htlcs_msat, - value_to_counterparty_after_htlcs_msat, + // Calculate dust exposure on commitment transaction + let dust_exposure_msat = commitment_htlcs + .iter() + .filter_map(|htlc| { + htlc.is_dust(local, dust_buffer_feerate, broadcaster_dust_limit_satoshis, channel_type) + .then_some(htlc.amount_msat) + }) + .sum(); + + if local || excess_feerate == 0 { + (dust_exposure_msat, None) + } else { + // Add any excess fees to dust exposure on counterparty transactions + let (excess_fees_msat, extra_accepted_htlc_excess_fees_msat) = + commit_plus_htlc_tx_fees_msat( + local, + &commitment_htlcs, + dust_buffer_feerate, + excess_feerate, + broadcaster_dust_limit_satoshis, channel_type, - )?; - - // Increment the feerate by a buffer to calculate dust exposure - let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw); - - // Calculate fees on commitment transaction - let nondust_htlc_count = next_commitment_htlcs - .iter() - .filter(|htlc| { - !htlc.is_dust(local, feerate_per_kw, broadcaster_dust_limit_satoshis, channel_type) - }) - .count(); - let commit_tx_fee_sat = commit_tx_fee_sat( + ); + ( + dust_exposure_msat + excess_fees_msat, + Some(dust_exposure_msat + extra_accepted_htlc_excess_fees_msat), + ) + } +} + +fn has_output( + is_outbound_from_holder: bool, holder_balance_before_fee_msat: u64, + counterparty_balance_before_fee_msat: u64, feerate_per_kw: u32, nondust_htlc_count: usize, + broadcaster_dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures, +) -> bool { + let commit_tx_fee_sat = commit_tx_fee_sat(feerate_per_kw, nondust_htlc_count, channel_type); + let (holder_balance_msat, counterparty_balance_msat) = saturating_sub_from_funder( + is_outbound_from_holder, + holder_balance_before_fee_msat, + counterparty_balance_before_fee_msat, + commit_tx_fee_sat.saturating_mul(1000), + ); + + // Make sure the commitment transaction has at least one output + let dust_limit_msat = broadcaster_dust_limit_satoshis * 1000; + let has_no_output = holder_balance_msat < dust_limit_msat + && counterparty_balance_msat < dust_limit_msat + && nondust_htlc_count == 0 + // 0FC channels always have a P2A output on the commitment transaction + && !channel_type.supports_anchor_zero_fee_commitments(); + !has_no_output +} + +fn get_next_commitment_stats( + local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, + value_to_holder_msat: u64, next_commitment_htlcs: &[HTLCAmountDirection], + addl_nondust_htlc_count: usize, feerate_per_kw: u32, assume_fee_spike: bool, + dust_exposure_limiting_feerate: Option<u32>, broadcaster_dust_limit_satoshis: u64, + channel_type: &ChannelTypeFeatures, +) -> Result<NextCommitmentStats, ()> { + if channel_type.supports_anchor_zero_fee_commitments() { + debug_assert_eq!(feerate_per_kw, 0); + } + + // Calculate balances after htlcs + let value_to_counterparty_msat = + (channel_value_satoshis * 1000).checked_sub(value_to_holder_msat).ok_or(())?; + let outbound_htlcs_value_msat: u64 = next_commitment_htlcs + .iter() + .filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat)) + .sum(); + let inbound_htlcs_value_msat: u64 = next_commitment_htlcs + .iter() + .filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat)) + .sum(); + let value_to_holder_after_htlcs_msat = + value_to_holder_msat.checked_sub(outbound_htlcs_value_msat).ok_or(())?; + let value_to_counterparty_after_htlcs_msat = + value_to_counterparty_msat.checked_sub(inbound_htlcs_value_msat).ok_or(())?; + + // Subtract the anchors from the channel funder + + // We MUST use checked subs here, as the funder's balance is not guaranteed to be greater + // than or equal to `total_anchors_sat`. + // + // This is because when the remote party sends an `update_fee` message, we build the new + // commitment transaction *before* checking whether the remote party's balance is enough to + // cover the total anchor sum. + + let total_anchors_sat = total_anchors_sat(channel_type); + let (holder_balance_before_fee_msat, counterparty_balance_before_fee_msat) = + checked_sub_from_funder( + is_outbound_from_holder, + value_to_holder_after_htlcs_msat, + value_to_counterparty_after_htlcs_msat, + total_anchors_sat.saturating_mul(1000), + )?; + + let (dust_exposure_msat, _extra_accepted_htlc_dust_exposure_msat) = get_dust_exposure_stats( + local, + next_commitment_htlcs, + feerate_per_kw, + dust_exposure_limiting_feerate, + broadcaster_dust_limit_satoshis, + channel_type, + ); + + let spiked_feerate = if assume_fee_spike && !channel_type.supports_anchors_zero_fee_htlc_tx() { + feerate_per_kw.saturating_mul(FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32) + } else { + feerate_per_kw + }; + + let spiked_nondust_htlc_count = next_commitment_htlcs + .iter() + .filter(|htlc| { + !htlc.is_dust(local, spiked_feerate, broadcaster_dust_limit_satoshis, channel_type) + }) + .count(); + + // For zero-reserve channels, we check two things independently: + // 1) Given the current set of HTLCs and feerate, does the commitment have at least one output ? + // + // We only assume fee spikes in legacy channels, and we do not allow + // `holder_selected_channel_reserve_satoshis` to be set to zero in such channels. It is + // nonetheless still possible to reach the no-outputs case in a fee spike with solely the + // counterparty selected reserve set to zero, so we still guard against this case here. + // + // We don't guard against no-outputs under fee spikes further below in + // `get_available_balances`; in the worst case, the receiver of the HTLC we just sent fails + // it back. + if !has_output( + is_outbound_from_holder, + holder_balance_before_fee_msat, + counterparty_balance_before_fee_msat, + spiked_feerate, + spiked_nondust_htlc_count, + broadcaster_dust_limit_satoshis, + channel_type, + ) { + return Err(()); + } + + // 2) Now including any additional non-dust HTLCs (usually the fee spike buffer HTLC), does the funder cover + // this bigger transaction fee ? The funder can dip below their dust limit to cover this case, as the + // commitment will have at least one output: the non-dust fee spike buffer HTLC offered by the counterparty. + let nondust_htlc_count = next_commitment_htlcs + .iter() + .filter(|htlc| { + !htlc.is_dust(local, feerate_per_kw, broadcaster_dust_limit_satoshis, channel_type) + }) + .count(); + // Note here we use the htlc count at the current feerate together with the spiked feerate; + // this makes sure that the holder can afford any fee bump between 1x to 2x from the current + // feerate if the fee spike multiple is included. + let commit_tx_fee_sat = commit_tx_fee_sat( + spiked_feerate, + nondust_htlc_count + addl_nondust_htlc_count, + channel_type, + ); + let (holder_balance_msat, counterparty_balance_msat) = checked_sub_from_funder( + is_outbound_from_holder, + holder_balance_before_fee_msat, + counterparty_balance_before_fee_msat, + commit_tx_fee_sat.saturating_mul(1000), + )?; + + Ok(NextCommitmentStats { + holder_balance_msat, + counterparty_balance_msat, + dust_exposure_msat, + #[cfg(any(test, fuzzing))] + nondust_htlc_count: nondust_htlc_count + addl_nondust_htlc_count, + #[cfg(any(test, fuzzing))] + commit_tx_fee_sat, + }) +} + +/// Determines the maximum value that the holder can splice out of the channel, accounting +/// for the updated reserves after said splice. This maximum also makes sure the local commitment +/// retains at least one output after the splice, which is particularly relevant for +/// zero-reserve channels. +// +// The equation to determine `max_splice_percentage_constraint_sat` is: +// 1) floor((c - s) / 100) == h - s - d +// We want the maximum value of s that will satisfy equation 1, therefore, we solve: +// 2) (c - s) / 100 < h - s - d + 1 +// where c: `channel_value_satoshis` +// s: `max_splice_percentage_constraint_sat` +// h: `local_balance_before_fee_sat` +// d: `post_splice_delta_above_reserve_sat` +// This results in: +// 3) s < (100h + 100 - 100d - c) / 99 +fn get_next_splice_out_maximum_sat( + is_outbound_from_holder: bool, channel_value_satoshis: u64, local_balance_before_fee_msat: u64, + remote_balance_before_fee_msat: u64, local_nondust_htlc_count: usize, + remote_nondust_htlc_count: usize, feerate_per_kw: u32, spiked_feerate: u32, + channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures, +) -> u64 { + let post_splice_delta_above_reserve_sat = if is_outbound_from_holder { + let nondust_htlc_count = cmp::max(local_nondust_htlc_count, remote_nondust_htlc_count); + let commit_tx_fee_sat = + commit_tx_fee_sat(spiked_feerate, nondust_htlc_count + 1, channel_type); + commit_tx_fee_sat + } else { + 0 + }; + let local_balance_before_fee_sat = local_balance_before_fee_msat / 1000; + let mut next_splice_out_maximum_sat = if channel_constraints + .counterparty_selected_channel_reserve_satoshis + != 0 + { + let dividend_sat = local_balance_before_fee_sat + .saturating_mul(100) + .saturating_add(100) + .saturating_sub(post_splice_delta_above_reserve_sat.saturating_mul(100)) + .saturating_sub(channel_value_satoshis); + // Calculate the greatest integer that is strictly less than the RHS of inequality 3 above + let max_splice_percentage_constraint_sat = dividend_sat.saturating_sub(1) / 99; + let max_splice_dust_limit_constraint_sat = local_balance_before_fee_sat + .saturating_sub(channel_constraints.holder_dust_limit_satoshis) + .saturating_sub(post_splice_delta_above_reserve_sat); + // Both constraints must be satisfied, so take the minimum of the two maximums + let max_splice_out_sat = + cmp::min(max_splice_percentage_constraint_sat, max_splice_dust_limit_constraint_sat); + #[cfg(debug_assertions)] + if max_splice_out_sat == 0 { + let current_balance_sat = + local_balance_before_fee_sat.saturating_sub(post_splice_delta_above_reserve_sat); + let v2_reserve_sat = get_v2_channel_reserve_satoshis( + channel_value_satoshis, + channel_constraints.holder_dust_limit_satoshis, + false, + ) + .unwrap(); + // If the holder cannot splice out anything, they must be at or + // below the v2 reserve + debug_assert!(current_balance_sat <= v2_reserve_sat); + } else { + let post_splice_reserve_sat = get_v2_channel_reserve_satoshis( + channel_value_satoshis.saturating_sub(max_splice_out_sat), + channel_constraints.holder_dust_limit_satoshis, + false, + ) + .unwrap(); + // If the holder can splice out some maximum, splicing out that + // maximum lands them at exactly the new v2 reserve + the + // `post_splice_delta_above_reserve_sat` + debug_assert_eq!( + local_balance_before_fee_sat.saturating_sub(max_splice_out_sat), + post_splice_reserve_sat.saturating_add(post_splice_delta_above_reserve_sat) + ); + // Splice out an additional satoshi, and check that we are offside + let offside_splice_out_sat = max_splice_out_sat + 1; + let post_splice_reserve_sat_result = get_v2_channel_reserve_satoshis( + channel_value_satoshis.saturating_sub(offside_splice_out_sat), + channel_constraints.holder_dust_limit_satoshis, + false, + ); + match post_splice_reserve_sat_result { + Ok(reserve) => debug_assert!( + local_balance_before_fee_sat.saturating_sub(offside_splice_out_sat) + < reserve.saturating_add(post_splice_delta_above_reserve_sat) + ), + Err(()) => (), + } + } + max_splice_out_sat + } else { + // In a zero-reserve channel, the holder is free to withdraw up to its `post_splice_delta_above_reserve_sat`. + local_balance_before_fee_sat.saturating_sub(post_splice_delta_above_reserve_sat) + }; + + // If the current `next_splice_out_maximum_sat` would produce a local commitment with no + // outputs, bump this maximum such that, after the splice, the holder's balance covers at + // least `dust_limit_satoshis` and, if they are the funder, `current_tx_fee_sat`. + // We don't include an additional non-dust inbound HTLC in the `current_tx_fee_sat`, + // because we don't mind if the holder dips below their dust limit to cover the fee for that + // inbound non-dust HTLC. + // + // We use the regular feerate instead of the spiked feerate here as zero-reserve is not + // allowed on legacy channels. + let current_tx_fee_sat = commit_tx_fee_sat(feerate_per_kw, 0, channel_type); + let mut trim_splice_out_max_if_no_outputs = |nondust_htlc_count, dust_limit_satoshis| { + if !has_output( + is_outbound_from_holder, + local_balance_before_fee_msat.saturating_sub(next_splice_out_maximum_sat * 1000), + remote_balance_before_fee_msat, feerate_per_kw, - nondust_htlc_count + addl_nondust_htlc_count, + nondust_htlc_count, + dust_limit_satoshis, channel_type, - ); + ) { + let min_balance_sat = if is_outbound_from_holder { + dust_limit_satoshis.saturating_add(current_tx_fee_sat) + } else { + dust_limit_satoshis + }; + next_splice_out_maximum_sat = + (local_balance_before_fee_msat / 1000).saturating_sub(min_balance_sat); + } + }; + trim_splice_out_max_if_no_outputs( + local_nondust_htlc_count, + channel_constraints.holder_dust_limit_satoshis, + ); + trim_splice_out_max_if_no_outputs( + remote_nondust_htlc_count, + channel_constraints.counterparty_dust_limit_satoshis, + ); - // Calculate dust exposure on commitment transaction - let dust_exposure_msat = next_commitment_htlcs - .iter() - .filter_map(|htlc| { - htlc.is_dust( - local, - dust_buffer_feerate, - broadcaster_dust_limit_satoshis, - channel_type, - ) - .then_some(htlc.amount_msat) - }) - .sum(); + if channel_value_satoshis < next_splice_out_maximum_sat + MIN_CHANNEL_VALUE_SATOSHIS { + next_splice_out_maximum_sat = + channel_value_satoshis.saturating_sub(MIN_CHANNEL_VALUE_SATOSHIS); + } - // Add any excess fees to dust exposure on counterparty transactions - let (dust_exposure_msat, extra_accepted_htlc_dust_exposure_msat) = if local { - (dust_exposure_msat, dust_exposure_msat) + next_splice_out_maximum_sat +} + +fn adjust_capacity_for_holder_reserved_fee( + outbound_capacity_msat: u64, local_nondust_htlc_count: usize, remote_nondust_htlc_count: usize, + feerate_per_kw: u32, spiked_feerate: u32, channel_constraints: &ChannelConstraints, + channel_type: &ChannelTypeFeatures, +) -> u64 { + let read_available_capacity = |nondust_htlc_count, htlc_dust_limit_sat| { + // Note here we use the htlc count at the current feerate together with the spiked feerate; + // this makes sure that the holder can afford any fee bump between 1x to 2x from the current + // feerate. + let max_commit_tx_fee_sat = + commit_tx_fee_sat(spiked_feerate, nondust_htlc_count + 2, channel_type); + let min_commit_tx_fee_sat = + commit_tx_fee_sat(spiked_feerate, nondust_htlc_count + 1, channel_type); + + // We should mind channel commit tx fee when computing how much of the available capacity + // can be used in the next htlc. Mirrors the logic in send_htlc. + // + // The fee depends on whether the amount we will be sending is above dust or not, + // and the answer will in turn change the amount itself — making it a circular + // dependency. + // This complicates the computation around dust-values, up to the one-htlc-value. + + // We will first subtract the fee as if we were above-dust. Then, if the resulting + // value ends up being below dust, we have this fee available again. In that case, + // match the value to right-below-dust. + let capacity_minus_max_commitment_fee_msat = + outbound_capacity_msat.saturating_sub(max_commit_tx_fee_sat * 1000); + if capacity_minus_max_commitment_fee_msat < htlc_dust_limit_sat * 1000 { + let capacity_minus_min_commitment_fee_msat = + outbound_capacity_msat.saturating_sub(min_commit_tx_fee_sat * 1000); + cmp::min(htlc_dust_limit_sat * 1000 - 1, capacity_minus_min_commitment_fee_msat) } else { - let (excess_fees_msat, extra_accepted_htlc_excess_fees_msat) = - commit_plus_htlc_tx_fees_msat( - local, - &next_commitment_htlcs, - dust_buffer_feerate, - excess_feerate, - broadcaster_dust_limit_satoshis, - channel_type, - ); - ( - dust_exposure_msat + excess_fees_msat, - dust_exposure_msat + extra_accepted_htlc_excess_fees_msat, + capacity_minus_max_commitment_fee_msat + } + }; + + let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(channel_type, feerate_per_kw); + let available_capacity_on_local_commitment = read_available_capacity( + local_nondust_htlc_count, + channel_constraints.holder_dust_limit_satoshis + real_htlc_timeout_tx_fee_sat, + ); + let available_capacity_on_remote_commitment = read_available_capacity( + remote_nondust_htlc_count, + channel_constraints.counterparty_dust_limit_satoshis + real_htlc_success_tx_fee_sat, + ); + cmp::min(available_capacity_on_local_commitment, available_capacity_on_remote_commitment) +} + +fn adjust_capacity_for_counterparty_reserved_fee( + outbound_capacity_msat: u64, remote_balance_before_fee_msat: u64, + local_nondust_htlc_count: usize, remote_nondust_htlc_count: usize, feerate_per_kw: u32, + channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures, +) -> u64 { + let read_available_capacity = |nondust_htlc_count, htlc_dust_limit_sat| { + let commit_tx_fee_sat = + commit_tx_fee_sat(feerate_per_kw, nondust_htlc_count + 1, channel_type); + // If the channel is inbound (i.e. counterparty pays the fee), we need to make sure + // sending a new HTLC won't reduce their balance below our reserve threshold. + if remote_balance_before_fee_msat + < commit_tx_fee_sat * 1000 + + channel_constraints.holder_selected_channel_reserve_satoshis * 1000 + { + // If another HTLC's fee would reduce the remote's balance below the reserve limit + // we've selected for them, we can only send dust HTLCs. + cmp::min(outbound_capacity_msat, htlc_dust_limit_sat * 1000 - 1) + } else { + outbound_capacity_msat + } + }; + let (real_htlc_success_tx_fee_sat, real_htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(channel_type, feerate_per_kw); + let available_capacity_on_local_commitment = read_available_capacity( + local_nondust_htlc_count, + channel_constraints.holder_dust_limit_satoshis + real_htlc_timeout_tx_fee_sat, + ); + let available_capacity_on_remote_commitment = read_available_capacity( + remote_nondust_htlc_count, + channel_constraints.counterparty_dust_limit_satoshis + real_htlc_success_tx_fee_sat, + ); + cmp::min(available_capacity_on_local_commitment, available_capacity_on_remote_commitment) +} + +fn adjust_min_max_htlc_for_dust_exposure( + pending_htlcs: &[HTLCAmountDirection], feerate_per_kw: u32, + dust_exposure_limiting_feerate: Option<u32>, max_dust_htlc_exposure_msat: u64, + channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures, + mut available_capacity_msat: u64, +) -> (u64, u64, u64) { + let mut next_outbound_htlc_minimum_msat = channel_constraints.counterparty_htlc_minimum_msat; + + let (local_dust_exposure_msat, _) = get_dust_exposure_stats( + true, + pending_htlcs, + feerate_per_kw, + dust_exposure_limiting_feerate, + channel_constraints.holder_dust_limit_satoshis, + channel_type, + ); + let (remote_dust_exposure_msat, extra_htlc_remote_dust_exposure_msat) = get_dust_exposure_stats( + false, + pending_htlcs, + feerate_per_kw, + dust_exposure_limiting_feerate, + channel_constraints.counterparty_dust_limit_satoshis, + channel_type, + ); + + // If we get close to our maximum dust exposure, we end up in a situation where we can send + // between zero and the remaining dust exposure limit remaining OR above the dust limit. + // Because we cannot express this as a simple min/max, we prefer to tell the user they can + // send above the dust limit (as the router can always overpay to meet the dust limit). + let mut remaining_msat_below_dust_exposure_limit = None; + let mut dust_exposure_dust_limit_msat = 0; + + let dust_buffer_feerate = get_dust_buffer_feerate(feerate_per_kw); + let (buffer_htlc_success_tx_fee_sat, buffer_htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(channel_type, dust_buffer_feerate); + let buffer_dust_limit_success_sat = + buffer_htlc_success_tx_fee_sat + channel_constraints.counterparty_dust_limit_satoshis; + let buffer_dust_limit_timeout_sat = + buffer_htlc_timeout_tx_fee_sat + channel_constraints.holder_dust_limit_satoshis; + + if let Some(extra_htlc_remote_dust_exposure) = extra_htlc_remote_dust_exposure_msat { + if extra_htlc_remote_dust_exposure > max_dust_htlc_exposure_msat { + // If adding an extra HTLC would put us over the dust limit in total fees, we cannot + // send any non-dust HTLCs. + available_capacity_msat = + cmp::min(available_capacity_msat, buffer_dust_limit_success_sat * 1000); + } + } + + if remote_dust_exposure_msat.saturating_add(buffer_dust_limit_success_sat * 1000) + > max_dust_htlc_exposure_msat.saturating_add(1) + { + // Note that we don't use the `counterparty_tx_dust_exposure` (with + // `htlc_dust_exposure_msat`) here as it only applies to non-dust HTLCs. + remaining_msat_below_dust_exposure_limit = + Some(max_dust_htlc_exposure_msat.saturating_sub(remote_dust_exposure_msat)); + dust_exposure_dust_limit_msat = + cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_success_sat * 1000); + } + + if local_dust_exposure_msat as i64 + buffer_dust_limit_timeout_sat as i64 * 1000 - 1 + > max_dust_htlc_exposure_msat.try_into().unwrap_or(i64::max_value()) + { + remaining_msat_below_dust_exposure_limit = Some(cmp::min( + remaining_msat_below_dust_exposure_limit.unwrap_or(u64::max_value()), + max_dust_htlc_exposure_msat.saturating_sub(local_dust_exposure_msat), + )); + dust_exposure_dust_limit_msat = + cmp::max(dust_exposure_dust_limit_msat, buffer_dust_limit_timeout_sat * 1000); + } + + if let Some(remaining_limit_msat) = remaining_msat_below_dust_exposure_limit { + if available_capacity_msat < dust_exposure_dust_limit_msat { + available_capacity_msat = cmp::min(available_capacity_msat, remaining_limit_msat); + } else { + next_outbound_htlc_minimum_msat = + cmp::max(next_outbound_htlc_minimum_msat, dust_exposure_dust_limit_msat); + } + } + + let dust_exposure_msat = cmp::max(local_dust_exposure_msat, remote_dust_exposure_msat); + + (next_outbound_htlc_minimum_msat, available_capacity_msat, dust_exposure_msat) +} + +fn get_available_balances( + is_outbound_from_holder: bool, channel_value_satoshis: u64, value_to_holder_msat: u64, + pending_htlcs: &[HTLCAmountDirection], feerate_per_kw: u32, + dust_exposure_limiting_feerate: Option<u32>, max_dust_htlc_exposure_msat: u64, + channel_constraints: ChannelConstraints, channel_type: &ChannelTypeFeatures, +) -> crate::ln::channel::AvailableBalances { + // When sizing the next HTLC add, we take the remote's view of the set of pending HTLCs in + // `ChannelContext::get_next_commitment_htlcs`, set this view to `pending_htlcs` here, and use this set of + // pending HTLCs to calculate stats on our own commitment below. + // + // This means we do *not* include `LocalRemoved` HTLCs. `LocalRemoved` and `LocalAnnounced` HTLCs are applied + // atomically to our own commitment upon the counterparty's next ack. + // + // `RemoteRemoved` HTLCs *are* included. While we don't expect these HTLCs to be present in our next + // commitment, we have not ack'ed these removals yet, so we expect the counterparty to count them when + // validating our own HTLC add. These HTLCs would also revert to `Committed` upon a disconnection. + + // Note that the feerate is 0 in zero-fee commitment channels, so this statement is a noop + let spiked_feerate = + feerate_per_kw.saturating_mul(if !channel_type.supports_anchors_zero_fee_htlc_tx() { + crate::ln::channel::FEE_SPIKE_BUFFER_FEE_INCREASE_MULTIPLE as u32 + } else { + 1 + }); + + let local_nondust_htlc_count = pending_htlcs + .iter() + .filter(|htlc| { + !htlc.is_dust( + true, + feerate_per_kw, + channel_constraints.holder_dust_limit_satoshis, + channel_type, ) - }; + }) + .count(); - Ok(NextCommitmentStats { - is_outbound_from_holder, - inbound_htlcs_count, - inbound_htlcs_value_msat, - holder_balance_before_fee_msat, - counterparty_balance_before_fee_msat, - nondust_htlc_count: nondust_htlc_count + addl_nondust_htlc_count, - commit_tx_fee_sat, - dust_exposure_msat, - extra_accepted_htlc_dust_exposure_msat, + let remote_nondust_htlc_count = pending_htlcs + .iter() + .filter(|htlc| { + !htlc.is_dust( + false, + feerate_per_kw, + channel_constraints.counterparty_dust_limit_satoshis, + channel_type, + ) }) + .count(); + + let outbound_htlcs_value_msat: u64 = + pending_htlcs.iter().filter_map(|htlc| htlc.outbound.then_some(htlc.amount_msat)).sum(); + let inbound_htlcs_value_msat: u64 = + pending_htlcs.iter().filter_map(|htlc| (!htlc.outbound).then_some(htlc.amount_msat)).sum(); + let total_anchors_sat = total_anchors_sat(channel_type); + let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = + saturating_sub_from_funder( + is_outbound_from_holder, + value_to_holder_msat.saturating_sub(outbound_htlcs_value_msat), + (channel_value_satoshis * 1000) + .checked_sub(value_to_holder_msat) + .unwrap() + .saturating_sub(inbound_htlcs_value_msat), + total_anchors_sat.saturating_mul(1000), + ); + + let next_splice_out_maximum_sat = get_next_splice_out_maximum_sat( + is_outbound_from_holder, + channel_value_satoshis, + local_balance_before_fee_msat, + remote_balance_before_fee_msat, + local_nondust_htlc_count, + remote_nondust_htlc_count, + feerate_per_kw, + spiked_feerate, + &channel_constraints, + channel_type, + ); + + let outbound_capacity_msat = local_balance_before_fee_msat + .saturating_sub(channel_constraints.counterparty_selected_channel_reserve_satoshis * 1000); + + let available_capacity_msat = if is_outbound_from_holder { + adjust_capacity_for_holder_reserved_fee( + outbound_capacity_msat, + local_nondust_htlc_count, + remote_nondust_htlc_count, + feerate_per_kw, + spiked_feerate, + &channel_constraints, + channel_type, + ) + } else { + adjust_capacity_for_counterparty_reserved_fee( + outbound_capacity_msat, + remote_balance_before_fee_msat, + local_nondust_htlc_count, + remote_nondust_htlc_count, + feerate_per_kw, + &channel_constraints, + channel_type, + ) + }; + + let (next_outbound_htlc_minimum_msat, mut available_capacity_msat, dust_exposure_msat) = + adjust_min_max_htlc_for_dust_exposure( + pending_htlcs, + feerate_per_kw, + dust_exposure_limiting_feerate, + max_dust_htlc_exposure_msat, + &channel_constraints, + channel_type, + available_capacity_msat, + ); + + available_capacity_msat = cmp::min( + available_capacity_msat, + channel_constraints + .counterparty_max_htlc_value_in_flight_msat + .saturating_sub(outbound_htlcs_value_msat), + ); + + if pending_htlcs.iter().filter(|htlc| htlc.outbound).count() + 1 + > channel_constraints.counterparty_max_accepted_htlcs as usize + { + available_capacity_msat = 0; } - fn commit_tx_fee_sat( - &self, feerate_per_kw: u32, nondust_htlc_count: usize, channel_type: &ChannelTypeFeatures, - ) -> u64 { - commit_tx_fee_sat(feerate_per_kw, nondust_htlc_count, channel_type) + + // Now adjust our min and max size HTLC to make sure both the local and the remote commitments still have + // at least one output at the current feerate. + let (next_outbound_htlc_minimum_msat, available_capacity_msat) = + adjust_min_max_htlc_if_max_dust_htlc_produces_no_output( + is_outbound_from_holder, + local_balance_before_fee_msat, + remote_balance_before_fee_msat, + local_nondust_htlc_count, + remote_nondust_htlc_count, + feerate_per_kw, + &channel_constraints, + channel_type, + next_outbound_htlc_minimum_msat, + available_capacity_msat, + ); + + crate::ln::channel::AvailableBalances { + inbound_capacity_msat: remote_balance_before_fee_msat + .saturating_sub(channel_constraints.holder_selected_channel_reserve_satoshis * 1000), + outbound_capacity_msat, + next_outbound_htlc_limit_msat: available_capacity_msat, + next_outbound_htlc_minimum_msat, + dust_exposure_msat, + next_splice_out_maximum_sat, } - fn subtract_non_htlc_outputs( - &self, is_outbound_from_holder: bool, value_to_self_after_htlcs: u64, - value_to_remote_after_htlcs: u64, channel_type: &ChannelTypeFeatures, - ) -> (u64, u64) { - let total_anchors_sat = if channel_type.supports_anchors_zero_fee_htlc_tx() { - ANCHOR_OUTPUT_VALUE_SATOSHI * 2 - } else { - 0 - }; +} - let mut local_balance_before_fee_msat = value_to_self_after_htlcs; - let mut remote_balance_before_fee_msat = value_to_remote_after_htlcs; +fn adjust_min_max_htlc_if_max_dust_htlc_produces_no_output( + is_outbound_from_holder: bool, local_balance_before_fee_msat: u64, + remote_balance_before_fee_msat: u64, local_nondust_htlc_count: usize, + remote_nondust_htlc_count: usize, feerate_per_kw: u32, + channel_constraints: &ChannelConstraints, channel_type: &ChannelTypeFeatures, + next_outbound_htlc_minimum_msat: u64, available_capacity_msat: u64, +) -> (u64, u64) { + let (next_outbound_htlc_minimum_msat, available_capacity_msat) = + adjust_boundaries_if_max_dust_htlc_produces_no_output( + true, + is_outbound_from_holder, + local_balance_before_fee_msat, + remote_balance_before_fee_msat, + feerate_per_kw, + local_nondust_htlc_count, + channel_constraints.holder_dust_limit_satoshis, + channel_type, + next_outbound_htlc_minimum_msat, + available_capacity_msat, + ); - // We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater - // than or equal to `total_anchors_sat`. - // - // This is because when the remote party sends an `update_fee` message, we build the new - // commitment transaction *before* checking whether the remote party's balance is enough to - // cover the total anchor sum. + let (next_outbound_htlc_minimum_msat, available_capacity_msat) = + adjust_boundaries_if_max_dust_htlc_produces_no_output( + false, + is_outbound_from_holder, + local_balance_before_fee_msat, + remote_balance_before_fee_msat, + feerate_per_kw, + remote_nondust_htlc_count, + channel_constraints.counterparty_dust_limit_satoshis, + channel_type, + next_outbound_htlc_minimum_msat, + available_capacity_msat, + ); + (next_outbound_htlc_minimum_msat, available_capacity_msat) +} - if is_outbound_from_holder { - local_balance_before_fee_msat = - local_balance_before_fee_msat.saturating_sub(total_anchors_sat * 1000); +fn adjust_boundaries_if_max_dust_htlc_produces_no_output( + local: bool, is_outbound_from_holder: bool, holder_balance_before_fee_msat: u64, + counterparty_balance_before_fee_msat: u64, feerate_per_kw: u32, nondust_htlc_count: usize, + dust_limit_satoshis: u64, channel_type: &ChannelTypeFeatures, + next_outbound_htlc_minimum_msat: u64, available_capacity_msat: u64, +) -> (u64, u64) { + // First, determine the biggest dust HTLC we could send + let (htlc_success_tx_fee_sat, htlc_timeout_tx_fee_sat) = + second_stage_tx_fees_sat(channel_type, feerate_per_kw); + let min_nondust_htlc_sat = + dust_limit_satoshis + if local { htlc_timeout_tx_fee_sat } else { htlc_success_tx_fee_sat }; + let max_dust_htlc_msat = (min_nondust_htlc_sat.saturating_mul(1000)).saturating_sub(1); + + // If this dust HTLC produces no outputs, then we have to say something! It is now possible to produce a + // commitment with no outputs. + if !has_output( + is_outbound_from_holder, + holder_balance_before_fee_msat.saturating_sub(max_dust_htlc_msat), + counterparty_balance_before_fee_msat, + feerate_per_kw, + nondust_htlc_count, + dust_limit_satoshis, + channel_type, + ) { + // If we are allowed to send non-dust HTLCs, set the min HTLC to the smallest non-dust HTLC... + if available_capacity_msat >= min_nondust_htlc_sat.saturating_mul(1000) { + ( + cmp::max( + min_nondust_htlc_sat.saturating_mul(1000), + next_outbound_htlc_minimum_msat, + ), + available_capacity_msat, + ) + // Otherwise, set the max HTLC to the biggest that still leaves our main balance output untrimmed. + // Note that this will be a dust HTLC. } else { - remote_balance_before_fee_msat = - remote_balance_before_fee_msat.saturating_sub(total_anchors_sat * 1000); + // Remember we've got no non-dust HTLCs on the commitment here + let current_tx_fee_sat = commit_tx_fee_sat(feerate_per_kw, 0, channel_type); + let spike_buffer_tx_fee_sat = commit_tx_fee_sat(feerate_per_kw, 1, channel_type); + // In case we are the funder, we must cover the greater of + // 1) The dust_limit_satoshis plus the fee of the existing commitment at the current feerate. + // 2) The fee of the commitment with an additional non-dust HTLC, aka the fee spike buffer HTLC. + // In this case we don't mind the holder balance output dropping below the dust limit, as + // this additional non-dust HTLC will create the single remaining output on the commitment. + let min_balance_msat = if is_outbound_from_holder { + cmp::max(dust_limit_satoshis + current_tx_fee_sat, spike_buffer_tx_fee_sat) * 1000 + // In case we are the fundee, we can send dust HTLCs as long as our own balance output + // remains above the dust limit. + } else { + dust_limit_satoshis * 1000 + }; + ( + next_outbound_htlc_minimum_msat, + // We make no assumptions about the size of `available_capacity_msat` passed to this + // function, we only care that the new `available_capacity_msat` is under + // `holder_balance_before_fee_msat - min_balance_msat` + cmp::min( + holder_balance_before_fee_msat.saturating_sub(min_balance_msat), + available_capacity_msat, + ), + ) } + // Otherwise, it is impossible to produce no outputs with this upcoming HTLC add, so we stay quiet + } else { + (next_outbound_htlc_minimum_msat, available_capacity_msat) + } +} + +pub(crate) trait TxBuilder { + fn get_channel_stats( + &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, + value_to_holder_msat: u64, pending_htlcs: &[HTLCAmountDirection], + addl_nondust_htlc_count: usize, feerate_per_kw: u32, assume_fee_spike: bool, + dust_exposure_limiting_feerate: Option<u32>, max_dust_htlc_exposure_msat: u64, + channel_constraints: ChannelConstraints, channel_type: &ChannelTypeFeatures, + ) -> Result<ChannelStats, ()>; + fn build_commitment_transaction<L: Logger>( + &self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey, + channel_parameters: &ChannelTransactionParameters, secp_ctx: &Secp256k1<secp256k1::All>, + value_to_self_msat: u64, htlcs_in_tx: Vec<HTLCOutputInCommitment>, feerate_per_kw: u32, + broadcaster_dust_limit_satoshis: u64, logger: &L, + ) -> (CommitmentTransaction, CommitmentStats); +} + +pub(crate) struct SpecTxBuilder {} - (local_balance_before_fee_msat, remote_balance_before_fee_msat) +impl TxBuilder for SpecTxBuilder { + fn get_channel_stats( + &self, local: bool, is_outbound_from_holder: bool, channel_value_satoshis: u64, + value_to_holder_msat: u64, pending_htlcs: &[HTLCAmountDirection], + addl_nondust_htlc_count: usize, feerate_per_kw: u32, assume_fee_spike: bool, + dust_exposure_limiting_feerate: Option<u32>, max_dust_htlc_exposure_msat: u64, + channel_constraints: ChannelConstraints, channel_type: &ChannelTypeFeatures, + ) -> Result<ChannelStats, ()> { + let commitment_stats = if local { + get_next_commitment_stats( + true, + is_outbound_from_holder, + channel_value_satoshis, + value_to_holder_msat, + pending_htlcs, + addl_nondust_htlc_count, + feerate_per_kw, + assume_fee_spike, + dust_exposure_limiting_feerate, + channel_constraints.holder_dust_limit_satoshis, + channel_type, + )? + } else { + get_next_commitment_stats( + false, + is_outbound_from_holder, + channel_value_satoshis, + value_to_holder_msat, + pending_htlcs, + addl_nondust_htlc_count, + feerate_per_kw, + assume_fee_spike, + dust_exposure_limiting_feerate, + channel_constraints.counterparty_dust_limit_satoshis, + channel_type, + )? + }; + + let available_balances = get_available_balances( + is_outbound_from_holder, + channel_value_satoshis, + value_to_holder_msat, + pending_htlcs, + feerate_per_kw, + dust_exposure_limiting_feerate, + max_dust_htlc_exposure_msat, + channel_constraints, + channel_type, + ); + + Ok(ChannelStats { commitment_stats, available_balances }) } fn build_commitment_transaction<L: Logger>( &self, local: bool, commitment_number: u64, per_commitment_point: &PublicKey, @@ -372,7 +1032,7 @@ impl TxBuilder for SpecTxBuilder { // The value going to each party MUST be 0 or positive, even if all HTLCs pending in the // commitment clear by failure. - let commit_tx_fee_sat = self.commit_tx_fee_sat( + let commit_tx_fee_sat = commit_tx_fee_sat( feerate_per_kw, htlcs_in_tx.len(), &channel_parameters.channel_type_features, @@ -384,12 +1044,21 @@ impl TxBuilder for SpecTxBuilder { .unwrap() .checked_sub(remote_htlc_total_msat) .unwrap(); - let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = self - .subtract_non_htlc_outputs( + + // We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater + // than or equal to `total_anchors_sat`. + // + // This is because when the remote party sends an `update_fee` message, we build the new + // commitment transaction *before* checking whether the remote party's balance is enough to + // cover the total anchor sum. + + let total_anchors_sat = total_anchors_sat(&channel_parameters.channel_type_features); + let (local_balance_before_fee_msat, remote_balance_before_fee_msat) = + saturating_sub_from_funder( channel_parameters.is_outbound_from_holder, value_to_self_after_htlcs_msat, value_to_remote_after_htlcs_msat, - &channel_parameters.channel_type_features, + total_anchors_sat.saturating_mul(1000), ); // We MUST use saturating subs here, as the funder's balance is not guaranteed to be greater @@ -399,17 +1068,12 @@ impl TxBuilder for SpecTxBuilder { // commitment transaction *before* checking whether the remote party's balance is enough to // cover the total fee. - let (value_to_self, value_to_remote) = if channel_parameters.is_outbound_from_holder { - ( - (local_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat), - remote_balance_before_fee_msat / 1000, - ) - } else { - ( - local_balance_before_fee_msat / 1000, - (remote_balance_before_fee_msat / 1000).saturating_sub(commit_tx_fee_sat), - ) - }; + let (value_to_self, value_to_remote) = saturating_sub_from_funder( + channel_parameters.is_outbound_from_holder, + local_balance_before_fee_msat / 1000, + remote_balance_before_fee_msat / 1000, + commit_tx_fee_sat, + ); let mut to_broadcaster_value_sat = if local { value_to_self } else { value_to_remote }; let mut to_countersignatory_value_sat = if local { value_to_remote } else { value_to_self }; diff --git a/lightning/src/sign/type_resolver.rs b/lightning/src/sign/type_resolver.rs deleted file mode 100644 index 405e346dda6..00000000000 --- a/lightning/src/sign/type_resolver.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::sign::{ChannelSigner, SignerProvider}; - -pub(crate) enum ChannelSignerType<SP: SignerProvider> { - // in practice, this will only ever be an EcdsaChannelSigner (specifically, Writeable) - Ecdsa(SP::EcdsaSigner), - #[cfg(taproot)] - #[allow(unused)] - Taproot(SP::TaprootSigner), -} - -#[cfg(test)] -impl<SP: SignerProvider> std::fmt::Debug for ChannelSignerType<SP> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ChannelSignerType").finish() - } -} - -impl<SP: SignerProvider> ChannelSignerType<SP> { - pub(crate) fn as_ref(&self) -> &dyn ChannelSigner { - match self { - ChannelSignerType::Ecdsa(ecs) => ecs, - #[cfg(taproot)] - #[allow(unused)] - ChannelSignerType::Taproot(tcs) => tcs, - } - } - - #[allow(unused)] - pub(crate) fn as_ecdsa(&self) -> Option<&SP::EcdsaSigner> { - match self { - ChannelSignerType::Ecdsa(ecs) => Some(ecs), - _ => None, - } - } - - #[allow(unused)] - pub(crate) fn as_mut_ecdsa(&mut self) -> Option<&mut SP::EcdsaSigner> { - match self { - ChannelSignerType::Ecdsa(ecs) => Some(ecs), - _ => None, - } - } -} diff --git a/lightning/src/sync/nostd_sync.rs b/lightning/src/sync/nostd_sync.rs index 12070741918..18055d1ebe4 100644 --- a/lightning/src/sync/nostd_sync.rs +++ b/lightning/src/sync/nostd_sync.rs @@ -61,7 +61,7 @@ impl<'a, T: 'a> LockTestExt<'a> for Mutex<T> { } type ExclLock = MutexGuard<'a, T>; #[inline] - fn unsafe_well_ordered_double_lock_self(&'a self) -> MutexGuard<T> { + fn unsafe_well_ordered_double_lock_self(&'a self) -> MutexGuard<'a, T> { self.lock().unwrap() } } @@ -132,7 +132,7 @@ impl<'a, T: 'a> LockTestExt<'a> for RwLock<T> { } type ExclLock = RwLockWriteGuard<'a, T>; #[inline] - fn unsafe_well_ordered_double_lock_self(&'a self) -> RwLockWriteGuard<T> { + fn unsafe_well_ordered_double_lock_self(&'a self) -> RwLockWriteGuard<'a, T> { self.write().unwrap() } } diff --git a/lightning/src/util/anchor_channel_reserves.rs b/lightning/src/util/anchor_channel_reserves.rs index 8026af03d58..d4db63a04c3 100644 --- a/lightning/src/util/anchor_channel_reserves.rs +++ b/lightning/src/util/anchor_channel_reserves.rs @@ -24,14 +24,14 @@ use crate::chain::chaininterface::FeeEstimator; use crate::chain::chainmonitor::ChainMonitor; use crate::chain::chainmonitor::Persist; use crate::chain::Filter; -use crate::events::bump_transaction::Utxo; -use crate::ln::chan_utils::max_htlcs; +use crate::ln::chan_utils::{max_htlcs, BASE_INPUT_WEIGHT}; use crate::ln::channelmanager::AChannelManager; use crate::prelude::new_hash_set; use crate::sign::ecdsa::EcdsaChannelSigner; use crate::sign::EntropySource; use crate::types::features::ChannelTypeFeatures; use crate::util::logger::Logger; +use crate::util::wallet_utils::Utxo; use bitcoin::constants::WITNESS_SCALE_FACTOR; use bitcoin::Amount; use bitcoin::FeeRate; @@ -240,11 +240,11 @@ pub fn get_supportable_anchor_channels( let mut total_fractional_amount = Amount::from_sat(0); let mut num_whole_utxos = 0; for utxo in utxos { - let satisfaction_fee = context + let spend_fee = context .upper_bound_fee_rate - .fee_wu(Weight::from_wu(utxo.satisfaction_weight)) + .fee_wu(Weight::from_wu(BASE_INPUT_WEIGHT + utxo.satisfaction_weight)) .unwrap_or(Amount::MAX); - let amount = utxo.output.value.checked_sub(satisfaction_fee).unwrap_or(Amount::MIN); + let amount = utxo.output.value.checked_sub(spend_fee).unwrap_or(Amount::MIN); if amount >= reserve_per_channel { num_whole_utxos += 1; } else { @@ -260,6 +260,13 @@ pub fn get_supportable_anchor_channels( num_whole_utxos + total_fractional_amount.to_sat() / reserve_per_channel.to_sat() / 2 } +/// Returns whether a channel of the given type requires an on-chain anchor reserve, i.e. uses +/// either the `anchors_zero_fee_htlc_tx` or `anchor_zero_fee_commitments` (TRUC / 0FC) variant. +fn is_anchor_channel_type(channel_type: &ChannelTypeFeatures) -> bool { + channel_type.supports_anchors_zero_fee_htlc_tx() + || channel_type.supports_anchor_zero_fee_commitments() +} + /// Verifies whether the anchor channel reserve provided by `utxos` is sufficient to support /// an additional anchor channel. /// @@ -296,7 +303,7 @@ where } else { continue; }; - if channel_monitor.channel_type_features().supports_anchors_zero_fee_htlc_tx() + if is_anchor_channel_type(&channel_monitor.channel_type_features()) && !channel_monitor.get_claimable_balances().is_empty() { anchor_channels.insert(channel_id); @@ -305,7 +312,7 @@ where // Also include channels that are in the middle of negotiation or anchor channels that don't have // a ChannelMonitor yet. for channel in a_channel_manager.get_cm().list_channels() { - if channel.channel_type.map_or(true, |ct| ct.supports_anchors_zero_fee_htlc_tx()) { + if channel.channel_type.map_or(true, |ct| is_anchor_channel_type(&ct)) { anchor_channels.insert(channel.channel_id); } } @@ -315,7 +322,8 @@ where #[cfg(test)] mod test { use super::*; - use bitcoin::{OutPoint, ScriptBuf, TxOut, Txid}; + use crate::ln::functional_test_utils::*; + use bitcoin::{OutPoint, ScriptBuf, Sequence, TxOut, Txid}; use std::str::FromStr; #[test] @@ -343,6 +351,7 @@ mod test { }, output: TxOut { value: amount, script_pubkey: ScriptBuf::new() }, satisfaction_weight: 1 * 4 + (1 + 1 + 72 + 1 + 33), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, } } @@ -361,6 +370,15 @@ mod test { assert_eq!(get_supportable_anchor_channels(&context, utxos.as_slice()), 3); } + #[test] + fn test_get_supportable_anchor_channels_accounts_for_input_weight() { + let context = AnchorChannelReserveContext::default(); + let reserve = get_reserve_per_channel(&context); + let utxo = make_p2wpkh_utxo(reserve - Amount::from_sat(1)); + + assert_eq!(get_supportable_anchor_channels(&context, &[utxo]), 0); + } + #[test] fn test_anchor_output_spend_transaction_weight() { // Example with smaller signatures: @@ -424,4 +442,48 @@ mod test { 1068 ); } + + #[test] + fn test_can_support_additional_anchor_channel_zero_fee_commitments() { + // Regression test: a channel that uses the `anchor_zero_fee_commitments` + // (option 41) variant is just as much an anchor channel — and requires + // the same on-chain reserve — as one using `anchors_zero_fee_htlc_tx`. + // The reserve check must therefore count it as an existing anchor + // channel when deciding whether the wallet can safely support an + // additional one. Currently `can_support_additional_anchor_channel` + // only counts channels whose features set `anchors_zero_fee_htlc_tx`, + // so a node whose reserves are exhausted by zero-fee-commitment + // channels is incorrectly told it can open another anchor channel. + let mut cfg = test_default_channel_config(); + cfg.channel_handshake_config.negotiate_anchor_zero_fee_commitments = true; + + let chanmon_cfgs = create_chanmon_cfgs(2); + let node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[Some(cfg.clone()), Some(cfg)]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + create_chan_between_nodes(&nodes[0], &nodes[1]); + + let channels = nodes[0].node.list_channels(); + assert_eq!(channels.len(), 1); + let channel_type = channels[0].channel_type.as_ref().unwrap(); + assert!(channel_type.supports_anchor_zero_fee_commitments()); + // Sanity check: a zero-fee-commitments channel does not also set the + // older anchors_zero_fee_htlc_tx feature. + assert!(!channel_type.supports_anchors_zero_fee_htlc_tx()); + + let context = AnchorChannelReserveContext::default(); + let reserve = get_reserve_per_channel(&context); + // Provide a single UTXO with enough value to cover one channel reserve. + let utxos = vec![make_p2wpkh_utxo(reserve * 2)]; + + // We already have one TRUC anchor channel and only enough reserve for + // a single channel; we must not authorize an additional one. + assert!(!can_support_additional_anchor_channel( + &context, + &utxos, + nodes[0].node, + &nodes[0].chain_monitor.chain_monitor, + )); + } } diff --git a/lightning/src/util/async_poll.rs b/lightning/src/util/async_poll.rs index 57df5b26cb0..23ca1aad603 100644 --- a/lightning/src/util/async_poll.rs +++ b/lightning/src/util/async_poll.rs @@ -164,31 +164,3 @@ const DUMMY_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new( pub(crate) fn dummy_waker() -> Waker { unsafe { Waker::from_raw(RawWaker::new(core::ptr::null(), &DUMMY_WAKER_VTABLE)) } } - -/// Marker trait to optionally implement `Sync` under std. -/// -/// This is not exported to bindings users as async is only supported in Rust. -#[cfg(feature = "std")] -pub use core::marker::Sync as MaybeSync; - -#[cfg(not(feature = "std"))] -/// Marker trait to optionally implement `Sync` under std. -/// -/// This is not exported to bindings users as async is only supported in Rust. -pub trait MaybeSync {} -#[cfg(not(feature = "std"))] -impl<T> MaybeSync for T where T: ?Sized {} - -/// Marker trait to optionally implement `Send` under std. -/// -/// This is not exported to bindings users as async is only supported in Rust. -#[cfg(feature = "std")] -pub use core::marker::Send as MaybeSend; - -#[cfg(not(feature = "std"))] -/// Marker trait to optionally implement `Send` under std. -/// -/// This is not exported to bindings users as async is only supported in Rust. -pub trait MaybeSend {} -#[cfg(not(feature = "std"))] -impl<T> MaybeSend for T where T: ?Sized {} diff --git a/lightning/src/util/config.rs b/lightning/src/util/config.rs index 420fad6b1e0..54977f47409 100644 --- a/lightning/src/util/config.rs +++ b/lightning/src/util/config.rs @@ -10,7 +10,6 @@ //! Various user-configurable channel limits and settings which ChannelManager //! applies for you. -use crate::ln::channel::MAX_FUNDING_SATOSHIS_NO_WUMBO; use crate::ln::channelmanager::{BREAKDOWN_TIMEOUT, MAX_LOCAL_BREAKDOWN_TIMEOUT}; #[cfg(fuzzing)] @@ -32,11 +31,11 @@ pub struct ChannelHandshakeConfig { /// A lower-bound of `1` is applied, requiring all channels to have a confirmed commitment /// transaction before operation. If you wish to accept channels with zero confirmations, /// manually accept them via [`Event::OpenChannelRequest`] using - /// [`ChannelManager::accept_inbound_channel_from_trusted_peer_0conf`]. + /// [`ChannelManager::accept_inbound_channel_from_trusted_peer`]. /// /// Default value: `6` /// - /// [`ChannelManager::accept_inbound_channel_from_trusted_peer_0conf`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer_0conf + /// [`ChannelManager::accept_inbound_channel_from_trusted_peer`]: crate::ln::channelmanager::ChannelManager::accept_inbound_channel_from_trusted_peer /// [`Event::OpenChannelRequest`]: crate::events::Event::OpenChannelRequest pub minimum_depth: u32, /// Set to the number of blocks we require our counterparty to wait to claim their money (ie @@ -63,17 +62,35 @@ pub struct ChannelHandshakeConfig { /// Default value: `1` (If the value is less than `1`, it is ignored and set to `1`, as is /// required by the protocol. pub our_htlc_minimum_msat: u64, - /// Sets the percentage of the channel value we will cap the total value of outstanding inbound - /// HTLCs to. + /// Sets the maximum percentage of the total channel value that can be allocated to inbound + /// HTLCs in announced channels. /// /// This can be set to a value between 1-100, where the value corresponds to the percent of the /// channel value in whole percentages. /// /// Note that: - /// * If configured to another value than the default value `10`, any new channels created with - /// the non default value will cause versions of LDK prior to 0.0.104 to refuse to read the - /// `ChannelManager`. + /// * This caps the total value for inbound HTLCs in-flight only, and there's currently + /// no way to configure the cap for the total value of outbound HTLCs in-flight. + /// + /// * The requirements for your node being online to ensure the safety of HTLC-encumbered funds + /// are different from the non-HTLC-encumbered funds. This makes this an important knob to + /// restrict exposure to loss due to being offline for too long. + /// See [`ChannelHandshakeConfig::our_to_self_delay`] and [`ChannelConfig::cltv_expiry_delta`] + /// for more information. + /// + /// Default value: `25` + /// + /// Minimum value: `1` (Any values less will be treated as `1` instead.) + /// + /// Maximum value: `100` (Any values larger will be treated as `100` instead.) + pub announced_channel_max_inbound_htlc_value_in_flight_percentage: u8, + /// Sets the maximum percentage of the total channel value that can be allocated to inbound + /// HTLCs in unannounced channels. + /// + /// This can be set to a value between 1-100, where the value corresponds to the percent of the + /// channel value in whole percentages. /// + /// Note that: /// * This caps the total value for inbound HTLCs in-flight only, and there's currently /// no way to configure the cap for the total value of outbound HTLCs in-flight. /// @@ -83,12 +100,12 @@ pub struct ChannelHandshakeConfig { /// See [`ChannelHandshakeConfig::our_to_self_delay`] and [`ChannelConfig::cltv_expiry_delta`] /// for more information. /// - /// Default value: `10` + /// Default value: `100` /// /// Minimum value: `1` (Any values less will be treated as `1` instead.) /// /// Maximum value: `100` (Any values larger will be treated as `100` instead.) - pub max_inbound_htlc_value_in_flight_percent_of_channel: u8, + pub unannounced_channel_max_inbound_htlc_value_in_flight_percentage: u8, /// If set, we attempt to negotiate the `scid_privacy` (referred to as `scid_alias` in the /// BOLTs) option for outbound private channels. This provides better privacy by not including /// our real on-chain channel UTXO in each invoice and requiring that our counterparty only @@ -247,7 +264,8 @@ impl Default for ChannelHandshakeConfig { minimum_depth: 6, our_to_self_delay: BREAKDOWN_TIMEOUT, our_htlc_minimum_msat: 1, - max_inbound_htlc_value_in_flight_percent_of_channel: 10, + announced_channel_max_inbound_htlc_value_in_flight_percentage: 25, + unannounced_channel_max_inbound_htlc_value_in_flight_percentage: 100, negotiate_scid_privacy: false, announce_for_forwarding: false, commit_upfront_shutdown_pubkey: true, @@ -265,11 +283,21 @@ impl Default for ChannelHandshakeConfig { #[cfg(fuzzing)] impl Readable for ChannelHandshakeConfig { fn read<R: crate::io::Read>(reader: &mut R) -> Result<Self, crate::ln::msgs::DecodeError> { + let minimum_depth = Readable::read(reader)?; + let our_to_self_delay = Readable::read(reader)?; + let our_htlc_minimum_msat = Readable::read(reader)?; + // Apply the same byte to both the announced and the unannounced maximums so as to + // not invalidate the existing fuzz corpus + let max_inbound_htlc_value_in_flight_percentage = Readable::read(reader)?; + Ok(Self { - minimum_depth: Readable::read(reader)?, - our_to_self_delay: Readable::read(reader)?, - our_htlc_minimum_msat: Readable::read(reader)?, - max_inbound_htlc_value_in_flight_percent_of_channel: Readable::read(reader)?, + minimum_depth, + our_to_self_delay, + our_htlc_minimum_msat, + announced_channel_max_inbound_htlc_value_in_flight_percentage: + max_inbound_htlc_value_in_flight_percentage, + unannounced_channel_max_inbound_htlc_value_in_flight_percentage: + max_inbound_htlc_value_in_flight_percentage, negotiate_scid_privacy: Readable::read(reader)?, announce_for_forwarding: Readable::read(reader)?, commit_upfront_shutdown_pubkey: Readable::read(reader)?, @@ -295,16 +323,13 @@ impl Readable for ChannelHandshakeConfig { #[derive(Copy, Clone, Debug)] pub struct ChannelHandshakeLimits { /// Minimum allowed satoshis when a channel is funded. This is supplied by the sender and so - /// only applies to inbound channels. + /// only applies to inbound channels. It is also enforced for inbound channels on splices in + /// which the counterparty's contribution is negative. /// /// Default value: `1000` - /// (Minimum of [`ChannelHandshakeConfig::their_channel_reserve_proportional_millionths`]) - pub min_funding_satoshis: u64, - /// Maximum allowed satoshis when a channel is funded. This is supplied by the sender and so - /// only applies to inbound channels. /// - /// Default value: `2^24 - 1` - pub max_funding_satoshis: u64, + /// Minimum value: `1000` (Any values less will be treated as `1000` instead.) + pub min_funding_satoshis: u64, /// The remote node sets a limit on the minimum size of HTLCs we can send to them. This allows /// you to limit the maximum minimum-size they can require. /// @@ -374,7 +399,6 @@ impl Default for ChannelHandshakeLimits { fn default() -> Self { ChannelHandshakeLimits { min_funding_satoshis: 1000, - max_funding_satoshis: MAX_FUNDING_SATOSHIS_NO_WUMBO, max_htlc_minimum_msat: u64::MAX, min_max_htlc_value_in_flight_msat: 0, max_channel_reserve_satoshis: u64::MAX, @@ -395,7 +419,6 @@ impl Readable for ChannelHandshakeLimits { fn read<R: crate::io::Read>(reader: &mut R) -> Result<Self, crate::ln::msgs::DecodeError> { Ok(Self { min_funding_satoshis: Readable::read(reader)?, - max_funding_satoshis: Readable::read(reader)?, max_htlc_minimum_msat: Readable::read(reader)?, min_max_htlc_value_in_flight_msat: Readable::read(reader)?, max_channel_reserve_satoshis: Readable::read(reader)?, @@ -458,7 +481,7 @@ pub enum MaxDustHTLCExposure { FeeRateMultiplier(u64), } -impl_writeable_tlv_based_enum_legacy!(MaxDustHTLCExposure, ; +impl_ser_tlv_based_enum_legacy!(MaxDustHTLCExposure, ; (1, FixedLimitMsat), (3, FeeRateMultiplier), ); @@ -928,6 +951,51 @@ pub enum HTLCInterceptionFlags { | Self::ToOfflinePrivateChannels as isize | Self::ToOnlinePrivateChannels as isize | Self::ToPublicChannels as isize, + /// If this flag is set, any attempts to forward a payment from a private channel (to anywhere) + /// will instead generate an [`Event::HTLCIntercepted`] which must be handled the same as any + /// other intercepted HTLC. + /// + /// This is useful for an LSP that may wish to apply a higher fee policy on their channels when + /// the HTLC comes from a private channel client. Note that HTLCs which do not pay the + /// configured fee rate or do not meet the [`ChannelConfig::cltv_expiry_delta`] will fail. + /// Thus, this cannot be used to allow forwarding for less than the public fees. + /// + /// Note that no HTLCs to unknown channels will be intercepted by this flag. For that, use + /// [`Self::ToUnknownSCIDs`]. + /// + /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted + FromPrivateChannels = 1 << 4, + /// If this flag is set, any attempts to forward a payment from a public channel to a private + /// channel will instead generate an [`Event::HTLCIntercepted`] which must be handled the same + /// as any other intercepted HTLC. + /// + /// This is useful for an LSP that may wish to take an additional fee on any HTLCs which are + /// forwarded to a private channel client but wishes to avoid taking that fee when forwarding + /// an HTLC from a private channel client to another private channel client. + /// + /// Note that HTLCs which do not pay the configured fee rate or do not meet the + /// [`ChannelConfig::cltv_expiry_delta`] will fail and not be intercepted. + /// + /// Note that no HTLCs to unknown channels will be intercepted by this flag. For that, use + /// [`Self::ToUnknownSCIDs`]. + /// + /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted + FromPublicToPrivateChannels = 1 << 5, + /// If this flag is set, any attempts to forward a payment from a public channel to another + /// public channel will instead generate an [`Event::HTLCIntercepted`] which must be handled + /// the same as any other intercepted HTLC. + /// + /// This primarily exists for completeness, and generally interception of HTLCs between public + /// channels is *strongly* discouraged. + /// + /// Note that HTLCs which do not pay the configured fee rate or do not meet the + /// [`ChannelConfig::cltv_expiry_delta`] will fail and not be intercepted. + /// + /// Note that no HTLCs to unknown channels will be intercepted by this flag. For that, use + /// [`Self::ToUnknownSCIDs`]. + /// + /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted + FromPublicToPublicChannels = 1 << 6, /// If this flag is set, any attempts to forward a payment to an unknown short channel id will /// instead generate an [`Event::HTLCIntercepted`] which must be handled the same as any other /// intercepted HTLC. @@ -939,7 +1007,7 @@ pub enum HTLCInterceptionFlags { /// delta meets your requirements before forwarding the HTLC. /// /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted - ToUnknownSCIDs = 1 << 4, + ToUnknownSCIDs = 1 << 7, /// If these flags are set, all HTLCs being forwarded over this node will instead generate an /// [`Event::HTLCIntercepted`] which must be handled the same as any other intercepted HTLC. /// @@ -949,7 +1017,7 @@ pub enum HTLCInterceptionFlags { /// validate the fee and CLTV delta meets your requirements before forwarding the HTLC. /// /// [`Event::HTLCIntercepted`]: crate::events::Event::HTLCIntercepted - AllValidHTLCs = Self::ToAllKnownSCIDs as isize | Self::ToUnknownSCIDs as isize, + AllValidHTLCs = 0xff, } impl Into<u8> for HTLCInterceptionFlags { @@ -1134,9 +1202,15 @@ impl UserConfig { /// Config structure for overriding channel handshake parameters. #[derive(Default)] pub struct ChannelHandshakeConfigUpdate { - /// Overrides the percentage of the channel value we will cap the total value of outstanding inbound HTLCs to. See - /// [`ChannelHandshakeConfig::max_inbound_htlc_value_in_flight_percent_of_channel`]. - pub max_inbound_htlc_value_in_flight_percent_of_channel: Option<u8>, + /// Overrides the maximum percentage of the total channel value that can be allocated to inbound + /// HTLCs in announced channels. See + /// [`ChannelHandshakeConfig::announced_channel_max_inbound_htlc_value_in_flight_percentage`]. + pub announced_channel_max_inbound_htlc_value_in_flight_percentage: Option<u8>, + + /// Overrides the maximum percentage of the total channel value that can be allocated to inbound + /// HTLCs in unannounced channels. See + /// [`ChannelHandshakeConfig::unannounced_channel_max_inbound_htlc_value_in_flight_percentage`]. + pub unannounced_channel_max_inbound_htlc_value_in_flight_percentage: Option<u8>, /// Overrides the smallest value HTLC we will accept to process. See [`ChannelHandshakeConfig::our_htlc_minimum_msat`]. pub htlc_minimum_msat: Option<u64>, @@ -1158,13 +1232,41 @@ pub struct ChannelHandshakeConfigUpdate { pub channel_reserve_proportional_millionths: Option<u32>, } +impl From<ChannelHandshakeConfig> for ChannelHandshakeConfigUpdate { + fn from(config: ChannelHandshakeConfig) -> Self { + Self { + announced_channel_max_inbound_htlc_value_in_flight_percentage: Some( + config.announced_channel_max_inbound_htlc_value_in_flight_percentage, + ), + unannounced_channel_max_inbound_htlc_value_in_flight_percentage: Some( + config.unannounced_channel_max_inbound_htlc_value_in_flight_percentage, + ), + htlc_minimum_msat: Some(config.our_htlc_minimum_msat), + minimum_depth: Some(config.minimum_depth), + to_self_delay: Some(config.our_to_self_delay), + max_accepted_htlcs: Some(config.our_max_accepted_htlcs), + channel_reserve_proportional_millionths: Some( + config.their_channel_reserve_proportional_millionths, + ), + } + } +} + impl ChannelHandshakeConfig { /// Applies the provided handshake config update. pub fn apply(&mut self, config: &ChannelHandshakeConfigUpdate) { if let Some(max_in_flight_percent) = - config.max_inbound_htlc_value_in_flight_percent_of_channel + config.announced_channel_max_inbound_htlc_value_in_flight_percentage + { + self.announced_channel_max_inbound_htlc_value_in_flight_percentage = + max_in_flight_percent; + } + + if let Some(max_in_flight_percent) = + config.unannounced_channel_max_inbound_htlc_value_in_flight_percentage { - self.max_inbound_htlc_value_in_flight_percent_of_channel = max_in_flight_percent; + self.unannounced_channel_max_inbound_htlc_value_in_flight_percentage = + max_in_flight_percent; } if let Some(htlc_minimum_msat) = config.htlc_minimum_msat { diff --git a/lightning/src/util/dyn_signer.rs b/lightning/src/util/dyn_signer.rs index cf1cac37903..5da284d25a4 100644 --- a/lightning/src/util/dyn_signer.rs +++ b/lightning/src/util/dyn_signer.rs @@ -12,8 +12,6 @@ use crate::ln::inbound_payment::ExpandedKey; use crate::ln::msgs::{UnsignedChannelAnnouncement, UnsignedGossipMessage}; use crate::ln::script::ShutdownScript; use crate::sign::ecdsa::EcdsaChannelSigner; -#[cfg(taproot)] -use crate::sign::taproot::TaprootChannelSigner; use crate::sign::InMemorySigner; use crate::sign::{ChannelSigner, ReceiveAuthKey}; use crate::sign::{EntropySource, HTLCDescriptor, OutputSpender, PhantomKeysManager}; @@ -25,20 +23,13 @@ use bitcoin::absolute::LockTime; use bitcoin::secp256k1::All; use bitcoin::{secp256k1, ScriptBuf, Transaction, TxOut, Txid}; use lightning_invoice::RawBolt11Invoice; -#[cfg(taproot)] -use musig2::types::{PartialSignature, PublicNonce}; use secp256k1::ecdsa::RecoverableSignature; use secp256k1::{ecdh::SharedSecret, ecdsa::Signature, PublicKey, Scalar, Secp256k1, SecretKey}; use types::payment::PaymentPreimage; -#[cfg(not(taproot))] /// A super-trait for all the traits that a dyn signer backing implements pub trait DynSignerTrait: EcdsaChannelSigner + Send + Sync {} -#[cfg(taproot)] -/// A super-trait for all the traits that a dyn signer backing implements -pub trait DynSignerTrait: EcdsaChannelSigner + TaprootChannelSigner + Send + Sync {} - /// Helper to allow DynSigner to clone itself pub trait InnerSign: DynSignerTrait { /// Clone into a Box @@ -60,67 +51,6 @@ impl DynSigner { } } -#[cfg(taproot)] -#[allow(unused_variables)] -impl TaprootChannelSigner for DynSigner { - fn generate_local_nonce_pair( - &self, commitment_number: u64, secp_ctx: &Secp256k1<All>, - ) -> PublicNonce { - todo!() - } - - fn partially_sign_counterparty_commitment( - &self, counterparty_nonce: PublicNonce, commitment_tx: &CommitmentTransaction, - inbound_htlc_preimages: Vec<PaymentPreimage>, - outbound_htlc_preimages: Vec<PaymentPreimage>, secp_ctx: &Secp256k1<All>, - ) -> Result<(crate::ln::msgs::PartialSignatureWithNonce, Vec<secp256k1::schnorr::Signature>), ()> - { - todo!(); - } - - fn finalize_holder_commitment( - &self, commitment_tx: &HolderCommitmentTransaction, - counterparty_partial_signature: crate::ln::msgs::PartialSignatureWithNonce, - secp_ctx: &Secp256k1<All>, - ) -> Result<PartialSignature, ()> { - todo!(); - } - - fn sign_justice_revoked_output( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!(); - } - - fn sign_justice_revoked_htlc( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!(); - } - - fn sign_holder_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, htlc_descriptor: &HTLCDescriptor, - secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!(); - } - - fn sign_counterparty_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, amount: u64, per_commitment_point: &PublicKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!(); - } - - fn partially_sign_closing_transaction( - &self, closing_tx: &ClosingTransaction, secp_ctx: &Secp256k1<All>, - ) -> Result<PartialSignature, ()> { - todo!(); - } -} - impl Clone for DynSigner { fn clone(&self) -> Self { DynSigner { inner: self.inner.box_clone() } @@ -160,7 +90,7 @@ delegate!(DynSigner, EcdsaChannelSigner, inner, fn sign_holder_htlc_transaction(, htlc_tx: &Transaction, input: usize, htlc_descriptor: &HTLCDescriptor, secp_ctx: &Secp256k1<All>) -> Result<Signature, ()>, fn sign_splice_shared_input(, channel_parameters: &ChannelTransactionParameters, - tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1<All>) -> Signature + tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1<All>) -> Result<Signature, ()> ); delegate!(DynSigner, ChannelSigner, @@ -231,8 +161,6 @@ delegate!(DynKeysInterface, SignerProvider, fn generate_channel_keys_id(, _inbound: bool, _user_channel_id: u128) -> [u8; 32], fn derive_channel_signer(, _channel_keys_id: [u8; 32]) -> Self::EcdsaSigner; type EcdsaSigner = DynSigner, - #[cfg(taproot)] - type TaprootSigner = DynSigner ); delegate!(DynKeysInterface, EntropySource, inner, @@ -246,25 +174,12 @@ delegate!(DynKeysInterface, OutputSpender, inner, locktime: Option<LockTime>, secp_ctx: &Secp256k1<All> ) -> Result<Transaction, ()> ); -#[cfg(not(taproot))] /// A supertrait for all the traits that a keys interface implements pub trait DynKeysInterfaceTrait: NodeSigner + OutputSpender + SignerProvider<EcdsaSigner = DynSigner> + EntropySource + Send + Sync { } -#[cfg(taproot)] -/// A supertrait for all the traits that a keys interface implements -pub trait DynKeysInterfaceTrait: - NodeSigner - + OutputSpender - + SignerProvider<EcdsaSigner = DynSigner, TaprootSigner = DynSigner> - + EntropySource - + Send - + Sync -{ -} - /// A dyn wrapper for PhantomKeysManager pub struct DynPhantomKeysInterface { inner: Box<PhantomKeysManager>, @@ -293,8 +208,6 @@ delegate!(DynPhantomKeysInterface, NodeSigner, impl SignerProvider for DynPhantomKeysInterface { type EcdsaSigner = DynSigner; - #[cfg(taproot)] - type TaprootSigner = DynSigner; fn get_destination_script(&self, channel_keys_id: [u8; 32]) -> Result<ScriptBuf, ()> { self.inner.get_destination_script(channel_keys_id) diff --git a/lightning/src/util/errors.rs b/lightning/src/util/errors.rs index eaaf0130ca2..cd72d60327f 100644 --- a/lightning/src/util/errors.rs +++ b/lightning/src/util/errors.rs @@ -9,7 +9,10 @@ //! Error types live here. +use bitcoin::secp256k1::PublicKey; + use crate::ln::script::ShutdownScript; +use crate::ln::types::ChannelId; #[allow(unused_imports)] use crate::prelude::*; @@ -90,6 +93,28 @@ impl fmt::Debug for APIError { } } +impl APIError { + pub(crate) fn no_such_peer(counterparty_node_id: &PublicKey) -> Self { + Self::ChannelUnavailable { + err: format!( + "No such peer for the passed counterparty_node_id {}", + counterparty_node_id + ), + } + } + + pub(crate) fn no_such_channel_for_peer( + channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) -> Self { + Self::ChannelUnavailable { + err: format!( + "No such channel_id {} for the passed counterparty_node_id {}", + channel_id, counterparty_node_id + ), + } + } +} + impl_writeable_tlv_based_enum_upgradable!(APIError, (0, APIMisuseError) => { (0, err, required), }, (2, FeeRateTooHigh) => { diff --git a/lightning/src/util/hash_tables.rs b/lightning/src/util/hash_tables.rs index b6555975191..545b034a5ae 100644 --- a/lightning/src/util/hash_tables.rs +++ b/lightning/src/util/hash_tables.rs @@ -6,11 +6,11 @@ pub use hashbrown::hash_map; mod hashbrown_tables { - #[cfg(all(feature = "std", not(test)))] + #[cfg(all(feature = "std", not(test), not(fuzzing)))] mod hasher { pub use std::collections::hash_map::RandomState; } - #[cfg(all(feature = "std", test))] + #[cfg(all(feature = "std", any(test, fuzzing)))] mod hasher { #![allow(deprecated)] // hash::SipHasher was deprecated in favor of something only in std. use core::hash::{BuildHasher, Hasher}; @@ -27,7 +27,10 @@ mod hashbrown_tables { impl RandomState { pub fn new() -> RandomState { - if std::env::var("LDK_TEST_DETERMINISTIC_HASHES").map(|v| v == "1").unwrap_or(false) + if cfg!(fuzzing) + || std::env::var("LDK_TEST_DETERMINISTIC_HASHES") + .map(|v| v == "1") + .unwrap_or(false) { RandomState::Deterministic } else { diff --git a/lightning/src/util/macro_logger.rs b/lightning/src/util/macro_logger.rs index 12f4f67962e..66b6720292b 100644 --- a/lightning/src/util/macro_logger.rs +++ b/lightning/src/util/macro_logger.rs @@ -169,6 +169,33 @@ macro_rules! log_spendable { }; } +/// The maximum number of characters to display in a network message log entry. +pub(crate) const LOG_MSG_MAX_LEN: usize = 512; + +/// Wraps a string slice for Display, truncating to [`LOG_MSG_MAX_LEN`] characters and +/// delegating sanitization to [`crate::types::string::PrintableString`]. +/// Useful for logging counterparty-provided messages. +pub(crate) struct DebugMsg<'a>(pub &'a str); +impl<'a> core::fmt::Display for DebugMsg<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + let (msg, was_truncated) = match self.0.char_indices().nth(LOG_MSG_MAX_LEN) { + Some((idx, _)) => (&self.0[..idx], true), + None => (self.0, false), + }; + core::fmt::Display::fmt(&crate::types::string::PrintableString(msg), f)?; + if was_truncated { + f.write_str("...")?; + } + Ok(()) + } +} + +macro_rules! log_msg { + ($obj: expr) => { + $crate::util::macro_logger::DebugMsg(&$obj) + }; +} + /// Create a new Record and log it. You probably don't want to use this macro directly, /// but it needs to be exported so `log_trace` etc can use it in external crates. #[doc(hidden)] @@ -226,3 +253,61 @@ macro_rules! log_gossip { $crate::log_given_level!($logger, $crate::util::logger::Level::Gossip, $($arg)*); ) } + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + + #[test] + fn debug_msg_short_string() { + let s = "hello world"; + assert_eq!(DebugMsg(s).to_string(), "hello world"); + } + + #[test] + fn debug_msg_truncates_at_limit() { + let s = "a".repeat(LOG_MSG_MAX_LEN + 100); + let result = DebugMsg(&s).to_string(); + // Should be exactly LOG_MSG_MAX_LEN 'a's followed by "..." + assert_eq!(result.len(), LOG_MSG_MAX_LEN + 3); + assert!(result.ends_with("...")); + } + + #[test] + fn debug_msg_no_truncation_at_exact_limit() { + let s = "a".repeat(LOG_MSG_MAX_LEN); + let result = DebugMsg(&s).to_string(); + assert_eq!(result.len(), LOG_MSG_MAX_LEN); + assert!(!result.ends_with("...")); + } + + #[test] + fn debug_msg_replaces_control_characters() { + let s = "hello\x00world\nfoo"; + let result = DebugMsg(s).to_string(); + assert_eq!(result, "hello\u{FFFD}world\u{FFFD}foo"); + } + + #[test] + fn debug_msg_uses_printable_string_sanitization() { + let s = "safe\u{202E}cipsxe.exe"; + assert_eq!(DebugMsg(s).to_string(), crate::types::string::PrintableString(s).to_string()); + } + + #[test] + fn debug_msg_multibyte_unicode() { + // Each emoji is multiple bytes but one character + let s = "\u{1F600}".repeat(LOG_MSG_MAX_LEN + 10); + let result = DebugMsg(&s).to_string(); + let char_count: usize = result.chars().count(); + // LOG_MSG_MAX_LEN emoji chars + 3 chars for "..." + assert_eq!(char_count, LOG_MSG_MAX_LEN + 3); + assert!(result.ends_with("...")); + } + + #[test] + fn debug_msg_empty_string() { + assert_eq!(DebugMsg("").to_string(), ""); + } +} diff --git a/lightning/src/util/mod.rs b/lightning/src/util/mod.rs index dcbea904b51..4f3e930caf4 100644 --- a/lightning/src/util/mod.rs +++ b/lightning/src/util/mod.rs @@ -20,7 +20,7 @@ pub mod mut_global; pub mod anchor_channel_reserves; -pub mod async_poll; +pub(crate) mod async_poll; #[cfg(fuzzing)] pub mod base32; #[cfg(not(fuzzing))] @@ -51,6 +51,7 @@ pub(crate) mod macro_logger; // These have to come after macro_logger to build pub mod config; pub mod logger; +pub mod wallet_utils; #[cfg(any(test, feature = "_test_utils"))] pub mod test_utils; diff --git a/lightning/src/util/native_async.rs b/lightning/src/util/native_async.rs index 0c380f2b1d1..31b07c2f3b5 100644 --- a/lightning/src/util/native_async.rs +++ b/lightning/src/util/native_async.rs @@ -9,8 +9,9 @@ #[cfg(all(test, feature = "std"))] use crate::sync::{Arc, Mutex}; -use crate::util::async_poll::{MaybeSend, MaybeSync}; +#[cfg(test)] +use alloc::boxed::Box; #[cfg(all(test, not(feature = "std")))] use alloc::rc::Rc; @@ -53,6 +54,34 @@ trait MaybeSendableFuture: Future<Output = ()> + MaybeSend + 'static {} #[cfg(test)] impl<F: Future<Output = ()> + MaybeSend + 'static> MaybeSendableFuture for F {} +/// Marker trait to optionally implement `Sync` under std. +/// +/// This is not exported to bindings users as async is only supported in Rust. +#[cfg(feature = "std")] +pub use core::marker::Sync as MaybeSync; + +#[cfg(not(feature = "std"))] +/// Marker trait to optionally implement `Sync` under std. +/// +/// This is not exported to bindings users as async is only supported in Rust. +pub trait MaybeSync {} +#[cfg(not(feature = "std"))] +impl<T> MaybeSync for T where T: ?Sized {} + +/// Marker trait to optionally implement `Send` under std. +/// +/// This is not exported to bindings users as async is only supported in Rust. +#[cfg(feature = "std")] +pub use core::marker::Send as MaybeSend; + +#[cfg(not(feature = "std"))] +/// Marker trait to optionally implement `Send` under std. +/// +/// This is not exported to bindings users as async is only supported in Rust. +pub trait MaybeSend {} +#[cfg(not(feature = "std"))] +impl<T> MaybeSend for T where T: ?Sized {} + /// A simple [`FutureSpawner`] which holds [`Future`]s until they are manually polled via /// [`Self::poll_futures`]. #[cfg(all(test, feature = "std"))] diff --git a/lightning/src/util/persist.rs b/lightning/src/util/persist.rs index cb4bdeb6a51..ddc285690bf 100644 --- a/lightning/src/util/persist.rs +++ b/lightning/src/util/persist.rs @@ -14,9 +14,10 @@ use alloc::sync::Arc; use bitcoin::hashes::hex::FromHex; -use bitcoin::{BlockHash, Txid}; +use bitcoin::Txid; use core::convert::Infallible; +use core::fmt; use core::future::Future; use core::mem; use core::ops::Deref; @@ -32,14 +33,15 @@ use crate::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; use crate::chain::chainmonitor::Persist; use crate::chain::channelmonitor::{ChannelMonitor, ChannelMonitorUpdate}; use crate::chain::transaction::OutPoint; +use crate::chain::BlockLocator; use crate::ln::types::ChannelId; use crate::sign::{ecdsa::EcdsaChannelSigner, EntropySource, SignerProvider}; use crate::sync::Mutex; use crate::util::async_poll::{ - dummy_waker, MaybeSend, MaybeSync, MultiResultFuturePoller, ResultFuture, TwoFutureJoiner, + dummy_waker, MultiResultFuturePoller, ResultFuture, TwoFutureJoiner, }; use crate::util::logger::Logger; -use crate::util::native_async::FutureSpawner; +use crate::util::native_async::{FutureSpawner, MaybeSend, MaybeSync}; use crate::util::ser::{Readable, ReadableArgs, Writeable}; use crate::util::wakers::Notifier; @@ -367,9 +369,194 @@ where } } -/// Provides additional interface methods that are required for [`KVStore`]-to-[`KVStore`] +/// An opaque token used for paginated listing operations. +/// +/// This token should be treated as an opaque value by callers. Pass the token returned from +/// one `list_paginated` call to the next call to continue pagination. The internal format +/// is implementation-defined and may change between versions. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PageToken(String); + +impl PageToken { + /// Creates a new `PageToken` from the given string. + pub fn new(token: String) -> Self { + PageToken(token) + } + + /// Returns the inner string representation of the `PageToken`. + pub fn as_str(&self) -> &str { + &self.0 + } +} + +impl fmt::Display for PageToken { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +/// Represents the response from a paginated `list` operation. +/// +/// Contains the list of keys and a token for retrieving the next page of results. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PaginatedListResponse { + /// A vector of keys, ordered from most recently created to least recently created. + pub keys: Vec<String>, + + /// A token that can be passed to the next call to continue pagination. + /// + /// Is `None` if there are no more pages to retrieve. + pub next_page_token: Option<PageToken>, +} + +/// Extends [`KVStoreSync`] with paginated key listing in reverse creation order. +/// +/// While [`KVStoreSync::list`] returns all keys at once in arbitrary order, this trait adds a +/// [`list_paginated`] method that returns keys in pages ordered from newest to oldest. This is +/// useful when a namespace may contain a large number of keys that would be expensive to retrieve +/// in a single call. +/// +/// Namespace and key requirements are inherited from [`KVStoreSync`]. +/// +/// For an asynchronous version of this trait, see [`PaginatedKVStore`]. +/// +/// [`list_paginated`]: Self::list_paginated +pub trait PaginatedKVStoreSync: KVStoreSync { + /// Returns a paginated list of keys that are stored under the given `secondary_namespace` in + /// `primary_namespace`, ordered from most recently created to least recently created. + /// + /// Implementations must return keys in reverse creation order (newest first). How creation + /// order is tracked is implementation-defined (e.g., storing creation timestamps, using an + /// incrementing ID, or another mechanism). Creation order (not last-updated order) is used + /// to prevent race conditions during pagination: if keys were ordered by update time, a key + /// updated mid-pagination could shift position, causing it to be skipped or returned twice + /// across pages. + /// + /// If `page_token` is provided, listing continues from where the previous page left off. + /// If `None`, listing starts from the most recently created entry. The `next_page_token` + /// in the returned [`PaginatedListResponse`] can be passed to subsequent calls to fetch + /// the next page. + /// + /// Implementations must generate a [`PageToken`] that encodes enough information to resume + /// listing from the correct position. Tokens must remain valid across multiple calls within + /// a reasonable timeframe. If the entry referenced by a token has been deleted, + /// implementations should resume from the next valid position rather than failing. + /// Tokens are scoped to a specific `(primary_namespace, secondary_namespace)` pair and should + /// not be used across different namespace pairs. + /// + /// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if + /// there are no more keys to return. + fn list_paginated( + &self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>, + ) -> Result<PaginatedListResponse, io::Error>; +} + +/// A wrapper around a [`PaginatedKVStoreSync`] that implements the [`PaginatedKVStore`] trait. +/// It is not necessary to use this type directly. +#[derive(Clone)] +pub struct PaginatedKVStoreSyncWrapper<K: Deref>(pub K) +where + K::Target: PaginatedKVStoreSync; + +/// This is not exported to bindings users as async is only supported in Rust. +impl<K: Deref> KVStore for PaginatedKVStoreSyncWrapper<K> +where + K::Target: PaginatedKVStoreSync, +{ + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> impl Future<Output = Result<Vec<u8>, io::Error>> + 'static + MaybeSend { + let res = self.0.read(primary_namespace, secondary_namespace, key); + + async move { res } + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend { + let res = self.0.write(primary_namespace, secondary_namespace, key, buf); + + async move { res } + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend { + let res = self.0.remove(primary_namespace, secondary_namespace, key, lazy); + + async move { res } + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> impl Future<Output = Result<Vec<String>, io::Error>> + 'static + MaybeSend { + let res = self.0.list(primary_namespace, secondary_namespace); + + async move { res } + } +} + +/// This is not exported to bindings users as async is only supported in Rust. +impl<K: Deref> PaginatedKVStore for PaginatedKVStoreSyncWrapper<K> +where + K::Target: PaginatedKVStoreSync, +{ + fn list_paginated( + &self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>, + ) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + MaybeSend { + let res = self.0.list_paginated(primary_namespace, secondary_namespace, page_token); + + async move { res } + } +} + +/// Extends [`KVStore`] with paginated key listing in reverse creation order. +/// +/// While [`KVStore::list`] returns all keys at once in arbitrary order, this trait adds a +/// [`list_paginated`] method that returns keys in pages ordered from newest to oldest. This is +/// useful when a namespace may contain a large number of keys that would be expensive to retrieve +/// in a single call. +/// +/// Namespace and key requirements are inherited from [`KVStore`]. +/// +/// For a synchronous version of this trait, see [`PaginatedKVStoreSync`]. +/// +/// [`list_paginated`]: Self::list_paginated +/// +/// This is not exported to bindings users as async is only supported in Rust. +pub trait PaginatedKVStore: KVStore { + /// Returns a paginated list of keys that are stored under the given `secondary_namespace` in + /// `primary_namespace`, ordered from most recently created to least recently created. + /// + /// Implementations must return keys in reverse creation order (newest first). How creation + /// order is tracked is implementation-defined (e.g., storing creation timestamps, using an + /// incrementing ID, or another mechanism). Creation order (not last-updated order) is used + /// to prevent race conditions during pagination: if keys were ordered by update time, a key + /// updated mid-pagination could shift position, causing it to be skipped or returned twice + /// across pages. + /// + /// If `page_token` is provided, listing continues from where the previous page left off. + /// If `None`, listing starts from the most recently created entry. The `next_page_token` + /// in the returned [`PaginatedListResponse`] can be passed to subsequent calls to fetch + /// the next page. + /// + /// Implementations must generate a [`PageToken`] that encodes enough information to resume + /// listing from the correct position. Tokens must remain valid across multiple calls within + /// a reasonable timeframe. If the entry referenced by a token has been deleted, + /// implementations should resume from the next valid position rather than failing. + /// Tokens are scoped to a specific `(primary_namespace, secondary_namespace)` pair and should + /// not be used across different namespace pairs. + /// + /// Returns an empty list if `primary_namespace` or `secondary_namespace` is unknown or if + /// there are no more keys to return. + fn list_paginated( + &self, primary_namespace: &str, secondary_namespace: &str, page_token: Option<PageToken>, + ) -> impl Future<Output = Result<PaginatedListResponse, io::Error>> + 'static + MaybeSend; +} + +/// Provides additional interface methods that are required for [`KVStoreSync`]-to-[`KVStoreSync`] /// data migration. -pub trait MigratableKVStore: KVStoreSync { +pub trait MigratableKVStoreSync: KVStoreSync { /// Returns *all* known keys as a list of `primary_namespace`, `secondary_namespace`, `key` tuples. /// /// This is useful for migrating data from [`KVStoreSync`] implementation to [`KVStoreSync`] @@ -380,6 +567,128 @@ pub trait MigratableKVStore: KVStoreSync { fn list_all_keys(&self) -> Result<Vec<(String, String, String)>, io::Error>; } +/// Provides additional interface methods that are required for [`KVStore`]-to-[`KVStore`] +/// data migration. +/// +/// This is not exported to bindings users as async is only supported in Rust. +pub trait MigratableKVStore: KVStore { + /// Returns *all* known keys as a list of `primary_namespace`, `secondary_namespace`, `key` tuples. + /// + /// This is useful for migrating data from [`KVStore`] implementation to [`KVStore`] + /// implementation. + /// + /// Must exhaustively return all entries known to the store to ensure no data is missed, but + /// may return the items in arbitrary order. + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<(String, String, String)>, io::Error>> + 'static + MaybeSend; +} + +impl<K> MigratableKVStore for K +where + K: Deref, + K::Target: MigratableKVStore, +{ + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<(String, String, String)>, io::Error>> + 'static + MaybeSend + { + self.deref().list_all_keys() + } +} + +/// This is not exported to bindings users as async is only supported in Rust. +impl<K: Deref> MigratableKVStore for KVStoreSyncWrapper<K> +where + K::Target: MigratableKVStoreSync, +{ + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<(String, String, String)>, io::Error>> + 'static + MaybeSend + { + let res = self.0.list_all_keys(); + + async move { res } + } +} + +type MigrationKey = (String, String, String); + +trait MigrationKVStore { + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<MigrationKey>, io::Error>> + MaybeSend; + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> impl Future<Output = Result<Vec<u8>, io::Error>> + MaybeSend; + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> impl Future<Output = Result<(), io::Error>> + MaybeSend; +} + +struct MigrationKVStoreSyncAdapter<'a, K: ?Sized>(&'a K); + +impl<K: MigratableKVStoreSync + ?Sized> MigrationKVStore for MigrationKVStoreSyncAdapter<'_, K> { + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<MigrationKey>, io::Error>> + MaybeSend { + let res = MigratableKVStoreSync::list_all_keys(self.0); + + async move { res } + } + + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> impl Future<Output = Result<Vec<u8>, io::Error>> + MaybeSend { + let res = KVStoreSync::read(self.0, primary_namespace, secondary_namespace, key); + + async move { res } + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> impl Future<Output = Result<(), io::Error>> + MaybeSend { + let res = KVStoreSync::write(self.0, primary_namespace, secondary_namespace, key, buf); + + async move { res } + } +} + +struct MigrationKVStoreAsyncAdapter<'a, K: ?Sized>(&'a K); + +impl<K: MigratableKVStore + ?Sized> MigrationKVStore for MigrationKVStoreAsyncAdapter<'_, K> { + fn list_all_keys( + &self, + ) -> impl Future<Output = Result<Vec<MigrationKey>, io::Error>> + MaybeSend { + MigratableKVStore::list_all_keys(self.0) + } + + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> impl Future<Output = Result<Vec<u8>, io::Error>> + MaybeSend { + KVStore::read(self.0, primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec<u8>, + ) -> impl Future<Output = Result<(), io::Error>> + MaybeSend { + KVStore::write(self.0, primary_namespace, secondary_namespace, key, buf) + } +} + +async fn migrate_kv_store_data_inner<S: MigrationKVStore, T: MigrationKVStore>( + source_store: S, target_store: T, +) -> Result<(), io::Error> { + let keys_to_migrate = source_store.list_all_keys().await?; + + for (primary_namespace, secondary_namespace, key) in &keys_to_migrate { + let data = source_store.read(primary_namespace, secondary_namespace, key).await?; + target_store.write(primary_namespace, secondary_namespace, key, data).await?; + } + + Ok(()) +} + /// Migrates all data from one store to another. /// /// This operation assumes that `target_store` is empty, i.e., any data present under copied keys @@ -388,17 +697,31 @@ pub trait MigratableKVStore: KVStoreSync { /// /// Will abort and return an error if any IO operation fails. Note that in this case the /// `target_store` might get left in an intermediate state. -pub fn migrate_kv_store_data<S: MigratableKVStore, T: MigratableKVStore>( +pub fn migrate_kv_store_data<S: MigratableKVStoreSync, T: MigratableKVStoreSync>( source_store: &mut S, target_store: &mut T, ) -> Result<(), io::Error> { - let keys_to_migrate = source_store.list_all_keys()?; - - for (primary_namespace, secondary_namespace, key) in &keys_to_migrate { - let data = source_store.read(primary_namespace, secondary_namespace, key)?; - target_store.write(primary_namespace, secondary_namespace, key, data)?; - } + poll_sync_future(migrate_kv_store_data_inner( + MigrationKVStoreSyncAdapter(source_store), + MigrationKVStoreSyncAdapter(target_store), + )) +} - Ok(()) +/// Migrates all data from one asynchronous store to another. +/// +/// This operation assumes that `target_store` is empty, i.e., any data present under copied keys +/// might get overriden. User must ensure `source_store` is not modified during operation, +/// otherwise no consistency guarantees can be given. +/// +/// Will abort and return an error if any IO operation fails. Note that in this case the +/// `target_store` might get left in an intermediate state. +pub async fn migrate_kv_store_data_async<S: MigratableKVStore, T: MigratableKVStore>( + source_store: &S, target_store: &T, +) -> Result<(), io::Error> { + migrate_kv_store_data_inner( + MigrationKVStoreAsyncAdapter(source_store), + MigrationKVStoreAsyncAdapter(target_store), + ) + .await } impl<ChannelSigner: EcdsaChannelSigner, K: KVStoreSync + ?Sized> Persist<ChannelSigner> for K { @@ -467,7 +790,7 @@ impl<ChannelSigner: EcdsaChannelSigner, K: KVStoreSync + ?Sized> Persist<Channel /// Read previously persisted [`ChannelMonitor`]s from the store. pub fn read_channel_monitors<K: Deref, ES: EntropySource, SP: SignerProvider>( kv_store: K, entropy_source: ES, signer_provider: SP, -) -> Result<Vec<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> +) -> Result<Vec<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> where K::Target: KVStoreSync, { @@ -477,7 +800,7 @@ where CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE, )? { - match <Option<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>>::read( + match <Option<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>>::read( &mut io::Cursor::new(kv_store.read( CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE, @@ -485,7 +808,7 @@ where )?), (&entropy_source, &signer_provider), ) { - Ok(Some((block_hash, channel_monitor))) => { + Ok(Some((best_block, channel_monitor))) => { let monitor_name = MonitorName::from_str(&stored_key)?; if channel_monitor.persistence_key() != monitor_name { return Err(io::Error::new( @@ -494,7 +817,7 @@ where )); } - res.push((block_hash, channel_monitor)); + res.push((best_block, channel_monitor)); }, Ok(None) => {}, Err(_) => { @@ -670,7 +993,7 @@ where /// Reads all stored channel monitors, along with any stored updates for them. pub fn read_all_channel_monitors_with_updates( &self, - ) -> Result<Vec<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { + ) -> Result<Vec<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { poll_sync_future(self.0.read_all_channel_monitors_with_updates()) } @@ -691,7 +1014,7 @@ where /// function to accomplish this. Take care to limit the number of parallel readers. pub fn read_channel_monitor_with_updates( &self, monitor_key: &str, - ) -> Result<(BlockHash, ChannelMonitor<SP::EcdsaSigner>), io::Error> { + ) -> Result<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>), io::Error> { poll_sync_future(self.0.read_channel_monitor_with_updates(monitor_key)) } @@ -858,7 +1181,7 @@ impl< /// deserialization as well. pub async fn read_all_channel_monitors_with_updates( &self, - ) -> Result<Vec<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { + ) -> Result<Vec<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { let primary = CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE; let secondary = CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE; let monitor_list = self.0.kv_store.list(primary, secondary).await?; @@ -889,7 +1212,7 @@ impl< /// `Arc` that can live for `'static` and be sent and accessed across threads. pub async fn read_all_channel_monitors_with_updates_parallel( self: &Arc<Self>, - ) -> Result<Vec<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> + ) -> Result<Vec<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> where K: MaybeSend + MaybeSync + 'static, L: MaybeSend + MaybeSync + 'static, @@ -939,7 +1262,7 @@ impl< /// function to accomplish this. Take care to limit the number of parallel readers. pub async fn read_channel_monitor_with_updates( &self, monitor_key: &str, - ) -> Result<(BlockHash, ChannelMonitor<SP::EcdsaSigner>), io::Error> { + ) -> Result<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>), io::Error> { self.0.read_channel_monitor_with_updates(monitor_key).await } @@ -1050,7 +1373,7 @@ impl< { pub async fn read_channel_monitor_with_updates( &self, monitor_key: &str, - ) -> Result<(BlockHash, ChannelMonitor<SP::EcdsaSigner>), io::Error> { + ) -> Result<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>), io::Error> { match self.maybe_read_channel_monitor_with_updates(monitor_key).await? { Some(res) => Ok(res), None => Err(io::Error::new( @@ -1067,14 +1390,14 @@ impl< async fn maybe_read_channel_monitor_with_updates( &self, monitor_key: &str, - ) -> Result<Option<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { + ) -> Result<Option<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { let monitor_name = MonitorName::from_str(monitor_key)?; let read_future = pin!(self.maybe_read_monitor(&monitor_name, monitor_key)); let list_future = pin!(self .kv_store .list(CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE, monitor_key)); let (read_res, list_res) = TwoFutureJoiner::new(read_future, list_future).await; - let (block_hash, monitor) = match read_res? { + let (best_block, monitor) = match read_res? { Some(res) => res, None => return Ok(None), }; @@ -1105,13 +1428,13 @@ impl< io::Error::new(io::ErrorKind::Other, "Monitor update failed") })?; } - Ok(Some((block_hash, monitor))) + Ok(Some((best_block, monitor))) } /// Read a channel monitor. async fn maybe_read_monitor( &self, monitor_name: &MonitorName, monitor_key: &str, - ) -> Result<Option<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { + ) -> Result<Option<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>, io::Error> { let primary = CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE; let secondary = CHANNEL_MONITOR_PERSISTENCE_SECONDARY_NAMESPACE; let monitor_bytes = self.kv_store.read(primary, secondary, monitor_key).await?; @@ -1120,12 +1443,12 @@ impl< if monitor_cursor.get_ref().starts_with(MONITOR_UPDATING_PERSISTER_PREPEND_SENTINEL) { monitor_cursor.set_position(MONITOR_UPDATING_PERSISTER_PREPEND_SENTINEL.len() as u64); } - match <Option<(BlockHash, ChannelMonitor<SP::EcdsaSigner>)>>::read( + match <Option<(BlockLocator, ChannelMonitor<SP::EcdsaSigner>)>>::read( &mut monitor_cursor, (&self.entropy_source, &self.signer_provider), ) { Ok(None) => Ok(None), - Ok(Some((blockhash, channel_monitor))) => { + Ok(Some((best_block, channel_monitor))) => { if channel_monitor.persistence_key() != *monitor_name { log_error!( self.logger, @@ -1137,7 +1460,7 @@ impl< "ChannelMonitor was stored under the wrong key", )) } else { - Ok(Some((blockhash, channel_monitor))) + Ok(Some((best_block, channel_monitor))) } }, Err(e) => { @@ -1316,7 +1639,7 @@ impl< async fn archive_persisted_channel(&self, monitor_name: MonitorName) { let monitor_key = monitor_name.to_string(); let monitor = match self.read_channel_monitor_with_updates(&monitor_key).await { - Ok((_block_hash, monitor)) => monitor, + Ok((_best_block, monitor)) => monitor, Err(_) => return, }; let primary = ARCHIVED_CHANNEL_MONITOR_PERSISTENCE_PRIMARY_NAMESPACE; @@ -1532,8 +1855,8 @@ impl From<u64> for UpdateName { #[cfg(test)] mod tests { use super::*; + use crate::chain::channelmonitor::ChannelMonitorUpdateStep; use crate::chain::ChannelMonitorUpdateStatus; - use crate::check_closed_broadcast; use crate::events::ClosureReason; use crate::ln::functional_test_utils::*; use crate::ln::msgs::BaseMessageHandler; @@ -1756,7 +2079,7 @@ mod tests { let reason = ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }; check_closed_event(&nodes[0], 1, reason, &[node_id_1], 100000); - check_closed_broadcast!(nodes[0], true); + check_closed_broadcast(&nodes[0], 1, true); check_added_monitors(&nodes[0], 1); let node_txn = nodes[0].tx_broadcaster.txn_broadcast(); @@ -1765,7 +2088,7 @@ mod tests { let dummy_block = create_dummy_block(nodes[0].best_block_hash(), 42, txn); connect_block(&nodes[1], &dummy_block); - check_closed_broadcast!(nodes[1], true); + check_closed_broadcast(&nodes[1], 1, true); let reason = ClosureReason::CommitmentTxConfirmed; let node_id_0 = nodes[0].node.get_our_node_id(); check_closed_event(&nodes[1], 1, reason, &[node_id_0], 100000); @@ -1939,6 +2262,96 @@ mod tests { .is_err()); } + // Confirm we still handle the `u64::MAX` `update_id` that pre-0.1 LDK used for post-close + // `ChannelMonitorUpdate`s, both when reading a leftover update from disk and when one is handed + // to the persister to write. + #[test] + fn legacy_closed_channel_update() { + let max_pending_updates = 7; + let chanmon_cfgs = create_chanmon_cfgs(2); + let kv_store = TestStore::new(false); + let persister = MonitorUpdatingPersister::new( + &kv_store, + &chanmon_cfgs[0].logger, + max_pending_updates, + &chanmon_cfgs[0].keys_manager, + &chanmon_cfgs[0].keys_manager, + &chanmon_cfgs[0].tx_broadcaster, + &chanmon_cfgs[0].fee_estimator, + ); + let mut node_cfgs = create_node_cfgs(2, &chanmon_cfgs); + let chain_mon_0 = test_utils::TestChainMonitor::new( + Some(&chanmon_cfgs[0].chain_source), + &chanmon_cfgs[0].tx_broadcaster, + &chanmon_cfgs[0].logger, + &chanmon_cfgs[0].fee_estimator, + &persister, + &chanmon_cfgs[0].keys_manager, + ); + node_cfgs[0].chain_monitor = chain_mon_0; + let node_chanmgrs = create_node_chanmgrs(2, &node_cfgs, &[None, None]); + let nodes = create_network(2, &node_cfgs, &node_chanmgrs); + + let _ = create_announced_chan_between_nodes(&nodes, 0, 1); + send_payment(&nodes[0], &vec![&nodes[1]][..], 8_000_000); + send_payment(&nodes[1], &vec![&nodes[0]][..], 4_000_000); + + let persisted_chan_data = persister.read_all_channel_monitors_with_updates().unwrap(); + assert_eq!(persisted_chan_data.len(), 1); + let (_, monitor) = &persisted_chan_data[0]; + let monitor_name = monitor.persistence_key(); + let monitor_key = monitor_name.to_string(); + assert_ne!(monitor.get_latest_update_id(), u64::MAX); + + let legacy_update = ChannelMonitorUpdate { + update_id: u64::MAX, + updates: vec![ChannelMonitorUpdateStep::ChannelForceClosed { should_broadcast: true }], + channel_id: Some(monitor.channel_id()), + }; + + // Store the update as a standalone file, as a pre-0.1 persister would have, and check that + // reading the monitor back replays it. + KVStoreSync::write( + &kv_store, + CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE, + &monitor_key, + UpdateName::from(u64::MAX).as_str(), + legacy_update.encode(), + ) + .unwrap(); + + let persisted_chan_data = persister.read_all_channel_monitors_with_updates().unwrap(); + assert_eq!(persisted_chan_data.len(), 1); + let (_, closed_monitor) = &persisted_chan_data[0]; + assert_eq!(closed_monitor.get_latest_update_id(), u64::MAX); + + let update_list = KVStoreSync::list( + &kv_store, + CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE, + &monitor_key, + ) + .unwrap(); + assert!(!update_list.is_empty()); + + // Writing a sentinel-id update should do a full monitor write rather than a standalone + // update file, and purge all stale update files. + let status = + persister.update_persisted_channel(monitor_name, Some(&legacy_update), closed_monitor); + assert_eq!(status, ChannelMonitorUpdateStatus::Completed); + + let update_list = KVStoreSync::list( + &kv_store, + CHANNEL_MONITOR_UPDATE_PERSISTENCE_PRIMARY_NAMESPACE, + &monitor_key, + ) + .unwrap(); + assert!(update_list.is_empty()); + + let persisted_chan_data = persister.read_all_channel_monitors_with_updates().unwrap(); + assert_eq!(persisted_chan_data.len(), 1); + assert_eq!(persisted_chan_data[0].1.get_latest_update_id(), u64::MAX); + } + fn persist_fn<P: Deref, ChannelSigner: EcdsaChannelSigner>(_persist: P) -> bool where P::Target: Persist<ChannelSigner>, diff --git a/lightning/src/util/scid_utils.rs b/lightning/src/util/scid_utils.rs index d57c529a41a..c5a9182d171 100644 --- a/lightning/src/util/scid_utils.rs +++ b/lightning/src/util/scid_utils.rs @@ -73,12 +73,12 @@ pub fn scid_from_parts( /// 3) payments intended to be intercepted will route using a fake scid (this is typically used so /// the forwarding node can open a JIT channel to the next hop) pub(crate) mod fake_scid { - use crate::crypto::chacha20::ChaCha20; use crate::prelude::*; use crate::sign::EntropySource; use crate::util::scid_utils; use bitcoin::constants::ChainHash; use bitcoin::Network; + use chacha20_poly1305::chacha20::{ChaCha20, Key, Nonce}; const TEST_SEGWIT_ACTIVATION_HEIGHT: u32 = 1; const MAINNET_SEGWIT_ACTIVATION_HEIGHT: u32 = 481_824; @@ -150,15 +150,15 @@ pub(crate) mod fake_scid { fn get_encrypted_vout( &self, block_height: u32, tx_index: u32, fake_scid_rand_bytes: &[u8; 32], ) -> u8 { - let mut salt = [0 as u8; 8]; + let mut salt = [0 as u8; 12]; let block_height_bytes = block_height.to_be_bytes(); - salt[0..4].copy_from_slice(&block_height_bytes); + salt[4..8].copy_from_slice(&block_height_bytes); let tx_index_bytes = tx_index.to_be_bytes(); - salt[4..8].copy_from_slice(&tx_index_bytes); + salt[8..12].copy_from_slice(&tx_index_bytes); - let mut chacha = ChaCha20::new(fake_scid_rand_bytes, &salt); + let mut chacha = ChaCha20::new(Key::new(*fake_scid_rand_bytes), Nonce::new(salt), 0); let mut vout_byte = [*self as u8]; - chacha.process_in_place(&mut vout_byte); + chacha.apply_keystream(&mut vout_byte); vout_byte[0] & NAMESPACE_ID_BITMASK } } @@ -180,7 +180,7 @@ pub(crate) mod fake_scid { let namespace = Namespace::Phantom; let valid_vout = namespace.get_encrypted_vout(block_height, tx_index, fake_scid_rand_bytes); block_height >= segwit_activation_height(chain_hash) - && valid_vout == scid_utils::vout_from_scid(scid) as u8 + && valid_vout as u16 == scid_utils::vout_from_scid(scid) } /// Returns whether the given fake scid falls into the intercept namespace. @@ -192,7 +192,7 @@ pub(crate) mod fake_scid { let namespace = Namespace::Intercept; let valid_vout = namespace.get_encrypted_vout(block_height, tx_index, fake_scid_rand_bytes); block_height >= segwit_activation_height(chain_hash) - && valid_vout == scid_utils::vout_from_scid(scid) as u8 + && valid_vout as u16 == scid_utils::vout_from_scid(scid) } #[cfg(test)] @@ -248,6 +248,15 @@ pub(crate) mod fake_scid { assert!(is_valid_phantom(&fake_scid_rand_bytes, valid_fake_scid, &testnet_genesis)); let invalid_fake_scid = scid_utils::scid_from_parts(1, 0, 12).unwrap(); assert!(!is_valid_phantom(&fake_scid_rand_bytes, invalid_fake_scid, &testnet_genesis)); + // A scid whose low byte matches the namespace value but whose high byte is set must be + // rejected (this was previously broken). + let high_byte_fake_scid = + scid_utils::scid_from_parts(1, 0, valid_encrypted_vout as u64 | 0x0100).unwrap(); + assert!(!is_valid_phantom( + &fake_scid_rand_bytes, + high_byte_fake_scid, + &testnet_genesis + )); } #[test] @@ -265,6 +274,15 @@ pub(crate) mod fake_scid { invalid_fake_scid, &testnet_genesis )); + // A scid whose low byte matches the namespace value but whose high byte is set must be + // rejected (this was previously broken). + let high_byte_fake_scid = + scid_utils::scid_from_parts(1, 0, valid_encrypted_vout as u64 | 0x0100).unwrap(); + assert!(!is_valid_intercept( + &fake_scid_rand_bytes, + high_byte_fake_scid, + &testnet_genesis + )); } #[test] diff --git a/lightning/src/util/ser.rs b/lightning/src/util/ser.rs index 6579c0353a3..eefe0457e43 100644 --- a/lightning/src/util/ser.rs +++ b/lightning/src/util/ser.rs @@ -13,7 +13,7 @@ //! [`ChannelManager`]: crate::ln::channelmanager::ChannelManager //! [`ChannelMonitor`]: crate::chain::channelmonitor::ChannelMonitor -use crate::io::{self, BufRead, Read, Write}; +use crate::io::{self, Read, Write}; use crate::io_extras::{copy, sink}; use crate::ln::interactivetxs::{TxInMetadata, TxOutMetadata}; use crate::ln::onion_utils::{HMAC_COUNT, HMAC_LEN, HOLD_TIME_LEN, MAX_HOPS}; @@ -22,10 +22,12 @@ use crate::sync::{Mutex, RwLock}; use core::cmp; use core::hash::Hash; use core::ops::Deref; +use core::str::FromStr; use alloc::collections::BTreeMap; use bitcoin::absolute::LockTime as AbsoluteLockTime; +use bitcoin::address::Address; use bitcoin::amount::{Amount, SignedAmount}; use bitcoin::consensus::Encodable; use bitcoin::constants::ChainHash; @@ -41,13 +43,14 @@ use bitcoin::secp256k1::ecdsa; use bitcoin::secp256k1::schnorr; use bitcoin::secp256k1::{PublicKey, SecretKey}; use bitcoin::transaction::{OutPoint, Transaction, TxOut}; +use bitcoin::FeeRate; use bitcoin::{consensus, Sequence, TxIn, Weight, Witness}; use dnssec_prover::rr::Name; +use lightning_invoice::Bolt11Invoice; + use crate::chain::ClaimId; -#[cfg(taproot)] -use crate::ln::msgs::PartialSignatureWithNonce; use crate::ln::msgs::{DecodeError, SerialId}; use crate::types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use crate::types::string::UntrustedString; @@ -74,72 +77,6 @@ impl<W: Write> Writer for W { } } -// TODO: Drop this entirely if rust-bitcoin releases a version bump with https://github.com/rust-bitcoin/rust-bitcoin/pull/3173 -/// Wrap buffering support for implementations of Read. -/// A [`Read`]er which keeps an internal buffer to avoid hitting the underlying stream directly for -/// every read, implementing [`BufRead`]. -/// -/// In order to avoid reading bytes past the first object, and those bytes then ending up getting -/// dropped, this BufReader operates in one-byte-increments. -struct BufReader<'a, R: Read> { - inner: &'a mut R, - buf: [u8; 1], - is_consumed: bool, -} - -impl<'a, R: Read> BufReader<'a, R> { - /// Creates a [`BufReader`] which will read from the given `inner`. - pub fn new(inner: &'a mut R) -> Self { - BufReader { inner, buf: [0; 1], is_consumed: true } - } -} - -impl<'a, R: Read> Read for BufReader<'a, R> { - #[inline] - fn read(&mut self, output: &mut [u8]) -> io::Result<usize> { - if output.is_empty() { - return Ok(0); - } - let mut offset = 0; - if !self.is_consumed { - output[0] = self.buf[0]; - self.is_consumed = true; - offset = 1; - } - self.inner.read(&mut output[offset..]).map(|len| len + offset) - } -} - -impl<'a, R: Read> BufRead for BufReader<'a, R> { - #[inline] - fn fill_buf(&mut self) -> io::Result<&[u8]> { - debug_assert!(false, "rust-bitcoin doesn't actually use this"); - if self.is_consumed { - let count = self.inner.read(&mut self.buf[..])?; - debug_assert!(count <= 1, "read gave us a garbage length"); - - // upon hitting EOF, assume the byte is already consumed - self.is_consumed = count == 0; - } - - if self.is_consumed { - Ok(&[]) - } else { - Ok(&self.buf[..]) - } - } - - #[inline] - fn consume(&mut self, amount: usize) { - debug_assert!(false, "rust-bitcoin doesn't actually use this"); - if amount >= 1 { - debug_assert_eq!(amount, 1, "Can only consume one byte"); - debug_assert!(!self.is_consumed, "Cannot consume more than had been read"); - self.is_consumed = true; - } - } -} - pub(crate) struct WriterWriteAdaptor<'a, W: Writer + 'a>(pub &'a mut W); impl<'a, W: Writer + 'a> Write for WriterWriteAdaptor<'a, W> { #[inline] @@ -314,6 +251,11 @@ impl<'a, T: Writeable> Writeable for &'a T { fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { (*self).write(writer) } + + #[inline] + fn serialized_length(&self) -> usize { + (*self).serialized_length() + } } /// A trait that various LDK types implement allowing them to be read in from a [`Read`]. @@ -607,6 +549,13 @@ macro_rules! impl_writeable_primitive { writer.write_all(&self.0.to_be_bytes()[(self.0.leading_zeros() / 8) as usize..$len]) } } + impl Writeable for HighZeroBytesDroppedBigSize<&$val_type> { + #[inline] + fn write<W: Writer>(&self, writer: &mut W) -> Result<(), io::Error> { + // Skip any full leading 0 bytes when writing (in BE): + writer.write_all(&self.0.to_be_bytes()[(self.0.leading_zeros() / 8) as usize..$len]) + } + } impl Readable for $val_type { #[inline] fn read<R: Read>(reader: &mut R) -> Result<$val_type, DecodeError> { @@ -733,7 +682,6 @@ impl_array!(16, u8); // for IPv6 impl_array!(32, u8); // for channel id & hmac impl_array!(PUBLIC_KEY_SIZE, u8); // for PublicKey impl_array!(64, u8); // for ecdsa::Signature and schnorr::Signature -impl_array!(66, u8); // for MuSig2 nonces impl_array!(1300, u8); // for OnionPacket.hop_data impl_array!(8, u16); @@ -749,12 +697,20 @@ impl_array!(HMAC_LEN * HMAC_COUNT, u8); /// This is not exported to bindings users as manual TLV building is not currently supported in bindings pub struct WithoutLength<T>(pub T); +impl Writeable for WithoutLength<&&String> { + #[inline] + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + w.write_all(self.0.as_bytes()) + } +} + impl Writeable for WithoutLength<&String> { #[inline] fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { w.write_all(self.0.as_bytes()) } } + impl LengthReadable for WithoutLength<String> { #[inline] fn read_from_fixed_length_buffer<R: LengthLimitedRead>(r: &mut R) -> Result<Self, DecodeError> { @@ -806,6 +762,14 @@ impl<T: Writeable> AsWriteableSlice for &Vec<T> { &self } } + +impl<T: Writeable> AsWriteableSlice for &&Vec<T> { + type Inner = T; + fn as_slice(&self) -> &[T] { + &self + } +} + impl<T: Writeable> AsWriteableSlice for &[T] { type Inner = T; fn as_slice(&self) -> &[T] { @@ -821,6 +785,11 @@ impl<S: AsWriteableSlice> Writeable for WithoutLength<S> { } Ok(()) } + + #[inline] + fn serialized_length(&self) -> usize { + self.0.as_slice().iter().map(|v| v.serialized_length()).sum() + } } impl<T: MaybeReadable> LengthReadable for WithoutLength<Vec<T>> { @@ -925,7 +894,9 @@ macro_rules! impl_for_map { #[inline] fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { let len: CollectionLength = Readable::read(r)?; - let mut ret = $constr(len.0 as usize); + let entry_size = ::core::mem::size_of::<K>() + ::core::mem::size_of::<V>(); + let max_alloc = MAX_BUF_SIZE / (entry_size + 1); + let mut ret = $constr(cmp::min(len.0 as usize, max_alloc)); for _ in 0..len.0 { let k = K::read(r)?; let v_opt = V::read(r)?; @@ -944,6 +915,37 @@ macro_rules! impl_for_map { impl_for_map!(BTreeMap, Ord, |_| BTreeMap::new()); impl_for_map!(HashMap, Hash, |len| hash_map_with_capacity(len)); +/// A wrapper used to serialize a `BTreeMap<u64, Vec<u8>>` with a few less bytes. +pub(crate) struct BigSizeKeyedMap<T>(pub T); + +impl Writeable for BigSizeKeyedMap<&BTreeMap<u64, Vec<u8>>> { + #[inline] + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + BigSize(self.0.len() as u64).write(w)?; + for (key, value) in self.0.iter() { + BigSize(*key).write(w)?; + value.write(w)?; + } + Ok(()) + } +} + +impl LengthReadable for BigSizeKeyedMap<BTreeMap<u64, Vec<u8>>> { + #[inline] + fn read_from_fixed_length_buffer<R: LengthLimitedRead>(r: &mut R) -> Result<Self, DecodeError> { + let len: BigSize = Readable::read(r)?; + let mut ret = BTreeMap::new(); + for _ in 0..len.0 { + let key: BigSize = Readable::read(r)?; + let value: Vec<u8> = Readable::read(r)?; + if ret.insert(key.0, value).is_some() { + return Err(DecodeError::InvalidValue); + } + } + Ok(BigSizeKeyedMap(ret)) + } +} + // HashSet impl<T> Writeable for HashSet<T> where @@ -1097,6 +1099,8 @@ impl_for_vec!(crate::ln::channelmanager::MonitorUpdateCompletionAction); impl_for_vec!(crate::ln::channelmanager::PaymentClaimDetails); impl_for_vec!(crate::ln::msgs::SocketAddress); impl_for_vec!((A, B), A, B); +impl_for_vec!(OutPoint); +impl_for_vec!(ScriptBuf); impl_for_vec!(SerialId); impl_for_vec!(TxInMetadata); impl_for_vec!(TxOutMetadata); @@ -1108,6 +1112,8 @@ impl_for_vec!(crate::routing::router::TrampolineHop); impl_for_vec_with_element_length_prefix!(crate::ln::msgs::UpdateAddHTLC); impl_writeable_for_vec_with_element_length_prefix!(&crate::ln::msgs::UpdateAddHTLC); impl_for_vec!(u32); +impl_for_vec!(crate::events::HTLCLocator); +impl_for_vec!(crate::ln::types::ChannelId); impl Writeable for Vec<Witness> { #[inline] @@ -1201,37 +1207,18 @@ impl Readable for SecretKey { } } -#[cfg(taproot)] -impl Writeable for musig2::types::PublicNonce { +impl Writeable for Sha256 { fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { - self.serialize().write(w) + w.write_all(&self[..]) } } -#[cfg(taproot)] -impl Readable for musig2::types::PublicNonce { +impl Readable for Sha256 { fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { - let buf: [u8; PUBLIC_KEY_SIZE * 2] = Readable::read(r)?; - musig2::types::PublicNonce::from_slice(&buf).map_err(|_| DecodeError::InvalidValue) - } -} - -#[cfg(taproot)] -impl Writeable for PartialSignatureWithNonce { - fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { - self.0.serialize().write(w)?; - self.1.write(w) - } -} + use bitcoin::hashes::Hash; -#[cfg(taproot)] -impl Readable for PartialSignatureWithNonce { - fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { - let partial_signature_buf: [u8; SECRET_KEY_SIZE] = Readable::read(r)?; - let partial_signature = musig2::types::PartialSignature::from_slice(&partial_signature_buf) - .map_err(|_| DecodeError::InvalidValue)?; - let public_nonce: musig2::types::PublicNonce = Readable::read(r)?; - Ok(PartialSignatureWithNonce(partial_signature, public_nonce)) + let buf: [u8; 32] = Readable::read(r)?; + Ok(Sha256::from_byte_array(buf)) } } @@ -1340,6 +1327,11 @@ impl<T: Writeable> Writeable for Box<T> { fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { T::write(&**self, w) } + + #[inline] + fn serialized_length(&self) -> usize { + T::serialized_length(&**self) + } } impl<T: Readable> Readable for Box<T> { @@ -1359,6 +1351,17 @@ impl<T: Writeable> Writeable for Option<T> { } Ok(()) } + + #[inline] + fn serialized_length(&self) -> usize { + match *self { + None => 1, + Some(ref data) => { + let data_len = data.serialized_length(); + BigSize(data_len as u64 + 1).serialized_length() + data_len + }, + } + } } impl<T: LengthReadable> Readable for Option<T> { @@ -1426,6 +1429,19 @@ impl Readable for Weight { } } +impl Writeable for FeeRate { + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + self.to_sat_per_kwu().write(w) + } +} + +impl Readable for FeeRate { + fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { + let sat_kwu: u64 = Readable::read(r)?; + Ok(FeeRate::from_sat_per_kwu(sat_kwu)) + } +} + impl Writeable for Txid { fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { w.write_all(&self[..]) @@ -1456,6 +1472,33 @@ impl Readable for BlockHash { } } +impl Writeable for [Option<BlockHash>; 12] { + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + for hash_opt in self { + match hash_opt { + Some(hash) => hash.write(w)?, + None => ([0u8; 32]).write(w)?, + } + } + Ok(()) + } +} + +impl Readable for [Option<BlockHash>; 12] { + fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { + use bitcoin::hashes::Hash; + + let mut res = [None; 12]; + for hash_opt in res.iter_mut() { + let buf: [u8; 32] = Readable::read(r)?; + if buf != [0; 32] { + *hash_opt = Some(BlockHash::from_slice(&buf[..]).unwrap()); + } + } + Ok(res) + } +} + impl Writeable for ChainHash { fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { w.write_all(self.as_bytes()) @@ -1485,6 +1528,39 @@ impl Readable for OutPoint { } } +impl Writeable for Address { + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + self.to_string().write(w)?; + Ok(()) + } +} + +impl Readable for Address { + fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { + let addr_string: String = Readable::read(r)?; + let addr = Address::from_str(&addr_string) + .map_err(|_| DecodeError::InvalidValue)? + .assume_checked(); + Ok(addr) + } +} + +impl Writeable for Bolt11Invoice { + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + self.to_string().write(w)?; + Ok(()) + } +} + +impl Readable for Bolt11Invoice { + fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { + let invoice_string: String = Readable::read(r)?; + let invoice = + Bolt11Invoice::from_str(&invoice_string).map_err(|_| DecodeError::InvalidValue)?; + Ok(invoice) + } +} + macro_rules! impl_consensus_ser { ($bitcoin_type: ty) => { impl Writeable for $bitcoin_type { @@ -1498,8 +1574,7 @@ macro_rules! impl_consensus_ser { impl Readable for $bitcoin_type { fn read<R: Read>(r: &mut R) -> Result<Self, DecodeError> { - let mut reader = BufReader::<_>::new(r); - match consensus::encode::Decodable::consensus_decode(&mut reader) { + match consensus::encode::Decodable::consensus_decode(r) { Ok(t) => Ok(t), Err(consensus::encode::Error::Io(ref e)) if e.kind() == io::ErrorKind::UnexpectedEof => @@ -1588,6 +1663,13 @@ impl Readable for () { } impl Writeable for String { + #[inline] + fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { + self.as_str().write(w) + } +} + +impl Writeable for &str { #[inline] fn write<W: Writer>(&self, w: &mut W) -> Result<(), io::Error> { CollectionLength(self.len() as u64).write(w)?; @@ -1754,6 +1836,12 @@ mod tests { assert_eq!(Hostname::read(&mut buf.as_slice()).unwrap().as_str(), "test"); } + #[test] + fn str_serialization_matches_string() { + let s = "test"; + assert_eq!(s.encode(), s.to_string().encode()); + } + #[test] /// Taproot will likely fill legacy signature fields with all 0s. /// This test ensures that doing so won't break serialization. diff --git a/lightning/src/util/ser_macros.rs b/lightning/src/util/ser_macros.rs index cc95fe619e8..e6f558b1071 100644 --- a/lightning/src/util/ser_macros.rs +++ b/lightning/src/util/ser_macros.rs @@ -21,6 +21,9 @@ macro_rules! _encode_tlv { ($stream: expr, $type: expr, $field: expr, (default_value, $default: expr) $(, $self: ident)?) => { $crate::_encode_tlv!($stream, $type, $field, required) }; + ($stream: expr, $type: expr, $field: expr, (default_value_vec, $default: expr) $(, $self: ident)?) => { + $crate::_encode_tlv!($stream, $type, $field, required_vec) + }; ($stream: expr, $type: expr, $field: expr, (static_value, $value: expr) $(, $self: ident)?) => { let _ = &$field; // Ensure we "use" the $field }; @@ -77,8 +80,8 @@ macro_rules! _encode_tlv { ($stream: expr, $type: expr, $field: expr, upgradable_option $(, $self: ident)?) => { $crate::_encode_tlv!($stream, $type, $field, option); }; - ($stream: expr, $type: expr, $field: expr, (option, encoding: ($fieldty: ty, $encoding: ident) $(, $self: ident)?)) => { - $crate::_encode_tlv!($stream, $type, $field.map(|f| $encoding(f)), option); + ($stream: expr, $type: expr, $field: expr, (option, encoding: ($fieldty: ty, $encoding: ident)) $(, $self: ident)?) => { + $crate::_encode_tlv!($stream, $type, $field.as_ref().map(|f| $encoding(f)), option); }; ($stream: expr, $type: expr, $field: expr, (option, encoding: $fieldty: ty) $(, $self: ident)?) => { $crate::_encode_tlv!($stream, $type, $field, option); @@ -200,6 +203,9 @@ macro_rules! _get_varint_length_prefixed_tlv_length { ($len: expr, $type: expr, $field: expr, (default_value, $default: expr) $(, $self: ident)?) => { $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field, required) }; + ($len: expr, $type: expr, $field: expr, (default_value_vec, $default: expr) $(, $self: ident)?) => { + $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field, required_vec) + }; ($len: expr, $type: expr, $field: expr, (static_value, $value: expr) $(, $self: ident)?) => {}; ($len: expr, $type: expr, $field: expr, required $(, $self: ident)?) => { BigSize($type).write(&mut $len).expect("No in-memory data may fail to serialize"); @@ -247,8 +253,7 @@ macro_rules! _get_varint_length_prefixed_tlv_length { $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field, option); }; ($len: expr, $type: expr, $field: expr, (option, encoding: ($fieldty: ty, $encoding: ident)) $(, $self: ident)?) => { - let field = $field.map(|f| $encoding(f)); - $crate::_get_varint_length_prefixed_tlv_length!($len, $type, field, option); + $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field.as_ref().map(|f| $encoding(f)), option); }; ($len: expr, $type: expr, $field: expr, upgradable_required $(, $self: ident)?) => { $crate::_get_varint_length_prefixed_tlv_length!($len, $type, $field, required); @@ -301,6 +306,15 @@ macro_rules! _check_decoded_tlv_order { $field = $default.into(); } }}; + ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (default_value_vec, $default: expr)) => {{ + $crate::_check_decoded_tlv_order!( + $last_seen_type, + $typ, + $type, + $field, + (default_value, $default) + ); + }}; ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, (static_value, $value: expr)) => {}; ($last_seen_type: expr, $typ: expr, $type: expr, $field: ident, required) => {{ // Note that $type may be 0 making the second comparison always false @@ -372,6 +386,9 @@ macro_rules! _check_missing_tlv { $field = $default.into(); } }}; + ($last_seen_type: expr, $type: expr, $field: ident, (default_value_vec, $default: expr)) => {{ + $crate::_check_missing_tlv!($last_seen_type, $type, $field, (default_value, $default)); + }}; ($last_seen_type: expr, $type: expr, $field: expr, (static_value, $value: expr)) => { $field = $value; }; @@ -440,6 +457,10 @@ macro_rules! _decode_tlv { ($outer_reader: expr, $reader: expr, $field: ident, (default_value, $default: expr)) => {{ $crate::_decode_tlv!($outer_reader, $reader, $field, required) }}; + ($outer_reader: expr, $reader: expr, $field: ident, (default_value_vec, $default: expr)) => {{ + let f: $crate::util::ser::WithoutLength<Vec<_>> = $crate::util::ser::LengthReadable::read_from_fixed_length_buffer(&mut $reader)?; + $field = $crate::util::ser::RequiredWrapper(Some(f.0)); + }}; ($outer_reader: expr, $reader: expr, $field: ident, (static_value, $value: expr)) => {{ }}; ($outer_reader: expr, $reader: expr, $field: ident, required) => {{ @@ -818,6 +839,58 @@ macro_rules! write_tlv_fields { } } +#[doc(hidden)] +#[macro_export] +macro_rules! _tlv_fields_serialized_length { + ({$(($type: expr, $field: expr, $fieldty: tt $(, $self: ident)?)),* $(,)*}) => { { + use $crate::util::ser::BigSize; + let len = { + #[allow(unused_mut)] + let mut len = $crate::util::ser::LengthCalculatingWriter(0); + $( + $crate::_get_varint_length_prefixed_tlv_length!(len, $type, &$field, $fieldty $(, $self)?); + )* + len.0 + }; + let mut len_calc = $crate::util::ser::LengthCalculatingWriter(0); + BigSize(len as u64).write(&mut len_calc).expect("No in-memory data may fail to serialize"); + len + len_calc.0 + } } +} + +/// Implements [`Writeable`] for a type serialized as a length-prefixed TLV stream. +/// +/// This is useful for types that share the TLV-writing format used by +/// [`impl_ser_tlv_based`] but need a custom read implementation. The field list uses the +/// same entries accepted by [`write_tlv_fields`], and the macro derives both `write` and +/// `serialized_length` from that list so the two paths stay aligned. +/// +/// The `$self` argument names the generated `self` binding, allowing field expressions to refer +/// to it explicitly. +/// +/// [`Writeable`]: crate::util::ser::Writeable +/// [`impl_ser_tlv_based`]: crate::impl_ser_tlv_based +/// [`write_tlv_fields`]: crate::write_tlv_fields +macro_rules! impl_writeable_tlv_based { + ($st: ty, $self: ident, {$(($type: expr, $field: expr, $fieldty: tt)),* $(,)*}) => { + impl $crate::util::ser::Writeable for $st { + fn write<W: $crate::util::ser::Writer>(&$self, writer: &mut W) -> Result<(), $crate::io::Error> { + write_tlv_fields!(writer, { + $(($type, $field, $fieldty)),* + }); + Ok(()) + } + + #[inline] + fn serialized_length(&$self) -> usize { + $crate::_tlv_fields_serialized_length!({ + $(($type, $field, $fieldty)),* + }) + } + } + } +} + /// Reads a prefix added by [`write_ver_prefix`], above. Takes the current version of the /// serialization logic for this object. This is compared against the /// `$min_version_that_can_read_this` added by [`write_ver_prefix`]. @@ -854,6 +927,9 @@ macro_rules! _init_tlv_based_struct_field { ($field: ident, (default_value, $default: expr)) => { $field.0.unwrap() }; + ($field: ident, (default_value_vec, $default: expr)) => { + $crate::_init_tlv_based_struct_field!($field, (default_value, $default)) + }; ($field: ident, (static_value, $value: expr)) => { $field }; @@ -905,6 +981,9 @@ macro_rules! _init_tlv_field_var { ($field: ident, (default_value, $default: expr)) => { let mut $field = $crate::util::ser::RequiredWrapper(None); }; + ($field: ident, (default_value_vec, $default: expr)) => { + $crate::_init_tlv_field_var!($field, (default_value, $default)); + }; ($field: ident, (static_value, $value: expr)) => { let $field; }; @@ -1007,6 +1086,9 @@ macro_rules! _decode_and_build { /// /// If `$fieldty` is `required`, then `$field` is a required field that is not an [`Option`] nor a [`Vec`]. /// If `$fieldty` is `(default_value, $default)`, then `$field` will be set to `$default` if not present. +/// If `$fieldty` is `(default_value_vec, $default)`, then `$field` is a [`Vec`] which will be set to `$default` +/// if not present. Elements are serialized individually without a count prefix (like `required_vec`). +/// The TLV is always written, even if the vec is empty (matching `default_value` behavior). /// If `$fieldty` is `(static_value, $static)`, then `$field` will be set to `$static`. /// If `$fieldty` is `option`, then `$field` is optional field. /// If `$fieldty` is `upgradable_option`, then `$field` is optional and read via [`MaybeReadable`]. @@ -1019,7 +1101,7 @@ macro_rules! _decode_and_build { /// `Some`. When reading, an optional field of type `$ty` is read, and after all TLV fields are /// read, the `$read` closure is called with the `Option<&$ty>` value. The `$read` closure should /// return a `Result<(), DecodeError>`. Legacy field values can be used in later -/// `default_value` or `static_value` fields by referring to the value by name. +/// `default_value`, `default_value_vec`, or `static_value` fields by referring to the value by name. /// If `$fieldty` is `(custom, $ty, $read, $write)` then, when writing, the same behavior as /// `legacy`, above is used. When reading, if a TLV is present, it is read as `$ty` and the /// `$read` method is called with `Some(decoded_$ty_object)`. If no TLV is present, the field @@ -1029,7 +1111,7 @@ macro_rules! _decode_and_build { /// /// For example, /// ``` -/// # use lightning::impl_writeable_tlv_based; +/// # use lightning::impl_ser_tlv_based; /// struct LightningMessage { /// tlv_integer: u32, /// tlv_default_integer: u32, @@ -1038,7 +1120,7 @@ macro_rules! _decode_and_build { /// tlv_upgraded_integer: u32, /// } /// -/// impl_writeable_tlv_based!(LightningMessage, { +/// impl_ser_tlv_based!(LightningMessage, { /// (0, tlv_integer, required), /// (1, tlv_default_integer, (default_value, 7)), /// (2, tlv_optional_integer, option), @@ -1053,7 +1135,7 @@ macro_rules! _decode_and_build { /// [`Writeable`]: crate::util::ser::Writeable /// [`Vec`]: crate::prelude::Vec #[macro_export] -macro_rules! impl_writeable_tlv_based { +macro_rules! impl_ser_tlv_based { ($st: ident, {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*}) => { impl $crate::util::ser::Writeable for $st { fn write<W: $crate::util::ser::Writer>(&self, writer: &mut W) -> Result<(), $crate::io::Error> { @@ -1065,18 +1147,9 @@ macro_rules! impl_writeable_tlv_based { #[inline] fn serialized_length(&self) -> usize { - use $crate::util::ser::BigSize; - let len = { - #[allow(unused_mut)] - let mut len = $crate::util::ser::LengthCalculatingWriter(0); - $( - $crate::_get_varint_length_prefixed_tlv_length!(len, $type, &self.$field, $fieldty, self); - )* - len.0 - }; - let mut len_calc = $crate::util::ser::LengthCalculatingWriter(0); - BigSize(len as u64).write(&mut len_calc).expect("No in-memory data may fail to serialize"); - len + len_calc.0 + $crate::_tlv_fields_serialized_length!({ + $(($type, self.$field, $fieldty, self)),* + }) } } @@ -1176,9 +1249,9 @@ macro_rules! _impl_writeable_tlv_based_enum_common { ($st: ident, $(($variant_id: expr, $variant_name: ident) => {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} ),* $(,)?; - // $tuple_variant_* are only passed from `impl_writeable_tlv_based_enum_*_legacy` + // $tuple_variant_* are only passed from legacy enum macros. $(($tuple_variant_id: expr, $tuple_variant_name: ident)),* $(,)?; - // $length_prefixed_* are only passed from `impl_writeable_tlv_based_enum_*` non-`legacy` + // $length_prefixed_* are only passed from non-legacy enum macros. $(($length_prefixed_tuple_variant_id: expr, $length_prefixed_tuple_variant_name: ident)),* $(,)?) => { impl $crate::util::ser::Writeable for $st { fn write<W: $crate::util::ser::Writer>(&self, writer: &mut W) -> Result<(), $crate::io::Error> { @@ -1226,8 +1299,8 @@ macro_rules! _impl_writeable_tlv_based_enum_common { /// TupleVariantA(), /// TupleVariantB(Vec<u8>), /// } -/// # use lightning::impl_writeable_tlv_based_enum; -/// impl_writeable_tlv_based_enum!(EnumName, +/// # use lightning::impl_ser_tlv_based_enum; +/// impl_ser_tlv_based_enum!(EnumName, /// (0, StructVariantA) => {(0, required_variant_field, required), (1, optional_variant_field, option)}, /// (1, StructVariantB) => {(0, variant_field_a, required), (1, variant_field_b, required), (2, variant_vec_field, optional_vec)}, /// (2, TupleVariantA) => {}, // Note that empty tuple variants have to use the struct syntax due to rust limitations @@ -1246,7 +1319,7 @@ macro_rules! _impl_writeable_tlv_based_enum_common { /// [`Writeable`]: crate::util::ser::Writeable /// [`DecodeError::UnknownRequiredFeature`]: crate::ln::msgs::DecodeError::UnknownRequiredFeature #[macro_export] -macro_rules! impl_writeable_tlv_based_enum { +macro_rules! impl_ser_tlv_based_enum { ($st: ident, $(($variant_id: expr, $variant_name: ident) => {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} @@ -1291,9 +1364,9 @@ macro_rules! impl_writeable_tlv_based_enum { } } -/// See [`impl_writeable_tlv_based_enum`] and use that unless backwards-compatibility with tuple +/// See [`impl_ser_tlv_based_enum`] and use that unless backwards-compatibility with tuple /// variants is required. -macro_rules! impl_writeable_tlv_based_enum_legacy { +macro_rules! impl_ser_tlv_based_enum_legacy { ($st: ident, $(($variant_id: expr, $variant_name: ident) => {$(($type: expr, $field: ident, $fieldty: tt)),* $(,)*} ),* $(,)*; @@ -1329,7 +1402,7 @@ macro_rules! impl_writeable_tlv_based_enum_legacy { /// Implement [`MaybeReadable`] and [`Writeable`] for an enum, with struct variants stored as TLVs and /// tuple variants stored directly. /// -/// This is largely identical to [`impl_writeable_tlv_based_enum`], except that odd variants will +/// This is largely identical to [`impl_ser_tlv_based_enum`], except that odd variants will /// return `Ok(None)` instead of `Err(`[`DecodeError::UnknownRequiredFeature`]`)`. It should generally be preferred /// when [`MaybeReadable`] is practical instead of just [`Readable`] as it provides an upgrade path for /// new variants to be added which are simply ignored by existing clients. @@ -1590,7 +1663,7 @@ mod tests { other_field: u32, } - impl_writeable_tlv_based!(OuterStructOptionalEnumV1, { + impl_ser_tlv_based!(OuterStructOptionalEnumV1, { (0, inner_enum, upgradable_option), (2, other_field, required), }); @@ -1615,7 +1688,7 @@ mod tests { other_field: u32, } - impl_writeable_tlv_based!(OuterStructOptionalEnumV2, { + impl_ser_tlv_based!(OuterStructOptionalEnumV2, { (0, inner_enum, upgradable_option), (2, other_field, required), }); @@ -1666,7 +1739,7 @@ mod tests { other_field: u32, } - impl_writeable_tlv_based!(OuterOuterStruct, { + impl_ser_tlv_based!(OuterOuterStruct, { (0, outer_struct, upgradable_option), (2, other_field, required), }); @@ -1934,7 +2007,7 @@ mod tests { // old_field: u8, new_field: (u8, u8), } - impl_writeable_tlv_based!(ExpandedField, { + impl_ser_tlv_based!(ExpandedField, { (0, old_field, (legacy, u8, |_| Ok(()), |us: &ExpandedField| Some(us.new_field.0))), (1, new_field, (default_value, (old_field.ok_or(DecodeError::InvalidValue)?, 0))), }); @@ -1956,6 +2029,59 @@ mod tests { assert_eq!(read, ExpandedField { new_field: (42, 0) }); } + #[derive(Debug, PartialEq, Eq)] + struct DefaultValueVecStruct { + items: Vec<u32>, + } + impl_ser_tlv_based!(DefaultValueVecStruct, { + (1, items, (default_value_vec, vec![4, 5, 6])), + }); + + #[test] + fn test_default_value_vec() { + // Non-empty vec round-trips correctly. + let instance = DefaultValueVecStruct { items: vec![1, 2, 3] }; + let encoded = instance.encode(); + let decoded: DefaultValueVecStruct = Readable::read(&mut &encoded[..]).unwrap(); + assert_eq!(decoded, instance); + + // Empty TLV stream falls back to the default. + let empty_encoded = <Vec<u8>>::from_hex("00").unwrap(); // zero-length TLV stream + let decoded: DefaultValueVecStruct = Readable::read(&mut &empty_encoded[..]).unwrap(); + assert_eq!(decoded, DefaultValueVecStruct { items: vec![4, 5, 6] }); + + // Empty vec round-trips to empty vec (TLV is always written). + let empty_vec = DefaultValueVecStruct { items: vec![] }; + let encoded = empty_vec.encode(); + let decoded: DefaultValueVecStruct = Readable::read(&mut &encoded[..]).unwrap(); + assert_eq!(decoded, DefaultValueVecStruct { items: vec![] }); + } + + #[derive(Debug, PartialEq, Eq)] + struct LegacyToVecStruct { + new_items: Vec<u32>, + } + impl_ser_tlv_based!(LegacyToVecStruct, { + (0, old_item, (legacy, u32, |_| Ok(()), + |us: &LegacyToVecStruct| us.new_items.first().copied())), + (1, new_items, (default_value_vec, + old_item.map(|v| vec![v]).unwrap_or_default())), + }); + + #[test] + fn test_default_value_vec_with_legacy_fallback() { + // New format: round-trips via the new TLV. + let instance = LegacyToVecStruct { new_items: vec![10, 20, 30] }; + let encoded = instance.encode(); + let decoded: LegacyToVecStruct = Readable::read(&mut &encoded[..]).unwrap(); + assert_eq!(decoded, instance); + + // Old format: only the legacy type-0 field is present, falls back via default expression. + let old_encoded = <Vec<u8>>::from_hex("0600040000002a").unwrap(); // TLV len 6, type 0, len 4, value 42u32 + let decoded: LegacyToVecStruct = Readable::read(&mut &old_encoded[..]).unwrap(); + assert_eq!(decoded, LegacyToVecStruct { new_items: vec![42] }); + } + #[test] fn required_vec_with_encoding() { // Ensure that serializing a required vec with a specified encoding will survive a ser round @@ -1964,7 +2090,7 @@ mod tests { struct MyCustomStruct { tlv_field: Vec<u8>, } - impl_writeable_tlv_based!(MyCustomStruct, { + impl_ser_tlv_based!(MyCustomStruct, { (0, tlv_field, (required_vec, encoding: (Vec<u8>, WithoutLength))), }); diff --git a/lightning/src/util/sweep.rs b/lightning/src/util/sweep.rs index b70eb274085..883cc4a4d8e 100644 --- a/lightning/src/util/sweep.rs +++ b/lightning/src/util/sweep.rs @@ -12,7 +12,7 @@ use crate::chain::chaininterface::{ BroadcasterInterface, ConfirmationTarget, FeeEstimator, TransactionType, }; use crate::chain::channelmonitor::{ANTI_REORG_DELAY, ARCHIVAL_DELAY_BLOCKS}; -use crate::chain::{self, BestBlock, Confirm, Filter, Listen, WatchedOutput}; +use crate::chain::{self, BlockLocator, Confirm, Filter, Listen, WatchedOutput}; use crate::io; use crate::ln::msgs::DecodeError; use crate::ln::types::ChannelId; @@ -97,7 +97,7 @@ impl TrackedSpendableOutput { } } -impl_writeable_tlv_based!(TrackedSpendableOutput, { +impl_ser_tlv_based!(TrackedSpendableOutput, { (0, descriptor, required), (2, channel_id, option), (3, counterparty_node_id, option), @@ -309,7 +309,7 @@ impl OutputSpendStatus { } } -impl_writeable_tlv_based_enum!(OutputSpendStatus, +impl_ser_tlv_based_enum!(OutputSpendStatus, (0, PendingInitialBroadcast) => { (0, delayed_until_height, option), }, @@ -386,7 +386,7 @@ where /// If chain data is provided via the [`Confirm`] interface or via filtered blocks, users also /// need to register their [`Filter`] implementation via the given `chain_data_source`. pub fn new( - best_block: BestBlock, broadcaster: B, fee_estimator: E, chain_data_source: Option<F>, + best_block: BlockLocator, broadcaster: B, fee_estimator: E, chain_data_source: Option<F>, output_spender: O, change_destination_source: D, kv_store: K, logger: L, ) -> Self { let outputs = Vec::new(); @@ -472,7 +472,7 @@ where /// Gets the latest best block which was connected either via the [`Listen`] or /// [`Confirm`] interfaces. - pub fn current_best_block(&self) -> BestBlock { + pub fn current_best_block(&self) -> BlockLocator { self.sweeper_state.lock().unwrap().best_block } @@ -488,12 +488,18 @@ where return Ok(()); } - let result = self.regenerate_and_broadcast_spend_if_necessary_internal().await; - - // Release the pending sweep flag again, regardless of result. - self.pending_sweep.store(false, Ordering::Release); + // Use an RAII guard so the flag is released even if this future is dropped mid-await + // (e.g. cancelled by `tokio::time::timeout` or `select!`). A bare `store(false)` after + // the await would never run on cancellation, leaving the sweeper permanently disabled. + struct PendingSweepGuard<'a>(&'a AtomicBool); + impl<'a> Drop for PendingSweepGuard<'a> { + fn drop(&mut self) { + self.0.store(false, Ordering::Release); + } + } + let _guard = PendingSweepGuard(&self.pending_sweep); - result + self.regenerate_and_broadcast_spend_if_necessary_internal().await } /// Regenerates and broadcasts the spending transaction for any outputs that are pending @@ -734,7 +740,7 @@ where fn best_block_updated_internal( &self, sweeper_state: &mut SweeperState, header: &Header, height: u32, ) { - sweeper_state.best_block = BestBlock::new(header.block_hash(), height); + sweeper_state.best_block.update_for_new_tip(header.block_hash(), height); self.prune_confirmed_outputs(sweeper_state); sweeper_state.dirty = true; @@ -766,7 +772,7 @@ where self.best_block_updated_internal(&mut state_lock, header, height); } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { let mut state_lock = self.sweeper_state.lock().unwrap(); assert!(state_lock.best_block.height > fork_point.height, @@ -854,11 +860,11 @@ where #[derive(Debug, Clone)] struct SweeperState { outputs: Vec<TrackedSpendableOutput>, - best_block: BestBlock, + best_block: BlockLocator, dirty: bool, } -impl_writeable_tlv_based!(SweeperState, { +impl_ser_tlv_based!(SweeperState, { (0, outputs, required_vec), (2, best_block, required), (_unused, dirty, (static_value, false)), @@ -889,7 +895,7 @@ impl< K: KVStore, L: Logger, O: OutputSpender, - > ReadableArgs<(B, E, Option<F>, O, D, K, L)> for (BestBlock, OutputSweeper<B, D, E, F, K, L, O>) + > ReadableArgs<(B, E, Option<F>, O, D, K, L)> for (BlockLocator, OutputSweeper<B, D, E, F, K, L, O>) where D::Target: ChangeDestinationSource, { @@ -986,7 +992,7 @@ where /// If chain data is provided via the [`Confirm`] interface or via filtered blocks, users also /// need to register their [`Filter`] implementation via the given `chain_data_source`. pub fn new( - best_block: BestBlock, broadcaster: B, fee_estimator: E, chain_data_source: Option<F>, + best_block: BlockLocator, broadcaster: B, fee_estimator: E, chain_data_source: Option<F>, output_spender: O, change_destination_source: D, kv_store: K, logger: L, ) -> Self { let change_destination_source = @@ -1054,7 +1060,7 @@ where /// Gets the latest best block which was connected either via [`Listen`] or [`Confirm`] /// interfaces. - pub fn current_best_block(&self) -> BestBlock { + pub fn current_best_block(&self) -> BlockLocator { self.sweeper.current_best_block() } @@ -1111,7 +1117,7 @@ where self.sweeper.filtered_block_connected(header, txdata, height); } - fn blocks_disconnected(&self, fork_point: BestBlock) { + fn blocks_disconnected(&self, fork_point: BlockLocator) { self.sweeper.blocks_disconnected(fork_point); } } @@ -1157,7 +1163,7 @@ impl< L: Logger, O: OutputSpender, > ReadableArgs<(B, E, Option<F>, O, D, K, L)> - for (BestBlock, OutputSweeperSync<B, D, E, F, K, L, O>) + for (BlockLocator, OutputSweeperSync<B, D, E, F, K, L, O>) where D::Target: ChangeDestinationSourceSync, K::Target: KVStoreSync, @@ -1172,7 +1178,155 @@ where let kv_store = KVStoreSyncWrapper(kv_store); let args = (a, b, c, d, change_destination_source, kv_store, e); let (best_block, sweeper) = - <(BestBlock, OutputSweeper<_, _, _, _, _, _, _>)>::read(reader, args)?; + <(BlockLocator, OutputSweeper<_, _, _, _, _, _, _>)>::read(reader, args)?; Ok((best_block, OutputSweeperSync { sweeper })) } } + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::*; + use crate::chain::transaction::OutPoint; + use crate::sign::{ChangeDestinationSource, OutputSpender}; + use crate::util::async_poll::dummy_waker; + use crate::util::logger::Record; + use crate::util::native_async::MaybeSend; + + use bitcoin::hashes::Hash as _; + use bitcoin::secp256k1::All; + use bitcoin::transaction::Version; + use bitcoin::{Amount, BlockHash, ScriptBuf, Transaction, TxOut, Txid}; + + use core::future as core_future; + use core::pin::pin; + use core::sync::atomic::Ordering; + use core::task::Poll; + + struct DummyBroadcaster; + impl BroadcasterInterface for DummyBroadcaster { + fn broadcast_transactions(&self, _: &[(&Transaction, TransactionType)]) {} + } + + struct DummyFeeEstimator; + impl FeeEstimator for DummyFeeEstimator { + fn get_est_sat_per_1000_weight(&self, _: ConfirmationTarget) -> u32 { + 1000 + } + } + + struct DummyFilter; + impl Filter for DummyFilter { + fn register_tx(&self, _: &Txid, _: &bitcoin::Script) {} + fn register_output(&self, _: WatchedOutput) {} + } + + struct DummyLogger; + impl Logger for DummyLogger { + fn log(&self, _: Record) {} + } + + struct DummyOutputSpender; + impl OutputSpender for DummyOutputSpender { + fn spend_spendable_outputs( + &self, _: &[&SpendableOutputDescriptor], _: Vec<TxOut>, _: ScriptBuf, _: u32, + _: Option<LockTime>, _: &Secp256k1<All>, + ) -> Result<Transaction, ()> { + Ok(Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: Vec::new(), + output: Vec::new(), + }) + } + } + + struct DummyChangeDestSource; + impl ChangeDestinationSource for DummyChangeDestSource { + fn get_change_destination_script<'a>( + &'a self, + ) -> impl Future<Output = Result<ScriptBuf, ()>> + MaybeSend + 'a { + core_future::ready(Ok(ScriptBuf::new())) + } + } + + struct PendingKVStore; + impl KVStore for PendingKVStore { + fn read( + &self, _: &str, _: &str, _: &str, + ) -> impl Future<Output = Result<Vec<u8>, io::Error>> + 'static + MaybeSend { + core_future::ready(Err(io::Error::new(io::ErrorKind::NotFound, ""))) + } + fn write( + &self, _: &str, _: &str, _: &str, _: Vec<u8>, + ) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend { + core_future::pending() + } + fn remove( + &self, _: &str, _: &str, _: &str, _: bool, + ) -> impl Future<Output = Result<(), io::Error>> + 'static + MaybeSend { + core_future::ready(Ok(())) + } + fn list( + &self, _: &str, _: &str, + ) -> impl Future<Output = Result<Vec<String>, io::Error>> + 'static + MaybeSend { + core_future::ready(Ok(Vec::new())) + } + } + + #[test] + fn pending_sweep_flag_resets_after_future_drop() { + let best_block = BlockLocator::new(BlockHash::all_zeros(), 1_000); + + let sweeper: OutputSweeper< + DummyBroadcaster, + Box<DummyChangeDestSource>, + DummyFeeEstimator, + DummyFilter, + PendingKVStore, + DummyLogger, + DummyOutputSpender, + > = OutputSweeper::new( + best_block, + DummyBroadcaster, + DummyFeeEstimator, + None, + DummyOutputSpender, + Box::new(DummyChangeDestSource), + PendingKVStore, + DummyLogger, + ); + + // Inject a tracked output directly so the sweep loop has work to do. + let descriptor = SpendableOutputDescriptor::StaticOutput { + outpoint: OutPoint { txid: Txid::all_zeros(), index: 0 }, + output: TxOut { value: Amount::from_sat(100_000), script_pubkey: ScriptBuf::new() }, + channel_keys_id: None, + }; + sweeper.sweeper_state.lock().unwrap().outputs.push(TrackedSpendableOutput { + descriptor, + channel_id: None, + counterparty_node_id: None, + status: OutputSpendStatus::PendingInitialBroadcast { delayed_until_height: None }, + }); + + // Start a sweep, poll once (the persist step stays Pending because our KVStore's + // `write` future is `future::pending()`), then drop the future to mimic + // cancellation - the sort of thing a `tokio::time::timeout` wrapper produces. + { + let mut fut = pin!(sweeper.regenerate_and_broadcast_spend_if_necessary()); + let waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&waker); + assert!(matches!(fut.as_mut().poll(&mut ctx), Poll::Pending)); + } + + // Once the future has been dropped, `pending_sweep` must be cleared. The bug + // is that the flag is only ever cleared after the inner future returns, so a + // dropped future leaves it stuck `true` and every subsequent call to + // `regenerate_and_broadcast_spend_if_necessary` short-circuits with `Ok(())`, + // permanently disabling the sweeper. + assert!( + !sweeper.pending_sweep.load(Ordering::Acquire), + "pending_sweep flag was not reset when the future was dropped", + ); + } +} diff --git a/lightning/src/util/test_channel_signer.rs b/lightning/src/util/test_channel_signer.rs index 3bacd76a610..668bbebad05 100644 --- a/lightning/src/util/test_channel_signer.rs +++ b/lightning/src/util/test_channel_signer.rs @@ -34,19 +34,11 @@ use bitcoin::sighash::EcdsaSighashType; use bitcoin::transaction::Transaction; use bitcoin::Txid; -#[cfg(taproot)] -use crate::ln::msgs::PartialSignatureWithNonce; -#[cfg(taproot)] -use crate::sign::taproot::TaprootChannelSigner; use crate::sign::HTLCDescriptor; use crate::util::dyn_signer::DynSigner; use bitcoin::secp256k1; -#[cfg(taproot)] -use bitcoin::secp256k1::All; use bitcoin::secp256k1::{ecdsa::Signature, Secp256k1}; use bitcoin::secp256k1::{PublicKey, SecretKey}; -#[cfg(taproot)] -use musig2::types::{PartialSignature, PublicNonce}; /// Initial value for revoked commitment downward counter pub const INITIAL_REVOKED_COMMITMENT_NUMBER: u64 = 1 << 48; @@ -103,7 +95,6 @@ pub enum SignerOp { ReleaseCommitmentSecret, ValidateHolderCommitment, SignCounterpartyCommitment, - ValidateCounterpartyRevocation, SignHolderCommitment, SignJusticeRevokedOutput, SignJusticeRevokedHtlc, @@ -112,6 +103,7 @@ pub enum SignerOp { SignClosingTransaction, SignHolderAnchorInput, SignChannelAnnouncementWithFundingKey, + SignSpliceSharedInput, } impl SignerOp { @@ -121,7 +113,6 @@ impl SignerOp { SignerOp::ReleaseCommitmentSecret, SignerOp::ValidateHolderCommitment, SignerOp::SignCounterpartyCommitment, - SignerOp::ValidateCounterpartyRevocation, SignerOp::SignHolderCommitment, SignerOp::SignJusticeRevokedOutput, SignerOp::SignJusticeRevokedHtlc, @@ -130,6 +121,7 @@ impl SignerOp { SignerOp::SignClosingTransaction, SignerOp::SignHolderAnchorInput, SignerOp::SignChannelAnnouncementWithFundingKey, + SignerOp::SignSpliceSharedInput, ] } } @@ -186,7 +178,7 @@ impl TestChannelSigner { self.get_enforcement_state().disabled_signer_ops.insert(signer_op); } - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] fn is_signer_available(&self, signer_op: SignerOp) -> bool { !self.get_enforcement_state().disabled_signer_ops.contains(&signer_op) } @@ -196,7 +188,7 @@ impl ChannelSigner for TestChannelSigner { fn get_per_commitment_point( &self, idx: u64, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<PublicKey, ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::GetPerCommitmentPoint) { return Err(()); } @@ -204,7 +196,7 @@ impl ChannelSigner for TestChannelSigner { } fn release_commitment_secret(&self, idx: u64) -> Result<[u8; 32], ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::ReleaseCommitmentSecret) { return Err(()); } @@ -236,10 +228,6 @@ impl ChannelSigner for TestChannelSigner { } fn validate_counterparty_revocation(&self, idx: u64, _secret: &SecretKey) -> Result<(), ()> { - #[cfg(test)] - if !self.is_signer_available(SignerOp::ValidateCounterpartyRevocation) { - return Err(()); - } let mut state = self.state.lock().unwrap(); if !self.disable_all_state_policy_checks { assert!(idx == state.last_counterparty_revoked_commitment || idx == state.last_counterparty_revoked_commitment - 1, "expecting to validate the current or next counterparty revocation - trying {}, current {}", idx, state.last_counterparty_revoked_commitment); @@ -272,7 +260,7 @@ impl EcdsaChannelSigner for TestChannelSigner { ) -> Result<(Signature, Vec<Signature>), ()> { self.verify_counterparty_commitment_tx(channel_parameters, commitment_tx, secp_ctx); - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignCounterpartyCommitment) { return Err(()); } @@ -317,7 +305,7 @@ impl EcdsaChannelSigner for TestChannelSigner { &self, channel_parameters: &ChannelTransactionParameters, commitment_tx: &HolderCommitmentTransaction, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<Signature, ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignHolderCommitment) { return Err(()); } @@ -354,7 +342,7 @@ impl EcdsaChannelSigner for TestChannelSigner { input: usize, amount: u64, per_commitment_key: &SecretKey, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<Signature, ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignJusticeRevokedOutput) { return Err(()); } @@ -375,7 +363,7 @@ impl EcdsaChannelSigner for TestChannelSigner { input: usize, amount: u64, per_commitment_key: &SecretKey, htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<Signature, ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignJusticeRevokedHtlc) { return Err(()); } @@ -396,7 +384,7 @@ impl EcdsaChannelSigner for TestChannelSigner { &self, htlc_tx: &Transaction, input: usize, htlc_descriptor: &HTLCDescriptor, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<Signature, ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignHolderHtlcTransaction) { return Err(()); } @@ -462,7 +450,7 @@ impl EcdsaChannelSigner for TestChannelSigner { input: usize, amount: u64, per_commitment_point: &PublicKey, htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<Signature, ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignCounterpartyHtlcTransaction) { return Err(()); } @@ -483,7 +471,7 @@ impl EcdsaChannelSigner for TestChannelSigner { &self, channel_parameters: &ChannelTransactionParameters, closing_tx: &ClosingTransaction, secp_ctx: &Secp256k1<secp256k1::All>, ) -> Result<Signature, ()> { - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignClosingTransaction) { return Err(()); } @@ -504,7 +492,7 @@ impl EcdsaChannelSigner for TestChannelSigner { anchor_tx.input[input].previous_output.vout == 0 || anchor_tx.input[input].previous_output.vout == 1 ); - #[cfg(test)] + #[cfg(any(test, feature = "_test_utils"))] if !self.is_signer_available(SignerOp::SignHolderAnchorInput) { return Err(()); } @@ -521,70 +509,15 @@ impl EcdsaChannelSigner for TestChannelSigner { fn sign_splice_shared_input( &self, channel_parameters: &ChannelTransactionParameters, tx: &Transaction, input_index: usize, secp_ctx: &Secp256k1<secp256k1::All>, - ) -> Signature { + ) -> Result<Signature, ()> { + #[cfg(any(test, feature = "_test_utils"))] + if !self.is_signer_available(SignerOp::SignSpliceSharedInput) { + return Err(()); + } self.inner.sign_splice_shared_input(channel_parameters, tx, input_index, secp_ctx) } } -#[cfg(taproot)] -#[allow(unused)] -impl TaprootChannelSigner for TestChannelSigner { - fn generate_local_nonce_pair( - &self, commitment_number: u64, secp_ctx: &Secp256k1<All>, - ) -> PublicNonce { - todo!() - } - - fn partially_sign_counterparty_commitment( - &self, counterparty_nonce: PublicNonce, commitment_tx: &CommitmentTransaction, - inbound_htlc_preimages: Vec<PaymentPreimage>, - outbound_htlc_preimages: Vec<PaymentPreimage>, secp_ctx: &Secp256k1<All>, - ) -> Result<(PartialSignatureWithNonce, Vec<secp256k1::schnorr::Signature>), ()> { - todo!() - } - - fn finalize_holder_commitment( - &self, commitment_tx: &HolderCommitmentTransaction, - counterparty_partial_signature: PartialSignatureWithNonce, secp_ctx: &Secp256k1<All>, - ) -> Result<PartialSignature, ()> { - todo!() - } - - fn sign_justice_revoked_output( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!() - } - - fn sign_justice_revoked_htlc( - &self, justice_tx: &Transaction, input: usize, amount: u64, per_commitment_key: &SecretKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!() - } - - fn sign_holder_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, htlc_descriptor: &HTLCDescriptor, - secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!() - } - - fn sign_counterparty_htlc_transaction( - &self, htlc_tx: &Transaction, input: usize, amount: u64, per_commitment_point: &PublicKey, - htlc: &HTLCOutputInCommitment, secp_ctx: &Secp256k1<All>, - ) -> Result<secp256k1::schnorr::Signature, ()> { - todo!() - } - - fn partially_sign_closing_transaction( - &self, closing_tx: &ClosingTransaction, secp_ctx: &Secp256k1<All>, - ) -> Result<PartialSignature, ()> { - todo!() - } -} - impl TestChannelSigner { fn verify_counterparty_commitment_tx<'a, T: secp256k1::Signing + secp256k1::Verification>( &self, channel_parameters: &ChannelTransactionParameters, diff --git a/lightning/src/util/test_utils.rs b/lightning/src/util/test_utils.rs index bcf39fde482..7af41c19586 100644 --- a/lightning/src/util/test_utils.rs +++ b/lightning/src/util/test_utils.rs @@ -7,9 +7,11 @@ // You may not use this file except in accordance with one or both of these // licenses. +use alloc::collections::BTreeMap; + use crate::blinded_path::message::MessageContext; use crate::blinded_path::message::{BlindedMessagePath, MessageForwardNode}; -use crate::blinded_path::payment::{BlindedPaymentPath, ReceiveTlvs}; +use crate::blinded_path::payment::{BlindedPaymentPath, PaymentContext, ReceiveTlvs}; use crate::chain; use crate::chain::chaininterface; #[cfg(any(test, feature = "_externalize_tests"))] @@ -20,9 +22,8 @@ use crate::chain::channelmonitor::{ ChannelMonitor, ChannelMonitorUpdate, ChannelMonitorUpdateStep, MonitorEvent, }; use crate::chain::transaction::OutPoint; +use crate::chain::BlockLocator; use crate::chain::WatchedOutput; -use crate::events::bump_transaction::sync::WalletSourceSync; -use crate::events::bump_transaction::Utxo; #[cfg(any(test, feature = "_externalize_tests"))] use crate::ln::chan_utils::CommitmentTransaction; use crate::ln::channel_state::ChannelDetails; @@ -50,7 +51,6 @@ use crate::sign::{self, ReceiveAuthKey}; use crate::sign::{ChannelSigner, PeerStorageKey}; use crate::sync::RwLock; use crate::types::features::{ChannelFeatures, InitFeatures, NodeFeatures}; -use crate::util::async_poll::MaybeSend; use crate::util::config::UserConfig; use crate::util::dyn_signer::{ DynKeysInterface, DynKeysInterfaceTrait, DynPhantomKeysInterface, DynSigner, @@ -58,16 +58,18 @@ use crate::util::dyn_signer::{ use crate::util::logger::{Logger, Record}; #[cfg(feature = "std")] use crate::util::mut_global::MutGlobal; +use crate::util::native_async::MaybeSend; use crate::util::persist::{KVStore, KVStoreSync, MonitorName}; use crate::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use crate::util::test_channel_signer::{EnforcementState, TestChannelSigner}; use crate::util::wakers::Notifier; +use crate::util::wallet_utils::{ConfirmedUtxo, Utxo, WalletSourceSync}; use bitcoin::amount::Amount; use bitcoin::block::Block; use bitcoin::constants::genesis_block; use bitcoin::constants::ChainHash; -use bitcoin::hash_types::{BlockHash, Txid}; +use bitcoin::hash_types::Txid; use bitcoin::hashes::{hex::FromHex, Hash}; use bitcoin::network::Network; use bitcoin::script::{Builder, Script, ScriptBuf}; @@ -178,6 +180,7 @@ pub struct TestRouter<'a> { pub network_graph: Arc<NetworkGraph<&'a TestLogger>>, pub next_routes: Mutex<VecDeque<(RouteParameters, Option<Result<Route, &'static str>>)>>, pub next_blinded_payment_paths: Mutex<Vec<BlindedPaymentPath>>, + pub next_payment_context_metadata: Mutex<Option<BTreeMap<u64, Vec<u8>>>>, pub scorer: &'a RwLock<TestScorer>, } @@ -189,6 +192,7 @@ impl<'a> TestRouter<'a> { let entropy_source = Arc::new(RandomBytes::new([42; 32])); let next_routes = Mutex::new(VecDeque::new()); let next_blinded_payment_paths = Mutex::new(Vec::new()); + let next_payment_context_metadata = Mutex::new(None); Self { router: DefaultRouter::new( Arc::clone(&network_graph), @@ -200,10 +204,15 @@ impl<'a> TestRouter<'a> { network_graph, next_routes, next_blinded_payment_paths, + next_payment_context_metadata, scorer, } } + pub fn set_next_payment_context_metadata(&self, metadata: BTreeMap<u64, Vec<u8>>) { + *self.next_payment_context_metadata.lock().unwrap() = Some(metadata); + } + pub fn expect_find_route(&self, query: RouteParameters, result: Result<Route, &'static str>) { let mut expected_routes = self.next_routes.lock().unwrap(); expected_routes.push_back((query, Some(result))); @@ -231,7 +240,7 @@ impl<'a> Router for TestRouter<'a> { assert_eq!(find_route_query, *params); if let Some(res) = find_route_res { if let Ok(ref route) = res { - assert_eq!(route.route_params, Some(find_route_query)); + assert_eq!(route.route_params, find_route_query); let scorer = self.scorer.read().unwrap(); let scorer = ScorerAccountingForInFlightHtlcs::new(scorer, &inflight_htlcs); for path in &route.paths { @@ -319,9 +328,16 @@ impl<'a> Router for TestRouter<'a> { fn create_blinded_payment_paths<T: secp256k1::Signing + secp256k1::Verification>( &self, recipient: PublicKey, local_node_receive_key: ReceiveAuthKey, - first_hops: Vec<ChannelDetails>, tlvs: ReceiveTlvs, amount_msats: Option<u64>, + first_hops: Vec<ChannelDetails>, mut tlvs: ReceiveTlvs, amount_msats: Option<u64>, secp_ctx: &Secp256k1<T>, ) -> Result<Vec<BlindedPaymentPath>, ()> { + if let Some(metadata) = self.next_payment_context_metadata.lock().unwrap().take() { + match &mut tlvs.payment_context { + PaymentContext::Bolt12Offer(ctx) => ctx.payment_metadata = Some(metadata), + PaymentContext::AsyncBolt12Offer(ctx) => ctx.payment_metadata = Some(metadata), + PaymentContext::Bolt12Refund(ctx) => ctx.payment_metadata = Some(metadata), + } + } let mut expected_paths = self.next_blinded_payment_paths.lock().unwrap(); if expected_paths.is_empty() { self.router.create_blinded_payment_paths( @@ -458,8 +474,6 @@ impl EntropySource for OnlyReadsKeysInterface { impl SignerProvider for OnlyReadsKeysInterface { type EcdsaSigner = TestChannelSigner; - #[cfg(taproot)] - type TaprootSigner = TestChannelSigner; fn generate_channel_keys_id(&self, _inbound: bool, _user_channel_id: u128) -> [u8; 32] { unreachable!(); @@ -509,6 +523,7 @@ pub struct TestChainMonitor<'a> { &'a TestKeysInterface, >, pub keys_manager: &'a TestKeysInterface, + pub logger: &'a TestLogger, /// If this is set to Some(), the next update_channel call (not watch_channel) must be a /// ChannelForceClosed event for the given channel_id with should_broadcast set to the given /// boolean. @@ -518,12 +533,48 @@ pub struct TestChainMonitor<'a> { pub expect_monitor_round_trip_fail: Mutex<Option<ChannelId>>, #[cfg(feature = "std")] pub write_blocker: Mutex<Option<std::sync::mpsc::Receiver<()>>>, + /// When set to `true`, `release_pending_monitor_events` will not auto-flush pending + /// deferred operations. This allows tests to control exactly when queued monitor updates + /// are applied to the in-memory monitor. + pub pause_flush: AtomicBool, } impl<'a> TestChainMonitor<'a> { pub fn new( chain_source: Option<&'a TestChainSource>, broadcaster: &'a dyn SyncBroadcaster, logger: &'a TestLogger, fee_estimator: &'a TestFeeEstimator, persister: &'a dyn SyncPersist, keys_manager: &'a TestKeysInterface, + ) -> Self { + Self::with_deferred( + chain_source, + broadcaster, + logger, + fee_estimator, + persister, + keys_manager, + false, + ) + } + + pub fn new_deferred( + chain_source: Option<&'a TestChainSource>, broadcaster: &'a dyn SyncBroadcaster, + logger: &'a TestLogger, fee_estimator: &'a TestFeeEstimator, + persister: &'a dyn SyncPersist, keys_manager: &'a TestKeysInterface, + ) -> Self { + Self::with_deferred( + chain_source, + broadcaster, + logger, + fee_estimator, + persister, + keys_manager, + true, + ) + } + + fn with_deferred( + chain_source: Option<&'a TestChainSource>, broadcaster: &'a dyn SyncBroadcaster, + logger: &'a TestLogger, fee_estimator: &'a TestFeeEstimator, + persister: &'a dyn SyncPersist, keys_manager: &'a TestKeysInterface, deferred: bool, ) -> Self { Self { added_monitors: Mutex::new(Vec::new()), @@ -537,15 +588,22 @@ impl<'a> TestChainMonitor<'a> { persister, keys_manager, keys_manager.get_peer_storage_key(), + deferred, ), keys_manager, + logger, expect_channel_force_closed: Mutex::new(None), expect_monitor_round_trip_fail: Mutex::new(None), #[cfg(feature = "std")] write_blocker: Mutex::new(None), + pause_flush: AtomicBool::new(false), } } + pub fn pending_operation_count(&self) -> usize { + self.chain_monitor.pending_operation_count() + } + pub fn complete_sole_pending_chan_update(&self, channel_id: &ChannelId) { let (_, latest_update) = self.latest_monitor_update_id.lock().unwrap().get(channel_id).unwrap().clone(); @@ -564,7 +622,7 @@ impl<'a> TestChainMonitor<'a> { // underlying `ChainMonitor`. let mut w = TestVecWriter(Vec::new()); monitor.write(&mut w).unwrap(); - let new_monitor = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read( + let new_monitor = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read( &mut io::Cursor::new(&w.0), (self.keys_manager, self.keys_manager), ) @@ -601,7 +659,7 @@ impl<'a> chain::Watch<TestChannelSigner> for TestChainMonitor<'a> { // monitor to a serialized copy and get he same one back. let mut w = TestVecWriter(Vec::new()); monitor.write(&mut w).unwrap(); - let new_monitor = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read( + let new_monitor = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read( &mut io::Cursor::new(&w.0), (self.keys_manager, self.keys_manager), ) @@ -657,18 +715,24 @@ impl<'a> chain::Watch<TestChannelSigner> for TestChainMonitor<'a> { let monitor = self.chain_monitor.get_monitor(channel_id).unwrap(); w.0.clear(); monitor.write(&mut w).unwrap(); - let new_monitor = <(BlockHash, ChannelMonitor<TestChannelSigner>)>::read( + let new_monitor = <(BlockLocator, ChannelMonitor<TestChannelSigner>)>::read( &mut io::Cursor::new(&w.0), (self.keys_manager, self.keys_manager), ) .unwrap() .1; + // failed_back_htlc_ids is an in-memory-only dedup guard that is intentionally not + // serialized. Copy it to the deserialized monitor for the comparison, then clear + // it so it doesn't leak into the rest of the test. + let failed_back = monitor.inner.lock().unwrap().failed_back_htlc_ids.clone(); + new_monitor.inner.lock().unwrap().failed_back_htlc_ids = failed_back; if let Some(chan_id) = self.expect_monitor_round_trip_fail.lock().unwrap().take() { assert_eq!(chan_id, channel_id); assert!(new_monitor != *monitor); } else { assert!(new_monitor == *monitor); } + new_monitor.inner.lock().unwrap().failed_back_htlc_ids.clear(); self.added_monitors.lock().unwrap().push((channel_id, new_monitor)); update_res } @@ -676,6 +740,14 @@ impl<'a> chain::Watch<TestChannelSigner> for TestChainMonitor<'a> { fn release_pending_monitor_events( &self, ) -> Vec<(OutPoint, ChannelId, Vec<MonitorEvent>, PublicKey)> { + // Auto-flush pending operations so that the ChannelManager can pick up monitor + // completion events. When not in deferred mode the queue is empty so this only + // costs a lock acquisition. It ensures standard test helpers (route_payment, etc.) + // work with deferred chain monitors. + if !self.pause_flush.load(Ordering::Acquire) { + let count = self.chain_monitor.pending_operation_count(); + self.chain_monitor.flush(count, &self.logger); + } return self.chain_monitor.release_pending_monitor_events(); } } @@ -835,6 +907,8 @@ pub struct TestPersister { /// The queue of update statuses we'll return. If none are queued, ::Completed will always be /// returned. pub update_rets: Mutex<VecDeque<chain::ChannelMonitorUpdateStatus>>, + /// When we get a persist_new_channel call, we push the monitor name here. + pub new_channel_persistences: Mutex<Vec<MonitorName>>, /// When we get an update_persisted_channel call *with* a ChannelMonitorUpdate, we insert the /// [`ChannelMonitor::get_latest_update_id`] here. pub offchain_monitor_updates: Mutex<HashMap<MonitorName, HashSet<u64>>>, @@ -845,9 +919,15 @@ pub struct TestPersister { impl TestPersister { pub fn new() -> Self { let update_rets = Mutex::new(VecDeque::new()); + let new_channel_persistences = Mutex::new(Vec::new()); let offchain_monitor_updates = Mutex::new(new_hash_map()); let chain_sync_monitor_persistences = Mutex::new(VecDeque::new()); - Self { update_rets, offchain_monitor_updates, chain_sync_monitor_persistences } + Self { + update_rets, + new_channel_persistences, + offchain_monitor_updates, + chain_sync_monitor_persistences, + } } /// Queue an update status to return. @@ -857,8 +937,9 @@ impl TestPersister { } impl<Signer: sign::ecdsa::EcdsaChannelSigner> Persist<Signer> for TestPersister { fn persist_new_channel( - &self, _monitor_name: MonitorName, _data: &ChannelMonitor<Signer>, + &self, monitor_name: MonitorName, _data: &ChannelMonitor<Signer>, ) -> chain::ChannelMonitorUpdateStatus { + self.new_channel_persistences.lock().unwrap().push(monitor_name); if let Some(update_ret) = self.update_rets.lock().unwrap().pop_front() { return update_ret; } @@ -1801,7 +1882,7 @@ impl TestNodeSigner { impl NodeSigner for TestNodeSigner { fn get_expanded_key(&self) -> ExpandedKey { - unreachable!() + ExpandedKey::new([42; 32]) } fn get_peer_storage_key(&self) -> PeerStorageKey { @@ -1927,8 +2008,6 @@ impl NodeSigner for TestKeysInterface { impl SignerProvider for TestKeysInterface { type EcdsaSigner = TestChannelSigner; - #[cfg(taproot)] - type TaprootSigner = TestChannelSigner; fn generate_channel_keys_id(&self, inbound: bool, user_channel_id: u128) -> [u8; 32] { let mut override_keys = self.override_next_keys_id.lock().unwrap(); @@ -1983,6 +2062,7 @@ pub trait TestSignerFactory: Send + Sync { /// Make a dynamic signer fn make_signer( &self, seed: &[u8; 32], now: Duration, v2_remote_key_derivation: bool, + phantom_seed: Option<&[u8; 32]>, ) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>>; } @@ -1992,12 +2072,13 @@ struct DefaultSignerFactory(); impl TestSignerFactory for DefaultSignerFactory { fn make_signer( &self, seed: &[u8; 32], now: Duration, v2_remote_key_derivation: bool, + phantom_seed: Option<&[u8; 32]>, ) -> Box<dyn DynKeysInterfaceTrait<EcdsaSigner = DynSigner>> { let phantom = sign::PhantomKeysManager::new( seed, now.as_secs(), now.subsec_nanos(), - seed, + if let Some(provided_seed) = phantom_seed { provided_seed } else { seed }, v2_remote_key_derivation, ); let dphantom = DynPhantomKeysInterface::new(phantom); @@ -2029,7 +2110,7 @@ impl TestKeysInterface { let factory = DefaultSignerFactory(); let now = Duration::from_secs(genesis_block(network).header.time as u64); - let backing = factory.make_signer(seed, now, true); + let backing = factory.make_signer(seed, now, true, None); Self::build(backing) } @@ -2041,7 +2122,21 @@ impl TestKeysInterface { let factory = DefaultSignerFactory(); let now = Duration::from_secs(genesis_block(network).header.time as u64); - let backing = factory.make_signer(seed, now, false); + let backing = factory.make_signer(seed, now, false, None); + Self::build(backing) + } + + pub fn with_settings( + seed: &[u8; 32], network: Network, v1_derivation: bool, phantom_seed: Option<&[u8; 32]>, + ) -> Self { + #[cfg(feature = "std")] + let factory = SIGNER_FACTORY.get(); + + #[cfg(not(feature = "std"))] + let factory = DefaultSignerFactory(); + + let now = Duration::from_secs(genesis_block(network).header.time as u64); + let backing = factory.make_signer(seed, now, !v1_derivation, phantom_seed); Self::build(backing) } @@ -2128,6 +2223,10 @@ impl TestChainSource { self.watched_outputs.lock().unwrap().remove(&(outpoint, script_pubkey.clone())); self.watched_txn.lock().unwrap().remove(&(outpoint.txid, script_pubkey)); } + pub fn remove_watched_by_txid(&self, txid: Txid) { + self.watched_outputs.lock().unwrap().retain(|(op, _)| op.txid != txid); + self.watched_txn.lock().unwrap().retain(|(tid, _)| *tid != txid); + } } impl UtxoLookup for TestChainSource { @@ -2240,7 +2339,7 @@ impl Drop for TestScorer { pub struct TestWalletSource { secret_key: SecretKey, - utxos: Mutex<Vec<Utxo>>, + utxos: Mutex<Vec<ConfirmedUtxo>>, secp: Secp256k1<bitcoin::secp256k1::All>, } @@ -2249,21 +2348,13 @@ impl TestWalletSource { Self { secret_key, utxos: Mutex::new(Vec::new()), secp: Secp256k1::new() } } - pub fn add_utxo(&self, outpoint: bitcoin::OutPoint, value: Amount) -> TxOut { - let public_key = bitcoin::PublicKey::new(self.secret_key.public_key(&self.secp)); - let utxo = Utxo::new_v0_p2wpkh(outpoint, value, &public_key.wpubkey_hash().unwrap()); - self.utxos.lock().unwrap().push(utxo.clone()); - utxo.output - } - - pub fn add_custom_utxo(&self, utxo: Utxo) -> TxOut { - let output = utxo.output.clone(); + pub fn add_utxo(&self, prevtx: Transaction, vout: u32) { + let utxo = ConfirmedUtxo::new_p2wpkh(prevtx, vout).unwrap(); self.utxos.lock().unwrap().push(utxo); - output } pub fn remove_utxo(&self, outpoint: bitcoin::OutPoint) { - self.utxos.lock().unwrap().retain(|utxo| utxo.outpoint != outpoint); + self.utxos.lock().unwrap().retain(|utxo| utxo.outpoint() != outpoint); } pub fn clear_utxos(&self) { @@ -2276,12 +2367,12 @@ impl TestWalletSource { let utxos = self.utxos.lock().unwrap(); for i in 0..tx.input.len() { if let Some(utxo) = - utxos.iter().find(|utxo| utxo.outpoint == tx.input[i].previous_output) + utxos.iter().find(|utxo| utxo.outpoint() == tx.input[i].previous_output) { let sighash = SighashCache::new(&tx).p2wpkh_signature_hash( i, - &utxo.output.script_pubkey, - utxo.output.value, + &utxo.output().script_pubkey, + utxo.output().value, EcdsaSighashType::All, )?; #[cfg(not(feature = "grind_signatures"))] @@ -2306,7 +2397,17 @@ impl TestWalletSource { impl WalletSourceSync for TestWalletSource { fn list_confirmed_utxos(&self) -> Result<Vec<Utxo>, ()> { - Ok(self.utxos.lock().unwrap().clone()) + let utxos = self.utxos.lock().unwrap(); + Ok(utxos.iter().map(|ConfirmedUtxo { utxo, .. }| utxo.clone()).collect()) + } + + fn get_prevtx(&self, outpoint: bitcoin::OutPoint) -> Result<Transaction, ()> { + let utxos = self.utxos.lock().unwrap(); + utxos + .iter() + .find(|confirmed_utxo| confirmed_utxo.utxo.outpoint == outpoint) + .map(|ConfirmedUtxo { prevtx, .. }| prevtx.clone()) + .ok_or(()) } fn get_change_script(&self) -> Result<ScriptBuf, ()> { diff --git a/lightning/src/util/time.rs b/lightning/src/util/time.rs index c6041543572..626e96e1350 100644 --- a/lightning/src/util/time.rs +++ b/lightning/src/util/time.rs @@ -7,7 +7,7 @@ //! A simple module which either re-exports [`std::time::Instant`] or a mocked version of it for //! tests. -#[cfg(not(test))] +#[cfg(all(not(test), not(fuzzing)))] pub use std::time::Instant; #[cfg(test)] pub use test::Instant; diff --git a/lightning/src/util/wakers.rs b/lightning/src/util/wakers.rs index 17edadfd822..1a0f08b5e66 100644 --- a/lightning/src/util/wakers.rs +++ b/lightning/src/util/wakers.rs @@ -165,6 +165,8 @@ impl Future { /// Registers a callback to be called upon completion of this future. If the future has already /// completed, the callback will be called immediately. /// + /// Note that callbacks *must not* reenter this [`Future`] or the corresponding [`Notifier`]. + /// /// This is not exported to bindings users, use the bindings-only `register_callback_fn` instead pub fn register_callback(&self, callback: Box<dyn FutureCallback>) { let mut state = self.state.lock().unwrap(); @@ -182,6 +184,8 @@ impl Future { // here. /// Registers a callback to be called upon completion of this future. If the future has already /// completed, the callback will be called immediately. + /// + /// Note that callbacks *must not* reenter this [`Future`] or the corresponding [`Notifier`]. #[cfg(c_bindings)] pub fn register_callback_fn<F: 'static + FutureCallback>(&self, callback: F) { self.register_callback(Box::new(callback)); diff --git a/lightning/src/util/wallet_utils.rs b/lightning/src/util/wallet_utils.rs new file mode 100644 index 00000000000..fe6b4f129e3 --- /dev/null +++ b/lightning/src/util/wallet_utils.rs @@ -0,0 +1,979 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE +// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Utilities for wallet integration with LDK. + +use core::future::Future; +use core::ops::Deref; +use core::pin::pin; +use core::task; + +use crate::chain::chaininterface::fee_for_weight; +use crate::chain::ClaimId; +use crate::io_extras::sink; +use crate::ln::chan_utils::{ + BASE_INPUT_WEIGHT, BASE_TX_SIZE, EMPTY_SCRIPT_SIG_WEIGHT, P2WSH_TXOUT_WEIGHT, + SEGWIT_MARKER_FLAG_WEIGHT, +}; +use crate::prelude::*; +use crate::sign::{P2TR_KEY_PATH_WITNESS_WEIGHT, P2WPKH_WITNESS_WEIGHT}; +use crate::sync::Mutex; +use crate::util::async_poll::dummy_waker; +use crate::util::hash_tables::{new_hash_map, HashMap}; +use crate::util::logger::Logger; +use crate::util::native_async::{MaybeSend, MaybeSync}; + +use bitcoin::amount::Amount; +use bitcoin::consensus::Encodable; +use bitcoin::constants::WITNESS_SCALE_FACTOR; +use bitcoin::key::TweakedPublicKey; +use bitcoin::{ + OutPoint, Psbt, PubkeyHash, Script, ScriptBuf, Sequence, Transaction, TxOut, WPubkeyHash, + Weight, +}; + +/// An input that must be included in a transaction when performing coin selection through +/// [`CoinSelectionSource::select_confirmed_utxos`]. It is guaranteed to be a SegWit input, so it +/// must have an empty [`TxIn::script_sig`] when spent. +/// +/// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig +#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub struct Input { + /// The unique identifier of the input. + pub outpoint: OutPoint, + /// The UTXO being spent by the input. + pub previous_utxo: TxOut, + /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and + /// [`TxIn::witness`], each with their lengths included, required to satisfy the output's + /// script. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + pub satisfaction_weight: u64, +} + +/// An unspent transaction output that is available to spend resulting from a successful +/// [`CoinSelection`] attempt. +#[derive(Clone, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +pub struct Utxo { + /// The unique identifier of the output. + pub outpoint: OutPoint, + /// The output to spend. + pub output: TxOut, + /// The upper-bound weight consumed by the input's full [`TxIn::script_sig`] and [`TxIn::witness`], each + /// with their lengths included, required to satisfy the output's script. The weight consumed by + /// the input's `script_sig` must account for [`WITNESS_SCALE_FACTOR`]. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + pub satisfaction_weight: u64, + /// The sequence number to use in the [`TxIn`] when spending the UTXO. + /// + /// [`TxIn`]: bitcoin::TxIn + pub sequence: Sequence, +} + +impl_ser_tlv_based!(Utxo, { + (1, outpoint, required), + (3, output, required), + (5, satisfaction_weight, required), + (7, sequence, (default_value, Sequence::ENABLE_RBF_NO_LOCKTIME)), +}); + +impl Utxo { + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a legacy P2PKH output. + pub fn new_p2pkh(outpoint: OutPoint, value: Amount, pubkey_hash: &PubkeyHash) -> Self { + let script_sig_size = 1 /* script_sig length */ + + 1 /* OP_PUSH73 */ + + 73 /* sig including sighash flag */ + + 1 /* OP_PUSH33 */ + + 33 /* pubkey */; + Self { + outpoint, + output: TxOut { value, script_pubkey: ScriptBuf::new_p2pkh(pubkey_hash) }, + satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + 1, /* empty witness */ + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + } + } + + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a P2WPKH nested in P2SH output. + pub fn new_nested_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { + let script_sig_size = 1 /* script_sig length */ + + 1 /* OP_0 */ + + 1 /* OP_PUSH20 */ + + 20 /* pubkey_hash */; + Self { + outpoint, + output: TxOut { + value, + script_pubkey: ScriptBuf::new_p2sh( + &ScriptBuf::new_p2wpkh(pubkey_hash).script_hash(), + ), + }, + satisfaction_weight: script_sig_size * WITNESS_SCALE_FACTOR as u64 + + P2WPKH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + } + } + + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a SegWit v0 P2WPKH output. + pub fn new_v0_p2wpkh(outpoint: OutPoint, value: Amount, pubkey_hash: &WPubkeyHash) -> Self { + Self { + outpoint, + output: TxOut { value, script_pubkey: ScriptBuf::new_p2wpkh(pubkey_hash) }, + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + P2WPKH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + } + } + + /// Returns a `Utxo` with the `satisfaction_weight` estimate for a keypath spend of a SegWit v1 P2TR output. + pub fn new_v1_p2tr( + outpoint: OutPoint, value: Amount, tweaked_public_key: TweakedPublicKey, + ) -> Self { + Self { + outpoint, + output: TxOut { value, script_pubkey: ScriptBuf::new_p2tr_tweaked(tweaked_public_key) }, + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + P2TR_KEY_PATH_WITNESS_WEIGHT, + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + } + } +} + +/// An unspent transaction output with at least one confirmation. +/// +/// Can be used as an input to contribute to a channel's funding transaction either when using the +/// v2 channel establishment protocol or when splicing. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct ConfirmedUtxo { + /// The unspent [`TxOut`] found in [`prevtx`]. + /// + /// [`TxOut`]: bitcoin::TxOut + /// [`prevtx`]: Self::prevtx + pub(crate) utxo: Utxo, + + /// The transaction containing the unspent [`TxOut`] referenced by [`utxo`]. + /// + /// [`TxOut`]: bitcoin::TxOut + /// [`utxo`]: Self::utxo + pub(crate) prevtx: Transaction, +} + +impl_ser_tlv_based!(ConfirmedUtxo, { + (1, utxo, required), + (3, _sequence, (legacy, Sequence, + |read_val: Option<&Sequence>| { + if let Some(sequence) = read_val { + // Utxo contains sequence now, so update it if the value read here differs since + // this indicates Utxo::sequence was read with default_value + let utxo: &mut Utxo = utxo.0.as_mut().expect("utxo is required"); + if utxo.sequence != *sequence { + utxo.sequence = *sequence; + } + } + Ok(()) + }, + |utxo: &ConfirmedUtxo| Some(utxo.utxo.sequence))), + (5, prevtx, required), +}); + +impl ConfirmedUtxo { + fn new<F: FnOnce(&bitcoin::Script) -> bool>( + prevtx: Transaction, vout: u32, witness_weight: Weight, script_filter: F, + ) -> Result<Self, ()> { + Ok(ConfirmedUtxo { + utxo: Utxo { + outpoint: bitcoin::OutPoint { txid: prevtx.compute_txid(), vout }, + output: prevtx + .output + .get(vout as usize) + .filter(|output| script_filter(&output.script_pubkey)) + .ok_or(())? + .clone(), + satisfaction_weight: EMPTY_SCRIPT_SIG_WEIGHT + witness_weight.to_wu(), + sequence: Sequence::ENABLE_RBF_NO_LOCKTIME, + }, + prevtx, + }) + } + + /// Creates an input spending a P2WPKH output from the given `prevtx` at index `vout`. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2wpkh(prevtx: Transaction, vout: u32) -> Result<Self, ()> { + let witness_weight = Weight::from_wu(P2WPKH_WITNESS_WEIGHT) + - if cfg!(feature = "grind_signatures") { + // Guarantees a low R signature + Weight::from_wu(1) + } else { + Weight::ZERO + }; + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2wpkh) + } + + /// Creates an input spending a P2WSH output from the given `prevtx` at index `vout`. + /// + /// Requires passing the weight of witness needed to satisfy the output's script. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2wsh(prevtx: Transaction, vout: u32, witness_weight: Weight) -> Result<Self, ()> { + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2wsh) + } + + /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. + /// + /// This is meant for inputs spending a taproot output using the key path. See + /// [`new_p2tr_script_spend`] for when spending using a script path. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`new_p2tr_script_spend`]: Self::new_p2tr_script_spend + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2tr_key_spend(prevtx: Transaction, vout: u32) -> Result<Self, ()> { + let witness_weight = Weight::from_wu(P2TR_KEY_PATH_WITNESS_WEIGHT); + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2tr) + } + + /// Creates an input spending a P2TR output from the given `prevtx` at index `vout`. + /// + /// Requires passing the weight of witness needed to satisfy a script path of the taproot + /// output. See [`new_p2tr_key_spend`] for when spending using the key path. + /// + /// Uses [`Sequence::ENABLE_RBF_NO_LOCKTIME`] as the [`TxIn::sequence`], which can be overridden + /// by [`set_sequence`]. + /// + /// Returns `Err` if no such output exists in `prevtx` at index `vout`. + /// + /// [`new_p2tr_key_spend`]: Self::new_p2tr_key_spend + /// + /// [`TxIn::sequence`]: bitcoin::TxIn::sequence + /// [`set_sequence`]: Self::set_sequence + pub fn new_p2tr_script_spend( + prevtx: Transaction, vout: u32, witness_weight: Weight, + ) -> Result<Self, ()> { + ConfirmedUtxo::new(prevtx, vout, witness_weight, Script::is_p2tr) + } + + #[cfg(test)] + pub(crate) fn new_p2pkh(prevtx: Transaction, vout: u32) -> Result<Self, ()> { + ConfirmedUtxo::new(prevtx, vout, Weight::ZERO, Script::is_p2pkh) + } + + /// The outpoint of the UTXO being spent. + pub fn outpoint(&self) -> bitcoin::OutPoint { + self.utxo.outpoint + } + + /// The unspent output. + pub fn output(&self) -> &TxOut { + &self.utxo.output + } + + /// The sequence number to use in the [`TxIn`]. + /// + /// [`TxIn`]: bitcoin::TxIn + pub fn sequence(&self) -> Sequence { + self.utxo.sequence + } + + /// Sets the sequence number to use in the [`TxIn`]. + /// + /// [`TxIn`]: bitcoin::TxIn + pub fn set_sequence(&mut self, sequence: Sequence) { + self.utxo.sequence = sequence; + } + + /// Converts the [`ConfirmedUtxo`] into a [`Utxo`]. + pub fn into_utxo(self) -> Utxo { + self.utxo + } + + /// Converts the [`ConfirmedUtxo`] into a [`TxOut`]. + pub fn into_output(self) -> TxOut { + self.utxo.output + } +} + +/// The result of a successful coin selection attempt for a transaction requiring additional UTXOs +/// to cover its fees. +#[derive(Clone, Debug)] +pub struct CoinSelection { + /// The set of UTXOs (with at least 1 confirmation) to spend and use within a transaction + /// requiring additional fees. + pub confirmed_utxos: Vec<ConfirmedUtxo>, + /// An additional output tracking whether any change remained after coin selection. This output + /// should always have a value above dust for its given `script_pubkey`. It should not be + /// spent until the transaction it belongs to confirms to ensure mempool descendant limits are + /// not met. This implies no other party should be able to spend it except us. + pub change_output: Option<TxOut>, +} + +impl CoinSelection { + pub(crate) fn satisfaction_weight(&self) -> u64 { + self.confirmed_utxos.iter().map(|ConfirmedUtxo { utxo, .. }| utxo.satisfaction_weight).sum() + } + + pub(crate) fn input_amount(&self) -> Amount { + self.confirmed_utxos.iter().map(|ConfirmedUtxo { utxo, .. }| utxo.output.value).sum() + } +} + +/// An abstraction over a bitcoin wallet that can perform coin selection over a set of UTXOs and can +/// sign for them. The coin selection method aims to mimic Bitcoin Core's `fundrawtransaction` RPC, +/// which most wallets should be able to satisfy. Otherwise, consider implementing [`WalletSource`], +/// which can provide a default implementation of this trait when used with [`Wallet`]. +/// +/// For a synchronous version of this trait, see [`CoinSelectionSourceSync`]. +/// +/// This is not exported to bindings users as async is only supported in Rust. +// Note that updates to documentation on this trait should be copied to the synchronous version. +pub trait CoinSelectionSource { + /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are + /// available to spend. Implementations are free to pick their coin selection algorithm of + /// choice, as long as the following requirements are met: + /// + /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction + /// throughout coin selection, but must not be returned as part of the result. + /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction + /// throughout coin selection. In some cases, like when funding an anchor transaction, this + /// set is empty. Implementations should ensure they handle this correctly on their end, + /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be + /// provided, in which case a zero-value empty OP_RETURN output can be used instead. + /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the + /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. + /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this + /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC + /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for + /// anchor transactions, we will try your coin selection again with the same input-output + /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions + /// cannot be downsized. + /// + /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of + /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require + /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and + /// delaying block inclusion. + /// + /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they + /// can be re-used within new fee-bumped iterations of the original claiming transaction, + /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a + /// transaction associated with it, and all of the available UTXOs have already been assigned to + /// other claims, implementations must be willing to double spend their UTXOs. The choice of + /// which UTXOs to double spend is left to the implementation, but it must strive to keep the + /// set of other claims being double spent to a minimum. + /// + /// If `claim_id` is not set, then the selection should be treated as if it were for a unique + /// claim and must NOT be double-spent rather than being kept to a minimum. + /// + /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a; + /// Signs and provides the full witness for all inputs within the transaction known to the + /// trait (i.e., any provided via [`CoinSelectionSource::select_confirmed_utxos`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a; +} + +impl<C: Deref> CoinSelectionSource for C +where + C::Target: CoinSelectionSource, +{ + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a { + self.deref().select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ) + } + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { + self.deref().sign_psbt(psbt) + } +} + +/// An alternative to [`CoinSelectionSource`] that can be implemented and used along [`Wallet`] to +/// provide a default implementation to [`CoinSelectionSource`]. +/// +/// For a synchronous version of this trait, see [`WalletSourceSync`]. +/// +/// This is not exported to bindings users as async is only supported in Rust. +// Note that updates to documentation on this trait should be copied to the synchronous version. +pub trait WalletSource { + /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. + fn list_confirmed_utxos<'a>( + &'a self, + ) -> impl Future<Output = Result<Vec<Utxo>, ()>> + MaybeSend + 'a; + + /// Returns the previous transaction containing the UTXO referenced by the outpoint. + fn get_prevtx<'a>( + &'a self, outpoint: OutPoint, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a; + + /// Returns a script to use for change above dust resulting from a successful coin selection + /// attempt. + fn get_change_script<'a>( + &'a self, + ) -> impl Future<Output = Result<ScriptBuf, ()>> + MaybeSend + 'a; + + /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within + /// the transaction known to the wallet (i.e., any provided via + /// [`WalletSource::list_confirmed_utxos`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a; +} + +/// A wrapper over [`WalletSource`] that implements [`CoinSelectionSource`] by preferring UTXOs +/// that would avoid conflicting double spends. If not enough UTXOs are available to do so, +/// conflicting double spends may happen. +/// +/// For a synchronous version of this wrapper, see [`WalletSync`]. +/// +/// This is not exported to bindings users as async is only supported in Rust. +// Note that updates to documentation on this struct should be copied to the synchronous version. +pub struct Wallet<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> +where + W::Target: WalletSource + MaybeSend, +{ + source: W, + logger: L, + // TODO: Do we care about cleaning this up once the UTXOs have a confirmed spend? We can do so + // by checking whether any UTXOs that exist in the map are no longer returned in + // `list_confirmed_utxos`. + locked_utxos: Mutex<HashMap<OutPoint, Option<ClaimId>>>, +} + +impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> Wallet<W, L> +where + W::Target: WalletSource + MaybeSend, +{ + /// Returns a new instance backed by the given [`WalletSource`] that serves as an implementation + /// of [`CoinSelectionSource`]. + pub fn new(source: W, logger: L) -> Self { + Self { source, logger, locked_utxos: Mutex::new(new_hash_map()) } + } + + /// Performs coin selection on the set of UTXOs obtained from + /// [`WalletSource::list_confirmed_utxos`]. Its algorithm can be described as "smallest + /// above-dust-after-spend first", with a slight twist: we may skip UTXOs that are above dust at + /// the target feerate after having spent them in a separate claim transaction if + /// `force_conflicting_utxo_spend` is unset to avoid producing conflicting transactions. If + /// `tolerate_high_network_feerates` is set, we'll attempt to spend UTXOs that contribute at + /// least 1 satoshi at the current feerate, otherwise, we'll only attempt to spend those which + /// contribute at least twice their fee. + async fn select_confirmed_utxos_internal( + &self, utxos: &[Utxo], claim_id: Option<ClaimId>, force_conflicting_utxo_spend: bool, + tolerate_high_network_feerates: bool, target_feerate_sat_per_1000_weight: u32, + preexisting_tx_weight: u64, input_amount_sat: Amount, target_amount_sat: Amount, + max_tx_weight: u64, + ) -> Result<CoinSelection, ()> { + debug_assert!(!(claim_id.is_none() && force_conflicting_utxo_spend)); + + // P2WSH and P2TR outputs are both the heaviest-weight standard outputs at 34 bytes + let max_coin_selection_weight = max_tx_weight + .checked_sub(preexisting_tx_weight + P2WSH_TXOUT_WEIGHT) + .ok_or_else(|| { + log_debug!( + self.logger, + "max_tx_weight is too small to accommodate the preexisting tx weight plus a P2WSH/P2TR output" + ); + })?; + + let mut selected_amount; + let mut total_fees; + let mut selected_utxos; + { + let mut locked_utxos = self.locked_utxos.lock().unwrap(); + let mut eligible_utxos = utxos + .iter() + .filter_map(|utxo| { + if let Some(utxo_claim_id) = locked_utxos.get(&utxo.outpoint) { + // TODO(splicing): For splicing (i.e., claim_id.is_none()), ideally we'd + // allow force_conflicting_utxo_spend for an RBF attempt. However, we'd need + // something similar to a ClaimId to identify a splice. + if (utxo_claim_id.is_none() || claim_id.is_none()) + || (*utxo_claim_id != claim_id && !force_conflicting_utxo_spend) + { + log_trace!( + self.logger, + "Skipping UTXO {} to prevent conflicting spend", + utxo.outpoint + ); + return None; + } + } + let fee_to_spend_utxo = Amount::from_sat(fee_for_weight( + target_feerate_sat_per_1000_weight, + BASE_INPUT_WEIGHT + utxo.satisfaction_weight, + )); + let should_spend = if tolerate_high_network_feerates { + utxo.output.value > fee_to_spend_utxo + } else { + utxo.output.value >= fee_to_spend_utxo * 2 + }; + if should_spend { + Some((utxo, fee_to_spend_utxo)) + } else { + log_trace!( + self.logger, + "Skipping UTXO {} due to dust proximity after spend", + utxo.outpoint + ); + None + } + }) + .collect::<Vec<_>>(); + eligible_utxos.sort_unstable_by_key(|(utxo, fee_to_spend_utxo)| { + utxo.output.value - *fee_to_spend_utxo + }); + + selected_amount = input_amount_sat; + total_fees = Amount::from_sat(fee_for_weight( + target_feerate_sat_per_1000_weight, + preexisting_tx_weight, + )); + selected_utxos = VecDeque::new(); + // Invariant: `selected_utxos_weight` is never greater than `max_coin_selection_weight` + let mut selected_utxos_weight = 0; + for (utxo, fee_to_spend_utxo) in eligible_utxos { + if selected_amount >= target_amount_sat + total_fees { + break; + } + // First skip any UTXOs with prohibitive satisfaction weights + if BASE_INPUT_WEIGHT + utxo.satisfaction_weight > max_coin_selection_weight { + continue; + } + // If adding this UTXO to `selected_utxos` would push us over the + // `max_coin_selection_weight`, remove UTXOs from the front to make room + // for this new UTXO. + while selected_utxos_weight + BASE_INPUT_WEIGHT + utxo.satisfaction_weight + > max_coin_selection_weight + && !selected_utxos.is_empty() + { + let (smallest_value_after_spend_utxo, fee_to_spend_utxo): (Utxo, Amount) = + selected_utxos.pop_front().unwrap(); + selected_amount -= smallest_value_after_spend_utxo.output.value; + total_fees -= fee_to_spend_utxo; + selected_utxos_weight -= + BASE_INPUT_WEIGHT + smallest_value_after_spend_utxo.satisfaction_weight; + } + selected_amount += utxo.output.value; + total_fees += fee_to_spend_utxo; + selected_utxos_weight += BASE_INPUT_WEIGHT + utxo.satisfaction_weight; + selected_utxos.push_back((utxo.clone(), fee_to_spend_utxo)); + } + if selected_amount < target_amount_sat + total_fees { + log_debug!( + self.logger, + "Insufficient funds to meet target feerate {} sat/kW while remaining under {} WU", + target_feerate_sat_per_1000_weight, + max_coin_selection_weight, + ); + return Err(()); + } + // Once we've selected enough UTXOs to cover `target_amount_sat + total_fees`, + // we may be able to remove some small-value ones while still covering + // `target_amount_sat + total_fees`. + while !selected_utxos.is_empty() + && selected_amount - selected_utxos.front().unwrap().0.output.value + >= target_amount_sat + total_fees - selected_utxos.front().unwrap().1 + { + let (smallest_value_after_spend_utxo, fee_to_spend_utxo) = + selected_utxos.pop_front().unwrap(); + selected_amount -= smallest_value_after_spend_utxo.output.value; + total_fees -= fee_to_spend_utxo; + } + for (utxo, _) in &selected_utxos { + locked_utxos.insert(utxo.outpoint, claim_id); + } + } + + let remaining_amount = selected_amount - target_amount_sat - total_fees; + let change_script = self.source.get_change_script().await?; + let change_output_fee = fee_for_weight( + target_feerate_sat_per_1000_weight, + (8 /* value */ + change_script.consensus_encode(&mut sink()).unwrap() as u64) + * WITNESS_SCALE_FACTOR as u64, + ); + let change_output_amount = + Amount::from_sat(remaining_amount.to_sat().saturating_sub(change_output_fee)); + let change_output = if change_output_amount < change_script.minimal_non_dust() { + log_debug!(self.logger, "Coin selection attempt did not yield change output"); + None + } else { + Some(TxOut { script_pubkey: change_script, value: change_output_amount }) + }; + + let mut confirmed_utxos = Vec::with_capacity(selected_utxos.len()); + for (utxo, _) in selected_utxos { + let prevtx = self.source.get_prevtx(utxo.outpoint).await?; + let prevtx_id = prevtx.compute_txid(); + if prevtx_id != utxo.outpoint.txid + || prevtx.output.get(utxo.outpoint.vout as usize).is_none() + { + log_error!( + self.logger, + "Tx {} from wallet source doesn't contain output referenced by outpoint: {}", + prevtx_id, + utxo.outpoint, + ); + return Err(()); + } + + confirmed_utxos.push(ConfirmedUtxo { utxo, prevtx }); + } + + Ok(CoinSelection { confirmed_utxos, change_output }) + } +} + +impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> CoinSelectionSource + for Wallet<W, L> +where + W::Target: WalletSource + MaybeSend + MaybeSync, +{ + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a { + async move { + let utxos = self.source.list_confirmed_utxos().await?; + // TODO: Use fee estimation utils when we upgrade to bitcoin v0.30.0. + let total_output_size: u64 = must_pay_to + .iter() + .map( + |output| 8 /* value */ + 1 /* script len */ + output.script_pubkey.len() as u64, + ) + .sum(); + let total_satisfaction_weight: u64 = + must_spend.iter().map(|input| input.satisfaction_weight).sum(); + let total_input_weight = + (BASE_INPUT_WEIGHT * must_spend.len() as u64) + total_satisfaction_weight; + + let preexisting_tx_weight = SEGWIT_MARKER_FLAG_WEIGHT + + total_input_weight + + ((BASE_TX_SIZE + total_output_size) * WITNESS_SCALE_FACTOR as u64); + let input_amount_sat = must_spend.iter().map(|input| input.previous_utxo.value).sum(); + let target_amount_sat = must_pay_to.iter().map(|output| output.value).sum(); + + let configs = [(false, false), (false, true), (true, false), (true, true)]; + for (force_conflicting_utxo_spend, tolerate_high_network_feerates) in configs { + if claim_id.is_none() && force_conflicting_utxo_spend { + continue; + } + log_debug!( + self.logger, + "Attempting coin selection targeting {} sat/kW (force_conflicting_utxo_spend = {}, tolerate_high_network_feerates = {})", + target_feerate_sat_per_1000_weight, + force_conflicting_utxo_spend, + tolerate_high_network_feerates + ); + let attempt = self + .select_confirmed_utxos_internal( + &utxos, + claim_id, + force_conflicting_utxo_spend, + tolerate_high_network_feerates, + target_feerate_sat_per_1000_weight, + preexisting_tx_weight, + input_amount_sat, + target_amount_sat, + max_tx_weight, + ) + .await; + if attempt.is_ok() { + return attempt; + } + } + Err(()) + } + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { + self.source.sign_psbt(psbt) + } +} + +/// An alternative to [`CoinSelectionSourceSync`] that can be implemented and used along +/// [`WalletSync`] to provide a default implementation to [`CoinSelectionSourceSync`]. +/// +/// For an asynchronous version of this trait, see [`WalletSource`]. +// Note that updates to documentation on this trait should be copied to the asynchronous version. +pub trait WalletSourceSync { + /// Returns all UTXOs, with at least 1 confirmation each, that are available to spend. + fn list_confirmed_utxos(&self) -> Result<Vec<Utxo>, ()>; + + /// Returns the previous transaction containing the UTXO referenced by the outpoint. + fn get_prevtx(&self, outpoint: OutPoint) -> Result<Transaction, ()>; + + /// Returns a script to use for change above dust resulting from a successful coin selection + /// attempt. + fn get_change_script(&self) -> Result<ScriptBuf, ()>; + + /// Signs and provides the full [`TxIn::script_sig`] and [`TxIn::witness`] for all inputs within + /// the transaction known to the wallet (i.e., any provided via + /// [`WalletSource::list_confirmed_utxos`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + /// + /// [`TxIn::script_sig`]: bitcoin::TxIn::script_sig + /// [`TxIn::witness`]: bitcoin::TxIn::witness + fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()>; +} + +struct WalletSourceSyncWrapper<T: Deref>(T) +where + T::Target: WalletSourceSync; + +// Implement `Deref` directly on WalletSourceSyncWrapper so that it can be used directly +// below, rather than via a wrapper. +impl<T: Deref> Deref for WalletSourceSyncWrapper<T> +where + T::Target: WalletSourceSync, +{ + type Target = Self; + fn deref(&self) -> &Self { + self + } +} + +impl<T: Deref> WalletSource for WalletSourceSyncWrapper<T> +where + T::Target: WalletSourceSync, +{ + fn list_confirmed_utxos<'a>( + &'a self, + ) -> impl Future<Output = Result<Vec<Utxo>, ()>> + MaybeSend + 'a { + let utxos = self.0.list_confirmed_utxos(); + async move { utxos } + } + + fn get_prevtx<'a>( + &'a self, outpoint: OutPoint, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { + let prevtx = self.0.get_prevtx(outpoint); + Box::pin(async move { prevtx }) + } + + fn get_change_script<'a>( + &'a self, + ) -> impl Future<Output = Result<ScriptBuf, ()>> + MaybeSend + 'a { + let script = self.0.get_change_script(); + async move { script } + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { + let signed_psbt = self.0.sign_psbt(psbt); + async move { signed_psbt } + } +} + +/// A wrapper over [`WalletSourceSync`] that implements [`CoinSelectionSourceSync`] by preferring +/// UTXOs that would avoid conflicting double spends. If not enough UTXOs are available to do so, +/// conflicting double spends may happen. +/// +/// For an asynchronous version of this wrapper, see [`Wallet`]. +// Note that updates to documentation on this struct should be copied to the asynchronous version. +pub struct WalletSync<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> +where + W::Target: WalletSourceSync + MaybeSend, +{ + wallet: Wallet<WalletSourceSyncWrapper<W>, L>, +} + +impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> WalletSync<W, L> +where + W::Target: WalletSourceSync + MaybeSend, +{ + /// Constructs a new [`WalletSync`] instance. + pub fn new(source: W, logger: L) -> Self { + Self { wallet: Wallet::new(WalletSourceSyncWrapper(source), logger) } + } +} + +impl<W: Deref + MaybeSync + MaybeSend, L: Logger + MaybeSync + MaybeSend> CoinSelectionSourceSync + for WalletSync<W, L> +where + W::Target: WalletSourceSync + MaybeSend + MaybeSync, +{ + fn select_confirmed_utxos( + &self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &[TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> Result<CoinSelection, ()> { + let fut = self.wallet.select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ); + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match pin!(fut).poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + unreachable!( + "Wallet::select_confirmed_utxos should not be pending in a sync context" + ); + }, + } + } + + fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()> { + let fut = self.wallet.sign_psbt(psbt); + let mut waker = dummy_waker(); + let mut ctx = task::Context::from_waker(&mut waker); + match pin!(fut).poll(&mut ctx) { + task::Poll::Ready(result) => result, + task::Poll::Pending => { + unreachable!("Wallet::sign_psbt should not be pending in a sync context"); + }, + } + } +} + +/// An abstraction over a bitcoin wallet that can perform coin selection over a set of UTXOs and can +/// sign for them. The coin selection method aims to mimic Bitcoin Core's `fundrawtransaction` RPC, +/// which most wallets should be able to satisfy. Otherwise, consider implementing +/// [`WalletSourceSync`], which can provide a default implementation of this trait when used with +/// [`WalletSync`]. +/// +/// For an asynchronous version of this trait, see [`CoinSelectionSource`]. +// Note that updates to documentation on this trait should be copied to the asynchronous version. +pub trait CoinSelectionSourceSync { + /// Performs coin selection of a set of UTXOs, with at least 1 confirmation each, that are + /// available to spend. Implementations are free to pick their coin selection algorithm of + /// choice, as long as the following requirements are met: + /// + /// 1. `must_spend` contains a set of [`Input`]s that must be included in the transaction + /// throughout coin selection, but must not be returned as part of the result. + /// 2. `must_pay_to` contains a set of [`TxOut`]s that must be included in the transaction + /// throughout coin selection. In some cases, like when funding an anchor transaction, this + /// set is empty. Implementations should ensure they handle this correctly on their end, + /// e.g., Bitcoin Core's `fundrawtransaction` RPC requires at least one output to be + /// provided, in which case a zero-value empty OP_RETURN output can be used instead. + /// 3. Enough inputs must be selected/contributed for the resulting transaction (including the + /// inputs and outputs noted above) to meet `target_feerate_sat_per_1000_weight`. + /// 4. The final transaction must have a weight smaller than `max_tx_weight`; if this + /// constraint can't be met, return an `Err`. In the case of counterparty-signed HTLC + /// transactions, we will remove a chunk of HTLCs and try your algorithm again. As for + /// anchor transactions, we will try your coin selection again with the same input-output + /// set when you call [`ChannelMonitor::rebroadcast_pending_claims`], as anchor transactions + /// cannot be downsized. + /// + /// Implementations must take note that [`Input::satisfaction_weight`] only tracks the weight of + /// the input's `script_sig` and `witness`. Some wallets, like Bitcoin Core's, may require + /// providing the full input weight. Failing to do so may lead to underestimating fee bumps and + /// delaying block inclusion. + /// + /// The `claim_id` must map to the set of external UTXOs assigned to the claim, such that they + /// can be re-used within new fee-bumped iterations of the original claiming transaction, + /// ensuring that claims don't double spend each other. If a specific `claim_id` has never had a + /// transaction associated with it, and all of the available UTXOs have already been assigned to + /// other claims, implementations must be willing to double spend their UTXOs. The choice of + /// which UTXOs to double spend is left to the implementation, but it must strive to keep the + /// set of other claims being double spent to a minimum. + /// + /// [`ChannelMonitor::rebroadcast_pending_claims`]: crate::chain::channelmonitor::ChannelMonitor::rebroadcast_pending_claims + fn select_confirmed_utxos( + &self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &[TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> Result<CoinSelection, ()>; + + /// Signs and provides the full witness for all inputs within the transaction known to the + /// trait (i.e., any provided via [`CoinSelectionSourceSync::select_confirmed_utxos`]). + /// + /// If your wallet does not support signing PSBTs you can call `psbt.extract_tx()` to get the + /// unsigned transaction and then sign it with your wallet. + fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()>; +} + +impl<C: Deref> CoinSelectionSourceSync for C +where + C::Target: CoinSelectionSourceSync, +{ + fn select_confirmed_utxos( + &self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &[TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> Result<CoinSelection, ()> { + self.deref().select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ) + } + fn sign_psbt(&self, psbt: Psbt) -> Result<Transaction, ()> { + self.deref().sign_psbt(psbt) + } +} + +pub(crate) struct CoinSelectionSourceSyncWrapper<T: CoinSelectionSourceSync>(pub(crate) T); + +impl<T: CoinSelectionSourceSync> CoinSelectionSource for CoinSelectionSourceSyncWrapper<T> { + fn select_confirmed_utxos<'a>( + &'a self, claim_id: Option<ClaimId>, must_spend: Vec<Input>, must_pay_to: &'a [TxOut], + target_feerate_sat_per_1000_weight: u32, max_tx_weight: u64, + ) -> impl Future<Output = Result<CoinSelection, ()>> + MaybeSend + 'a { + let coins = self.0.select_confirmed_utxos( + claim_id, + must_spend, + must_pay_to, + target_feerate_sat_per_1000_weight, + max_tx_weight, + ); + async move { coins } + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> impl Future<Output = Result<Transaction, ()>> + MaybeSend + 'a { + let psbt = self.0.sign_psbt(psbt); + async move { psbt } + } +} diff --git a/pending_changelog/3137-accept-dual-funding-without-contributing.txt b/pending_changelog/3137-accept-dual-funding-without-contributing.txt index 9ea8de24e54..5e1d0de2d86 100644 --- a/pending_changelog/3137-accept-dual-funding-without-contributing.txt +++ b/pending_changelog/3137-accept-dual-funding-without-contributing.txt @@ -7,9 +7,8 @@ differentiate between an inbound request for a dual-funded (V2) or non-dual-funded (V1) channel to be opened, with value being either of the enum variants `InboundChannelFunds::DualFunded` and `InboundChannelFunds::PushMsat(u64)` corresponding to V2 and V1 channel open requests respectively. - * If `manually_accept_inbound_channels` is false, then V2 channels will be accepted automatically; the - same behaviour as V1 channels. Otherwise, `ChannelManager::accept_inbound_channel()` can also be used - to manually accept an inbound V2 channel. + * Similar to V1 channels, `ChannelManager::accept_inbound_channel()` can also be used + to accept an inbound V2 channel. * 0conf dual-funded channels are not supported. * RBF of dual-funded channel funding transactions is not supported. diff --git a/pending_changelog/4304.txt b/pending_changelog/4304.txt new file mode 100644 index 00000000000..8c1580a2f4c --- /dev/null +++ b/pending_changelog/4304.txt @@ -0,0 +1,3 @@ +## Backwards Compatibility + +* Downgrade is not possible while the node has in-flight trampoline forwards. diff --git a/pending_changelog/4373.txt b/pending_changelog/4373.txt new file mode 100644 index 00000000000..e606063f93c --- /dev/null +++ b/pending_changelog/4373.txt @@ -0,0 +1,4 @@ +## Backwards Compat + * Setting `OptionalBolt11PaymentParams::declared_total_mpp_value_override` or + `RecipientOnionFields::total_mpp_amount_msat` for a payment will break + downgrade to 0.2 until the payment completes. diff --git a/pending_changelog/4388-splice-failed-discard-funding.txt b/pending_changelog/4388-splice-failed-discard-funding.txt new file mode 100644 index 00000000000..67680f49cb1 --- /dev/null +++ b/pending_changelog/4388-splice-failed-discard-funding.txt @@ -0,0 +1,21 @@ +# API Updates + + * `Event::SpliceNegotiationFailed` no longer carries `contributed_inputs` or `contributed_outputs` fields. + Instead, a separate `Event::DiscardFunding` event with `FundingInfo::Contribution` is emitted + for UTXO cleanup. + + * `Event::DiscardFunding` with `FundingInfo::Contribution` is also emitted without a + corresponding `Event::SpliceNegotiationFailed` when `ChannelManager::funding_contributed` returns an + error (e.g., channel or peer not found, wrong channel state, duplicate contribution). + +# Backwards Compatibility + + * Older serializations that included `contributed_inputs` and `contributed_outputs` in + `SpliceNegotiationFailed` will have those fields silently ignored on deserialization (they were odd TLV + fields). A `DiscardFunding` event will not be produced when reading these older serializations. + +# Forward Compatibility + + * Downgrading will not set the removed `contributed_inputs`/`contributed_outputs` fields on + `SpliceNegotiationFailed`, so older code expecting those fields will see empty vectors for splice + failures. diff --git a/pending_changelog/4514-splice-negotiation-failed.txt b/pending_changelog/4514-splice-negotiation-failed.txt new file mode 100644 index 00000000000..809bf7cb86d --- /dev/null +++ b/pending_changelog/4514-splice-negotiation-failed.txt @@ -0,0 +1,11 @@ +# API Updates + + * `Event::SplicePending` has been renamed to `Event::SpliceNegotiated`. + + * `Event::SpliceFailed` has been renamed to `Event::SpliceNegotiationFailed`. + + * `Event::SpliceNegotiationFailed` now includes a `reason` field + (`NegotiationFailureReason`) indicating why the negotiation round failed, + and a `contribution` field returning the `FundingContribution` for retry. + + * `FundingContribution` now exposes `feerate()` and `inputs()` accessor methods. diff --git a/pending_changelog/4656.txt b/pending_changelog/4656.txt new file mode 100644 index 00000000000..b5d5400d9a4 --- /dev/null +++ b/pending_changelog/4656.txt @@ -0,0 +1,2 @@ +## API Updates +* The `LSPS2ServiceHandler` now expects LDK's `ChannelClosed` events to be forwarded to the new `channel_closed` method. (#4656) diff --git a/pending_changelog/4687-pending-splice-details.txt b/pending_changelog/4687-pending-splice-details.txt new file mode 100644 index 00000000000..7b1ea2ca8ef --- /dev/null +++ b/pending_changelog/4687-pending-splice-details.txt @@ -0,0 +1,18 @@ +# API Updates + + * `ChannelDetails` now has a `splice_details` field + (`Option<SpliceDetails>`) reporting any pending splice attempts on a channel. + Each splice or RBF round is reported as a `SpliceCandidateDetails` in + `SpliceDetails::candidates`, with the stage it has reached given by + `SpliceCandidateStatus` (spanning a contribution committed via + `ChannelManager::funding_contributed` but not yet negotiating, the in-flight + negotiation, and a negotiated candidate awaiting confirmation). `SpliceDetails` + also reports the confirmed candidate's progress (`ConfirmedSpliceCandidate`) + and the txid of any `splice_locked` received from the counterparty. + +# Backwards Compatibility + + * A pending splice negotiated before upgrading from a prior LDK version (e.g. + 0.2) cannot be RBF'd, as older versions persisted neither its feerate nor our + contribution. Splicing such a channel instead queues a new splice that begins + once the inherited splice locks. diff --git a/pending_changelog/4833-delayed-payment-max-witness-length.txt b/pending_changelog/4833-delayed-payment-max-witness-length.txt new file mode 100644 index 00000000000..e97f07f68be --- /dev/null +++ b/pending_changelog/4833-delayed-payment-max-witness-length.txt @@ -0,0 +1,5 @@ +# API Updates + * `DelayedPaymentOutputDescriptor::MAX_WITNESS_LENGTH` was removed in favor of + the new `DelayedPaymentOutputDescriptor::max_witness_length` method, which + returns a tighter witness weight by accounting for the descriptor's + `to_self_delay` (#4833). diff --git a/pending_changelog/matt-commit-to-metadata.txt b/pending_changelog/matt-commit-to-metadata.txt new file mode 100644 index 00000000000..5e13e134f88 --- /dev/null +++ b/pending_changelog/matt-commit-to-metadata.txt @@ -0,0 +1,6 @@ +# Backwards compat + * Payment metadata is now committed to in the HMAC used to build payment secrets. + As such, any existing BOLT 11 invoices issued with payment metadata will be + implicitly invalidated on upgrade and any BOLT 11 invoices issued with payment + metadata will be invalidated on downgrade. If this is problematic for you + please reach out. diff --git a/pending_changelog/route-params-required.txt b/pending_changelog/route-params-required.txt new file mode 100644 index 00000000000..ea47deb1ad6 --- /dev/null +++ b/pending_changelog/route-params-required.txt @@ -0,0 +1,4 @@ +# Backwards Compatibility + * `Route`s serialized by LDK versions prior to 0.0.117 can no longer be + deserialized, as parts of the now-required `Route::route_params` were not + written by those versions. diff --git a/possiblyrandom/src/lib.rs b/possiblyrandom/src/lib.rs index 9cbbad7f13d..f27788d03fa 100644 --- a/possiblyrandom/src/lib.rs +++ b/possiblyrandom/src/lib.rs @@ -20,16 +20,13 @@ #![no_std] -#[cfg(feature = "getrandom")] +#[cfg(any(feature = "getrandom", not(any(target_os = "unknown", target_os = "none"))))] extern crate getrandom; /// Possibly fills `dest` with random data. May fill it with zeros. #[inline] pub fn getpossiblyrandom(dest: &mut [u8]) { - #[cfg(feature = "getrandom")] - if getrandom::getrandom(dest).is_err() { - dest.fill(0); - } - #[cfg(not(feature = "getrandom"))] dest.fill(0); + #[cfg(any(feature = "getrandom", not(any(target_os = "unknown", target_os = "none"))))] + let _ = getrandom::getrandom(dest); }