Skip to content

feat(#217): resync trade_key_index to the max recovered index - #239

Open
codaMW wants to merge 1 commit into
MostroP2P:mainfrom
codaMW:feat/217-trade-key-resync
Open

feat(#217): resync trade_key_index to the max recovered index#239
codaMW wants to merge 1 commit into
MostroP2P:mainfrom
codaMW:feat/217-trade-key-resync

Conversation

@codaMW

@codaMW codaMW commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Refs #217 sub-issue of #142. Stacked on #215 (consumes the RestoreData
handling that PR introduces).

Problem

After a restore, the local trade_key_index counter is still at its
post-install value (0/1) while the recovered trades already occupy higher
indexes. The next order the user creates therefore reuses a trade key already
bound to a recovered trade a correctness bug (the daemon rejects the reused
index with CantDo(InvalidTradeIndex), and worse, two trades would share a key).

What it does

When a valid RestoreData is processed, raise trade_key_index to the maximum
recovered index across both orders and disputes. Monotonic a restore never
rewinds the counter.

  • identity::ensure_trade_key_index_at_least(floor) bumps the counter to
    max(current, floor) under the identity write lock. No-op when already ahead
    (idempotent). Persists with the same discipline as derive_trade_key: if the
    counter moves, the write must succeed or the call fails a bumped-but-
    unpersisted counter would regress on the next restart and reopen this bug.
  • orders::recovered_max_trade_index(info) the max trade_index over
    restore_orders and restore_disputes. Indexes are i64 on the wire;
    u32::try_from drops negatives and anything beyond u32::MAX rather than
    truncating a garbage value into the counter. Returns None when the restore
    carried no trades (nothing to resync to).
  • Wired into restore_session's Restored arm: resync before returning the
    info. A resync failure fails the restore rather than returning "success" with
    a counter that could hand out a reused key.

Lands on the payload shape available today (trade_index is already present)
does not wait for the snapshot contract in #216.

Acceptance criteria

  • trade_key_index raised to max(order indexes, dispute indexes) on a
    valid restore
  • Never lowered, including a restore returning fewer/older trades
  • Regression: after restoring at index > 1, the next derive_trade_key()
    returns a fresh index, not a recovered one
  • Idempotent: applying the same RestoreData twice leaves the counter
    unchanged

Tests

  • load_derive_then_delete_identity_lifecycle extended (kept in the one
    stateful test so parallel threads never race the identity_lock singleton):
    a floor below current is a no-op (never lowers), a higher floor raises,
    re-applying the same floor is idempotent, and the next derive_trade_key()
    returns a fresh index past the recovered set.
  • recovered_max_trade_index: None when empty, max spans both orders and
    disputes, negatives and out-of-range values dropped.
  • The wiring into the Restored arm is exercised end-to-end by Restore: RestoreSession handshake (send, correlate reply, subscribe) #215's
    restore_e2e_tests (needs a live regtest daemon).

cargo test --lib 115 passed / clippy -D warnings clean / flutter analyze
clean.

Merge order

Stacked on #215 depends on its RestoreData handling. Since #215 isn't in
main yet, this PR's diff currently includes #215's commits; the resync itself is
commit aa7409b (identity.rs + orders.rs, ~153 lines). Once #215 merges,
I'll rebase this onto main and the diff will show only the resync.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented reuse of trade key indexes by synchronizing recovered indexes with stored state.
    • Ignored invalid trade indexes during session restoration.
    • Ensured restored sessions account for the highest valid trade index across orders and disputes.
  • Tests

    • Added coverage for index persistence, rollback, idempotency, and no-op behavior.
    • Expanded session restoration tests for empty data, multiple collections, and invalid indexes.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fb8bbc8-f1e1-4d01-8686-ac3508ef10e1

📥 Commits

Reviewing files that changed from the base of the PR and between 39c6492 and e22ef66.

📒 Files selected for processing (2)
  • rust/src/api/identity.rs
  • rust/src/api/orders.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • rust/src/api/orders.rs
  • rust/src/api/identity.rs

Walkthrough

The restore flow computes the highest valid trade index across orders and disputes. It then raises the persisted identity trade-key index before returning restore data. Tests cover idempotency, invalid values, persistence, publication, and fresh derivation.

Changes

Restore recovery flow

Layer / File(s) Summary
Monotonic trade-key resynchronization
rust/src/api/identity.rs
Adds an idempotent helper that persists raised trade-key indices, rolls back failed updates, publishes changes, and validates subsequent derivation.
Recovered index integration
rust/src/api/orders.rs
Computes the maximum valid trade index across restored orders and disputes, applies it after successful restoration, and tests aggregation and invalid-value filtering.

Estimated code review effort: 3 (Moderate) | ~22 minutes

Sequence Diagram(s)

sequenceDiagram
  participant OrdersAPI
  participant DaemonReply
  participant IdentityAPI
  OrdersAPI->>DaemonReply: receive restored orders and disputes
  OrdersAPI->>OrdersAPI: compute maximum valid trade_index
  OrdersAPI->>IdentityAPI: ensure_trade_key_index_at_least(floor)
  IdentityAPI-->>OrdersAPI: persist and publish updated index
  OrdersAPI-->>OrdersAPI: return RestoreSessionInfo
Loading

Possibly related issues

Possibly related PRs

Poem

I'm a rabbit guarding keys,
No recycled numbers, please.
Orders and disputes align,
Fifty hops to fifty-one! 🐇

🚥 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 clearly describes resynchronizing trade_key_index to the maximum recovered index, which is the pull request's main change.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
rust/src/api/orders.rs (1)

2922-2953: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Doc comment for restore_session is misattached to recovered_max_trade_index.

Lines 2922-2929 describe restore_session's send/await behavior and key-correlation design, but there's no blank line before Line 2930, so the whole block (2922-2936) becomes one contiguous rustdoc comment attached to fn recovered_max_trade_index (Line 2937) instead. pub async fn restore_session() (Line 2953) ends up with no doc comment of its own.

♻️ Proposed fix
-/// Send a `RestoreSession` to the active daemon and return the user's active
-/// trades/disputes. Mirrors create_order's send/await, minus the order payload.
-///
-/// Correlation: the request is sent from a fresh TRADE key (event.sender) while
-/// the Seal carries the IDENTITY key (event.identity). The daemon looks up
-/// trades by identity/master key and replies to the trade key
-/// (mostro restore_session.rs: master_key = event.identity, reply -> event.sender),
-/// so we subscribe on the trade key and correlate the reply by that pubkey.
-/// Highest trade-key index across all recovered orders and disputes (`#217`).
+/// Highest trade-key index across all recovered orders and disputes (`#217`).
 ///
 /// The counter must be raised to this so the next `derive_trade_key()` cannot
 /// hand out an index a recovered trade already owns. Returns `None` when the
 /// restore carried no trades (nothing to resync to). Indexes are `i64` on the
 /// wire; a value that is negative or beyond `u32::MAX` is not a real trade
 /// index, so it is dropped rather than truncated into the counter.
 fn recovered_max_trade_index(
     info: &mostro_core::message::RestoreSessionInfo,
 ) -> Option<u32> {

And restore the removed lines as the doc comment directly above pub async fn restore_session() (Line 2952-2953).

🤖 Prompt for 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.

In `@rust/src/api/orders.rs` around lines 2922 - 2953, Separate the
restore_session-specific rustdoc from recovered_max_trade_index by ending it
before the helper’s documentation, then restore that documentation immediately
above pub async fn restore_session. Keep the recovered_max_trade_index
explanation attached only to recovered_max_trade_index and preserve the existing
restore_session send/await and key-correlation details on the public function.
rust/src/api/identity.rs (1)

297-317: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Keep ensure_trade_key_index_at_least out of the Dart-callable API.

This setter is only used by the ignore-marked orders::restore_session() path and by tests; make it non-pub instead of pub(crate) and regenerate FRB bindings if needed.

♻️ Proposed fix
-pub async fn ensure_trade_key_index_at_least(floor: u32) -> Result<()> {
+async fn ensure_trade_key_index_at_least(floor: u32) -> Result<()> {
🤖 Prompt for 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.

In `@rust/src/api/identity.rs` around lines 297 - 317, Make
ensure_trade_key_index_at_least private by removing its public visibility, since
it is only used internally by orders::restore_session() and tests. Confirm the
change does not expose it through Dart-callable or generated FRB bindings, and
regenerate bindings only if required.

Source: Path instructions

🤖 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 `@rust/src/api/orders.rs`:
- Around line 2952-3033: Update the Action::CantDo rejection handling to
correlate restore requests with take_matching_restore(trade_pubkey_hex) instead
of the nonce-based take_matching_request path when the request kind is Restore.
Ensure the matched Restore waiter receives the rejection so restore_session()
returns the actual reason immediately, while preserving nonce-based correlation
for other request kinds.

---

Nitpick comments:
In `@rust/src/api/identity.rs`:
- Around line 297-317: Make ensure_trade_key_index_at_least private by removing
its public visibility, since it is only used internally by
orders::restore_session() and tests. Confirm the change does not expose it
through Dart-callable or generated FRB bindings, and regenerate bindings only if
required.

In `@rust/src/api/orders.rs`:
- Around line 2922-2953: Separate the restore_session-specific rustdoc from
recovered_max_trade_index by ending it before the helper’s documentation, then
restore that documentation immediately above pub async fn restore_session. Keep
the recovered_max_trade_index explanation attached only to
recovered_max_trade_index and preserve the existing restore_session send/await
and key-correlation details on the public function.
🪄 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 Plus

Run ID: e7ce263d-b70c-4593-98a1-8b519886f74c

📥 Commits

Reviewing files that changed from the base of the PR and between 1931ac0 and aa7409b.

📒 Files selected for processing (3)
  • rust/src/api/identity.rs
  • rust/src/api/orders.rs
  • rust/src/mostro/actions.rs

Comment thread rust/src/api/orders.rs
@codaMW
codaMW force-pushed the feat/217-trade-key-resync branch from 8b4ef14 to e7471ce Compare July 30, 2026 09:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/src/api/identity.rs (1)

1-1: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Un-rolled-back persist failure in ensure_trade_key_index_at_least lets a retried restore silently report success without ever persisting the raised counter. The root cause is in identity.rs; orders.rs only surfaces the downstream effect.

  • rust/src/api/identity.rs#L414-434: state.identity_info.trade_key_index = raised is set before db.save_identity and never rolled back on failure; combined with the raised == current no-op short-circuit, a retry with the same floor silently returns Ok(()) without retrying the persist. Also missing a publish_index call so the secure-storage mirror (issue #249) never learns of the raised index. Roll back trade_key_index to current on save failure and call publish_index(trade_key_index_tx(), raised) after a successful save.
  • rust/src/api/orders.rs#L3040-3051: because of the above, a retried restore_session() call can return Ok(info) even though the DB's trade_key_index was never durably raised — no local change needed once the identity.rs fix lands, but flagging so the contract is understood.
🤖 Prompt for 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.

In `@rust/src/api/identity.rs` at line 1, Update ensure_trade_key_index_at_least
to restore state.identity_info.trade_key_index to current when db.save_identity
fails, allowing retries to persist the requested floor instead of taking the
raised == current no-op path; after a successful save, call
publish_index(trade_key_index_tx(), raised) to update the secure-storage mirror.
🤖 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 `@rust/src/api/identity.rs`:
- Around line 414-434: Update ensure_trade_key_index_at_least to restore the
in-memory index if save_identity fails, allowing a retry with the same floor to
persist it again. After a successful persistence, publish the raised index
through the existing trade-key index channel, such as
publish_index/trade_key_index_tx, and preserve the no-op behavior when the
current index already meets the floor. Extend the lifecycle test to assert
publication and add coverage for persistence failure followed by a successful
retry.

In `@rust/src/api/orders.rs`:
- Around line 3040-3051: The restore path in the match arm handling
DaemonReply::Restored depends on ensure_trade_key_index_at_least retrying
persistence when the stored counter is already high enough. Update
ensure_trade_key_index_at_least in identity.rs so each call verifies or retries
the durable counter update instead of returning early solely because the
in-memory/current value meets the floor, while preserving monotonicity and
propagating persistence failures to the restore caller.

---

Outside diff comments:
In `@rust/src/api/identity.rs`:
- Line 1: Update ensure_trade_key_index_at_least to restore
state.identity_info.trade_key_index to current when db.save_identity fails,
allowing retries to persist the requested floor instead of taking the raised ==
current no-op path; after a successful save, call
publish_index(trade_key_index_tx(), raised) to update the secure-storage mirror.
🪄 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 Plus

Run ID: b4e4403f-d7c9-4a2d-abc8-afb81a0494a7

📥 Commits

Reviewing files that changed from the base of the PR and between aa7409b and e7471ce.

📒 Files selected for processing (2)
  • rust/src/api/identity.rs
  • rust/src/api/orders.rs

Comment thread rust/src/api/identity.rs
Comment thread rust/src/api/orders.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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 `@rust/src/api/identity.rs`:
- Around line 414-415: Update ensure_trade_key_index_at_least to require a
durable database handle from app_db::db() before calling
ensure_trade_key_index_at_least_with; return an error when storage is
unavailable so neither the in-memory trade_key_index nor its publisher is
changed. Add coverage verifying the None-storage path preserves both states
unchanged.
🪄 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 Plus

Run ID: 292f1b3b-80a6-45f8-97d0-377a30a34947

📥 Commits

Reviewing files that changed from the base of the PR and between e7471ce and d6204da.

📒 Files selected for processing (1)
  • rust/src/api/identity.rs

Comment thread rust/src/api/identity.rs Outdated
@codaMW

codaMW commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed, mirroring derive_trade_key rather than the proposed one-liner, since require_durable_storage returns () (a guard), not a handle, and the unconditional version would break web. Now require_durable_storage(db)? on native (refuses the resync when there's no store, so we never bump+publish without persisting), web exempt with the same rationale as derive_trade_key (no init_db on web, IndexedDB has no save_identity yet, so the Flutter mirror is web's durable record until #233). The None-refuses path is covered by the existing require_durable_storage test that derive_trade_key also relies on.

@codaMW
codaMW force-pushed the feat/217-trade-key-resync branch from 7b97e5a to 39c6492 Compare August 4, 2026 07:04
@codaMW

codaMW commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto current main now that #225 (the #215 RestoreSession handshake this was stacked on) has merged. Squashed the four iterative review-fix commits into one clean commit. The only conflict was additive test-module overlap in orders.rs main added admin_pubkey_from_payload tests, this PR added the recovered_max_trade_index tests; kept both. The production resync hook merged cleanly against #225's Restored arm. Green on cargo test (238) / clippy / wasm / flutter analyze. Ready for review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
rust/src/api/identity.rs (1)

424-433: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Persist the WebAssembly resynchronization before returning success.

On WebAssembly, Lines 424-433 pass None to the core. The core updates only memory and calls publish_index, which ignores Sender::send failures. Tokio broadcast sends only to active receivers, and a successful send does not confirm that a receiver observed the value. (docs.rs)

restore_session() can succeed without a durable trade_key_index. If the application stops before the Flutter mirror writes the event, a later startup can reuse the old index. Persist this Rust-owned protocol state through IndexedDB before returning success.

As per coding guidelines, Rust owns protocol-layer persistence and must use indexed_db_futures on web.

#!/usr/bin/env bash
set -euo pipefail

# Confirm the resolved Tokio version and locate every durable consumer of this index.
rg -n -C 3 '(^tokio\s*=|name = "tokio")' --glob 'Cargo.toml' --glob 'Cargo.lock' .
rg -n -C 6 \
  'publish_index|trade_key_index_tx|TradeKeyIndexStream|trade_key_index|indexed_db_futures|save_identity' \
  --glob '*.rs' --glob '*.dart' .
🤖 Prompt for 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.

In `@rust/src/api/identity.rs` around lines 424 - 433, Update the WebAssembly path
around ensure_trade_key_index_at_least_with so the resynchronized trade-key
index is durably persisted through indexed_db_futures/IndexedDB before returning
success, rather than relying on publish_index or the Flutter mirror. Keep the
existing non-WebAssembly flow unchanged, and propagate persistence failures
instead of treating the in-memory update as successful.

Source: Coding guidelines

🤖 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 `@rust/src/api/identity.rs`:
- Around line 452-465: The resync failure path must preserve the raised
trade_key_index and track that persistence is pending instead of restoring
current. Update the resync logic around save_identity and the same-floor
short-circuit to retry or reconcile pending durability rather than return
success without writing, and prevent derive_trade_key_with from issuing keys
until the floor is persisted or reconciled. Add a targeted failing-store test
covering failure, recovery, and attempted derivation.
- Around line 447-450: Update recovered_max_trade_index to reject u32::MAX
before ensure_trade_key_index_at_least_with can persist it, using the existing
error path and an i64::from(u32::MAX) + 1 limit. Preserve the no-op behavior in
the raised == current branch for valid recovered indices.

---

Duplicate comments:
In `@rust/src/api/identity.rs`:
- Around line 424-433: Update the WebAssembly path around
ensure_trade_key_index_at_least_with so the resynchronized trade-key index is
durably persisted through indexed_db_futures/IndexedDB before returning success,
rather than relying on publish_index or the Flutter mirror. Keep the existing
non-WebAssembly flow unchanged, and propagate persistence failures instead of
treating the in-memory update as successful.
🪄 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 Plus

Run ID: db232fb6-c95d-4ca6-8f92-082a6cb20895

📥 Commits

Reviewing files that changed from the base of the PR and between d6204da and 39c6492.

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

Comment thread rust/src/api/identity.rs
Comment thread rust/src/api/identity.rs
Comment on lines +452 to +465
state.identity_info.trade_key_index = raised;
if let Some(db) = db {
if let Err(e) = db.save_identity(&state.identity_info).await {
// Roll back the in-memory bump on a failed persist. Without this, a
// retried restore with the same floor would see `raised == current`,
// take the no-op short-circuit above, and return Ok(()) WITHOUT ever
// re-attempting the write — silently leaving the durable counter
// un-raised and reopening the key-reuse bug this closes. (Unlike
// derive_trade_key_with, which safely keeps its forward mutation
// because it has no idempotency short-circuit to defeat.)
state.identity_info.trade_key_index = current;
return Err(anyhow!(
"StorageError: failed to persist resynced trade_key_index {raised}: {e}"
));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not roll back the in-memory safety floor after a save error.

Line 462 restores current after save_identity fails. If current is 22 and the recovered floor is 23, a transient failure leaves the state at 22. If storage recovers before a later derivation, derive_trade_key_with calculates 23 at Line 370 and can issue an index already present in the recovered set. The restore error does not block that later derivation.

Do not only remove the rollback. Lines 448-450 would then let a same-floor retry report success without another write. Keep a pending-durability state, retain the non-regressing floor, and block derivation until the floor is persisted or reconciled. Add a failing-store test for this sequence.

As per coding guidelines, add targeted tests when expanding complex asynchronous workflows.

🤖 Prompt for 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.

In `@rust/src/api/identity.rs` around lines 452 - 465, The resync failure path
must preserve the raised trade_key_index and track that persistence is pending
instead of restoring current. Update the resync logic around save_identity and
the same-floor short-circuit to retry or reconcile pending durability rather
than return success without writing, and prevent derive_trade_key_with from
issuing keys until the floor is persisted or reconciled. Add a targeted
failing-store test covering failure, recovery, and attempted derivation.

Source: Coding guidelines

Refs MostroP2P#217 (sub-issue of MostroP2P#142). After a restore, the local trade_key_index
counter is still at its post-install value while recovered trades already
occupy higher indexes, so the next order reuses a trade key already bound to
a recovered trade — the daemon rejects the reused index with
CantDo(InvalidTradeIndex), and two trades would share a key.

When a valid RestoreData is processed, raise trade_key_index to the maximum
recovered index across orders and disputes. Monotonic (a restore never
rewinds the counter) and idempotent.

- identity::ensure_trade_key_index_at_least(floor) bumps the counter to
  max(current, floor) under the identity write lock; persists with the same
  discipline as derive_trade_key (rolls back the in-memory bump on a persist
  failure so a bumped-but-unpersisted counter can't regress on restart and
  reopen the bug), and requires durable storage on native (web exempt, same
  rationale as derive_trade_key).
- orders::recovered_max_trade_index(info) — the max trade_index over
  restore_orders and restore_disputes; u32::try_from drops negatives and
  out-of-range values rather than truncating garbage. None when the restore
  carried no trades.
- Wired into restore_session's Restored arm: resync before returning the
  info. A resync failure fails the restore rather than returning success with
  a counter that could hand out a reused key.
@codaMW
codaMW force-pushed the feat/217-trade-key-resync branch from 39c6492 to e22ef66 Compare August 4, 2026 12:11
@codaMW

codaMW commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough pass. Went through each finding against the current code:

1. u32::MAX terminal index fixed. Good catch. recovered_max_trade_index now drops u32::MAX itself (not just values above it): storing it as the counter would make the next derive_trade_key compute u32::MAX + 1 and overflow panic in debug, wrap to 0 in release, reissuing index 0, the exact reuse this resync prevents. Added assertions for the u32::MAX and only-u32::MAX->None cases.

2. Rollback on persist failure this is intentional, and I believe correct as written. The trace:

  • On a failed save_identity, the durable counter is unchanged (the write failed). Rolling the in-memory value back to current keeps memory consistent with what is actually persisted. Keeping the raised value in memory while storage still holds the old one is precisely the bumped-but-unpersisted state the require_durable_storage precondition exists to prevent: a session loss then reloads the old durable index and reopens the key-reuse bug (Trade-key counter is durable only in mostro.db — secure-storage copy is never updated #249).
  • The Err propagates through the ? in restore_session's Restored arm (orders.rs), so a failed resync fails the whole restore. The caller doesn't proceed to normal operation it retries restore_session() from scratch, which re-derives the floor and re-attempts the write. That fresh call doesn't hit the raised == current no-op short-circuit (it's a new invocation with the identity reloaded), so the "same-floor retry silently reports success without writing" concern doesn't arise.
  • The "storage recovers, a later derive_trade_key computes an already-recovered index and reuses" path requires operating normally after a failed restore. The ? prevents that derivation for trading happens after a successful restore, not a failed one.

So the alternative (keep the raised value in memory on failure) would introduce the memory/durable divergence this is designed to avoid. A failing-store regression test is a fair ask and I'd like to add one, but there's no failing-store harness in the repo yet (even derive_trade_key's own durability-failure path is untested), and a standalone mock means ~34 delegating methods for the Storage trait. I'd rather add a reusable failing-store test helper as a focused follow-up than bolt a large mock onto this PR happy to file that.

3. WASM durability intentionally consistent with derive_trade_key. Both flows exempt web for the same reason: init_db isn't called there and IndexedDB's save_identity is currently a stub (#233 "the IndexedDB storage backend is a stub, nothing persists across a reload"). On web the published index is the durable record via the Flutter mirror until #233 lands. Making the resync path alone persist through IndexedDB would diverge from the established derive_trade_key pattern and isn't possible until #233 implements the backend. Web durability for both flows is tracked there.

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.

1 participant