Skip to content

feat(bond): Phase 4 — timeout slash for the taker bond#744

Merged
grunch merged 4 commits into
mainfrom
feat/bond-phase-4-timeout-slash
May 29, 2026
Merged

feat(bond): Phase 4 — timeout slash for the taker bond#744
grunch merged 4 commits into
mainfrom
feat/bond-phase-4-timeout-slash

Conversation

@grunch
Copy link
Copy Markdown
Member

@grunch grunch commented May 22, 2026

What

Implements Phase 4 of the anti-abuse bond (docs/ANTI_ABUSE_BOND.md §9, issue #711): when a waiting-state timeout actually elapses, the responsible party's bond is slashed instead of always released.

Gate flags

enabled && slash_on_waiting_timeout && apply_to ∈ { take, both }. With any of these off, behaviour is identical to today (bonds released).

How

  • bond::slash_or_release_on_timeout (src/app/bond/slash.rs): derives the responsible side from the waiting state alone (§9.2: WaitingBuyerInvoice → buyer, WaitingPayment → seller). When armed and the responsible party holds a Locked bond, it reuses the Phase 2 apply_bond_resolution primitive with BondSlashReason::Timeout — settling the bond HTLC + CAS to PendingPayout and releasing every other bond. Otherwise it falls back to releasing all bonds (Phase 1 behaviour), which also drains stray bonds left from a previously-enabled period.
  • The §3.1 buyer/seller → bond-row mapping is the one already baked into the reused primitive; bond.pubkey equals the order's buyer_pubkey/seller_pubkey, so under apply_to = take only the taker side ever resolves a bond and the maker-responsible §9.2 rows fall through to release.
  • The bond config is passed in (the scheduler hands it Settings::get_bond()) instead of read from the global OnceLock, so the gate is unit-testable.
  • Wired into scheduler::job_cancel_orders, replacing the unconditional Phase 1 release in the persist-success branch. The pre-cancel order snapshot (waiting status + trade pubkeys intact) is passed so the resolution matches the bonded party.
  • bond::notify_bond_slashed sends the slashed user an Action::BondSlashed (mostro-core 0.11.5) forfeiture notice, carrying Payload::Order (amount = slashed bond amount). Best-effort, and sent only after the slash is confirmed to have landed — confirmed via the durable slashed_reason = Timeout metadata (written atomically with the slash CAS and never cleared by the concurrent Phase 3 payout job), not a transient state = PendingPayout check the scheduler could invalidate. A transient settle_hold_invoice failure leaves the bond Locked with no slash metadata, so it never produces a false "your bond was slashed" message.

Safety invariants

  • §9.1 honoured: only the elapsed-timeout scheduler path can slash. Cooperative / unilateral / admin cancels still go through the Phase 1 release_bonds_for_order_or_warn helpers, untouched (verified: single non-test call site).
  • No mis-slash: a counterparty going silent never costs the other party their bond — the responsible-side resolution is keyed on the waiting state, and the maker-responsible rows release rather than slash (covered by tests).
  • No false forfeiture notice: the notice is gated on a re-read of the durable slashed_reason = Timeout metadata, so a transient settle failure (bond stays Locked) or a concurrent payout-job transition (PendingPayout → Slashed) can neither suppress a true notice nor fabricate a false one.

mostro-core dependency

Bumps the pin 0.11.4 → 0.11.5, which adds the additive Action::BondSlashed (released upstream in mostro-core#149). Serde-additive: clients that don't know the variant ignore it and fall back to the Action::Canceled they already receive for the order. The spec (§9.3, §14.3, phase table) is updated to reflect this — earlier drafts assumed Phase 4 was daemon-only.

Tests

cargo test (337 passed), cargo clippy --all-targets --all-features clean, cargo fmt clean. 10 new unit tests in bond::slash:

  • all four §9.2 worked rows (sell+WaitingBuyerInvoice→slash, buy+WaitingPayment→slash, sell+WaitingPayment→no slash/maker, buy+WaitingBuyerInvoice→no slash/maker);
  • slash_on_waiting_timeout = false → release, no slash;
  • apply_to = make → no taker slash;
  • no [anti_abuse_bond] config → release;
  • transient settle failure → bond stays Locked, no notice;
  • timeout-slash confirmation survives the concurrent payout-job progression (all post-slash states with slashed_reason = timeout confirm; Locked/no-metadata and LostDispute do not);
  • Action::BondSlashed targets the slashed taker only.

How to test (manual regtest walkthrough)

These scenarios exercise Phase 4 end-to-end against polar/regtest LND.
The cast:

  • User A — the maker. Posts a sell-order in S1, S3–S6; a buy-order in S2.
  • User B — the taker. Posts the anti-abuse bond (apply_to = "take").

No solver is involved: Phase 4 slashes are automatic on timeout, not
solver-directed (those are Phase 2). The recipient payout reuses the
Phase 3 machinery unchanged.

Common setup before every scenario:

  1. In settings.toml, shrink the waiting-state timeout and enable the
    bond on the taker side:
    [mostro]
    # Waiting-state deadline (default 900 = 15 min). Shrink it so the
    # scheduler fires in ~1–2 min instead of 15.
    expiration_seconds = 60
    
    [anti_abuse_bond]
    enabled = true
    apply_to = "take"
    slash_on_waiting_timeout = true      # the Phase 4 gate
    slash_node_share_pct = 0.5
    payout_invoice_window_seconds = 60   # Phase 3 payout; shrink for faster runs
    payout_max_retries = 3
    payout_claim_window_days = 1
  2. Start mostrod with RUST_LOG=info,mostro=debug. Watch for
    Bond slashed on waiting-state timeout (the Phase 4 slash) and the
    bond payout: prefix (the Phase 3 drain).
  3. sqlite3 mostro.db "select id, state, slashed_reason, node_share_sats, payout_invoice from bonds;"
    is the source of truth per row.

Timing note: an order becomes eligible for the timeout once
taken_at + expiration_seconds has passed, and job_cancel_orders ticks
every 60 s — so allow up to expiration_seconds + 60 s after the order
parks in its waiting state.

S1 — Buyer silent past WaitingBuyerInvoice (sell-order): slash taker, republish, pay seller

The main happy-path slash + Phase 3 payout. §9.2 row
sell / WaitingBuyerInvoice → buyer = taker → slash.

  1. A posts a sell-order for, say, 100 000 sats.
  2. B takes it without including a payout invoice. The bond bolt11
    arrives as Action::PayBondInvoice; B pays it. Confirm the bond row
    goes requested → locked and the order moves to waiting-buyer-invoice.
  3. B goes silent — never sends the payout invoice. Wait
    expiration_seconds + up to one 60 s tick.
  4. On the tick, confirm the log line Bond slashed on waiting-state timeout. DB: bond row state = pending-payout, slashed_reason = timeout, node_share_sats = 500 (50 % of a 1 000 sat bond at these
    defaults), slashed_at set. The bond HTLC is settled at slash
    time
    , so Mostro's wallet is already up by the bond amount.
  5. B (the slashed buyer/taker) receives two messages:
    Action::Canceled (the order) and Action::BondSlashed (the
    forfeiture notice; amount = the slashed bond amount). The order is
    republished to A as Action::NewOrder and returns to pending
    (still takeable by others).
  6. Phase 3 takes over (reuse): the non-slashed counterparty for
    WaitingBuyerInvoice is the seller = A. Within ~60 s A
    receives an Action::AddBondInvoice for 500 sats. A replies with a
    bolt11 → send_payment → bond row state = slashed. Confirm A
    actually received 500 sats.

S2 — Seller silent past WaitingPayment (buy-order): slash taker, pay buyer

§9.2 row buy / WaitingPayment → seller = taker → slash. Confirms
recipient resolution lands on the buyer, not hard-coded for sell-orders.

  1. A posts a buy-order (maker = buyer; taker will be seller).
  2. B takes it. B pays the bond (Action::PayBondInvoice) → bond
    locks → order moves to waiting-payment and B receives the trade
    hold invoice (Action::PayInvoice). B pays the bond but not
    the trade hold invoice.
  3. B goes silent. Wait for the timeout + tick.
  4. Confirm bond → pending-payout / timeout; B receives
    Action::Canceled + Action::BondSlashed; the order republishes to
    pending.
  5. Phase 3: the recipient for WaitingPayment is the buyer = A.
    Confirm A — not B — receives Action::AddBondInvoice, and the
    payout completes as in S1.

S3 — Cancel before the timeout never slashes (attack invariant, §9.1)

  1. A posts a sell-order; B takes it (no invoice), pays the bond →
    waiting-buyer-invoice.
  2. Before expiration_seconds elapses, one party cancels (e.g. B
    initiates a cooperative cancel and A co-signs).
  3. Confirm: the bond row → released (HTLC cancelled, funds back to B).
    No slashed_reason, no Action::BondSlashed, no
    Action::AddBondInvoice. This is the anti-theft invariant — a
    counterparty cannot cancel at minute N−1 to steal a bond.

S4 — Responsible party is the maker → no slash

§9.2 "no slash" row. The taker holds a bond but is not the responsible
party.

  1. A posts a sell-order. B takes it with a payout invoice, so
    the order skips waiting-buyer-invoice and parks at waiting-payment,
    awaiting the seller = A = maker to pay the trade hold invoice. B
    pays the bond → locks.
  2. A (the seller/maker) never pays the hold invoice. Wait for the
    timeout + tick.
  3. Responsible party = seller = maker, who holds no bond under
    apply_to = take. Confirm: no slash — B's bond is released,
    the order is canceled ((WaitingPayment, Sell)Canceled), and
    B receives no Action::BondSlashed.
    • Buy-order mirror: a buy-order stuck in waiting-buyer-invoice
      (buyer = maker responsible) behaves identically — no slash, order
      canceled.

S5 — slash_on_waiting_timeout = false → no slash

  1. Set slash_on_waiting_timeout = false; restart mostrod.
  2. Repeat S1 steps 1–3 (B silent at waiting-buyer-invoice).
  3. On the timeout tick, confirm the bond is released, not slashed:
    no slashed_reason, no Action::BondSlashed, no
    Action::AddBondInvoice. The gate is off, so Phase 4 falls back to
    the Phase 1 always-release behaviour.

S6 — slash_node_share_pct = 1.0: node-only slash still notifies (race regression)

Targets the concurrency fix: the Phase 3 payout job can flip a node-only
slash straight pending-payout → slashed within the confirmation
window, so the notice must key off the durable slashed_reason = timeout
metadata, not the transient state.

  1. Set slash_node_share_pct = 1.0; restart.
  2. Run S1 steps 1–4 (B silent → slash). Bond → pending-payout / timeout, node_share_sats = the full bond amount.
  3. The next Phase 3 tick flips the row directly to slashed
    (finalize_node_only), with no Action::AddBondInvoice and no
    send_payment (the node keeps everything).
  4. Confirm B still receives Action::BondSlashed, even though the
    row may already read slashed by the time you inspect it. (Before the
    fix, the notice would be silently dropped in this race.)

Quick sanity grep

sqlite3 mostro.db "select state, slashed_reason, count(*) from bonds group by state, slashed_reason;"

After S1, S2, S6 you should see rows in slashed | timeout; after S3,
S4, S5, rows in released with NULL slashed_reason. No timeout-slashed
row should be stuck in pending-payout once Phase 3 has drained it.

Refs: #711.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Orders stuck in waiting state can now trigger bond slashing on timeout when anti‑abuse bond settings apply
    • Slashed users receive a forfeiture notification showing the amount
  • Reliability

    • Scheduler runs timeout slash/release before cancelling orders, avoiding duplicate notifications and leaving orders eligible for retry on transient errors
  • Dependencies

    • Core runtime bumped to v0.11.5
  • Documentation

    • Anti‑abuse bond spec updated to document Phase 4 timeout‑slash rollout and notifications

Review Change Stack

Implements Phase 4 of the anti-abuse bond (docs/ANTI_ABUSE_BOND.md §9):
when a waiting-state timeout actually elapses, the responsible party's
bond is slashed instead of always released. Gated by
`enabled && slash_on_waiting_timeout && apply_to ∈ { take, both }`.

- `bond::slash_or_release_on_timeout` (src/app/bond/slash.rs): derives the
  responsible side from the waiting state (§9.2: WaitingBuyerInvoice →
  buyer, WaitingPayment → seller) and, when armed and the responsible
  party holds a Locked bond, reuses the Phase 2 `apply_bond_resolution`
  primitive with `BondSlashReason::Timeout` (settle the HTLC + CAS to
  PendingPayout, releasing every other bond). Otherwise it falls back to
  releasing all bonds, exactly as Phase 1 did — so feature-off and
  no-bond paths are unchanged, and stray bonds from a prior enabled
  period still drain. The §3.1 buyer/seller → bond mapping lives in the
  reused primitive; `bond.pubkey` equals the order's buyer/seller pubkey.
- The bond config is passed in (scheduler hands it `Settings::get_bond()`)
  rather than read from the global `OnceLock`, so the gate is
  unit-testable without mutating process-wide state.
- Wires the dispatch into `scheduler::job_cancel_orders`, replacing the
  unconditional Phase 1 release in the persist-success branch. The
  pre-cancel order snapshot (waiting status + trade pubkeys intact) is
  passed so the buyer/seller resolution matches the bonded party.
- `bond::notify_bond_slashed` sends the slashed user an
  `Action::BondSlashed` (mostro-core 0.11.5) forfeiture notice. It is
  best-effort and sent only after the slash is confirmed to have landed
  (row re-reads as PendingPayout), so a transient settle failure — which
  leaves the bond Locked for retry — never produces a false notice.

§9.1 invariant preserved: only the elapsed-timeout scheduler path can
slash; cooperative / unilateral / admin cancels still go through the
Phase 1 release helpers untouched.

Bumps the mostro-core pin to 0.11.5 (adds `Action::BondSlashed`) and
updates the spec (§9.3, §14.3, phase table) to reflect it.

Tests: 9 new unit tests covering all four §9.2 rows, the
slash_on_waiting_timeout=false gate, apply_to=make, no-config release,
the transient-settle-failure safety case, and the BondSlashed
notification target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08b41251-a711-4b8c-b9ad-7f28233aad42

📥 Commits

Reviewing files that changed from the base of the PR and between 00762ef and 888dfb0.

📒 Files selected for processing (1)
  • src/scheduler.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/scheduler.rs

Walkthrough

Bumps mostro-core to 0.11.5 and documents Phase 4; adds slash_or_release_on_timeout dispatcher that conditionally slashes or releases bonds on waiting-timeout, confirms slashes from durable DB state, enqueues Action::BondSlashed only after confirmation, adds notify_bond_slashed, tests, and integrates into the scheduler.

Changes

Phase 4 Timeout-Slash Implementation

Layer / File(s) Summary
Dependency bump and Phase 4 specification
Cargo.toml, docs/ANTI_ABUSE_BOND.md
mostro-core bumped to 0.11.5; docs mark Phase 4 shipped, describe slash_or_release_on_timeout scheduler integration, require durable confirmation before forfeiture notifications, and add the Action::BondSlashed protocol tag and payload shape.
Phase 4 dispatcher, confirmation, notification, and tests
src/app/bond/slash.rs
Adds imports/error typing updates; slash_or_release_on_timeout derives responsible side from waiting status, gates on config, calls apply_bond_resolution for timeout slashes or releases bonds; timeout_slash_confirmed verifies persisted slashed_reason == Timeout; notify_bond_slashed parses pubkey, resolves order kind, and enqueues Action::BondSlashed only for confirmed slashes. Extensive unit tests added.
Module export and scheduler integration
src/app/bond/mod.rs, src/scheduler.rs
Exports notify_bond_slashed and slash_or_release_on_timeout from bond module. Scheduler's job_cancel_orders calls the dispatcher as a pre-mutation step, triggers notify_bond_slashed only on confirmed slashes, logs errors and continues reprocessing next tick, and removes unconditional post-persist bond release.

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler as job_cancel_orders
  participant Dispatcher as slash_or_release_on_timeout
  participant Resolution as apply_bond_resolution
  participant DB as Database
  participant Confirm as timeout_slash_confirmed
  participant Notifier as notify_bond_slashed
  Scheduler->>Dispatcher: order, bond_cfg
  Dispatcher->>Dispatcher: derive responsible side
  Dispatcher->>Dispatcher: gate on enabled/slash_on_waiting_timeout/apply_to
  Dispatcher->>Resolution: apply timeout slash
  Resolution->>DB: persist slashed_reason / state transitions
  Dispatcher->>Confirm: verify durable slashed_reason
  Confirm->>DB: query bond row
  Dispatcher->>Notifier: if confirmed, enqueue notification
  Notifier->>DB: read bond/order details, parse pubkey, resolve kind
  Notifier->>Notifier: enqueue Action::BondSlashed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • AndreaDiazCorreia
  • arkanoider
  • Catrya

Poem

"🐰 I hopped through code and found a bond,
A timeout ticked — the rules respond.
Confirm the mark before you tell,
Durable truth protects us well.
A careful tap, a soft hooray, we log and hop away. 🥕"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and clearly summarizes the primary change: implementation of Phase 4 timeout slash functionality for the taker bond, which is the main focus of the entire changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bond-phase-4-timeout-slash

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b71eae20fd

ℹ️ 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".

Comment thread src/app/bond/slash.rs Outdated
The forfeiture-notice gate re-read the bond and required state ==
PendingPayout. But the Phase 3 payout scheduler runs concurrently
(every 60s) and can move a just-slashed row off PendingPayout within the
confirmation window — e.g. finalize_node_only flips a node-only slash
(slash_node_share_pct = 1.0) straight to Slashed. In that race the slash
already succeeded, yet the check returned None and Action::BondSlashed
was silently dropped.

Confirm instead via the durable `slashed_reason = Timeout` metadata,
written atomically with the Locked → PendingPayout CAS and never cleared
by any later transition (Slashed / Forfeited / Failed, or the
Failed → PendingPayout resurrection). A transient settle failure leaves
the bond Locked with NULL slash metadata, so a false notice is still
impossible; a concurrent dispute slash (LostDispute) is not mistaken for
a timeout slash.

Adds a regression test covering all post-slash states, the Locked
no-metadata case, and the LostDispute distinction. Spec §9.3 updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@grunch
Copy link
Copy Markdown
Member Author

grunch commented May 22, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

mostronatorcoder[bot]
mostronatorcoder Bot previously approved these changes May 26, 2026
Copy link
Copy Markdown
Contributor

@mostronatorcoder mostronatorcoder Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. I reviewed the current head commit 6c38953. The timeout-slash path correctly reuses the existing bond slash primitive, confirms the slash via durable slashed_reason metadata before emitting BondSlashed, and preserves the money-safety invariant that maker-responsible waiting-timeouts under apply_to=take release the taker bond instead of slashing it. I also ran cargo test slash and cargo clippy --all-targets --all-features -- -D warnings, both passed.

Copy link
Copy Markdown

@ermeme ermeme Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hermes Agent Review

Verdict: Changes Requested

I found one blocking issue: the timeout-slash path can still be lost on a transient LND settle failure. The scheduler persists the order as Canceled before attempting slash_or_release_on_timeout, and slash_one keeps the bond Locked on transient settle errors. Because the order is no longer eligible for the next timeout tick, the slash is not retried automatically and can be dropped entirely.

Please make the slash step retryable, or delay the cancel-state persistence until after the slash succeeds.

Comment thread src/scheduler.rs Outdated
… retry

Addresses the Hermes review on #744: the slash was gated on persist
success, but `find_order_by_seconds` only re-evaluates orders still in
`WaitingBuyerInvoice` / `WaitingPayment`. Once either persistence step
moved the order out of those states the next tick stopped seeing it, and
a transient `settle_hold_invoice` failure inside `slash_one` (which
leaves the bond `Locked`) silently dropped the slash entirely — the
CLTV expiry then *released* the funds back to the abuser, which is the
opposite of slashing.

Two persistence steps strip eligibility, not one as the review noted:
`update_order_to_initial_state` on the republish path (sell-order at
`WaitingBuyerInvoice` / buy-order at `WaitingPayment`, line 354) writes
`status = 'pending'` directly to the DB, and `order_updated.update`
(line 426) writes `'canceled'`. The slash must run before the earlier
of the two.

Fix: hoist `slash_or_release_on_timeout` + `notify_bond_slashed` to
**right after** the status/kind read, before the match that enters the
republish or cancel branch. The slash primitives stay idempotent on
retry:

- HTLC `settle_hold_invoice`: `is_already_settled_error` lets a retry
  proceed.
- Bond CAS: `WHERE state = Locked` — if a previous tick already moved
  the bond to `PendingPayout` the row count is 0 and the function
  returns `Ok(None)` (no duplicate `BondSlashed` notice).
- Bond release: `release_bonds_for_order_or_warn` only touches
  `Requested`/`Locked` bonds, so a previously-released bond is a no-op.

`notify_bond_slashed` fires immediately on first success so a later
persist failure can't lose it: by the next tick the function returns
`Ok(None)` (bond is no longer `Locked`), so the notice never duplicates
either.

Tests: 337 pass unchanged (the existing slash matrix already covers
both the durable-metadata confirmation and the transient-settle "no
notice" path; the reorder doesn't change the per-call slash contract).
`cargo fmt`, `cargo clippy --all-targets --all-features` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/scheduler.rs`:
- Around line 388-393: The Err(e) arm currently only logs and falls through,
allowing subsequent code to remove the order from find_order_by_seconds and lose
the promised retry; change the Err(e) branch in scheduler_timeout (the match
around slash_or_release_on_timeout for order.id) to abort further handling for
that order—e.g., return early or continue so the order stays in
find_order_by_seconds (do not proceed to the cancel/republish code below) so the
retry on the next tick can occur.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8c6f51b6-cca5-4c6f-bf96-e841e233fc3a

📥 Commits

Reviewing files that changed from the base of the PR and between 6c38953 and 00762ef.

📒 Files selected for processing (1)
  • src/scheduler.rs

Comment thread src/scheduler.rs
…y can occur

Addresses the Hermes follow-up on #744. The `Err(e)` arm of the
timeout-slash dispatch was logging and falling through, after which the
republish path's `update_order_to_initial_state` (or the cancel path's
`order_updated.update`) strips the order out of `find_order_by_seconds`'s
`status ∈ {WaitingBuyerInvoice, WaitingPayment}` eligibility window —
making "next tick re-attempts the slash" (per the warn message) a lie.

`Err` from `slash_or_release_on_timeout` is exclusively a DB-read
failure (the LND transient settle failure surfaces as `Ok(None)` via
the `timeout_slash_confirmed` guard, not as `Err`). On a DB error we
don't even know whether the slash applies, so persisting the cancel
would lose work whose applicability we couldn't determine.

Fix: `continue` on `Err`. The slash primitive is idempotent — a retry
that finds the HTLC settled or the bond already in `PendingPayout` is a
no-op — so the order staying eligible for the next tick is safe.

The related `Ok(None)`-with-still-Locked case (transient LND settle
failure inside `slash_one`) is gracefully accepted by the slash function's
existing docstring contract ("`apply_bond_resolution` is best-effort and
leaves the bond `Locked`"); it was not part of this review finding and
is out of scope.

Tests: 337 pass unchanged. `cargo fmt`, `cargo clippy --all-targets
--all-features` clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

Copy link
Copy Markdown
Member

@Catrya Catrya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK

Copy link
Copy Markdown
Collaborator

@arkanoider arkanoider left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK!

@grunch grunch merged commit 82336e3 into main May 29, 2026
8 checks passed
@grunch grunch deleted the feat/bond-phase-4-timeout-slash branch May 29, 2026 23:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants