Detect spent orchard notes via PIR#1675
Closed
p0mvn wants to merge 3 commits into
Closed
Conversation
Wire the spendability PIR client from the SDK fork into the app so orchard note spendability is detected via PIR rather than waiting for full shard-tree scanning. PIR runs at startup and again whenever foundTransactions or syncReachedUpToDate fires, with cancelInFlight to coalesce rapid events. Detected-spend placeholder appears in the transaction list until scanning catches up.
All PIR code paths (startup check, sync-triggered check, pending-spend placeholder, getPIRPendingSpends call) are now gated behind the pirSpendability WalletConfig flag, disabled by default. PIR "Detected spend" rows are non-tappable — the button is disabled and the coordinator ignores taps on PIR placeholder transactions. Made-with: Cursor
The synthetic "Detected Spend" row had three UX issues: - It appeared at the bottom of the activity list because it had no timestamp, falling through to an unstable height-based sort. Give it the current time so it sorts to the top where users expect to see recent activity. - Tapping the row navigated to an empty transaction detail screen. Guard the tap handler to return .none for PIR placeholders. - The loading spinner was too close to the amount text. Add trailing padding to separate them visually. Made-with: Cursor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Integrate the nullifier PIR (Private Information Retrieval) service so the app can detect spent orchard notes before the block scanner catches up. Without PIR, a spent note's balance appears available until full shard-tree scanning confirms the spend — which can take a long time and would lead to broadcast failures if the user tries to spend the stale balance.
How it works
registerForSynchronizersUpdate), the app firescheckSpendabilityPIRwhich calls the PIR server viasdkSynchronizer.checkWalletSpendability(pirUrl). The server checks each unspent orchard note's nullifier against a compact nullifier database.foundTransactions(new transactions discovered mid-sync) orsyncReachedUpToDate(sync completed) fires,checkSpendabilityPIRis triggered again — alongside the normal transaction refresh..cancellable(id: PIRCheckCancelId, cancelInFlight: true)so rapid events during a long sync coalesce into a single server request.checkSpendabilityPIRResultstores the result in@Shared(.pirSpendabilityResult)and triggers both a transaction list refresh and a balance update.fetchedTransactionsprocesses the transaction list, it also callsgetPIRPendingSpends()which queries the localpir_spent_notesSQLite table for notes not yet confirmed by the scanner. If any exist, a synthetic "Detected spend"TransactionStaterow is appended to the activity feed. This placeholder auto-reconciles — once the scanner catches up and the note appears inorchard_received_note_spends,getPIRPendingSpendsreturns empty and the placeholder disappears.Feature flag
All PIR code paths are gated behind
WalletConfig.isEnabled(.pirSpendability):registerForSynchronizersUpdate— only sendscheckSpendabilityPIRif flag is oncheckSpendabilityPIR— early-returns.noneif flag is offfoundTransactions/syncReachedUpToDate— only triggers PIR check if flag is onfetchTransactionsForTheSelectedAccount— only callsgetPIRPendingSpends()if flag is onfetchedTransactions— only appends the PIR placeholder if flag is onThe flag is defined in
WalletConfig(notFeatureFlags) as the single source of truth, togglable viaWalletConfigProvider/ debug flows.Demo
Detect Spend During Sync
https://www.youtube.com/watch?v=TMFQUpdcnLo
Once the spend is detected, the balance decreases by the full note amount. The decreased balance remains spendable — enabled by the sync nullifier PIR — until the wallet scans the spend and discovers the output change note.
Comparison to No PIR via Debug Settings
https://www.youtube.com/watch?v=JOKyPA1pqwI
Without PIR, the full balance appears available even though it is partially spent, which would lead to a broadcast failure. Triggering a PIR check via the debug menu shows the balance dropping to the correct spendable amount.
Screenshot
Related PRs
librustzcash(0.19.x backport): zcash_client_sqlite: Backport PIR spent-note tracking to 0.19.x zcash/librustzcash#2268zcash-swift-wallet-sdk: zcash-swift-wallet-sdk: Nullifier PIR integration zcash/zcash-swift-wallet-sdk#1674zodl-ios: Detect spent orchard notes via PIR #1675Test plan
pirSpendabilityflag — verify no PIR requests are made and no placeholder appearsChecklist
PIRSpendabilityTests.swift— 10 tests covering success/failure, flag gating, placeholder insertion)