Skip to content

fix(wallet): treat unreachable relative locktimes as unsatisfied - #565

Open
trakshan-mishra wants to merge 1 commit into
bitcoindevkit:masterfrom
trakshan-mishra:fix/older-satisfier-height-overflow
Open

trakshan-mishra wants to merge 1 commit into
bitcoindevkit:masterfrom
trakshan-mishra:fix/older-satisfier-height-overflow

Conversation

@trakshan-mishra

Copy link
Copy Markdown

Fixes #557.

Problem

Older::check_older (src/wallet/utils.rs) computes the height at which a relative locktime is satisfied as create_height + n and unwraps the sum with expect("Overflowing addition"). Wallet::finalize_psbt maps an unconfirmed previous transaction to a create_height of u32::MAX, so that addition overflows and panics for any older(n) with n greater than zero.

The Older satisfier is only consulted when the input's nSequence does not already satisfy the CSV branch, so this is reachable through ordinary use: spending an unconfirmed UTXO of a descriptor that contains an older() branch which is not the one being used, e.g. spending via pk(A) of or_d(pk(A),and_v(v:pk(B),older(144))). Since Wallet::sign finalizes by default, callers reach it without opting in.

Fix

Return false when the satisfaction height does not fit in a u32, rather than panicking. Such a height can never be reached, so the branch is simply not satisfied — the same answer the comparison would give with wider integers. This matches the behaviour the issue asks for ("the older() branch should simply be treated as not satisfied").

The change is in the Satisfier impl, so it covers both call sites: finalization in src/wallet/mod.rs and policy extraction in src/descriptor/policy.rs.

After::check_after performs no addition and is unaffected.

Tests

  • test_finalize_psbt_with_unconfirmed_input_and_unused_csv_branch (tests/wallet.rs) reproduces the reported panic end to end. Verified to fail on master with panicked at src/wallet/utils.rs:117: Overflowing addition, and to pass with this change.
  • test_check_older_unreachable_satisfaction_height_is_not_satisfied covers the overflow directly at the unit level.
  • test_check_older_compares_against_the_satisfaction_height covers the ordinary satisfied and unsatisfied heights, which had no unit coverage before.

Checklist

  • All commits are GPG signed.
  • The PR description links to the issue it solves.
  • Ran the just pre-push steps locally: cargo +nightly fmt --all -- --check, both cargo check variants, clippy with -D warnings on the stable and unstable surfaces, cargo test --workspace on both surfaces, and cargo doc with -D warnings. All pass.
  • Tests reproducing the bug (and now passing) have been added.
  • No API change, so nothing breaking.

One note: cargo +nightly fmt on current master also wants to reformat src/descriptor/policy.rs, which is unrelated to this fix (and looks like the nightly drift tracked in #535), so I left that file untouched.

`Older::check_older` added the relative locktime to the previous
transaction's confirmation height and unwrapped the sum with
`expect("Overflowing addition")`. `Wallet::finalize_psbt` maps an
unconfirmed previous transaction to a confirmation height of `u32::MAX`,
so finalizing a PSBT that spends an unconfirmed UTXO of a descriptor
carrying an `older(n)` branch panicked for every `n` greater than zero.

The `Older` satisfier is only consulted when the input's `nSequence` does
not already satisfy the CSV branch, so this is reachable through ordinary
use: spending an unconfirmed output through a branch that is not the
`older()` one, which `Wallet::sign` finalizes by default.

Return `false` when the satisfaction height does not fit in a `u32`. Such
a height can never be reached, so the branch is simply not satisfied,
which is the same answer the comparison would give with wider integers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

finalize_psbt panics with "Overflowing addition" when spending an unconfirmed UTXO of a descriptor with an older() branch

1 participant