Skip to content

feat(bond): Phase 1.5 — dedicated PayBondInvoice action and WaitingTakerBond status#731

Closed
grunch wants to merge 4 commits into
mainfrom
feat/bond-phase-1.5-pay-bond-invoice
Closed

feat(bond): Phase 1.5 — dedicated PayBondInvoice action and WaitingTakerBond status#731
grunch wants to merge 4 commits into
mainfrom
feat/bond-phase-1.5-pay-bond-invoice

Conversation

@grunch
Copy link
Copy Markdown
Member

@grunch grunch commented May 9, 2026

Summary

Retires the Phase 1 reuse of `Action::PayInvoice` for bonds. With `mostro-core` 0.11.0 (which ships `Action::PayBondInvoice`, `Status::WaitingTakerBond`, and `Payload::BondResolution`), the bond DM now ships under a dedicated action discriminator and the order's status flips to `WaitingTakerBond` while the bond is outstanding.

Wire-side, the order continues to publish as NIP-69 `pending` — preserving the non-blockability invariant (`docs/ANTI_ABUSE_BOND.md` §2 principle 8): a malicious or absent taker cannot park an order off-market by initiating a take and never paying the bond. The mapping lives in `nip33::create_status_tags`: `WaitingTakerBond → (true, Status::Pending)`.

Companion to `docs/ANTI_ABUSE_BOND.md` §6.5.

Changes

`mostro-core` pin

  • `Cargo.toml`: `0.10.0 → 0.11.0` so the new variants are reachable from `mostrod`.

`src/app/bond/flow.rs`

  • Module doc rewritten — Phase 1.5 retires the Phase 1 "alternative path".
  • `request_taker_bond` ships the bond DM as `Action::PayBondInvoice`; the embedded `SmallOrder` echoes `Status::WaitingTakerBond` so bond-aware clients render the bond-payment phase distinctly.
  • `on_bond_invoice_accepted` accepts both `Pending` and `WaitingTakerBond` as pre-trade resume states (was just `Pending`).
  • `on_bond_invoice_canceled` flips the order back to `Pending` when its bond is canceled (CLTV expiry / abandonment) AND no other active bond remains. Supersede paths are unaffected because a fresh bond is already in flight.

`src/app/take_buy.rs` + `src/app/take_sell.rs`

  • Status guard widens to `Pending | WaitingTakerBond` (both pre-trade; `supersede_prior_taker_bonds` gates "anyone already locked?" independently).
  • Bond branch now republishes with `Status::WaitingTakerBond` (NIP-33 still emits `s = pending` per the mapping) and persists, before calling `request_taker_bond`.

`src/app/cancel.rs::cancel_action_generic`

  • Pre-trade guard widens from `Pending` to `Pending | WaitingTakerBond`. Cooperative-cancel logic does NOT apply — the trade flow has not started in either status. Both routes inside the branch (maker self-cancel via `cancel_pending_order_from_maker`, taker self-cancel via `cancel_order_by_taker`) stay unchanged.

`src/nip33.rs`

  • `create_status_tags` maps `WaitingTakerBond → (true, Status::Pending)`.
  • `create_fiat_amt_array` and `create_source_tag` widen to treat `WaitingTakerBond` like `Pending` so the order remains fully discoverable for re-take during the bond window (range fiat amounts + `mostro:` source tag).
  • Two new tests pin the contract:
    • `waiting_taker_bond_publishes_as_pending_on_the_wire` — the load-bearing assertion.
    • `waiting_taker_bond_keeps_source_tag_for_re_takeability`.

`src/util.rs::get_ratings_for_pending_order`

  • Widens to attach the rating tag for `WaitingTakerBond` orders too — same wire-side semantics as `Pending`.

Test plan

  • `cargo build`
  • `cargo fmt --all -- --check` — clean
  • `cargo clippy --all-targets` — clean
  • `cargo test` — 260/260 green (was 258 pre-PR; +2 nip33 non-blockability tests)
  • Manual on regtest: take a sell-order with bonds enabled; bond DM arrives as `pay-bond-invoice` (not `pay-invoice`). After bond locks, trade flow continues with `add-invoice`.
  • Manual on regtest: take a buy-order with bonds enabled; two `pay-*-invoice` actions arrive in sequence (`pay-bond-invoice` then `pay-invoice` for the trade hold). Both visible as distinct action types.
  • Manual on regtest: while a taker is mid-bond, second take from another pubkey supersedes (prior taker gets `Action::Canceled`); maker can still cancel the order; observers continue to see the order as `s = pending` on Nostr throughout.

Not covered (deferred to follow-up phases)

No schema changes (no migration). No slashing yet — that lands in Phase 2 (`BondResolution`-driven dispute slash) which can now build on the clean API this PR establishes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Enhanced bond lifecycle management with improved order state handling during the taker bond phase.
    • Orders can now be re-taken while awaiting taker bond confirmation.
  • Chores

    • Updated core dependency to latest version.

Review Change Stack

…kerBond status

Retires the Phase 1 reuse of `Action::PayInvoice` for bonds. With
`mostro-core` 0.11.0 (which ships `Action::PayBondInvoice`,
`Status::WaitingTakerBond`, and `Payload::BondResolution`), the bond
DM now ships under a dedicated action discriminator and the order's
status flips to `WaitingTakerBond` while the bond is outstanding.

Wire-side, the order continues to publish as NIP-69 `pending` —
preserving the non-blockability invariant (`docs/ANTI_ABUSE_BOND.md`
§2 principle 8): a malicious or absent taker cannot park an order
off-market by initiating a take and never paying the bond. The
mapping lives in `nip33::create_status_tags`:
`WaitingTakerBond → (true, Status::Pending)`. Other potential takers
continue to see the order as available; whichever bond locks first
wins, and `supersede_prior_taker_bonds` releases stale `Requested`
bonds on every fresh take.

Bumps and changes:

- `Cargo.toml`: `mostro-core 0.10.0 → 0.11.0`.

- `src/app/bond/flow.rs`:
  - Module doc rewritten to describe Phase 1.5 reality (no more
    "alternative path").
  - `request_taker_bond` ships the bond DM as `Action::PayBondInvoice`
    and the embedded `SmallOrder` echoes `Status::WaitingTakerBond`
    so bond-aware clients can render the bond-payment phase
    distinctly.
  - `on_bond_invoice_accepted` accepts both `Pending` and
    `WaitingTakerBond` as the pre-trade state from which to resume,
    so the legitimate `Pending → WaitingTakerBond → trade-flow`
    transition isn't blocked by the defense-in-depth check.
  - `on_bond_invoice_canceled` now flips the order back to `Pending`
    when its bond is canceled (CLTV expiry / abandonment) AND no
    other active bond remains. Supersede paths leave the order
    alone because a fresh bond is already in flight.

- `src/app/take_buy.rs`, `src/app/take_sell.rs`:
  - Status guard widens to accept `Pending` OR `WaitingTakerBond`
    (both are pre-trade for take-validation purposes;
    `supersede_prior_taker_bonds` continues to gate "is anyone
    already locked?" independently).
  - Bond branch now republishes the order with
    `Status::WaitingTakerBond` (mapped to NIP-69 `pending` by
    `create_status_tags`) before persisting.

- `src/app/cancel.rs::cancel_action_generic`:
  - Pre-trade guard widens from `Pending` to `Pending |
    WaitingTakerBond`. Cooperative-cancel logic does NOT apply —
    the trade flow has not started in either status. Both routes
    inside the branch (maker self-cancel via
    `cancel_pending_order_from_maker`, taker self-cancel via
    `cancel_order_by_taker`) stay unchanged.

- `src/nip33.rs`:
  - `create_status_tags` maps `WaitingTakerBond` to
    `(true, Status::Pending)`.
  - `create_fiat_amt_array` and `create_source_tag` widen to treat
    `WaitingTakerBond` like `Pending`, so the order remains fully
    discoverable on the wire (range fiat amounts + `mostro:` source
    tag) for re-take during the bond window.
  - Two new tests pin the contract:
    `waiting_taker_bond_publishes_as_pending_on_the_wire` (the
    load-bearing assertion) and
    `waiting_taker_bond_keeps_source_tag_for_re_takeability`.

- `src/util.rs::get_ratings_for_pending_order` widens to attach the
  rating tag for `WaitingTakerBond` orders too — same wire-side
  semantics as `Pending`.

Validation: `cargo build`, `cargo fmt --all -- --check`,
`cargo clippy --all-targets`, `cargo test` (260 / 260 green —
including the two new nip33 tests). No schema changes; no
`sqlx-data.json` refresh needed.

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

coderabbitai Bot commented May 9, 2026

Warning

Rate limit exceeded

@grunch has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 5 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 317d75dd-c3a5-4c52-8ff4-30cd078a9a43

📥 Commits

Reviewing files that changed from the base of the PR and between 0eefe6f and f4aa5d7.

📒 Files selected for processing (3)
  • docs/ANTI_ABUSE_BOND.md
  • src/app/take_buy.rs
  • src/app/take_sell.rs

Walkthrough

This PR extends the bond system lifecycle from Phase 1 to Phase 1.5 by introducing a new WaitingTakerBond order status that distinguishes the bond-outstanding phase from the trade-holding phase. The mostro-core dependency is updated to support this new status and action type. Bond request, acceptance, and cancellation flows are redesigned; take and cancellation handlers are unified to accept both Pending and WaitingTakerBond as pre-trade states; expiry queries, wire protocol mapping, and reputation context are updated accordingly.

Changes

Bond Lifecycle Phase 1.5 Extension

Layer / File(s) Summary
Dependency Update
Cargo.toml
mostro-core upgraded from 0.10.0 to 0.11.0 to enable new bond status and action types.
Bond Lifecycle Core
src/app/bond/flow.rs
Module documentation, request flow, and acceptance/cancellation handlers updated. request_taker_bond creates SmallOrder with WaitingTakerBond status and uses PayBondInvoice action. on_bond_invoice_accepted resumes only for Pending or WaitingTakerBond. on_bond_invoice_canceled adds Phase 1.5 cleanup: resets WaitingTakerBond to Pending when the only active bond is released, republishing the order event.
Take Action Integration
src/app/take_buy.rs, src/app/take_sell.rs
Both take_buy_action and take_sell_action now accept Pending and WaitingTakerBond as valid entry states. When bond required, order is persisted to WaitingTakerBond via update_order_event before requesting bond and exiting.
Cancellation Routing
src/app/cancel.rs
cancel_action_generic pre-trade path extended to treat both Pending and WaitingTakerBond as maker-revert cases, applying same bond lookup vs. pubkey matching decision logic.
Expiry Query & Database
src/db.rs
find_order_by_date query expanded to return expired orders for both pending and waiting-taker-bond statuses; tests verify inclusion/exclusion behavior across state boundaries.
Wire Protocol Mapping
src/nip33.rs
NIP-33 tag generation treats WaitingTakerBond as wire-visible pending: fiat_amount array logic mirrors Pending, status tag emits "pending", source tag generated for re-takeability during bond window; tests added.
Reputation & Context
src/util.rs
get_ratings_for_pending_order extended to attach reputation context for both Pending and WaitingTakerBond, ensuring consistent ratings across re-take attempts within bond window.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • MostroP2P/mostro#719: Introduces anti-abuse taker-bond system; this PR extends and implements the Phase 1.5 cleanup logic and order status wiring that Phase 1 laid out.
  • MostroP2P/mostro#725: Documents Action::PayBondInvoice and Status::WaitingTakerBond; this PR implements these concepts across bond flow, take/cancel handlers, DB queries, and NIP-33 protocol mapping.
  • MostroP2P/mostro#501: Establishes get_ratings_for_pending_order utility; this PR extends its usage to cover WaitingTakerBond state for consistent reputation context during re-takes.

Suggested reviewers

  • Catrya
  • arkanoider

Poem

🐰 A bond arrives, then waits in phase,
With PayBondInvoice set to grace,
When canceled cleanly, resets fast,
Back to Pending, shadows past—
Phase One-Point-Five, a tidy dance!

🚥 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 summarizes the main change: introducing Phase 1.5 with a new PayBondInvoice action and WaitingTakerBond status, which are the primary features across all modified files.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bond-phase-1.5-pay-bond-invoice

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.

@grunch grunch requested review from Catrya and arkanoider and removed request for arkanoider May 9, 2026 17:47
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: 013ca4ba64

ℹ️ 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/take_sell.rs
Comment thread src/app/bond/flow.rs Outdated
Two P2 review nits on the Phase 1.5 PR:

- `db::find_order_by_date` (used by `job_expire_pending_older_orders`
  in the scheduler) filtered on `status == 'pending'` only. After
  Phase 1.5 flips orders to `WaitingTakerBond` while the bond is
  outstanding, those rows were silently skipped by the expiry job —
  meaning a bond-enabled take could keep a publicly-publishable
  order (`pending` on the wire per the non-blockability mapping) past
  its `expires_at`, and the Phase 1 "always release on every exit
  path" guarantee would not fire on the expiry side. Widen the
  query to `status IN ('pending', 'waiting-taker-bond')`. Two new
  tests pin the contract: pending and waiting-taker-bond orders are
  both expired when overdue; post-trade-flow statuses are not.

- `on_bond_invoice_canceled` was using
  `find_active_bonds_for_order(...).unwrap_or_default()`, which
  collapses transient SQLite/DB errors into "no active bonds" and
  flips the order back to `Pending`. Under a supersede race (a
  fresh bond is already in flight on the same order), this would
  clobber a legitimate `WaitingTakerBond` and violate the supersede
  guard. Switch to `match`: on error, log with `bond_id` /
  `order_id` and return early without touching the order. The order
  stays in `WaitingTakerBond` (wire still publishes as `pending`,
  non-blockability is preserved), and the next event on this order
  re-runs the cleanup.

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

grunch commented May 9, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 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.

@MostroP2P MostroP2P deleted a comment from coderabbitai Bot May 9, 2026
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: 2

🤖 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/app/take_buy.rs`:
- Around line 37-45: The current take path now accepts Status::WaitingTakerBond
but still gates calling supersede_prior_taker_bonds on the current
taker_bond_required() result, which lets a take skip active-bond reconciliation;
modify the logic in the take entry (the if that checks Status::Pending /
Status::WaitingTakerBond) so that when the order status is WaitingTakerBond you
always run supersede_prior_taker_bonds (i.e., treat WaitingTakerBond like a case
that forces reconciliation) instead of only calling it when
taker_bond_required() returns true—update the condition to call
supersede_prior_taker_bonds if taker_bond_required() || order.status ==
Status::WaitingTakerBond (or otherwise pass a flag into
supersede_prior_taker_bonds) so outstanding Requested/Locked bonds are
released/rejected correctly.

In `@src/app/take_sell.rs`:
- Around line 59-67: When accepting orders in the take_sell path, always run the
prior-bonds reconciliation/supersede logic before treating a WaitingTakerBond as
a valid entry point, even if bond handling is currently disabled; specifically,
ensure the call to supersede_prior_taker_bonds (or the reconciliation routine)
is executed before proceeding when order.check_status(Status::WaitingTakerBond)
succeeds so leftover Requested/Locked bonds cannot be bypassed; adjust the
control flow in the function that contains order.check_status and the
WaitingTakerBond branch to invoke supersede_prior_taker_bonds unconditionally
for WaitingTakerBond (matching the buy path behavior) and apply the same change
to the analogous block around the 81-91 region.
🪄 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: f365f4ca-c782-4783-8fdf-abdb8dca45f6

📥 Commits

Reviewing files that changed from the base of the PR and between 1ade789 and 0eefe6f.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • Cargo.toml
  • src/app/bond/flow.rs
  • src/app/cancel.rs
  • src/app/take_buy.rs
  • src/app/take_sell.rs
  • src/db.rs
  • src/nip33.rs
  • src/util.rs

Comment thread src/app/take_buy.rs
Comment thread src/app/take_sell.rs
@grunch grunch requested a review from arkanoider May 9, 2026 18:12
The supersede pass was gated on `bond::taker_bond_required()`, which
silently skipped reconciliation when bonds were once enabled and then
disabled with a leftover Requested/Locked bond still attached to the
order. The reviewer's concern is valid: a `Locked` prior bond should
keep blocking the take (the trade is committed), and a `Requested`
prior bond should be released and the prior taker notified —
regardless of whether bonds are currently required for *this* take.

Drop the conditional and always call
`supersede_prior_taker_bonds`. The helper is a no-op when no active
bond exists for the order (returns `Ok(0)`), so always-calling is
safe and cheap. This is a more robust fix than gating on
`taker_bond_required() || status == WaitingTakerBond`: it covers
config-toggle races and any future state we haven't anticipated.

Applied identically to `take_buy_action` and `take_sell_action`.
`bond_required` is still computed below for the bond-DM dispatch
branch (we only enqueue a fresh `pay-bond-invoice` when bonds are
currently required), but reconciliation is unconditional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

  • Updating the database causes the build to fail.
2026-05-11T16:03:41.749701Z ERROR mostrod::db: Failed to run migrations on existing database: migration 20260423120000 was previously applied but has been modified
Error: MostroInternalErr(DbAccessError("migration 20260423120000 was previously applied but has been modified"))
  • When multiple users try to take the same order, the latest attempt invalidates the previous one if the previous user has not yet paid the bond. In my database, I have 4 bonds for the same order: the first 3 are in released status, and the last one is in requested status. If I make a 5th attempt, the 4th attempt will also be set to released, and so on.

    This means a malicious user could repeatedly take orders without paying the bond and effectively block the order book.

    The correct behavior should be to create a new hold invoice for each attempt while keeping all of them in requested status. Once one of them is paid, the remaining ones should be canceled so they can no longer be paid

… wins

Replace the supersede-on-retake model with concurrent taker bonds:
multiple Requested bonds may coexist on an order, and the first to
reach Locked wins (cancelling the others at lock-time rather than at
retake-time). The TTL on each LND hold invoice still prevents a
malicious taker from blocking the order book.

Why: the supersede model surfaced as a UX bug — a taker who is
slightly slower to pay receives FAILURE_REASON_INCORRECT_PAYMENT_DETAILS
from LND with no Mostro-level explanation, because their hold invoice
was cancelled the moment another taker pressed "take". Under concurrent
bonds, all takers keep payable invoices and the race resolves at
payment time.

Updates §2.8 (non-blockability), §3.1 (maker/taker axis), §6 (Phase 1
historical notes), §6.3 (client UX), §6.5.1 (Phase 1.5 scope:
take handler check, on_bond_invoice_accepted resolution, additive
migration for taker_* columns, find_active_bond_by_taker helper,
maker/taker self-cancel semantics), §6.5.3 (tests rewritten for
A/B/C concurrent takers, locked-bond gate, range-order per-bond
pricing, lock-race NOT EXISTS guard), and §6.5.4 (acceptance:
supersede_prior_taker_bonds is removed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@grunch grunch closed this May 11, 2026
@grunch grunch deleted the feat/bond-phase-1.5-pay-bond-invoice branch May 11, 2026 21:26
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.

2 participants