Skip to content

fix(platform-wallet): let a shielded sync pass observe cancellation - #5733

Closed
llbartekll wants to merge 5 commits into
fix/reopen-quiesce-gate-on-startfrom
fix/cancellable-shielded-sync-pass
Closed

llbartekll wants to merge 5 commits into
fix/reopen-quiesce-gate-on-startfrom
fix/cancellable-shielded-sync-pass

Conversation

@llbartekll

Copy link
Copy Markdown
Contributor

Issue being fixed or feature implemented

Closes #5200.

Stacked on #5663 (base branch fix/reopen-quiesce-gate-on-start); retarget to v5.1-dev once that merges.

A shielded sync pass had no cancellation point: ShieldedSyncManager::sync_now → NetworkShieldedCoordinator::sync ran to completion, and only the loop's sleep between passes watched the registry token. So stop() returned at once, quiesce() waited for the whole pass (10 s drain budget, then ErrorShutdownIncomplete, code 27) and destroy joined the loop for up to 37 s. On dashwallet-ios network switches that was 47 s of teardown spent waiting for a pass that nobody needed any more.

What was done?

  • Manager owns a pass token (manager/shielded_sync.rs): pass_cancel: Mutex<CancellationToken>, pass_token() hands each pass a child. stop() cancels the current parent and rotates in a fresh one, then cancels the loop as before. Owning it in the manager (instead of reusing the loop's registry token) makes passes dispatched from host threads (sync_now(true), sync_wallet) cancellable too, and since drain_pass calls stop() first, quiesce, clear_shielded and shutdown all cut a running pass short. The token is taken right after the slot CAS and before the gate check, which closes the race with drain_pass's hold-then-stop order (commented in place).
  • Coordinator (wallet/shielded/coordinator.rs): sync(force, cancel). Bails with an empty summary if cancelled before any network work; after the scan, skips the network-only steps (spend re-drives, stranded releases, identity-shield re-drives) when cancelled, but still runs the store-only steps and the persister fan-out for what the scan consumed, and does not stamp the caught-up cooldown.
  • Note scan (wallet/shielded/sync.rs): sync_notes_across is split into the network wrapper (builds the SDK stream) and sync_notes_from_stream (snapshot, write lock, consume, finalize). The consumer checks the token before taking the write lock and select!s on it between batches (biased); on cancel it drops the stream (and its up-to-16 in-flight fetches) and runs the same finalize over the consumed prefix.
  • A pass cancelled before it did anything is not reported (no timestamp, no completion event, like losing the slot CAS). A cancelled pass that consumed batches has flushed them, so it is reported like any other pass and the host applies the deltas.
  • No host-visible type changes; ShieldedSyncPassSummary is untouched. Three test callers in balance_tests.rs and two examples pass a fresh token.

Why finalizing a partial scan is safe: batches arrive as a contiguous ascending prefix (the SDK's reorder buffer only releases the batch at its watermark), the watermark is aligned_start + total_notes_scanned from consumed batches, the checkpoint id is the real post-append tree_size() and so stays strictly monotonic, and nothing after the loop assumes the tip was reached. Today's ? error exits already leave appended leaves without a checkpoint or watermark; a cancelled pass is strictly better than an errored one. The fan-out is necessary, not just safe: save_note is gated by the watermark, so an unflushed note would never be re-emitted to the host.

How Has This Been Tested?

New tests (RUST_MIN_STACK=4194304 cargo test -p platform-wallet --features shielded …):

  • wallet::shielded::sync::cancel_tests (synthetic batch stream, file-backed store): a pass cancelled between two batches returns promptly, appends both, watermarks at 8 and has a checkpoint covering the appended leaves (witness(7)); a pre-scan cancel touches nothing and never builds the stream; a later pass re-delivering the realigned range appends only the new notes and checkpoints past the partial one.
  • manager::shielded_sync::tests: stop() cancels the in-flight token and rotates a fresh one; quiesce() returns within 2 s when the pass observes its token, instead of waiting out the 10 s budget.
  • wallet::shielded::coordinator::tests::should_skip_network_and_cooldown_when_cancelled_before_scan: pre-cancelled token → empty summary (the mock SDK would otherwise fail every wallet), no cooldown stamp, empty tree.

Existing suites: balance_tests (15), manager:: (129), wallet::shielded::coordinator (23) pass; cargo clippy -p platform-wallet --features shielded --tests --examples clean; platform-wallet-ffi compiles with shielded.

Not run on a device. The manual check is #5200's scenario: switch networks in dashwallet-ios while a shielded pass runs; shielded_sync_stop should return code 0 within about one batch and destroy in milliseconds.

Breaking Changes

None for hosts. NetworkShieldedCoordinator::sync gains a cancel: &CancellationToken parameter (crate-internal callers updated).

Checklist:

  • I have performed a self-review of my own code
  • My Rust, FFI, WASM, Swift or Kotlin changes follow the coding conventions
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding section if my code contains any
  • I have made corresponding changes to the documentation if needed
  • If I added or changed GroveDB structure, I described it in the area's structure.rs, regenerated grovedb-structure.json, and checked the structure viewer link posted on this pull request
  • If I added or changed a system contract's document schemas, I checked the contract viewer link posted on this pull request

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

🤖 Generated with Claude Code

Supersedes #5664, whose squash was merged into the base branch by mistake and reverted there (ba3fb97).

llbartekll and others added 5 commits October 29, 2026 12:16
A shielded sync pass had no cancellation point: only the loop's sleep
between passes watched the registry token, so `stop()` returned at once
but `quiesce()` waited for the whole pass (10 s, then code 27) and
`destroy` for up to 37 s on measured iOS teardowns.

The manager now owns a pass-cancellation token that `stop()` cancels and
rotates, so passes dispatched from host threads (`sync_now(true)`,
`sync_wallet`) are cancellable too. The coordinator checks it before
every network step, and the note scan selects on it between batches.
A cancelled scan drops its in-flight fetches and then finalizes the
prefix it consumed exactly as a completed pass would (checkpoint, note
saves, spend replay, watermark) and flushes that to the persisters, so
the store and the host mirror stay consistent; `quiesce` keeps its
barrier by waiting for that finalize. The caught-up cooldown is not
stamped for a cancelled pass.

Closes #5200.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d nothing

`build_per_wallet_summary` mints an all-zero `Ok` for every registered
wallet, so the manager's `is_empty()` check could not tell a pass
cancelled before its scan from a completed one and still stamped the
timestamp and emitted a completion event. The coordinator now hands back
the empty summary when a cancelled pass consumed nothing, and the
stranded-release prefetch (a read-only RPC) is selected against the
token so a stop that lands during it does not wait the request out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed pass

A cancelled pass finalizes `aligned_start + scanned`, and `aligned_start`
is the union MIN watermark, so a subwallet that was already further
along could be written backwards. Finalize `max(prior, new)` and leave
an uncovered subwallet's row alone on a cancelled pass; a completed
pass keeps the unconditional write (its `ScannedThisSession` stamp
feeds the restore gate). A pass cancelled before its first batch now
records nothing, so the coordinator's empty-pass gate fires.

Take the pass token before the `is_syncing` CAS in both entry points:
a `stop()` between the CAS and `pass_token()` rotated the parent and
handed the admitted pass an uncancelled child.

Check cancellation ahead of the cooldown fast path, whose skip summary
reported one caught-up wallet for a pass that never ran.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…shielded-sync-pass

# Conflicts:
#	packages/rs-platform-wallet/src/manager/shielded_sync.rs
@coderabbitai

coderabbitai Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@thepastaclaw

thepastaclaw commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

⛔ Final review complete — 3 blocking finding(s) (commit c4ca8f1) · triage: normal

lklimek commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Correctness: a cancelled partial scan can leave permanently incomplete activity history

A non-empty cancelled pass still calls derive_activity_into_changeset, so it can persist a scan-derived activity entry before the remaining notes have been scanned. If the resumed scan discovers more notes with the same proof-anchor height, they join the same cluster, but the cmx-overlap check produces only confirmations for the existing entry. The confirmation handler leaves an already-confirmed scan-derived row unchanged, so its amount and cmx set never expand.

I reproduced this using the actual derive_activity_from_scan_data implementation at c4ca8f12:

  • Partial input: note A, value 100, proof height 1000 → persist one entry for 100.
  • Resumed input: A plus note B, value 200, at the same proof height → 0 new entries, 1 confirmation, leaving the saved amount at 100.
  • The same complete input with no partial entry produces an entry for 300.

This makes restored history depend on cancellation timing. The raw notes can remain correct while the displayed history permanently omits part of the value; an ordinary full rescan retaining the existing activity rows does not repair it. This reproducer exercises the classifier, not the full SDK/network flow.

A scoped correction would be to defer creation of scan-derived activity entries until an uncancelled scan, while still finalizing and persisting the cancelled prefix's raw notes, spent marks and cursors. Please add a cancel/resume regression with owned notes split across batches sharing a proof height, and compare the resulting history with an uninterrupted scan. The existing cancellation stream tests use decrypted: vec![], so they do not cover this case.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Final validation — Phase 1 + Phase 2

The cancellation ownership and ordered-prefix mechanics are sound, but partial finalization can incorrectly establish complete-balance provenance and permanently omit restored activity. Post-scan retry helpers also continue initiating network requests after cancellation, and the new quiesce test contains a scheduling race. Verification was static at the exact requested head; no builds or tests were run, and the current Kotlin SDK build/test check passes.

🔴 3 blocking | 🟡 3 suggestion(s)

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: rust-quality); reviewer 6: gpt-6.1-sol (agent: phase2-reviewer, role: security-auditor); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: normal by gpt-6.1-sol (effort low) — The cancellation and partial-scan finalization changes are intricate, but they do not themselves change consensus rules, funds movement or coin selection, cryptography, key handling, peer-facing deserialization, or storage migrations.
  • Phase 1 reviewers: muse-spark-1.3-contributor — general (completed, effort xhigh); agent phase1-reviewer, muse-spark-1.3-contributor — rust-quality (completed, effort xhigh); agent phase1-reviewer
  • Phase 1 model: muse-spark-1.3-contributor — not quota-gated; passed over gemini-3.8-flash-high (antigravity quota lookup failed), glm-5.3-flash (not used above high effort; tier asks max)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — architecture-layering (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — rust-quality (completed, effort high); agent phase2-reviewer, gpt-6.1-sol — security-auditor (completed, effort high); agent phase2-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/shielded/sync.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/shielded/sync.rs:783-790: Do not mark a cancelled prefix as a complete balance scan
  When a cancelled pass advances an account, it calls `set_last_synced_note_index`. Both store implementations also set `last_scanned_index` and `balance_source = ScannedThisSession`, whose contract requires a complete scan. This changes availability, not just metadata: `local_balance_snapshot` accepts that source as sufficient evidence to return `Ready` for an otherwise unhydrated account. A newly registered account can therefore consume a prefix containing no owned notes, cancel before its receipts, and expose a ready zero balance. The emitted partial watermark is also interpreted as completed-scan evidence during restoration. Separate resumable progress from completed-scan provenance in the Rust store and persistence contract. Preserve note deltas and resume indices on cancellation without certifying complete coverage, and test the local snapshot both immediately after cancellation and after restoration.

In `packages/rs-platform-wallet/src/wallet/shielded/coordinator.rs`:
- [BLOCKING] packages/rs-platform-wallet/src/wallet/shielded/coordinator.rs:1423-1431: Defer immutable activity derivation until the scan completes
  Continuing all store-only processing after a partial scan also runs `derive_activity_into_changeset` unconditionally at lines 1511–1513. That derivation groups notes by their discovering proof height and skips an entire cluster if any commitment overlaps an existing activity row. On a cold restore, cancel after discovering receipt A, then resume and discover receipt B proven at the same height. The cancelled pass saves an activity row for A; the resumed cluster contains A and B, overlaps A's row, and produces only a confirmation for that existing row. B never receives an activity entry, and A's amount and commitment set are not expanded on later passes. Keep flushing raw notes, spent marks, and resume indices, but defer creation of immutable scan-derived history until completion or explicitly support extending incomplete clusters. Add a cancel/resume regression with owned receipts sharing a proof height.
- [BLOCKING] packages/rs-platform-wallet/src/wallet/shielded/coordinator.rs:1467-1470: Propagate cancellation through the post-scan retry helpers
  The spend-redrive gate uses a cancellation snapshot taken immediately after scanning. If stop arrives afterward, the subwallet loop and `redrive_pending_spends` continue through every pending record without observing it. The identity-shield path checks between wallets, but its helper cannot cancel the read-only nonce fetch or observe cancellation before the subsequent broadcast and remaining records. This is not merely waiting for one already-submitted broadcast: new requests continue to start after stop, potentially consuming multiple default 10-second timeouts and retries and exhausting the quiesce budget. Thread the pass token through both retry helpers, check it between records and before each new request, and make read-only waits cancellable. Preserve conservative reservation and ambiguity handling for broadcasts already submitted, and still perform the mandatory changeset flush.
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/coordinator.rs:1438-1444: Exercise coordinator finalization after mid-scan cancellation
  Both new coordinator cancellation tests cancel before entering the scan, so they return at the initial cancellation check. The synthetic stream tests invoke only the scan helper. Consequently, no test exercises the coordinator's partial-pass summary, persister fan-out, skipped redrive/release work, or cooldown handling after consumed batches. Add a coordinator-level mid-scan cancellation test that asserts the consumed changeset reaches the wallet persister, the summary reports useful partial work, no post-scan network request starts, and the cancelled pass does not establish a caught-up cooldown.

In `packages/rs-platform-wallet/src/manager/shielded_sync.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/manager/shielded_sync.rs:796-798: Capture the test pass token before publishing the occupied slot
  This multi-threaded test publishes `is_syncing = true` before obtaining its pass token. The parent can observe the occupied slot and call `quiesce()` while the spawned task is preempted between those statements. Stop then cancels and rotates the parent token; the spawned task subsequently obtains the fresh, uncancelled lineage and waits indefinitely, causing the two-second assertion to fail under a valid scheduling interleaving. Capture the token before claiming the slot, matching the production entry points.

In `packages/rs-platform-wallet/src/wallet/shielded/sync/cancel_tests.rs`:
- [SUGGESTION] packages/rs-platform-wallet/src/wallet/shielded/sync/cancel_tests.rs:43-71: Test cancellation with owned notes and scan-detected spends
  The synthetic batches have empty ciphertexts and an empty `decrypted` vector, while their nullifiers belong to no stored note. They therefore test tree appends, watermarks, and checkpoints but not receipt persistence, outgoing-note recovery, or spent-note deltas after cancellation. These deltas are central to the partial-finalization contract because the advanced watermark prevents incoming receipts from being emitted again. Add a cancelled-prefix test with a real owned receipt and a matching scanned spend, and assert that both store state and the returned changeset contain the expected receipt and spent mark.

Comment on lines +783 to +790
let advanced = prior.max(new_index);
if cancelled && advanced == prior {
continue;
}
store
.set_last_synced_note_index(*id, new_index)
.set_last_synced_note_index(*id, advanced)
.map_err(|e| PlatformWalletError::ShieldedStoreError(e.to_string()))?;
changeset.record_synced_index(*id, new_index);
changeset.record_synced_index(*id, advanced);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Do not mark a cancelled prefix as a complete balance scan

When a cancelled pass advances an account, it calls set_last_synced_note_index. Both store implementations also set last_scanned_index and balance_source = ScannedThisSession, whose contract requires a complete scan. This changes availability, not just metadata: local_balance_snapshot accepts that source as sufficient evidence to return Ready for an otherwise unhydrated account. A newly registered account can therefore consume a prefix containing no owned notes, cancel before its receipts, and expose a ready zero balance. The emitted partial watermark is also interpreted as completed-scan evidence during restoration. Separate resumable progress from completed-scan provenance in the Rust store and persistence contract. Preserve note deltas and resume indices on cancellation without certifying complete coverage, and test the local snapshot both immediately after cancellation and after restoration.

source: gpt-6.1-sol (phase2-reviewer: general)

Comment on lines +1423 to +1431
// Everything below that talks to the network is skipped once the
// pass is cancelled; the store-only steps and the host flush of
// what the scan consumed still run (see the method docs).
//
// Snapshotted here on purpose: the scan is the step that was or
// was not cut short, and the cooldown decision below is about
// whether that scan reached the tip. A stop that lands during the
// store-only steps after a complete scan does not change that.
let cancelled = cancel.is_cancelled();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Defer immutable activity derivation until the scan completes

Continuing all store-only processing after a partial scan also runs derive_activity_into_changeset unconditionally at lines 1511–1513. That derivation groups notes by their discovering proof height and skips an entire cluster if any commitment overlaps an existing activity row. On a cold restore, cancel after discovering receipt A, then resume and discover receipt B proven at the same height. The cancelled pass saves an activity row for A; the resumed cluster contains A and B, overlaps A's row, and produces only a confirmation for that existing row. B never receives an activity entry, and A's amount and commitment set are not expanded on later passes. Keep flushing raw notes, spent marks, and resume indices, but defer creation of immutable scan-derived history until completion or explicitly support extending incomplete clusters. Add a cancel/resume regression with owned receipts sharing a proof height.

source: gpt-6.1-sol (phase2-reviewer: general)

Comment on lines 1467 to +1470
// Runs before the balance read so freed notes are reflected in
// this pass's balances.
if let Some((snapshot, recorded)) = stranded_release {
// this pass's balances. Skipped on a cancelled pass: both halves
// broadcast, and a skipped release simply waits for the next pass.
if let Some((snapshot, recorded)) = stranded_release.filter(|_| !cancelled) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔴 Blocking: Propagate cancellation through the post-scan retry helpers

The spend-redrive gate uses a cancellation snapshot taken immediately after scanning. If stop arrives afterward, the subwallet loop and redrive_pending_spends continue through every pending record without observing it. The identity-shield path checks between wallets, but its helper cannot cancel the read-only nonce fetch or observe cancellation before the subsequent broadcast and remaining records. This is not merely waiting for one already-submitted broadcast: new requests continue to start after stop, potentially consuming multiple default 10-second timeouts and retries and exhausting the quiesce budget. Thread the pass token through both retry helpers, check it between records and before each new request, and make read-only waits cancellable. Preserve conservative reservation and ambiguity handling for broadcasts already submitted, and still perform the mandatory changeset flush.

source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality); gpt-6.1-sol (phase2-reviewer: general)

Comment on lines +796 to +798
wedge_sync_slot(&holder);
let _slot = SyncSlotGuard(&holder.is_syncing);
holder.pass_token().cancelled().await;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Capture the test pass token before publishing the occupied slot

This multi-threaded test publishes is_syncing = true before obtaining its pass token. The parent can observe the occupied slot and call quiesce() while the spawned task is preempted between those statements. Stop then cancels and rotates the parent token; the spawned task subsequently obtains the fresh, uncancelled lineage and waits indefinitely, causing the two-second assertion to fail under a valid scheduling interleaving. Capture the token before claiming the slot, matching the production entry points.

Suggested change
wedge_sync_slot(&holder);
let _slot = SyncSlotGuard(&holder.is_syncing);
holder.pass_token().cancelled().await;
let pass_cancel = holder.pass_token();
wedge_sync_slot(&holder);
let _slot = SyncSlotGuard(&holder.is_syncing);
pass_cancel.cancelled().await;

source: gpt-6.1-sol (phase2-reviewer: general, architecture-layering, rust-quality, security-auditor)

Comment on lines +1438 to +1444
if cancelled
&& notes.total_scanned == 0
&& crate::changeset::merge::Merge::is_empty(&notes.changeset)
{
tracing::debug!("Coordinator sync cancelled before the scan consumed anything");
return summary;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Exercise coordinator finalization after mid-scan cancellation

Both new coordinator cancellation tests cancel before entering the scan, so they return at the initial cancellation check. The synthetic stream tests invoke only the scan helper. Consequently, no test exercises the coordinator's partial-pass summary, persister fan-out, skipped redrive/release work, or cooldown handling after consumed batches. Add a coordinator-level mid-scan cancellation test that asserts the consumed changeset reaches the wallet persister, the summary reports useful partial work, no post-scan network request starts, and the cancelled pass does not establish a caught-up cooldown.

source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality)

Comment on lines +43 to +71
/// A batch of `len` notes at `start_index`. The notes are opaque to the
/// scanner: an empty ciphertext never OVK-recovers, the single subwallet
/// is the stream driver so no IVK trial-decryption runs, and a nullifier
/// nobody owns is a no-op in the spend replay. Only the commitment is
/// load-bearing — a distinct small value is a valid Pallas element.
fn batch(start_index: u64, len: u64, total_count: u64) -> ShieldedChunkBatch {
let notes = (0..len)
.map(|i| {
let mut cmx = [0u8; 32];
let pos = start_index + i;
cmx[0] = (pos as u8) + 1;
cmx[1] = (pos >> 8) as u8;
ShieldedEncryptedNote {
cmx: cmx.to_vec(),
nullifier: vec![0xAA; 32],
cv_net: vec![],
encrypted_note: vec![],
}
})
.collect();
ShieldedChunkBatch {
start_index,
notes,
decrypted: vec![],
block_height: 1_000 + start_index,
is_partial: false,
total_count,
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🟡 Suggestion: Test cancellation with owned notes and scan-detected spends

The synthetic batches have empty ciphertexts and an empty decrypted vector, while their nullifiers belong to no stored note. They therefore test tree appends, watermarks, and checkpoints but not receipt persistence, outgoing-note recovery, or spent-note deltas after cancellation. These deltas are central to the partial-finalization contract because the advanced watermark prevents incoming receipts from being emitted again. Add a cancelled-prefix test with a real owned receipt and a matching scanned spend, and assert that both store state and the returned changeset contain the expected receipt and spent mark.

source: muse-spark-1.3-contributor (phase1-reviewer: general, rust-quality)

@llbartekll
llbartekll deleted the branch fix/reopen-quiesce-gate-on-start November 3, 2026 12:06
@llbartekll llbartekll closed this Nov 3, 2026
@llbartekll

Copy link
Copy Markdown
Contributor Author

Replaced by #5743 (same branch, now against v5.1-dev). The correctness finding above and thepastaclaw's three blocking findings here are fixed there in 7fd0e14; summary in #5743 (comment).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants