Repository navigation
fix(wallet): treat unreachable relative locktimes as unsatisfied - #565
trakshan-mishra wants to merge 3 commits into
Conversation
`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.
j-kon
left a comment
There was a problem hiding this comment.
ACK 7461a43
Verified:
- Confirmed panic reproduction on master at
src/wallet/utils.rs:117in both debug and release builds when finalizing an unconfirmed input in a descriptor with an unusedolder(n)branch. - Confirmed PR #565 cleanly resolves the panic and passes all unit/integration tests in both profiles.
- Verified arithmetic boundary transitions around
u32::MAX(create_height = u32::MAX - 1,u32::MAX - 100, etc.) and confirmed exact>=comparison matches BIP68 relative locktime semantics. - Verified that returning
falseon unreachable height correctly signals an unsatisfied condition to Miniscript satisfiers.
|
Thank you for the fix! I'm not a big fan of relying on the overflow to decide that the timelock isn't met yet. Wouldn't it be cleaner to get rid of the so |
|
Thanks, that makes sense. The One thing I ran into while looking at this: So I'd go with an enum: pub(crate) enum ConfirmationHeight {
Confirmed(u32),
Unconfirmed,
}
Does that shape work for you, or would you rather have a third |
`finalize_psbt` used `u32::MAX` as the confirmation height of an unconfirmed previous transaction, and `check_older` only returned `false` for it because adding the relative locktime overflowed. Add a `ConfirmationHeight` enum with `Confirmed(u32)` and `Unconfirmed` variants and use it for `Older::create_height`. `check_older` now returns `false` for `Unconfirmed` directly. `None` keeps its existing meaning: the previous transaction was not found, and the height is treated as 0. An overflowing `Confirmed` height still returns `false` instead of panicking.
|
I went ahead and pushed the enum change so you can see the code rather than the description: |
Thanks for pushing this as a separate commit. The enum approach looks cleaner and makes the unconfirmed state explicit while keeping None semantics separate. Since this replaces the commit I previously ACKed, I’ll re-review the bc886ff delta and rerun the relevant CSV/unconfirmed regression tests before updating my review. |
yan-pi
left a comment
There was a problem hiding this comment.
tACK bc886ff
I agree that the enum change is cleaner than using u32::MAX. It makes the unconfirmed case explicit while preserving the existing None behavior.
Could we add a unit test for create_height = None? The current tests cover confirmed, unconfirmed, and confirmed-height overflow, but not the not-found path.
IMO This is non-blocking.
`finalize_psbt` passes `None` when the previous transaction is not among the wallet's canonical transactions, and `check_older` then counts the relative locktime from height 0. Test both sides of that boundary.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #565 +/- ##
==========================================
- Coverage 81.91% 81.68% -0.23%
==========================================
Files 25 25
Lines 6535 6344 -191
Branches 302 302
==========================================
- Hits 5353 5182 -171
+ Misses 1075 1055 -20
Partials 107 107
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Fixes #557.
Problem
Older::check_older(src/wallet/utils.rs) computes the height at which a relative locktime is satisfied ascreate_height + nand unwraps the sum withexpect("Overflowing addition").Wallet::finalize_psbtmaps an unconfirmed previous transaction to acreate_heightofu32::MAX, so that addition overflows and panics for anyolder(n)withngreater than zero.The
Oldersatisfier is only consulted when the input'snSequencedoes not already satisfy the CSV branch, so this is reachable through ordinary use: spending an unconfirmed UTXO of a descriptor that contains anolder()branch which is not the one being used, e.g. spending viapk(A)ofor_d(pk(A),and_v(v:pk(B),older(144))). SinceWallet::signfinalizes by default, callers reach it without opting in.Fix
Return
falsewhen the satisfaction height does not fit in au32, 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 ("theolder()branch should simply be treated as not satisfied").The change is in the
Satisfierimpl, so it covers both call sites: finalization insrc/wallet/mod.rsand policy extraction insrc/descriptor/policy.rs.After::check_afterperforms 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 withpanicked at src/wallet/utils.rs:117: Overflowing addition, and to pass with this change.test_check_older_unreachable_satisfaction_height_is_not_satisfiedcovers the overflow directly at the unit level.test_check_older_compares_against_the_satisfaction_heightcovers the ordinary satisfied and unsatisfied heights, which had no unit coverage before.Checklist
just pre-pushsteps locally:cargo +nightly fmt --all -- --check, bothcargo checkvariants,clippywith-D warningson the stable and unstable surfaces,cargo test --workspaceon both surfaces, andcargo docwith-D warnings. All pass.One note:
cargo +nightly fmton current master also wants to reformatsrc/descriptor/policy.rs, which is unrelated to this fix (and looks like the nightly drift tracked in #535), so I left that file untouched.