feat(bond): Phase 7 — maker timeout slash#775
Conversation
…skew PR #753 removed the BITCOIN_PRICES static when it migrated price reads to the PriceManager, while PR #770 (merged independently) added BitcoinPriceManager::set_price_for_test writing to that static — the combination doesn't compile under cargo test on main. Re-point the test seam at a small cfg(test) override map consulted by price::get_bitcoin_price before the global manager, so unit tests keep seeding deterministic prices without installing the global PriceManager (whose OnceLock would leak one test's configuration into the rest of the binary). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fill the maker-responsible rows of the §9.2 responsibility table: a waiting-state timeout now slashes the maker's bond when the maker is the responsible party, gated per posting role (apply_to must cover the responsible side — taker since Phase 4, maker since Phase 7). - slash_or_release_on_timeout resolves the responsible bond through the range-aware resolve_slash_target (the maker bond of a range order lives on the range root), and gates the slash on applies_to_maker() / applies_to_taker() per the responsible side. - A maker-responsible timeout on a range order goes through the Phase 6 partial-slash path: record_maker_slice_slash inserts a proportional child row with reason=Timeout and the parent HTLC stays Locked; the single settle happens at range close. The helper now returns whether it inserted, so the BondSlashed notice fires exactly once (a scheduler retry that finds the child already recorded reports None). - The scheduler's terminal cancel branch now runs resolve_range_maker_bond_at_close_or_warn after the Canceled status persists, so a slashed range settles and distributes promptly instead of waiting for the 5-minute reconciliation sweep (which remains the backstop). The republish branch never closes — the maker stays committed there. - The timeout release loop retains a range maker parent bond (resolved only at range close), alongside the existing republish carve-out. Tests mirror Phase 4 from the maker side: non-range sell/buy slashes, the per-role apply_to gate, and the range path (proportional child, parent stays Locked, root resolution from a descendant slice, per-slice idempotency without re-notification). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 23 minutes and 51 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
✨ 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 |
…ase 4.5 row Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b634a5cead
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Tested :
Tested all 9 points manually and everything worked fine for me! |
Anti-Abuse Bond — Phase 7: maker timeout slash
Implements spec §12 (issue #711): timeout slash for the maker bond. Symmetric to Phase 4 — no new mechanism, the
job_cancel_ordersdispatch already exists; theapply_togate widens to the maker side and the bond lookup becomes range-aware.Gate:
enabled && slash_on_waiting_timeout && apply_to ∈ { make, both }(checked against the responsible party's posting role, so a leftover maker bond underapply_to = takestill releases, never slashes).Daemon-only: no
mostro-corechange, no migration, no new protocol variant (reusesAction::BondSlashedfrom Phase 4 and the Phase 6 child-slash schema).enabled = falsekeeps today's behaviour bit-for-bit.What changed
src/app/bond/slash.rs—slash_or_release_on_timeoutapply_tomust cover that role.resolve_slash_target(the Phase 2/6 resolver), which adds the range-root fallback: a range order's maker bond lives on the range root, found by walkingrange_parent_id.slash_oneprimitive, confirmed through the durableslashed_reason = Timeoutwitness.record_maker_slice_slashinserts a proportional child row (PendingPayout,reason = Timeout) and the parent HTLC staysLocked; the single settle happens at range close. The reported bond is the child row, so theBondSlashednotice carries the slice's slashed amount, not the whole parent bond.record_maker_slice_slashnow returns whether it actually inserted the child. The timeout path keys the one-shotBondSlashednotice on that, so a scheduler retry (order-persist failed, next tick re-runs) never re-notifies the maker — the same no-duplicate-notice guarantee Phase 4 gets from theLockedre-check, which a range parent (deliberately stillLocked) can't provide.src/scheduler.rs—job_cancel_ordersCanceledstatus persists, the cancel branch now runsresolve_range_maker_bond_at_close_or_warn: a maker-responsible timeout terminates the range (no remainder is spawned on a cancel), so the parent settles and distributes (per-slice counterparty shares + maker refund) promptly instead of waiting for the 5-minute reconciliation sweep — which remains the backstop on transient failure. The close helper is idempotent and a cheap no-op for non-range / already-resolved bonds. The republish branch never closes: the order returns to the book with the maker still committed.Phase 4 invariants preserved
How to test
Automated
Key new unit tests (
src/app/bond/slash.rs, mirroring Phase 4 from the maker side):timeout_maker_responsible_slashes_maker_bond_sell_order— sell /WaitingPayment(seller = maker silent): maker bond →pending-payout,reason = timeout, HTLC settled inline exactly once.timeout_maker_responsible_buy_order_slashes_maker_and_releases_taker— buy /WaitingBuyerInvoicemirror underapply_to = both: maker slashed, the non-responsible taker's bond released, only one settle call.timeout_maker_slash_skipped_when_apply_to_take_only— per-role gate: a leftoverLockedmaker bond underapply_to = takereleases, never slashes.timeout_range_maker_slash_records_child_and_keeps_parent_locked— range: proportional child recorded (40/100 × 1000 = 400,reason = timeout), parent staysLockedwithslashed_share_sats = 400, no settle call, taker bond released.timeout_range_maker_slash_resolves_root_bond_from_descendant_slice— therange_parent_idwalk finds the root's maker bond from a remainder slice; child recorded against the slice's own order id.timeout_range_maker_slash_is_idempotent_per_slice— an already-slashed slice reportsNone: no duplicate child row, no re-notification.Manual (regtest / Polar)
Enable the feature in
settings.toml— the maker side must be bonded and the timeout slash armed:Handy inspection queries:
Action::PayBondInvoice→ order publishes). A taker takes it; the order enterswaiting-paymentand the maker (seller) never pays the trade hold invoice. Whenexpiration_secondselapses: the order is cancelled outright (not republished), the maker bond flips tostate = pending-payout, slashed_reason = timeout(HTLC settled — check Mostro's wallet), the maker receivesAction::BondSlashed(amount = the bond) plus the order'sAction::Canceled, and the taker (winner) receivesAction::AddBondInvoicefor the counterparty share. Reply with a bolt11 →BondInvoiceAcceptedthenBondPayoutCompleted.waiting-buyer-invoiceand the maker (buyer) never adds the invoice. Same outcome as step 1: cancel + maker bond slashed withtimeout.apply_to = "make", the taker (buyer) goes silent inwaiting-buyer-invoice. The order is republished to the book and the maker bond stayslocked(it follows the order, not the failed take). Nothing is slashed — underapply_to = makethe taker posted no bond. Withapply_to = "both"the same scenario slashes the taker's bond (Phase 4) and still retains the maker's.expiration_secondselapses → the maker bond is released, never slashed.Locked, close settles once. Maker creates a range sell order (e.g. min 10 / max 50 USD), bond locked (sized againstmax_amount). A taker takes a slice (e.g. 20 USD) and the maker goes silent inwaiting-payment. On timeout, in one tick:parent_bond_idset,child_order_id= the slice,amount_sats = round(bond × slice_fiat / max_fiat),state = pending-payout,slashed_reason = timeout;Action::BondSlashedwith the child's (proportional) amount, not the whole bond;state = slashed(HTLC settled once), a refund row appears (child_order_idNULL, recipient = the maker);Action::AddBondInvoicefor their counterparty share and the maker gets a separateAction::AddBondInvoicefor the unslashed remainder.settle(1000) − winner 200 − refund 600 = 200. Child + refund rows sum to the parentamount_sats.UNIQUE (parent_bond_id, child_order_id)) andAction::BondSlashedis sent exactly once.apply_to = "take"and repeat step 1 (the maker now posts no bond): the timeout cancels the order with nothing slashed — old behaviour. Converselyslash_on_waiting_timeout = falsereleases every bond on timeout even withapply_to = "make".enabled = false→ no bond messages, timeouts cancel/republish exactly as before this PR.🤖 Generated with Claude Code