Repository navigation
fix(platform-wallet): keep the cached identity in step with the chain around updates - #5487
Conversation
An identity update carries the cached identity's revision plus one, read when the update starts. A caller that records the revision before it submits, to tell later whether the update can still land (Drive takes an identity update only at one above the identity's revision), cannot see that read, and a refresh of the cache in between moves it. `update_identity_with_external_signer_at_revision` takes the revision the transition must carry and fails with `IdentityRevisionMismatch` before signing when the cache would give it another one, so the recorded revision is the only one the update can land at. The existing entry point passes `None` and behaves as before. Exposed as `platform_wallet_update_identity_with_signer_at_revision` in the FFI (a new symbol, so the Kotlin JNI binding is untouched) and as `ManagedPlatformWallet.updateIdentity(..., revision:)` in Swift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…landed at After a broadcast the cache took its own revision plus one. A refresh between the snapshot and that apply had already stored the landed revision, so the cache went one past the chain, and the next update asked for at that revision would be refused by Drive. It now takes the higher of what it holds and the transition's revision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Walkthrough
Merge Risk:
|
|
Waiting for bot review — coderabbitai not yet · thepastaclaw ✓. Wait for the missing reviews, or a writer can post |
|
⛔ Final review complete — 1 blocking finding(s) (commit 8df7515) |
thepastaclaw
left a comment
There was a problem hiding this comment.
Final validation — Phase 1 + Phase 2
Static verification found no blocking defects: the revision check precedes signing and broadcast, and the cache revision reconciliation avoids incrementing an already-refreshed revision. Two regression-coverage suggestions and one FFI documentation nit remain. Current Rust wallet, Swift SDK, and Kotlin SDK checks pass; PR Hygiene remains pending, and no local builds or tests were run.
🟡 2 suggestion(s) | 💬 1 nitpick(s)
Review provenance
Source: reviewer 1: gemini-3.8-flash-high (agent: phase1-reviewer, role: general); reviewer 2: gemini-3.8-flash-high (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
normalbygpt-6.1-sol(effort low) — The change adds revision preconditions and concurrent cache-update handling across Rust, C FFI, and Swift, but is contained and does not meet the large-or-intricate requirement for critical. - Phase 1 reviewers:
gemini-3.8-flash-high— general (completed, effort high); agentphase1-reviewer,gemini-3.8-flash-high— rust-quality (completed, effort high); agentphase1-reviewer - Phase 1 model:
gemini-3.8-flash-high— antigravity quota: weekly 71% left, 5h 100% left - Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort high); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort high); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-wallet/src/wallet/identity/network/update.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/update.rs:267-270: Add regression coverage for post-confirmation revision reconciliation
The three new tests exercise only `update_revision`, not this separate cache reconciliation fix. Replacing this assignment with the previous `cached_revision + 1` would leave those tests passing while restoring the concurrent-refresh bug. Add coverage of the local-apply path for a submitted transition at revision 5 when the cache remains at 4, has refreshed to 5, or has advanced to 6, asserting final revisions of 5, 5, and 6 respectively. Exercise the production local-apply path through a mocked broadcast or an extracted helper used by that path.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/identity/network/update.rs:399-409: Exercise revision rejection through the public async method
The helper tests do not exercise `update_identity_with_external_signer_at_revision`, so they would still pass if the public method stopped forwarding the requested revision or moved validation after signing or broadcast. Add an async regression test using a cached identity, a recording signer, and an SDK mock with no network expectations; assert the structured mismatch fields, zero signer calls, and unchanged cached state. Cover both mismatch directions and assert `(expected, actual)` explicitly, including `(5, 4)` for the cache-behind case, which currently checks only the error variant. The crate already enables SDK mocks in its dev-dependencies, and `network/signing_key.rs` demonstrates local wallet construction with `SdkBuilder::new_mock()`.
In `packages/rs-platform-wallet-ffi/src/identity_update.rs`:
- [NITPICK] packages/rs-platform-wallet-ffi/src/identity_update.rs:327-334: Document the new FFI entry point's pointer safety contract
The new public unsafe entry point documents revision behavior but not the caller obligations for its raw pointers. Its shared implementation reads the identity buffer, constructs slices from the supplied arrays, reads nested public-key buffers, and borrows the signer across worker execution. Add a `# Safety` section specifying readable lengths and alignment for nonempty arrays and nested buffers, a valid signer handle from the SDK's signer constructors, and the required lifetimes through completion of the call. The neighboring credit-transfer entry point provides a useful model. This is a documentation gap, not an ownership-layer violation; the crate explicitly allows `clippy::missing_safety_doc`, so CI does not enforce it.
|
@coderabbitai review No review for |
|
Bots are done — your move: post |
|
Bots are done — your move: post |
…te cache step - Move the post-broadcast local apply into `apply_landed_update` and test it: a transition at 5 leaves a cache at 4, 5 or 6 at 5, 5 and 6, for an add-only and a disable-only update. The revision step now runs for every landed update, not only one that adds or disables keys. - Test `update_identity_with_external_signer_at_revision` itself on the mock SDK: a mismatch in either direction returns the structured (expected, actual), never consults the signer and leaves the cache unchanged; at the matching revision the same call reaches the signer and the SDK. - Document the pointer, signer and threading contract of the identity update FFI entry points, and correct the stale cache note on `update_identity_with_external_signer`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — coderabbitai not yet · thepastaclaw requested changes — dismiss the review or push a fix, 1 thread unresolved — resolve it. Wait for the missing reviews, or a writer can post |
thepastaclaw
left a comment
There was a problem hiding this comment.
Re-review — Final validation — Phase 1 + Phase 2
The revision preflight is correctly ordered, the public-path tests cover both mismatch directions, and all three prior findings are fixed. Two blocking cache races remain: newer cached key state can be overwritten by an older landed transition payload, and an older refresh can overwrite the locally applied landed update. The Rust workspace test check was still in progress in the supplied CI snapshot.
🔴 2 blocking | 🟡 1 suggestion(s)
2 finding(s) not shown inline (the lines are not part of this PR's diff)
🔴 Blocking: Prevent a stale refresh from undoing the landed cache update
packages/rs-platform-wallet/src/wallet/identity/network/loading.rs:339-365
refresh_identity fetches outside the wallet-manager lock and then unconditionally assigns the fetched identity under the write lock. An older fetch can therefore complete after apply_landed_update: it may fetch revision 4, the update may land at revision 5 and update the cache, and the refresh can then restore revision 4 and the old key set. A subsequent revision-qualified update sees the wrong cached revision and may build a duplicate revision-5 transition instead of refusing or submitting the intended revision. Apply fetched identities monotonically against the current cached revision, preserving newer key and persistence state, or serialize refresh application with landed-update reconciliation. The same policy should cover the other fetched-identity writer in load_identity_by_index_inner.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, rust-quality, security-auditor)
🟡 Suggestion: Preserve revision mismatch as a typed FFI error
packages/rs-platform-wallet-ffi/src/error.rs:1125-1137
IdentityRevisionMismatch is the expected, pre-broadcast result of the new revision-qualified API, but the blanket FFI conversion falls through to ErrorUnknown. Mobile callers therefore cannot reliably distinguish an expected stale-cache/preflight mismatch from an SDK failure or another unmapped error without parsing the rendered message, and the expected/actual fields are lost as structured data. Append a dedicated FFI result code and mirror it in the Swift and Kotlin result-code mappings so callers can safely discard or reconcile the recorded revision.
source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, ffi-engineer, rust-quality, security-auditor)
Review provenance
Source: reviewer 1: muse-spark-1.3-contributor (agent: phase1-reviewer, role: general); reviewer 2: muse-spark-1.3-contributor (agent: phase1-reviewer, role: rust-quality); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 4: gpt-6.1-sol (agent: phase2-reviewer, role: architecture-layering); reviewer 5: gpt-6.1-sol (agent: phase2-reviewer, role: ffi-engineer); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: rust-quality); reviewer 7: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)
- Triage:
criticalbygpt-6.1-sol(effort low) — The change is large and intricate and modifies identity update signing, key-handling, and broadcast behavior in packages/rs-platform-wallet/src/wallet/identity/network/update.rs and its FFI/Swift boundaries. - Phase 1 reviewers:
muse-spark-1.3-contributor— general (completed, effort xhigh); agentphase1-reviewer,muse-spark-1.3-contributor— rust-quality (completed, effort xhigh); agentphase1-reviewer - Phase 1 model:
muse-spark-1.3-contributor— not quota-gated; passed overgemini-3.8-flash-high(lane failed),glm-5.3-flash(not used above high effort; tier asks max) - Single stage: Phase 1 and Phase 2 reviewed this head side by side, with no blocker gate between them (triage tier)
- Fresh verifier:
gpt-6.1-sol— final-verifier; agentsol-verifier - Phase 2 reviewers:
gpt-6.1-sol— general (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— architecture-layering (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— ffi-engineer (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— rust-quality (completed, effort xhigh); agentphase2-reviewer,gpt-6.1-sol— security-auditor (completed, effort xhigh); agentphase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.
In `packages/rs-platform-wallet/src/wallet/identity/network/update.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/identity/network/update.rs:289-297: Do not apply an older transition payload over newer cached identity state
`apply_landed_update` makes only the revision monotonic. It still applies `added_keys` and `disabled_ids` unconditionally, even when `cached_revision` is already greater than `transition_revision`. For example, an update at revision 5 can add key 3, a later update can land at revision 6 and disable key 3, and the cache can be refreshed to revision 6 before the revision-5 confirmation applies. The `add_key` call then restores the revision-5 active key and emits an upsert that can overwrite the newer persisted key row, so the wallet may select a disabled key for signing despite reporting revision 6. Apply the transition payload only when it is not older than the cached identity, or revision-guard each key mutation and its persistence changeset.
In `packages/rs-platform-wallet/src/wallet/identity/network/loading.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/identity/network/loading.rs:339-365: Prevent a stale refresh from undoing the landed cache update
`refresh_identity` fetches outside the wallet-manager lock and then unconditionally assigns the fetched identity under the write lock. An older fetch can therefore complete after `apply_landed_update`: it may fetch revision 4, the update may land at revision 5 and update the cache, and the refresh can then restore revision 4 and the old key set. A subsequent revision-qualified update sees the wrong cached revision and may build a duplicate revision-5 transition instead of refusing or submitting the intended revision. Apply fetched identities monotonically against the current cached revision, preserving newer key and persistence state, or serialize refresh application with landed-update reconciliation. The same policy should cover the other fetched-identity writer in `load_identity_by_index_inner`.
In `packages/rs-platform-wallet-ffi/src/error.rs`:
- [SUGGESTION] packages/rs-platform-wallet-ffi/src/error.rs:1125-1137: Preserve revision mismatch as a typed FFI error
`IdentityRevisionMismatch` is the expected, pre-broadcast result of the new revision-qualified API, but the blanket FFI conversion falls through to `ErrorUnknown`. Mobile callers therefore cannot reliably distinguish an expected stale-cache/preflight mismatch from an SDK failure or another unmapped error without parsing the rendered message, and the `expected`/`actual` fields are lost as structured data. Append a dedicated FFI result code and mirror it in the Swift and Kotlin result-code mappings so callers can safely discard or reconcile the recorded revision.
…let-update-at-revision
… and fetches A landed update whose transition is older than the cached identity (the cache was refreshed past it) no longer applies its added and disabled keys: the cache already holds them and what later updates did to them, and the payload's upserts could re-enable a key a newer update had disabled. A fetched identity at a lower revision than the cache's no longer replaces it, in refresh_identity and in the index loader. Such a fetch was answered before an update the cache already took, and could put the cache back to the pre-update revision and keys. The loader still adds its breadcrumbs, riding on the cache's newer keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sult code IdentityRevisionMismatch is the expected refusal of the revision-qualified update, but it reached hosts as ErrorUnknown, so they could only tell it from an SDK failure by its text. It now maps to ErrorIdentityRevisionMismatch (62; 61 is held by open #5395), mirrored as a typed error in Swift (identityRevisionMismatch) and Kotlin (PlatformWallet.IdentityRevisionMismatch), each with a pin test, and recorded in the error-code registry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lands A landed update whose cache was refreshed to its revision or past it skipped the payload but stored nothing: a refresh stores no keys, so the added keys lost their derivation breadcrumbs and the disabled keys never reached the store. The touched keys are now stored from the cache's own copies (on-chain disabled_at included), with their breadcrumbs. A cache at exactly the transition's revision is treated the same way, since the refresh already holds the payload as Platform has it. The tests read what reached the store, and the fixture takes a store and an SDK for that. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch site Discovery layered the keys of a fetch onto a known identity without looking at the cache's revision, so an older discovery answer could still re-enable a key a newer update had disabled. It now carries its breadcrumbs on the cache's keys, through the same helper the index loader uses (ManagedIdentity::key_decisions_on_cached_keys). When a landed update meets a cache at or past its revision, a disabled key's row is stored without a breadcrumb, which keeps the one it holds instead of writing an unverified one; a store failure for disabled keys only is logged, as disable_keys does, and an added key missing from the cache is logged. The updateIdentity(revision:) doc names the typed identityRevisionMismatch error, and take_fetched_identity says what the revision does not order (key limits, the balance). Tests: the index loader keeps a newer cache in memory and in what it stores, and the refresh test tells a taken answer from a dropped one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…adcrumb When a landed update meets a cache at or past its revision, a disabled key is now stored with the breadcrumb disable_keys gives it (the wallet slot's), not none: the SQLite store replaces a key's whole record, and a buffered changeset merge keeps the last upsert, so a row stored without a breadcrumb loses the one it had. Whether a store failure is an error now follows whether the batch held an added key, not whether one was asked. Discovery takes a newer fetch of a known identity as the loader and refresh_identity do, so its revision moves with the keys it lays on, and it folds before persisting the snapshot. The tests share one capturing store and an armable failing one, and cover a store failure for disabled keys only and for an added key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Platform never removes a key, so a key in an older fetch that the cache lacks is on the identity: carrying the fetch's breadcrumbs on the cache's keys now keeps the fetch's copy of such a key instead of dropping it. The same holds for a landed update meeting a cache at its revision that lacks an added key: the update's copy is stored and taken into the cache, and a store failure for it is an error. Discovery leaves a known identity's revision and balance alone again, as before this PR, and only rebases an older fetch's breadcrumbs: taking an equal-revision fetch whole could put back an older balance under a newer balance watermark. The loader skips the fold for an identity it just added. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a load's fetch is older than the cache, the loader returned the cache as it was before storing the fetch's keys, so a key the fetch had and the cache lacked went into the cache and the store but not into what the call returned. It now returns the cache after storing them. The fold rule lives in one method, ManagedIdentity::key_decisions_for_fetch, used by the loader and discovery; the wallet-slot breadcrumb comes from ManagedIdentity::wallet_slot, shared with disable_keys. A disabled key id the cache lacks is logged on the at-or-past path as disable_keys logs it, and the docs say an added key the cache lacks is taken from the update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Waiting for bot review — thepastaclaw not yet. Wait for the missing reviews, or a writer can post |
|
/skip-bots |
|
Ready for review — |
llbartekll
left a comment
There was a problem hiding this comment.
Re-approving at 8df7515. 51d7be8 does what we agreed: the _at_revision API, IdentityRevisionMismatch, code 62 and the FFI/Swift/Kotlin/JNI revision params are gone, and everything outside the two wallet crates matches the merge-base again, so no breaking change. The fold, apply_landed_update, the 40203 heal and the persistence fixes are unchanged from bf9b415. The reworded store-failure error in 8df7515 is accurate on both branches, since store_keys_with_snapshot layers the keys in memory before the store. Rust workspace tests are green here now, so the wallet crates got a real CI run. Fine with leaving the withExtendedLifetime change for a sweep across all the call sites.
|
Policy satisfied — this can merge. |
…et-external-key-ids Ports external_key_ids onto the apply_landed_update from #5487: an added key named external is stored without a breadcrumb on both of its paths, and a disabled key carries none, as this branch already does in disable_keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2-document-request-version Brings in #5680 (yes/no votes name their poll by id, polls carry a typed purpose), #5487 and #5360. Conflict: protocol_gate_tests.rs imports; keep YesNoVotePollPurpose and the drive query types the document gate tests use. #5680 adds a poll to the yes/no poll state response only: no new method or request field, so no gate or request inventory verdict changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ill has The record names the revision the wallet read before the update, while the SDK builds the update from its own cached identity. The revision pin that was to close this was dropped from dashpay/platform#5487 in favour of settling a broadcast's outcome from its signed transition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its proved slot After the rebase onto #5487, placement no longer persists: load and discovery store the placed identity's snapshot with its verified keys in one changeset. Cover the adopted case, which now relies on that snapshot to carry the new wallet and index, and narrow VerifiedPlacement to the crate with the function that returns it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its proved slot After the rebase onto #5487, placement no longer persists: load and discovery store the placed identity's snapshot with its verified keys in one changeset. Cover the adopted case, which now relies on that snapshot to carry the new wallet and index, and narrow VerifiedPlacement to the crate with the function that returns it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Basic explanation
What this does: It keeps the wallet's cached copy of an identity from going backwards or past the chain around an identity update, and keeps key derivation info from being lost when a store fails or a refresh finds a new key. Every identity update carries a revision number, the cached revision plus one, and Drive takes it only at exactly the identity's revision plus one, so a cache that drifts gets the next update refused.
Value: Identity updates (adding or disabling keys, including App Connect sign-in in dashwallet-ios#1202) no longer get refused after a refresh hits a lagging node or races the post-broadcast step, and keys survive a full disk during a restore and a restart after a refresh.
Risks: Low to medium, wallet-side only; no consensus code and no public API changes. An identity at
u64::MAXis now refused instead of wrapping. Fetched identities are now folded into the cache by revision and proof height instead of replacing it.Issue being fixed or feature implemented
The wallet builds every identity update from its local cache, at the cached revision plus one, and Drive accepts an update only at exactly the state revision plus one. On base the cache can drift from the chain: a fetch answered by a node one block behind rolls it back, and a refresh between the broadcast and the local bump leaves it one revision past the chain. Either way the next update is refused. This is fixed inside the wallet, between the SDK and the cache: fetches are folded in by revision, and the post-broadcast step takes the transition's revision.
The revision pin is out. An earlier version of this PR also added a caller-supplied
revisionargument, so a caller that records an update's revision before sending it (dashwallet-ios#1202) could not be surprised by a cache that moved in between. The maintainers agreed to settle an update's fate through a broadcast outbox instead (plan in the discussion below), which needs the cache invariant this PR keeps: the cache never runs ahead of the chain. The argument, its error and its FFI/Swift/Kotlin/JNI surface are removed.Each piece kept here has a scenario test that drives the base code through a real flow (mock SDK, mock or real SQLite persister) and reproduces the defect:
loadIdentity(atIndex:), or a pull-to-refresh, within about one block hits a node one block behind, which the SDK's height tolerance accepts. On base the cache goes back to revision 1, the disabled key is enabled again, the added key disappears, and the balance goes back to its pre-fee value. The next update then carries revision 2 and Drive refuses it.apply_landed_update. A load or refresh hits an up-to-date node between the update's snapshot and its post-broadcast revision bump. On base the cache ends at 3 while the chain is at 2, so the next update is refused. The bump also overwrites the on-chaindisabled_atwith the local clock.refresh_identitystays in memory only. On base it is gone after a restart while the stored revision claims it.The SQLite keep-breadcrumb rule and the changeset merge rule are what make B3a safe. A refresh row carries no breadcrumb, and on base a breadcrumb-less upsert replaced the whole SQLite row and wiped the stored breadcrumb. The Swift and Kotlin persisters already kept it.
Removed after the maintainer's review. The scenarios found no field trigger for the rest, so it is out of this PR:
Constrainttracing.The external-key breadcrumb fix (#5501) moves to its own PR (link: #5540). The key-provenance work stays parked under #5525.
What was done?
rs-platform-wallet:update_identity_with_external_signercomputes the transition's revision withchecked_add, so an identity atu64::MAXis refused instead of wrapping.apply_landed_update, the post-broadcast step. If the cache is behind the transition, it takes the transition's revision (not its own plus one) and the update's keys and disables, as before. If the cache is already at the transition's revision or past it, it already holds the update as Platform has it, so the payload is not applied over it. The added keys are stored from the cache's copies with the breadcrumb the update gives them. Both branches store the revision and the update's keys as one snapshot write and return a failed store, so a disable of a key the cache lacks still stores the landed revision.ManagedIdentity::take_fetched_identity, the fetch fold shared by refresh, load by index and discovery. A fetch at the cache's revision or above is taken whole, key states included, as on base. A fetch at a lower revision leaves the cache's revision, keys and disables. It takes only a raised limit, or a disable it shows on a key the cache holds enabled, and never rewrites a disable the cache holds. Each key limit keeps the higher of the two values. The balance follows the proof's block height (fetch_with_metadataand the existing balance watermark), not the revision.IdentityManager::add_identity_unstoredand store it with its verified keys as one changeset (store_fetched_identity_keys).IdentityKeysChangeSet::merge: a later entry without a breadcrumb keeps the earlier entry's.InvalidIdentityRevisionError(40203). That error carries the revision Drive holds. On that refusal the cached revision moves down to Drive's, only while the cache is still at the revision the refused transition was built from, and is stored with the scalar snapshot; the call returns the error and the next update carries the right revision. The cache is never raised from it. The error comes from one node without a proof: a node that lies low costs one refused update, and the next honest fetch brings the cache back.rs-platform-wallet-storage: the SQLiteidentity_keyswriter keeps the breadcrumb (and its wallet id) a row holds when an upsert for the same row carries none. It does so only for a row that passes the reader's column checks (contradicts_columns, shared withload_state) and holds the same public key: the same type, data and hash. Any other stored row is replaced without its coordinates.Known limitations:
How Has This Been Tested?
cargo test -p platform-wallet -p platform-wallet-ffi -p platform-wallet-storage -p rs-unified-sdk-jni --all-features: 3193 pass, 0 fail.cargo clippyfor the same crates with--all-features --tests -- -D warningsis clean, and so iscargo fmt. The Swift, Kotlin and JNI sources are unchanged from v5.1-dev.Tests added here:
wallet/identity/network/lagging_fetch_tests.rs, A1:should_keep_a_landed_update_when_a_lagging_fetch_answers_older(load by index and refresh).should_keep_raised_key_limits_when_a_lagging_fetch_answers_older.should_not_put_the_cache_past_the_chain_when_a_fetch_lands_before_the_apply.should_keep_a_second_disable_when_two_lagging_fetches_answer_older. Two revision-2 answers after an update at 3 disables a key again keep the revision and the disable.rs-platform-wallet-storage/tests/integration/sqlite_identity_key_restore.rs, B1a:should_not_leave_watch_only_key_rows_when_the_disk_fills_during_a_restore.should_store_a_key_a_refresh_finds_so_it_survives_a_restart.should_store_a_key_core_re_enabled_so_it_survives_a_restart, a revision-0 key re-enabled by a refresh, through a restart.should_store_a_key_a_load_at_index_finds_with_its_breadcrumb.loading::tests::should_store_a_newly_loaded_identity_with_its_keys_in_one_changeset. It fails on the base load path, which writes three changesets, the first with breadcrumb-less key rows.update_revision_tests::should_carry_one_above_the_cached_revision,should_refuse_an_identity_at_the_last_revision.should_take_a_landed_update_only_into_a_cache_behind_it,should_store_an_added_key_with_its_breadcrumb_when_the_cache_is_ahead.should_take_drives_revision_from_an_update_refused_for_its_revision,should_keep_the_cached_revision_unless_drive_holds_a_lower_one. Landed update:should_store_a_disable_the_cache_already_holds_when_the_confirmation_comes_back,should_store_a_landed_revision_with_its_keys_in_one_write(an added key with a disable, a disable of a key the cache lacks, a failed store).should_keep_the_higher_of_each_key_limit,should_fold_an_older_fetch_s_key_decisions_into_the_cache_s_keys,should_keep_a_raised_limit_in_a_fetch_s_key_decisions,should_store_the_keys_a_refresh_adds_or_changes.should_keep_an_earlier_breadcrumb_when_a_later_key_entry_has_none.sqlite_identity_keys_reader.rs):should_keep_a_stored_breadcrumb_when_an_upsert_carries_none,should_keep_a_stored_wallet_id_with_the_breadcrumb_when_an_upsert_carries_neither,should_replace_a_stored_row_that_does_not_decode,should_replace_an_oversized_stored_row_without_reading_it,should_lend_a_stored_breadcrumb_only_to_the_same_key_from_a_valid_row.Breaking Changes
None. No public Rust, C ABI, Swift, Kotlin or JNI signature changes.
Checklist:
structure.rs, regeneratedgrovedb-structure.json, and checked the structure viewer link posted on this pull requestFor repository code-owners and collaborators only
🤖 Generated with Claude Code
PR Hygiene ·
8df7515wallet-storage(packages/rs-platform-wallet-storage/src/sqlite/schema/identity_keys.rs,packages/rs-platform-wallet-storage/tests/integration/main.rs,packages/rs-platform-wallet-storage/tests/integration/sqlite_identity_key_restore.rsand 1 more) — approved by lklimekrs-platform-wallet(packages/rs-platform-wallet/src/changeset/changeset.rs,packages/rs-platform-wallet/src/error.rs,packages/rs-platform-wallet/src/wallet/identity/network/discovery.rsand 7 more) — approved by llbartekllWhen every merge requirement is met, the
PR Hygienecheck passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.Summary by CodeRabbit