Skip to content

fix(swift-sdk): tie shielded completion suppression to a sync generation#3733

Open
QuantumExplorer wants to merge 2 commits into
v3.1-devfrom
platform-wallet/shielded-completion-generation
Open

fix(swift-sdk): tie shielded completion suppression to a sync generation#3733
QuantumExplorer wants to merge 2 commits into
v3.1-devfrom
platform-wallet/shielded-completion-generation

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 25, 2026

Issue being fixed or feature implemented

Follow-up to #3603 (shielded send end-to-end), which merged with a
boolean suppressShieldedCompletionEvents gate guarding the shielded
sync completion event after stop/clear.

A reviewer flagged that the boolean is bypassable: the FFI completion
callback re-dispatches onto the @MainActor, so a final, already-enqueued
event can land just after stop/clear returns. Because start cleared
the flag, a caller that stops (or clears) and restarts shielded sync in
the same actor turn re-opens the gate before the stale, queued
completion task runs — so the previous run's completion leaks into the
new run and is published as if it belonged to it.

What was done?

Replaced the boolean with a lock-guarded monotonic generation counter
(ShieldedSyncGenerationCounter):

  • The FFI completion callback snapshots the generation on the callback
    thread, before enqueueing the event onto the main actor.
  • stopShieldedSync / clearShielded bump the generation after the Rust
    drain returns.
  • handleShieldedSyncCompleted(_:generation:) drops any event whose
    snapshot no longer matches the current generation.
  • startShieldedSync / syncShieldedNow no longer reset anything — a new
    run's events carry the current generation and pass the guard, while a
    trailing event from a prior, stopped run still carries the older
    generation and is dropped, even on a same-turn restart.

This is a pure SDK-layer change (no FFI/Rust signature changes); the
example app's guard isBound remains as harmless defense-in-depth.

How Has This Been Tested?

Built the unified iOS framework + SwiftExampleApp for the simulator
(./build_ios.sh --target sim) — build succeeded. The change is
behaviorally identical for the common case (events from the live run
publish normally); the generation guard only differs from the old boolean
on the same-actor-turn stop→restart race, which is the bug being fixed.

Breaking Changes

None. handleShieldedSyncCompleted is an internal method; its new
generation: parameter is not part of the public API.

Checklist:

  • I have performed a self-review of my own code
  • 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

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of platform wallet shielded sync by preventing stale completion events from being processed after sync is stopped, cleared, or restarted, closing a race that could surface incorrect/old statuses.
  • Tests
    • Added regression tests validating correct handling of sync completion, restart, and stale-event drop scenarios.

Review Change Stack

Replace the boolean `suppressShieldedCompletionEvents` gate with a
lock-guarded monotonic generation counter. The boolean was bypassable:
a stop (set flag) followed by a restart (clear flag) in the same
@mainactor turn re-opened the gate before the stale, already-enqueued
completion task ran, leaking the previous run's completion event into
the new run.

The FFI completion callback now snapshots the generation on the callback
thread before enqueueing onto the main actor; stop/clear bump the
generation after the Rust drain returns; the main-actor handler drops any
event whose snapshot no longer matches the current generation. Start /
sync-now no longer reset anything — new events carry the current
generation and pass the guard, while a trailing event from a prior run
still carries the older generation and is dropped, even on a same-turn
restart.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer requested a review from shumkov as a code owner May 25, 2026 06:37
@github-actions github-actions Bot added this to the v3.1.0 milestone May 25, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbb2406a-6bab-4a3a-8fb5-6917649b2cdc

📥 Commits

Reviewing files that changed from the base of the PR and between 07421ab and a70eb9d.

📒 Files selected for processing (1)
  • packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/ShieldedSyncGenerationTests.swift

📝 Walkthrough

Walkthrough

Replaces a boolean suppression flag with a lock-guarded, nonisolated generation counter; FFI callback threads snapshot the generation and pass it into the main-actor handler, which discards completion events whose snapshot doesn't match the manager's current generation.

Changes

Shielded Sync Generation Counter

Layer / File(s) Summary
ShieldedSyncGenerationCounter data structure
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
Introduces a lock-guarded monotonic UInt64 counter with thread-safe current() and bump() methods for snapshotting and incrementing generation across FFI and main-actor boundaries.
Generation counter property in PlatformWalletManager
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManager.swift
Adds nonisolated let shieldedSyncGeneration property, replacing the prior suppressShieldedCompletionEvents boolean gate with generation-based suppression documented inline.
FFI callback thread-safe generation snapshot
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedSync.swift
shieldedSyncCompletedCallback captures the current generation on the FFI callback thread before scheduling main-actor work, ensuring the snapshot is immutable against concurrent bumps.
Event filtering by generation in handler
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedSync.swift
handleShieldedSyncCompleted accepts and validates a generation parameter, discarding events whose generation does not match the manager's current counter value.
Lifecycle methods generation bumping
packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedSync.swift
stopShieldedSync and clearShielded call bump() to invalidate trailing events; startShieldedSync and syncShieldedNow document that generation snapshots enable filtering of stale completions from prior stops.
Regression tests for generation behavior
packages/swift-sdk/SwiftExampleApp/SwiftExampleAppTests/ShieldedSyncGenerationTests.swift
Adds main-actor XCTest cases that validate generation semantics: stale completions are dropped after bump(), current-generation completions publish, same-turn restart flows behave correctly, and stale stragglers do not overwrite published events.

Sequence Diagram

sequenceDiagram
  participant FFIThread as FFI Callback Thread
  participant Manager as PlatformWalletManager
  participant MainActor as Main Actor
  participant Handler as handleShieldedSyncCompleted

  FFIThread->>FFIThread: shieldedSyncCompletedCallback()\nsnapshot shieldedSyncGeneration
  FFIThread->>Manager: read nonisolated generation
  FFIThread->>MainActor: enqueue handleShieldedSyncCompleted(event, generation)
  MainActor->>Handler: handleShieldedSyncCompleted(event, generation)
  Handler->>Manager: compare generation vs shieldedSyncGeneration.current()
  alt generations match
    Handler->>Handler: publish event (set lastShieldedSyncEvent)
  else generations mismatch
    Handler->>Handler: drop stale event
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 A counter so fine, monotonic and true,
Snapshots on threads where the callbacks pass through,
Stale events begone — bump and they're swept,
Main actor keeps only the freshest kept,
Hooray for safe syncs and quiet code stew! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: replacing a boolean gate with a generation counter to fix shielded completion suppression.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch platform-wallet/shielded-completion-generation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 25, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "ecc49a140c4da3a71d40898f79b5e0c8c94c7a37aa77f748b15484fc4d2505f1"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

Copy link
Copy Markdown
Collaborator

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

The generation-counter approach fixes the stale shielded-sync completion race at the Swift/Rust callback boundary without changing the FFI ownership contract. I did not find a functional or security regression in the implementation, but the PR still needs focused regression coverage for the exact stop/clear versus queued-completion ordering this fix relies on.

🟡 1 suggestion(s)

Suggestion: Add regression coverage for stale queued shielded completion events

packages/swift-sdk/Sources/SwiftDashSDK/PlatformWallet/PlatformWalletManagerShieldedSync.swift:68

This fix depends on a precise ordering guarantee: the FFI callback snapshots shieldedSyncGeneration before enqueueing onto @MainActor, then stopShieldedSync or clearShielded bumps the generation before that queued completion is delivered. I verified the implementation follows that pattern, but there is still no Swift test coverage exercising shielded-sync completion delivery or the stop/clear race. A later refactor could reintroduce the original boolean-gate problem while still compiling cleanly. Please add a focused regression test that enqueues a completion under generation N, bumps to N+1 before delivery and asserts the stale event is dropped, then verifies a completion captured under the current generation is published.

Inline posting hit GitHub HTTP 422, so this verified finding is posted as a top-level review body.

Regression coverage for the generation-gated suppression in
PlatformWalletManager.handleShieldedSyncCompleted(_:generation:). Asserts
a completion captured under a superseded generation is dropped (including
on a same-turn stop→restart, where a boolean gate would leak the stale
event), a current-generation completion is published, and a late
straggler does not clobber an already-published event.

Lives in the SwiftExampleAppTests Xcode target — the only test target on
this repo that links the real SwiftDashSDK (the SwiftTests SPM package
depends on a C mock, not the SDK). Verified locally via xcodebuild
test (4 cases pass on the iOS Simulator); not currently CI-enforced
since the Swift CI job is build-only.

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

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

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

Code Review

I verified the worktree at a70eb9d2edb4f1e18610368235c4463343d802e6 and confirmed the only delta since 07421abc64ef74739de6d12347371e62234987f9 is the new ShieldedSyncGenerationTests.swift regression suite. The underlying generation-based stale-event guard remains correctly implemented in the production shielded-sync path, and the new tests exercise the same-turn restart and late-straggler cases that previously needed coverage. I did not find any remaining correctness issues or false-positive review findings to carry forward.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants