fix: validate the dispute initiator before refunding the escrow (#805)#825
fix: validate the dispute initiator before refunding the escrow (#805)#825grunch wants to merge 1 commit into
Conversation
`admin_cancel_action` called `cancel_hold_invoice` — an irreversible refund of the seller's escrow — before resolving the dispute initiator, and that resolution can reject the request with `DisputeEventError` when neither `seller_dispute` nor `buyer_dispute` is set (or both are). On that path the seller was already refunded while the order stayed in `Dispute` and the status transition to `CanceledByAdmin` never ran, leaving the Lightning side and the DB permanently disagreeing with no way back. Move the initiator resolution above the refund so every check that can reject the call runs before the escrow is touched. The valid path is unchanged. Regression test: an order with a hold-invoice hash and no initiator flag now fails with `DisputeEventError` and is left in `Dispute`. Before the fix it returned `LnNodeError` — proof the refund RPC had already been dispatched. Closes #805
|
Warning Review limit reached
Next review available in: 10 seconds Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Strict review completed on the current head.
The change fixes the funds-safety ordering bug without altering the valid cancellation path: malformed/ambiguous initiator flags are now rejected before the irreversible hold-invoice cancellation, while the existing valid-hash test still proves that a legitimate request reaches the LND cancellation seam. The new regression test is a real before/after discriminator because the old ordering returns LnNodeError against the dead-LND harness before it can produce DisputeEventError.
I also checked authorization, dispute-status and bond-resolution ordering, the seller/buyer initiator mapping used in the kind-38386 dispute event, retry behavior in the malformed-state path, and the tracked CAS/atomicity concerns outside this PR's scope. No blocking regression found.
CI is green for build, tests, fmt, clippy, and the Rust 1.94.0 MSRV build.
| // and `cancel_hold_invoice` below is irreversible: running it first meant | ||
| // a rejected request could still refund the seller while leaving the order | ||
| // in `Dispute`, so the LN side and the DB disagreed with no way back. | ||
| // Every check that can reject the call now precedes the refund. |
There was a problem hiding this comment.
Non-blocking: Please scope this statement to the initiator validation. The handler still has fallible operations after the refund (new_dispute_event, update_order_event, DB updates, pubkey parsing, and DM delivery), so “Every check that can reject the call” is broader than the invariant this patch establishes and may imply atomicity that is still tracked separately in #810. “This initiator check now precedes the refund” would be precise.
Closes #805.
The bug
admin_cancel_actionrefunded the seller's escrow before it finished validating the request:When neither initiator flag is set (or both are), the handler returns
DisputeEventError— butcancel_hold_invoicehas already returned the funds to the seller. The order stays inDispute, the transition toCanceledByAdminnever runs, and the dispute row is never moved toSellerRefunded. The Lightning side and the DB disagree permanently, and the refund cannot be undone.The fix
Move the initiator resolution above the refund, so every check that can reject the call runs before the escrow is touched. This is the same ordering discipline already applied to the status guards and the bond-resolution validation just above it (see the existing Phase 2 comment). The valid path — a legitimately flagged initiator — is unchanged.
Test
New regression test
dispute_without_initiator_flag_errors_before_refunding: an order with a hold-invoicehashand no initiator flag must fail withDisputeEventErrorand be left inDispute.The test is a genuine before/after discriminator thanks to the existing
dead_lnd()harness (a realLndConnectoragainst a dead endpoint, so any RPC fails fast):Err(MostroInternalErr(LnNodeError("code=Unknown message=client error (Connect)")))— proof the refund RPC had already been dispatched.DisputeEventErrorwithout ever reaching LND.dispute_with_hash_reaches_ln_cancel_seamstill passes, confirming the legitimate refund path is untouched.Verification
cargo test— 1016 passed, 0 failedcargo clippy --all-targets -- -D warnings— cleancargo fmt --all— cleanNote
admin_settle.rshas related ordering/CAS concerns tracked separately in #809 and #810; this PR deliberately stays scoped to #805.