Skip to content

zcash_client_sqlite: Backport PIR spent-note tracking to 0.19.x#2268

Closed
p0mvn wants to merge 5 commits into
zcash:maint/zcash_client_sqlite-0.19.xfrom
p0mvn:sync-pir-0.19.x
Closed

zcash_client_sqlite: Backport PIR spent-note tracking to 0.19.x#2268
p0mvn wants to merge 5 commits into
zcash:maint/zcash_client_sqlite-0.19.xfrom
p0mvn:sync-pir-0.19.x

Conversation

@p0mvn
Copy link
Copy Markdown
Collaborator

@p0mvn p0mvn commented Apr 3, 2026

Backport of #2267 onto the maint/zcash_client_sqlite-0.19.x release line.

The change is SemVer-compatible and applies cleanly to the 0.19.x maintenance branch with no conflicts. The sync-pir-0.19.x branch is based directly on maint/zcash_client_sqlite-0.19.x (zero delta before the PIR commit).

Summary

Adds support for nullifier PIR (Private Information Retrieval) spent-note tracking to zcash_client_sqlite, gated behind a sync-nullifier-pir feature flag.

Nullifier PIR lets the wallet discover Orchard note spendability by querying an external PIR server for nullifier inclusion, rather than waiting for sequential shard-tree scanning to complete. This significantly reduces the time before notes become spendable.

What's included

  • pir_spent_notes migration: Created unconditionally (not feature-gated) to keep the migration DAG identical across all builds. When the feature is off, the table exists but is empty and unused.
  • wallet::pir module (sync-nullifier-pir feature): Queries for unspent notes eligible for PIR checking, pending-spend tracking, and idempotent spent-note insertion.
  • Wallet integration (sync-nullifier-pir feature): get_wallet_summary and note selection skip the unscanned-range spendability gate for Orchard notes when PIR is enabled.
  • Reorg safety: truncate_to_height unconditionally clears pir_spent_notes to prevent stale PIR exclusions after reorgs.

Design decisions

  • The migration is not feature-gated so that all builds share the same migration DAG and MIGRATION_ID graph. This avoids divergent database states.
  • PIR rows in spent_notes_clause are unconditional (no tx_unexpired_condition) because they reflect confirmed on-chain nullifier state, not pending transactions.
  • The module uses a separate testing submodule with a minimal standalone schema for unit tests, avoiding the need to spin up a full wallet database.

Motivation for backport

The downstream zcash-swift-wallet-sdk depends on zcash_client_sqlite = "0.19". Having the PIR feature available on the 0.19.x release line unblocks integration in the Swift SDK and iOS wallet without requiring a major version bump across the entire dependency chain.

Dependencies

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.

Related PRs

Made with Cursor

Nullifier PIR (Private Information Retrieval) lets the wallet discover
Orchard note spendability by querying an external PIR server for
nullifier inclusion, rather than waiting for sequential shard-tree
scanning to complete. This significantly reduces the time before
notes become spendable.

The implementation is gated behind the `sync-nullifier-pir` feature
flag and comprises:

- A `pir_spent_notes` table (migration created unconditionally to keep
  the DAG identical across all builds; empty when the feature is off).
- A `wallet::pir` module with queries for unspent notes eligible for
  PIR checking, pending-spend tracking, and idempotent insert logic.
- Feature-gated changes to `get_wallet_summary` and note selection
  that skip the unscanned-range spendability gate for Orchard notes
  when PIR is enabled.
- Unconditional `pir_spent_notes` cleanup in `truncate_to_height` to
  prevent stale exclusions from persisting after reorgs.

Made-with: Cursor
@nuttycom
Copy link
Copy Markdown
Collaborator

nuttycom commented Apr 3, 2026

If you want to do it this way, close #2267 and then we'll forward-port this instead; the maintenance branch will get merged back to main after the next zcash_client_sqlite patch release. However, please double-check that the API changes do not require a SemVer breaking release before suggesting a patch release (I haven't verified this yet.)

@p0mvn
Copy link
Copy Markdown
Collaborator Author

p0mvn commented Apr 3, 2026

If you want to do it this way, close #2267 and then we'll forward-port this instead; the maintenance branch will get merged back to main after the next zcash_client_sqlite patch release. However, please double-check that the API changes do not require a SemVer breaking release before suggesting a patch release (I haven't verified this yet.)

Thanks for the recommendation.

Closed #2267.

I am addressing the comment about tests

please double-check that the API changes do not require a SemVer breaking release before suggesting a patch release

On it. Will post an update

@p0mvn
Copy link
Copy Markdown
Collaborator Author

p0mvn commented Apr 3, 2026

please double-check that the API changes do not require a SemVer breaking release before suggesting a patch release

Done.

Verdict: non-breaking, patch release appropriate

Summary

  • No existing public type signatures change
  • No public enum variants added or removed
  • No public trait methods added or changed
  • No existing public function signatures change
  • All behavioral changes to existing code paths are either feature-gated or functionally no-ops without the feature
  • The only public value change (CURRENT_LEAF_MIGRATIONS) is by-design mutable across releases

The PIR tests were using a hand-crafted SQL schema (PIR_TEST_SCHEMA_SQL)
that could drift from the actual migration-created tables. Notably, the
test schema still used the old `tx` column name while the real schema
uses `transaction_id` (renamed in the account_delete_cascade migration).

Replace the standalone schema with WalletMigrator::init_or_migrate() so
the test database always matches production. The tests insert synthetic
prerequisite rows (one account, one transaction) into the fully migrated
schema, then exercise the PIR queries against it.

Also fixes create_pir_test_db_on_disk (used by the SDK crate for
concurrent-access tests) to use the same migration-based setup.

Made-with: Cursor
Comment on lines +361 to +363
/// Leaf migrations in the 0.19.6 release.
pub const V_0_19_6: &[Uuid] = &[pir_spent_notes::MIGRATION_ID];

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Note to reviewer: I observed that v0.19.5 has already been pushed to crates.io, but the tag has not been pushed here.

As an outcome, I made an intuitive guess that this should be v0.19.6.

Please let me know if this was mischosen.

@p0mvn p0mvn force-pushed the sync-pir-0.19.x branch from ed0a431 to 301e20e Compare April 3, 2026 22:53
@p0mvn
Copy link
Copy Markdown
Collaborator Author

p0mvn commented Apr 3, 2026

@nuttycom

The test comment has been addressed:
ed0a431

Semver check has been done.

Would appreciate a review and/or a recommendation on next steps. Thanks

p0mvn added a commit to valargroup/zcash-swift-wallet-sdk that referenced this pull request Apr 4, 2026
The upstream PR (zcash/librustzcash#2268) lives on p0mvn's fork.
Update all [patch.crates-io] entries from valargroup/librustzcash
to p0mvn/librustzcash at 301e20e (includes migration-based test
setup).

Made-with: Cursor
p0mvn added 2 commits April 3, 2026 21:22
rustfmt wants `crate::WalletDb` before the nested path
`crate::wallet::init::WalletMigrator`.

Made-with: Cursor
Clippy requires a Default implementation when a public `new()` method
takes no arguments. This was causing CI failures with
`clippy::new_without_default`.

Made-with: Cursor
p0mvn added a commit to valargroup/zcash-swift-wallet-sdk that referenced this pull request Apr 4, 2026
Add spend-client, spend-types from sync-nullifier-pir (ab81f98).
Enable the sync-nullifier-pir feature on zcash_client_sqlite.

All [patch.crates-io] entries point to valargroup/librustzcash at
9861871 (sync-pir-0.19.x branch), which carries PIR spent-note
tracking on top of the 0.19.x release line.

Related upstream PR: zcash/librustzcash#2268
p0mvn added a commit to valargroup/zcash-swift-wallet-sdk that referenced this pull request Apr 4, 2026
Add spend-client, spend-types from sync-nullifier-pir (ab81f98).
Enable the sync-nullifier-pir feature on zcash_client_sqlite.

All [patch.crates-io] entries point to valargroup/librustzcash at
9861871 (sync-pir-0.19.x branch), which carries PIR spent-note
tracking on top of the 0.19.x release line.

Related upstream PR: zcash/librustzcash#2268
@p0mvn p0mvn closed this Apr 4, 2026
p0mvn added a commit to valargroup/zcash-swift-wallet-sdk that referenced this pull request Apr 6, 2026
Add spend-client, spend-types from sync-nullifier-pir (ab81f98).
Enable the sync-nullifier-pir feature on zcash_client_sqlite.

All [patch.crates-io] entries point to valargroup/librustzcash at
9861871 (sync-pir-0.19.x branch), which carries PIR spent-note
tracking on top of the 0.19.x release line.

Related upstream PR: zcash/librustzcash#2268
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