Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 106 additions & 2 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use std::sync::Arc;
use uuid::Uuid;

// Constants for status filtering used across restore session functions
const EXCLUDED_ORDER_STATUSES: &str = "'expired','success','canceled','dispute','canceledbyadmin','completedbyadmin','settledbyadmin','cooperativelycanceled'";
const EXCLUDED_ORDER_STATUSES: &str = "'expired','success','canceled','dispute','canceled-by-admin','completed-by-admin','settled-by-admin','cooperatively-canceled'";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P1] Preserve restore context for outstanding bond payouts

After admin_cancel_action resolves a dispute, the order is canceled-by-admin and the dispute is seller-refunded, while apply_bond_resolution can leave a slashed bond in PendingPayout awaiting the winner's invoice. With this corrected exclusion, find_user_orders_by_master_key now drops that order; find_user_disputes_by_master_key also excludes the resolved dispute, and process_restore_session_work does not query bonds. A restoring client therefore loses the order/trade-index context needed to claim the payout. If no invoice is submitted before the claim deadline, process_one_bond forfeits the bond.

This is the admin-cancel branch of #784, but it is a behavior change introduced here: before this edit, canceled-by-admin orders were returned. Please preserve terminal orders with recoverable payouts owed to the restoring user, or land the bond-aware restore fix before enabling this exclusion. Add coverage for an admin-canceled order with an outstanding payout, alongside the terminal-order-with-no-obligation case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 77d7beb.

Both filters now carry an exception (HAS_CLAIMABLE_BOND_PAYOUT, src/db.rs:33): a terminal order survives while a bond on it is pending-payout or failed. failed belongs there because apply_invoice still accepts a payout invoice against such a row inside the payout_claim_window_days window, so the claim is live even after send_payment gave up. Both states are left for good on payout, slash or forfeit, so the exception retracts itself with no code to maintain.

Restore therefore keeps the canceled-by-admin order and its trade_index_buyer/trade_index_seller, which is what the claim needs.

One deliberate limitation, worth your call: the clause is order-scoped, not recipient-scoped, so both parties see such an order rather than only the winner. Narrowing it in SQL is not really available — bonds.pubkey is a trade key while the restore query joins on master keys, and the Phase 6 maker-refund row (parent_bond_id set, child_order_id NULL) pays bond.pubkey itself rather than the counterparty, so a "not the bonded side" filter would be wrong exactly where the money is owed to the bonded side. I took the over-inclusive side; say the word if you would rather have the extra complexity.

Coverage as requested: find_user_orders_by_master_key_keeps_terminal_orders_owing_a_payout pins all four arms — terminal with no obligation (excluded), pending-payout (kept), failed (kept), and slashed (excluded, the obligation is discharged). Verified failing with the exception disabled and passing with it.

const ACTIVE_DISPUTE_STATUSES: &str = "'initiated','in-progress'";

/// Terminal order statuses for the Phase 2 active-trade-pubkey cache: an
Expand All @@ -24,7 +24,7 @@ const ACTIVE_DISPUTE_STATUSES: &str = "'initiated','in-progress'";
/// disputed order is still active (buyer, seller and the assigned solver keep
/// messaging), so its trade keys must stay fast-pathed. See
/// `find_active_trade_pubkeys` and docs/TRANSPORT_V2_SPEC.md §6 Phase 2.
const TERMINAL_ORDER_STATUSES: &str = "'expired','success','canceled','canceledbyadmin','completedbyadmin','settledbyadmin','cooperativelycanceled'";
const TERMINAL_ORDER_STATUSES: &str = "'expired','success','canceled','canceled-by-admin','completed-by-admin','settled-by-admin','cooperatively-canceled'";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Keep pending payout recipients in the known-key cache

A canceled-by-admin order can still require a legitimate AddBondInvoice from the winning counterparty. After this change, find_active_trade_pubkeys removes that recipient's trade key on the next cache refresh if they have no other active order. On protocol v2 nodes configured with pow_first_contact > pow, accept_event then drops a response carrying the normal trade PoW before decrypting or dispatching it to add_bond_invoice_action. This can interrupt the payout even when the client has retained its session and order context.

Please include the recipient keys of recoverable bond payouts in the known-key query until their obligations are resolved. This needs coverage independently of restore: a terminal order with a pending payout should retain its recipient key and allow the normal-PoW response.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 77d7beb, same clause applied to the order query in find_active_trade_pubkeys (src/db.rs:63).

You were right to file this separately from the restore case — it is the sharper of the two. The restore gap costs a client its context; this one strands the payout even for a client that never lost its session, because the reply is dropped before decryption and nothing on either side can tell that it happened. Keying the exception on b.order_id = orders.id matches what resolve_payout_recipient actually loads, including the Phase 6 slice-slash child rows whose order_id is the slice order, so no special case was needed.

Covered independently of restore by find_active_trade_pubkeys_keeps_keys_of_terminal_orders_owing_a_payout: a canceled-by-admin order with a pending-payout bond retains all three participant keys, while an identical order with nothing outstanding drops out as before.

Two notes on the diff beyond the two fixes:

  • setup_orders_db builds its schema by hand and had no bonds table, so the new EXISTS broke three pre-existing tests with no such table: bonds. I added a bonds subset to the fixture plus an insert_bond helper — that is most of the line count.
  • The clause embeds pending-payout and failed as SQL literals, which is the very hazard refactor(db): derive SQL status lists from Status instead of hand-written literals #897 describes and this PR was born from. claimable_bond_states_match_bond_state_display pins them against BondState's own Display, so a rename fails the build instead of silently matching nothing.

Full suite 1190 passed / 0 failed / 2 ignored; cargo fmt --check and cargo clippy --all-targets --all-features -- -D warnings clean. Both new tests verified failing with the exception disabled.


#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
Expand Down Expand Up @@ -2078,6 +2078,45 @@ mod tests {
None,
)
.await;
// Terminal statuses whose serialized form is hyphenated. These are the
// ones a mis-spelled TERMINAL_ORDER_STATUSES silently fails to match,
// so they must be covered explicitly.
insert_order_with_pubkeys(
&pool,
uuid::Uuid::new_v4(),
"cooperatively-canceled",
Some("creator_coop"),
Some("buyer_coop"),
Some("seller_coop"),
)
.await;
insert_order_with_pubkeys(
&pool,
uuid::Uuid::new_v4(),
"canceled-by-admin",
Some("creator_cba"),
None,
None,
)
.await;
insert_order_with_pubkeys(
&pool,
uuid::Uuid::new_v4(),
"settled-by-admin",
Some("creator_sba"),
None,
None,
)
.await;
insert_order_with_pubkeys(
&pool,
uuid::Uuid::new_v4(),
"completed-by-admin",
Some("creator_cpa"),
None,
None,
)
.await;

// Active dispute with an assigned solver → solver key included.
sqlx::query(
Expand Down Expand Up @@ -2123,6 +2162,12 @@ mod tests {
"seller_succ",
"creator_canc",
"solver_settled",
"creator_coop",
"buyer_coop",
"seller_coop",
"creator_cba",
"creator_sba",
"creator_cpa",
] {
assert!(
!keys.contains(k),
Expand All @@ -2131,6 +2176,65 @@ mod tests {
}
}

/// Restore-session must not hand back orders that are already over.
///
/// Four of the eight excluded statuses serialize with hyphens
/// (`canceled-by-admin`, `settled-by-admin`, `completed-by-admin`,
/// `cooperatively-canceled`). A status list written without them still
/// filters the single-word ones, so the query keeps *looking* correct
/// while quietly restoring dead orders — which is why every hyphenated
/// status is asserted here individually.
#[tokio::test]
async fn find_user_orders_by_master_key_excludes_all_terminal_statuses() {
let pool = setup_orders_db().await.unwrap();
let master_key = "a".repeat(64);

async fn insert_for_master(pool: &SqlitePool, status: &str, master_key: &str) {
sqlx::query(
r#"
INSERT INTO orders (id, kind, event_id, status, premium, payment_method,
amount, fiat_code, fiat_amount, created_at, expires_at,
master_buyer_pubkey, trade_index_buyer)
VALUES (?1, 'buy', 'event123', ?2, 0, 'lightning',
100000, 'USD', 100, 1700000000, 1700086400, ?3, 1)
"#,
)
.bind(uuid::Uuid::new_v4())
.bind(status)
.bind(master_key)
.execute(pool)
.await
.unwrap();
}

// One live order — the only row the user should get back.
insert_for_master(&pool, "waiting-payment", &master_key).await;

for status in [
"expired",
"success",
"canceled",
"dispute",
"canceled-by-admin",
"completed-by-admin",
"settled-by-admin",
"cooperatively-canceled",
] {
insert_for_master(&pool, status, &master_key).await;
}

let orders = super::find_user_orders_by_master_key(&pool, &master_key)
.await
.unwrap();

let statuses: Vec<&str> = orders.iter().map(|o| o.status.as_str()).collect();
assert_eq!(
statuses,
vec!["waiting-payment"],
"restore must return only the live order, got {statuses:?}"
);
}

#[tokio::test]
async fn test_fetch_string_column_scalar() {
let pool = setup_db().await.unwrap();
Expand Down