Skip to content

[#1924] Split governance PCZT API for Keystone vs software wallets#1954

Merged
nullcopy merged 5 commits into
zcash:mainfrom
valargroup:roman/voting-keystone-api-split
May 14, 2026
Merged

[#1924] Split governance PCZT API for Keystone vs software wallets#1954
nullcopy merged 5 commits into
zcash:mainfrom
valargroup:roman/voting-keystone-api-split

Conversation

@p0mvn

@p0mvn p0mvn commented May 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Splits the governance PCZT JNI into two explicit paths so Keystone (and any other hardware-wallet integration that cannot expose the wallet seed) can drive a delegation round, while software wallets keep their UFVK/walletSeed validation invariant.

Why this shape

The previous buildGovernancePczt JNI required walletSeed and validated, inside Rust, that the seed derived the account UFVK before constructing the PCZT. That contract is correct for software wallets but is unsatisfiable on Keystone, which never surfaces the wallet seed. The Android Keystone integration was working around it by passing the hotkey seed as the wallet seed, which then failed with ufvk does not match walletSeed for network_id=1, account_index=0 on a real round, immediately after also tripping bundle_index 1 is not present in note bundle set because the call site was pre-filtering the note set to a single bundle (the Rust side re-chunks internally from the full snapshot using bundle_index).

We considered three alternatives before landing this shape:

  1. Loosening the seed/UFVK check to a runtime opt-in. Rejected: the check is a real safety invariant for software wallets; making it conditional inside Rust creates a quiet way to skip it.
  2. Keeping a single API and switching on a walletKind flag. Rejected: it forces the FFI to know about wallet flavors, hides which inputs are actually consumed in each branch, and still requires an explicit hotkey-address path for hardware wallets.
  3. Two narrowly typed APIs that share the construction core. Chosen: each entry point only takes inputs it can validate, the seed/UFVK invariant stays inside the seed path where it is meaningful, and the bundle/phase logic is shared so the on-disk state machine cannot drift between the two.

This also matches the iOS SDK contract (zcash-swift-wallet-sdk), which already exposes the explicit-FVK path for hardware-wallet flows.

What changed

  • buildGovernancePczt now takes pre-derived fvkBytes (Orchard FVK) and hotkeyRawAddress. This is the path Keystone uses; the host derives the FVK from the UFVK and the raw hotkey address from the hotkey seed before calling.
  • buildGovernancePcztFromSeed is the new software-wallet entry point. It takes ufvk, walletSeed, hotkeySeed and keeps the original UFVK<>walletSeed validation, then derives the hotkey raw address internally.
  • Both paths share build_governance_pczt_for_bundle, which validates the bundle index, enforces the round phase, persists the constructed delegation state, and only advances the phase to DelegationConstructed on success.
  • buildAndProveDelegation now takes hotkeyRawAddress directly. Keystone does not need to surface the hotkey seed beyond derivation, and software wallets derive once via the new helper.
  • New deriveHotkeyRawAddress JNI exposes raw-address derivation to the app layer so the Keystone path can stay seed-free end-to-end.
  • Mirror updates to TypesafeVotingBackend(Impl) and the JNI/SDK androidTest harnesses.

Manual testing

This was manually tested end-to-end on a Keystone hardware wallet: entering a poll no longer raises bundle_index 1 is not present in note bundle set, and constructing the delegation no longer raises ufvk does not match walletSeed. The software-wallet path was exercised through the existing app flow as well.

Author

  • Self-review your own code in GitHub web interface
  • Add automated tests as appropriate
  • Update the manual tests as appropriate
  • Check the code coverage report for the automated tests
  • Update documentation as appropriate
  • Run the demo app and try the changes
  • Pull in the latest changes from the main branch and squash your commits before assigning a reviewer

Reviewer

  • Check the code with the Code Review Guidelines checklist
  • Perform an ad hoc review
  • Review the automated tests
  • Review the manual tests
  • Review the documentation as appropriate
  • Run the demo app and try the changes

@p0mvn p0mvn force-pushed the roman/voting-keystone-api-split branch from e46377c to 5c12b8c Compare May 14, 2026 04:25
@p0mvn p0mvn changed the title Split governance PCZT API for Keystone vs software wallets [#1924] Split governance PCZT API for Keystone vs software wallets May 14, 2026
@p0mvn p0mvn force-pushed the roman/voting-keystone-api-split branch from 5c12b8c to cefc71a Compare May 14, 2026 04:33
Comment thread backend-lib/src/main/rust/voting/util.rs Outdated
p0mvn added 2 commits May 14, 2026 02:21
Hotkey material now derives from a named account-0 constant instead of accepting or reusing the caller's wallet account index. This matches zcash_voting's vote construction and signing behavior, where the hotkey spending key is derived through derive_spending_key and therefore fixed to account 0.

The seed-based governance PCZT path still uses the caller's account index for wallet seed to UFVK validation and PCZT ZIP-32 metadata, but uses the hotkey account constant for the hotkey raw address. The JNI, typesafe SDK, and tests now expose deriveHotkeyRawAddress without an accountIndex parameter, and a nonzero wallet-account instrumentation test verifies that explicit and seed PCZT paths remain aligned.
greg0x
greg0x previously approved these changes May 14, 2026
The split between `buildGovernancePczt` (Keystone path, trusts caller-derived
fvk + hotkey address) and `buildGovernancePcztFromSeed` (software path,
validates fvk against the wallet seed and re-derives the hotkey from a fixed
account index) carries real invariants but had no docs. KDoc now records both,
including the warning that adding an `accountIndex` parameter to
`deriveHotkeyRawAddress` would silently desync delegation construction from
vote signing.

`build_governance_pczt_explicit_and_seed_paths_match` asserted byte-identical
PCZTs across the two paths, which is stronger than what the implementation
actually guarantees — the two paths are not required to produce the same PCZT,
only valid ones. Replaced with `assertValidGovernancePczt`, which checks the
structural invariants that do hold: field-sized rk/sighash, a valid action
index, sighash re-extractable from the PCZT bytes, and `DELEGATION_CONSTRUCTED`
reached. Also adds coverage for `deriveHotkeyRawAddress` (determinism,
seed/network isolation, short-seed rejection) and for the typesafe forwarding
of both governance PCZT methods, which were previously stubbed as `unused()`
in the recording backend.
@nullcopy nullcopy deleted the branch zcash:main May 14, 2026 20:42
@nullcopy nullcopy closed this May 14, 2026
@greg0x greg0x reopened this May 14, 2026
@nullcopy nullcopy deleted the branch zcash:main May 14, 2026 20:53
@nullcopy nullcopy closed this May 14, 2026
@greg0x greg0x reopened this May 14, 2026
@nullcopy nullcopy changed the base branch from greg/voting-recovery-share-tracking to main May 14, 2026 20:56
@nullcopy nullcopy dismissed greg0x’s stale review May 14, 2026 20:56

The base branch was changed.

@greg0x greg0x requested a review from nullcopy May 14, 2026 21:55

@nullcopy nullcopy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

utACK 79589ef

@nullcopy nullcopy enabled auto-merge May 14, 2026 21:59
@nullcopy nullcopy merged commit 31739bd into zcash:main May 14, 2026
11 checks passed
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.

4 participants